Данная тема посвящена нюансам и особенностям разработки Lua скриптов для MoonLoader.
Список вопросов:
Начало разработки.
Взаимодействие с игроками.
Работа с переменными.
Работа с командами.
Начало разработки.
Каждый Lua скрипт при запуске создает поток «main» который и является основным местом работы со скриптом.
Сразу обговорим — скрипт «жив» пока «жив» поток main(), поэтому если мы хотим продолжать работать со скриптом после запуска — будем держать поток активным:
function main() -- Объявляем поток main()
wait(-1) -- Устанавливаем бесконечное ожидание
end -- закрываем функцию
Запускаем скрипт — работает!
Что ж, поставим себе цель — при запуске выводить сообщение «Привет мир» в чат.
Что нам для этого нужно?
Заходим на WIKI (Раздел «Функции» moonloader`a) и ищем функции чата — Поиск -> «Chat». Видим функцию sampAddChatMessage(message, color) — то что надо.
Вставляем:
function main()
sampAddChatMessage("Привет мир!", 0xFFFFFFFF) -- Выводим сообщение в чат
wait(-1) -- Устанавливаем бесконечное ожидание
end
Запускаем скрипт — ошибка?
(error) test.lua: opcode '0AF8' call caused an unhandled exception
Бежим на WIKI (Опять раздел «Функции» moonloader`a) и в поиск вводим наш опкод «0AF8».
Находим нашу функцию sampAddChatMessage()
Что это значит? Дело в том что нельзя вызывать функции для работы с SA:MP пока сам SA:MP не готов. Что делать? Добавим перед выводом сообщения такой код:
if not isSampfuncsLoaded() or not isSampLoaded() then -- Если SF или SA:MP не загружены
return -- Завершаем работу скрипта
end
while not isSampAvailable() do -- Ждём пока функция isSampAvailable() вернет true
wait(0) -- Устанавливаем минимальное ожидание, что бы наша игра не зависла
-- значение 0 говорит что мы ждём следующий кадр (Frame)
end
Теперь наш скрипт будет ждать пока самп полностью не загрузится и продолжит выполнение кода.
Что такое wait() и с чем его едят?
Wait позволяет ставить
поток
в режим ожидания на указанное количество мили-секунд (ms)
Wait нельзя в CallBack-функциях, это приведет к ошибке, а в последствии и смерти, скрипта.
Почему мы ставим 0? Потому-что это минимальная задержка.
Подведем краткий итог кода:
function main()
if not isSampfuncsLoaded() or not isSampLoaded() then
return
end
while not isSampAvailable() do
wait(0)
end
sampAddChatMessage("Привет мир!", 0xFFFFFFFF)
wait(-1)
end
Теперь при входе в игру / перезапуске скрипта в чат будет выводиться сообщение «Привет мир».
К списку вопросов
Взаимодействие с игроками
Перейдем к примерам посложнее — попробуем получить ник, ид и счет игрока в которого мы прицелились.
Цель поставили, какие нам нужны средства?
Опять бежим к WIKI, всё в тот же раздел функций и ищем функции связанные с целями, Поиск -> Target.
Смотрим в списке есть getCharPlayerIsTargeting(ped).
Она возвращает 2 значения — result, ped.
Первое значение — возвращает результат проверки на целится ли указанный Ped в кого-либо.
Второе — если указанный Ped целится — возвращает саму цель.
Как нам это использовать? Всё просто, уберем бесконечное ожидание и будем проверять значение result.
function main()
if not isSampfuncsLoaded() or not isSampLoaded() then
return
end
while not isSampAvailable() do
wait(0)
end
sampAddChatMessage("Привет мир!", 0xFFFFFFFF)
while true do -- Создаем бесконечный цикл, вместо бесконечного ожидания
wait(0) -- Опять таки чтобы наша игра не зависла, ждем след кадр
local result, ped = getCharPlayerIsTargeting(PLAYER_HANDLE) -- Каждый кадр получаем данные функции, PLAYER_HANDLE - возвращает ваш Handle
if result then -- Если result вернет true
print("True!") -- Выведем сообщение в лог / консоль
end
end
end
Что мы можем с этим делать? Теперь если мы целимся в кого-либо, мы увидим сообщения в moonloader.log об этом. Мы получили Ped игрока в которого мы целимся. Давайте используем эту информацию.
function main()
if not isSampfuncsLoaded() or not isSampLoaded() then
return
end
while not isSampAvailable() do
wait(0)
end
sampAddChatMessage("Привет мир!", 0xFFFFFFFF)
while true do
wait(0)
local result, ped = getCharPlayerIsTargeting(playerPed)
if result then
local result2, id = sampGetPlayerIdByCharHandle(ped) -- Попробуем получить ID игрока по его Ped
if result2 then -- Если получилось
local nickname = sampGetPlayerNickname(id) -- Запишем его ник
local score = sampGetPlayerScore(id) -- и счет
sampAddChatMessage(string.format("%s[%d] имеет счет %d", nickname, id, score), -1) -- И выведем это в чат
end
end
end
end
К списку вопросов
Работа с переменными
Что у нас получилось? Мы будем получать кучу сообщений в чат о игрока по пока целимся в кого-либо, но как убрать этот флуд?
Давайте создадим переменную перед main(), например targeting, и присвоим ей значение false.
И задействуем её в коде
local targeting = false -- Создаем булевую переменную
function main()
if not isSampfuncsLoaded() or not isSampLoaded() then
return
end
while not isSampAvailable() do
wait(0)
end
sampAddChatMessage("Привет мир!", 0xFFFFFFFF)
while true do
wait(0)
local result, ped = getCharPlayerIsTargeting(playerPed)
if result and not targeting then -- Если мы целимся, но ранее ни в кого не целились
local result2, id = sampGetPlayerIdByCharHandle(ped)
if result2 then
local nickname = sampGetPlayerNickname(id)
local score = sampGetPlayerScore(id)
sampAddChatMessage(string.format("%s[%d] имеет счет %d", nickname, id, score), -1)
targeting = true -- запишем что мы целимся в кого либо и наше условие выше станет ложным
end
elseif not result and targeting then -- Если условие обратное - скажем скрипту что мы не целимся
targeting = false
end
end
end
Отлично, теперь если мы целимся в игрока — получаем одно сообщение, меняем цель или заново целимся в того же игрока — снова получим о нём информацию.
К списку вопросов
Работа с командами.
Уже лучше, давайте теперь работаем с командами.
Уберем лишний код и оставим следующее:
function main()
if not isSampfuncsLoaded() or not isSampLoaded() then
return
end
while not isSampAvailable() do
wait(0)
end
while true do
wait(0)
end
end
Как добавить команду? Давайте опять порыщем на WIKI, Поиск -> Command
Нашли sampRegisterChatCommand(command, callback)? Отлично, можно уже приступать.
Зарегистрируем нашу команду, например «toster»:
function main()
if not isSampfuncsLoaded() or not isSampLoaded() then
return
end
while not isSampAvailable() do
wait(0)
end
sampRegisterChatCommand("toster", tosterCallBack) -- Регистрируем ДО бесконечного цикла.
-- Первый параметр - команда, без слэша.
-- Второй - функция-callback которая будет вызваться если будет введена команда.
wait(-1) -- Уберем бесконечный цикл, он тут вовсе не нужен, но нам же нужно чтобы скрипт работал
end
function tosterCallBack(params)
sampAddChatMessage("Дзынь!", -1)
end
Теперь при вводе команды «/toster» мы увидим в чате сообщение. Успех? Но как например вывести свой текст в чат? Легко!
sampAddChatMessage(params, -1)
Теперь мы выводим весь текст после команды «/toster» в чат. Например: /toster Привет жалкий мирок!
Помните я говорил что нельзя использовать wait() внутри callback-функций, это всё еще так, но есть тут одна хитрость!
function tosterCallBack()
sampAddChatMessage("Дзынь!", -1)
lua_thread.create(function() -- Создаем новый поток
wait(5000) -- Ждём 5 секунд
sampAddChatMessage("Тоже дзынь, но на 5 секунд позже!", -1) -- Выводим текст в чат
end) -- Тут наш поток умирает :(
end
К списку вопросов
На сегодня всё, теперь вы чертовы волшебники, поняв как всё работает, можно приступать к изучению примеров Lua скриптов!
Тема будет дополняться по мере изучения популярных вопросов по Lua скриптингу.
Я понимаю что вы невъебенные мастера луа скриптинга, но это тема не для вас
В этой теме я попробую ввести вас в lua скриптинг, в кратце расскажу про основы.
И так, начинаем.
Любой луа скрипт начинается с function main() — это главная функция, без неё не один луа скрипт работать не будет. С начала выполняются действия с функции main, а потом уже выполняются другие функции.
Если вы пишете скрипт для сампа, то в начале main’a пишем такие строки:
if not isSampfuncsLoaded() or not isSampLoaded() then return end
while not isSampAvailable() do wait(100) end
Первая строка это проверка на то, что сампфункс и самп загрузился, во второй строке если перевести на русский написано, пока самп не запустился делать wait(100), после этих проверок может быть ваш код.
Далее у нас будет
Код
while true do
wait(0)
— здесь может быть ваш код
end
Это бесконечный цикл.
Цикл это повторение.
Т.е это бесконечное повторение каких-то действий.
Вместо этого цикла можно использовать wait(-1) в конце main’a, но об этом посже.
Также забыл сказать что все функции, циклы и потоки закрываются end’ом. Пример вы можете увидеть в бесконечном цикле сверху.
Теперь давайте рассмотрим одну из самых простых функций луа — sampAddChatMessage(text, color)
Для начала напишем начальный код:
Код
function main()
if not isSampfuncsLoaded() or not isSampLoaded() then return end
while not isSampAvailable() do wait(100) end
while true do
wait(0)
— теперь пишем sampAddChatMessage — с помощью этой функции вы можете вывести текст в чат, который будет виден только вам.
sampAddChatMessage(«{FFFFFF}test», -1) — {FFFFFF} — это белый цвет, -1 — это ставим если перед текстом поставили какой-то цвет, в даном случае белый.
end
end — закрываем
Теперь при заходе в самп, вам будет флудить в чат текстом который вы написали в sampAddChatMessage.
На этом пока-что всё, 1 урок закончен.
Надеюсь кому-то он был полезен, в следующем уроке рассмотрим переменные и условия.
Всем пока.
Ресурсы являются ключевой частью MTA. Ресурс — это папка или zip-архив, содержащий набор файлов, а также meta-файл, который описывает серверу как нужно загружать ресурс и из каких файлов он состоит. Ресурс играет практически ту же роль, что и программа в операционной системе — он может быть запущен и остановлен, при этом несколько ресурсов могут быть запущены одновременно.
Все, связанное со скриптингом, находится в ресурсах. Назначение ресурса и определяет, является ли он модом, картой или чем-либо еще. MTA поставляется с ресурсами, которые вы можете выборочно использовать в своих модах, например, maplimits, позволяющий удерживать игроков в рамках указанных границ карты, или deathpickups, создающий пикапы с оружием.
Tip: Первым шагом в изучении Lua-скриптинга должен быть выбор Lua-редактора. Это намного упрощает скриптинг. Мы рекомендуем Notepad++ или LuaEdit. Также имеется неофициальный MTA Script Editor (на стадии разработки), который вы можете испытать. |
Создание работающего скрипта
Для начала мы узнаем, как пошагово сделать простой скрипт, который позволит игроку прогуливаться по городу.
Где находятся все скрипты?
Давайте взглянем на файловую структуру скрипта. Зайдите в папку сервера MTA и пройдите по следующему пути:
/server/mods/deathmatch/resources/
Вы увидите множество .zip-архивов, являющихся упакованными пробными скриптами, поставляемыми с MTA DM. Каждый файл — это «ресурс», все они будут распакованы и загружены сервером при его старте. Чтобы создать свой собственный ресурс, просто создайте папку и назовите ее так, как хотите. В нашем случае мы назовем ее «myserver».
Теперь вам нужно зайти в эту папку:
/server/mods/deathmatch/resources/myserver/
Идентификация вашего ресурса
Чтобы сервер мог узнать о содержимом того или иного ресурса, в нем должен быть создан файл meta.xml, перечисляющий его содержимое. Этот файл должен быть расположен в корневой директории ресурса, в нашем случае — это папка «myserver». Просто создайте текстовый файл, назовите его «meta.xml» и откройте с помощью Блокнота (notepad).
В файл meta.xml введите следующий код:
<meta> <info author="YourName" type="gamemode" name="Kontol memek" description="My Mta Sa" /> <script src="script.lua" type="server"/> </meta>
В теге <info /> есть поле «type», которое говорит о том, что данный ресурс — gamemode («мод», игровой режим), а не обычный include или map (карта), о которых мы поговорим чуть позже. Gamemode — то, что вам нужно, чтобы создать независимый сервер.
Тег <script /> оговаривает сценарии (скрипты), которые содержит ресурс, о них мы сейчас и поговорим.
Поле «type» говорит о том, что данный скрипт «script.lua» будет выполняться на стороне сервера.
Создание простого скрипта
Заметьте, что в теге <script /> ‘script.lua’ — файл не находится в какой-либо вложенной директории. Следовательно, мы создадим файл в той же папке, что и meta.xml. Теперь можно скопировать и вставить в script.lua следующий код:
local spawnX, spawnY, spawnZ = 1959.55, -1714.46, 10 function joinHandler() spawnPlayer(source, spawnX, spawnY, spawnZ) fadeCamera(source, true) setCameraTarget(source, source) outputChatBox("Welcome to My Server", source) end addEventHandler("onPlayerJoin", getRootElement(), joinHandler)
Этот скрипт заспавнит вас по координатам (x, y, z), указанным выше, когда вы зайдете на сервер. Обратите внимание, что функция fadeCamera обязательно должна быть, иначе экран будет черным. К тому же, в релизах новее DP2 вам нужно установить цель для камеры (иначе все, что увидит игрок — синее небо).
Переменная source указывает на того, кто вызвал срабатывание события. Так как данный код срабатывает при заходе какого-либо игрока, эта переменная используется для установления того, кто зашел. Так что спавнить будет именно этого игрока, а не всех сразу или кого-нибудь случайно.
Если присмотреться к addEventHandler, вы заметите три вещи: ‘onPlayerJoin’, указывающий на то, когда (почему) произойдет срабатывание; getRootElement(), который показывет благодаря кому/чему может произойти срабатывание (getRootElement() — это все/всё) и joinHandler, который отвечает за функцию, на которую произойдет переключение при срабывании события. Остальные подробности будут изложены позже и на отдельном примере, а теперь давайте просто запустим сервер и попрактикуемся!
Запуск скрипта
Чтобы запустить сервер, просто запустите исполняемый файл (на Windows — .exe) по адресу MTA San Andreas x.x/server, где x.x — номер версии MTA. Сначала будут показаны данные сервера; запомните номер порта (server port), который понадобится вам при подключении. Затем сервер загрузит все ресурсы в папку mods/deathmatch/resources/ и позже будет «ready to accept connections!», то есть готов принимать игроков.
Перед тем, как вы подключитесь к серверу, нужно обязательно запустить мод (gamemode). Введите «start myserver» и нажмите Enter. Сервер запустит мод, который вы только что создали, а также начнет отображать различные ошибки и предупреждения, если таковые будут. Теперь можно запустить клиент MTA DM и подключиться через «Quick Connect», воспользовавшись IP-адресом вашего сервера и номером порта, на который мы ранее обратили ваше внимание. Если все пройдет по плану, через несколько секунд ваш персонаж сможет пройтись по улицам Los Santos’а.
Затем мы добавим в скрипт команду, которую игроки смогут использовать для того, чтобы спавнить рядом с собой транспортное средство. Вы можете это пропустить и взглянуть на статью про более продвинутый скриптинг с использованием Map Manager, которая продолжит это руководство. Еще одним ответвлением данного руководства является Введение в скриптинг GUI: прочитав его, вы узнаете, как рисуется и программируется Graphical User Interface в MTA:DM.
Создание простой команды
Давайте вернемся к содержимому файла script.lua. Как уже было сказано, мы хотим предоставить команду для создания трансортного средства рядом с вашей текущей позицией в игре. Во-первых, нам понадобится создать функцию, которую мы будем вызывать, и обработчик команды, который сделает команду доступной для выбора игроком посредством ввода ее в консоли.
-- создаем функцию, вызываемую обработчиком команды, с аргументами: thePlayer, command, vehicleModel function createVehicleForPlayer(thePlayer, command, vehicleModel) -- создаем транспортное средство и другое end -- создаем обработчик команды addCommandHandler("createvehicle", createVehicleForPlayer)
Заметка: Клик по названию функции в образце кода перенаправит на соответствующую страницу с ее описанием.
Про обработчики команд
Первый аргумент addCommandHandler — имя команды, которая будет доступна игроку, второй аргумент — функция, на которую произойдет переключение, в данном случае — это createVehicleForPlayer.
Если у вас уже есть опыт в скриптинге, вы знаете, что функции вызываются примерно следующим образом:
functionName(argument1, argument2, argument3, ..)
functionName(thePlayer, commandName, argument3, ..)
Присмотревшись ко второму образцу (выше), мы увидим, что argument1 — thePlayer, а argument2 — commandName. thePlayer — тот, кто набрал команду, так что как бы вы ее не вводили, переменная будет содержать игрока, который ее активировал. commandName — команда, которую ввели. Так что при вводе «/greet», этот аргумент будет содержать «greet». Argument 3 — еще что-то, введенное игроком после, об этом вы узнаете чуть позже из данного руководства. Никогда не забывайте, что первые 2 аргумента являются стандартными, но назвать вы их можете по своему усмотрению. То есть важен порядок, а не название.
Мы уже вызывали таким образом функцию addCommandHandler, и так как createVehicleForPlayer — также функция, ее тоже можно так вызвать. Но мы для этого используем обработчик команд, который вызывает ее схожим образом, только внутренне.
Например: Кто-то вводит «createvehicle 468» в игровой консоли, чтобы заспавнить Sanchez, обработчик команд вызывает функцию createVehicleForPlayer, как если бы мы имели в скрипте следующую строку кода:
createVehicleForPlayer(thePlayer,"createvehicle","468") -- thePlayer - элемент типа player игрока, который ввел команду
Как можно заметить, предоставляются несколько параметров: игрок, который вызвал команду, сама команда, которую он ввел, и какой-нибудь текст, который он после нее ввел, в данном случае — «468» в качестве id трансопртного средства, отвчечающего за Sanchez. Первые два параметра одинаковы для всех обработчиков команд, о них вы можете почитать на странице addCommandHandler. Фактически, вам всегда придется определять как минимум эти два параметра, чтобы смочь использовать какие-нибудь другие, идущие после них (например, для обработки текста, введенного после команды, как id модели транспортного средства в нашем случае).
Заметка: Обработчик команды надо добавлять именно ПОСЛЕ функции, на которую он сошлется, иначе она не будет найдена. Порядок имеет значение!
Написание функции
Чтобы заполнить созданную нами функцию, нам следует подумать, что нам предстоит сделать:
- Получить позицию игрока, чтобы знать, где спавнить ТС (мы хотим, чтобы оно появлялось прямо рядом с игроком)
- Вычислить позицию, на которой мы хотим заспавнить ТС (мы же не хотим его появления на голове у игрока)
- Собственно, заспавнить ТС
- Проверить, заспавнилось ли оно успешно, в противном случае — вывести сообщение в чат
Чтобы разрешить все поставленные задчаи, нам понадобится задействовать несколько функций. А чтобы найти нужные нам функции, нужно перейти ко списку серверных функций. Для начала нам понадобится функция, которая получит координаты игрока. Так как все игроки являются элементами, мы сразу выбираем Element functions, где и находим функцию getElementPosition. Кликнув по имени функции из списка, вы получите ее описание. Там можно увидеть синтаксис, что она возвращает и, как правило, пример использования. Синтаксис сообщает какие аргументы мы можем или должны ей передать.
Для getElementPosition синтаксис таков:
float, float, float getElementPosition ( element theElement )
Три float перед именем функции и есть типы значений, которые она возвращает. В данном случае это значит, что функция возвращает три числа с плавающей точкой (x, y и z). Внутри круглых скобок указаны аргументы, которые ей необходимо передать. В данном случае это только элемент, чью позицию вы хотите получить, у нас он представлен игроком.
function createVehicleForPlayer(thePlayer, command, vehicleModel) -- get the position and put it in the x,y,z variables -- (local означает, что переменные существуют только здесь, в этой области, внутри этой функции) local x,y,z = getElementPosition(thePlayer) end
Затем нам надо сделать так, чтобы ТС не спавнилось прямо внутри игрока, поэтому мы прибавим небольшое число к переменной x, что повлечет за собой спавн ТС чуть восточнее самого игрока.
function createVehicleForPlayer(thePlayer, command, vehicleModel) local x,y,z = getElementPosition(thePlayer) -- получаем позицию игрока x = x + 5 -- прибавляем число 5 к позиции по оси x end
Теперь нам понадобится другая функция, чтобы непосредственно заспавнить транспортное средство. Мы снова ищем ее в списке серверных функций, на этот раз, так как мы говорим о транспорте — в разделе Vehicle functions, где выберем createVehicle. В синтаксисе этой функции указано только одно возвращаемое значение (что встречается наиболее часто) — элемент типа vehicle, представляющий только что созданное ТС. Также мы видим, что часть аргументов заключена в [ ], следовательно, они необязательны.
Внутри нашей функции у нас уже есть все аргументы, которые нужны функции createVehicle: Только что вычисленная позиция в переменных x,y,z и id модели, который мы получили через команду («createvehicle 468»), он доступен внутри функции в качестве переменной vehicleModel.
function createVehicleForPlayer(thePlayer, command, vehicleModel) local x,y,z = getElementPosition(thePlayer) -- получаем позицию игрока x = x + 5 -- прибавляем число 5 к позиции по оси x -- создаем ТС и сохраняем возвращенный элемент типа vehicle в переменной ''createdVehicle'' local createdVehicle = createVehicle(tonumber(vehicleModel),x,y,z) end
Этот код, конечно же, можно усовершенствовать различными путями, но как минимум мы добавим проверку на то, было ли ТС успешно создано. Как можно прочитать на странице createVehicle под Returns, функция возвращает false, если ТС создать не получилось. Стало быть, мы проверяем значение переменной createVehicle.
Теперь у нас есть готовый скрипт:
function createVehicleForPlayer(thePlayer, command, vehicleModel) local x,y,z = getElementPosition(thePlayer) -- получаем позицию игрока x = x + 5 -- прибавляем число 5 к позиции по оси x local createdVehicle = createVehicle(tonumber(vehicleModel),x,y,z) -- проверяем, является ли возвращаемое значение ''false'' if (createdVehicle == false) then -- если да, то выводим сообщение в чат, но только для игрока, который спавнил ТС. outputChatBox("Failed to create vehicle.",thePlayer) end end addCommandHandler("createvehicle", createVehicleForPlayer)
Как вы уже, наверное, заметили, вашему взору предстала новая функция — outputChatBox. Теперь вы самостоятельно можете изучить содержимое ее страницы-документации. Чтобы узнать больше о продвинутом скриптинге, почитайте про Map Manager.
Что вам следует знать
Вы уже кое-что прочитали о ресурсах, обработчиках команд и поиске функций в документации в первом разделе, но многое еще предстоит узнать. Этот раздел проведет довольно краткий обзор о некоторых из этих вещей, по возможности ссылаясь на соответствующие страницы.
Клиентские и серверные скрипты
Может быть, вы уже заметили эти или схожие термины (сервер/клиент) где-либо на данной вики, наиболее вероятно, вкупе с функциями. MTA не только поддерживает работающие на сервере скрипты, предоставляет команды (типа как мы писали выше) и другие возможности, но также и скрипты, выполняющиеся на клиенте MTA, который игроки используют для подключения к серверу. Причиной этому служит то, что некоторые предоставляемые MTA функции не могут быть серверными (например, GUI — Graphical User Interface, т.е. графический интерфейс пользователя), другие там просто работают лучше, но другим все же лучше быть серверными или попросту не работать на клиентской стороне.
Большинство сделанных вами скриптов (модов, карт), вероятно, будут серверными, как и та, которую мы написали в первом разделе. Если вы столкнетесь с чем-то, что не может быть реализовано на серверной стороне, возможно, вы сможете реализовать это на клиентской. Для написания клиентского скрипта, создайте обычный файл-скрипт (например, названный client.lua) и укажите его в meta.xml так:
<script src="client.lua" type="client" />
Атрибут type по умолчанию установлен на ‘server’, так что надобность указывать его существует только для клиентских скриптов. После этого, клиентский скрипт будет загружаться на компьютеры игроков при заходе. Подробнее о клиентских скриптах.
Более сложные ресурсы
Предыдущий раздел вкратце изложил, как добавлять в ресурс клиентские скрипты, но возможностей на самом деле намного больше. Как написано в самом начале статьи, ресурсы могут быть чем угодно. Их назначение определяется тем, что они делают. Давайте теоретически вообразим некоторые ресурсы, глядя на их файлы-содержимое, meta.xml и подумаем, что они могут делать:
Первый пример — Вспомогательный скрипт
/admin_commands /meta.xml /commands.lua /client.lua
<meta> <info author="Someguy" description="admin commands" /> <script src="commands.lua" /> <script src="client.lua" type="client" /> </meta>
- commands.lua предоставляет некоторые администраторские команды, такие как бан и заглушение игроков или еще что-нибудь, что может быть доступно для администраторов сервера
- client.lua предоставляет GUI, чтобы возможно было с легкостью выполнять вышеуказанные действия
Этот пример может выполняться все время (даже автозапускаться со стартом сервера), так как является полезным на протяжении всего игрового процесса и не конфликтует с ним, если администратор, конечно, сам этого не захочет.
Второй пример — Мод
/counterstrike /meta.xml /counterstrike.lua /buymenu.lua
<meta> <info author="Someguy" description="Counterstrike remake" type="gamemode" /> <script src="counterstrike.lua" /> <script src="buymenu.lua" type="client" /> </meta>
- counterstrike.lua содержит схожие с нижеперечисленными функции:
- Позволить игрокам выбирать свою команду и спавниться
- Обеспечить их оружием, целями и инструкциями (возможно, взятыми из игровой карты, см. ниже)
- Определить правила игры, напр., когда кончается раунд, что происходит при смерти игрока
- .. и, может быть, что-то еще
- buymenu.lua — клиентский скрипт, создающий меню для покупки оружия
Этот образец может быть назван модом, так как не только влияет на игровой процесс, но, по сути, и задает его рамки. Атрибут type говорит о том, что этот пример работает с Map Manager, уже другим ресурсом, написанным QA Team для управлениями модами и подгрузки карт. Очень рекомендуется основывать свои моды на предоставляемом им функционале.
Это также означает, что мод, возможно, не запустится без карты. Моды всегда должны пользоваться общим функционалом настолько широко, насколько это возможно. Образец карты — в следующем примере.
Третий пример — Карта
/cs-airport /meta.xml /airport.map /airport.lua
<meta> <info author="Someguy" description="Counterstrike airport map" type="map" gamemodes="counterstrike" /> <map src="airport.map" /> <script src="airport.lua" /> </meta>
- airport.map — XML-файл, предоставляющий моду информацию о карте, что включает в себя:
- Где игроки должны спавниться, с каким оружием, какие имеются команды
- Какие имеются цели
- Погода, время, ограничение по времени
- Предоставляемый транспорт
- airport.lua может содержать присущий данной карте функционал, что включает в себя:
- Открытие каких-либо дверей, подрыв чего-нибудь при определенных условиях
- Создание или передвижение определенных игровых объектов, или управление теми, что были созданы через .map-файл
- .. все что еще угодно, связанное с картами
Как вы уже заметили, атрибут type поменялся на ‘map’, сообщая Map Manager, что этот ресурс — карта, в то время как атрибут gamemodes говорит, с какими модами эта карта совместима, в данном случае — это мод из примера выше.
Сюрпризом может показаться то, что в ресурс-карту также входит и скрипт. Конечно, это совсем не обязательно для карты, но открывает широкий спектр возможностей для их создателей, позволяя создавать собственный мир с правилами мода, на котором он основывается.
Файл airport.map может выглядеть примерно так:
<map mode="deathmatch" version="1.0"> <terrorists> <spawnpoint posX="2332.23" posY="-12232.33" posZ="4.42223" skins="23-40" /> </terrorists> <counterterrorists> <spawnpoint posX="2334.23443" posY="-12300.233" posZ="10.2344" skins="40-50" /> </counterterrorists> <bomb posX="23342.23" posY="" posZ="" /> <vehicle posX="" posY="" posZ="" model="602" /> <vehicle posX="" posY="" posZ="" model="603" /> </map>
Когда мод запускается с картой, ресурс-карта автоматически запускается mapmanager’ом, и информация, которую он содержит, может быть прочитана ресурсом-модом. При смене карты, текущий ресурс-карта останавливается, а следующий — запускается. Для более детального разъяснения и образцов того, как ресурсы-карты используются основным скриптом, посетите страницу RU/Writing Gamemodes.
События
События — способ MTA сообщать скриптам о происходящем. Например, при смерти игрока, срабатывает событие onPlayerWasted. Чтобы при смерти игрока что-то происходило, вам придется проделать действия, схожие с добавлением обработчика команд, как об этом рассказано в первом разделе.
Этот пример будет выводить сообщение с именем игрока, который умер:
function playerDied(totalAmmo, killer, killerWeapon, bodypart) outputChatBox(getPlayerName(source).." умер!") end addEventHandler("onPlayerWasted",getRootElement(),playerDied)
Вместо того, чтобы сначала вывести список требуемых аргументов, страница документации для событий отображает, какие параметры передаются функции-обработчику, так же, как делает обработчик команд, просто это разнится от события к событию. Другим важным моментом является существующая в функциях-обработчиках переменная source. Ее необязательно добавлять в список параметров функции, но она, тем не менее, существует. Ее значение меняется от события к событию, для событий, связанных с игроком (как в образце выше), это — элемент типа player. В качестве другого образца служит базовый скрипт для респавна игрока в первом разделе, на его примере можно понять, как используется source.
Что делать теперь
Теперь вы знакомы с наиболее базовыми аспектами скриптинга в MTA, а также чуть-чуть с документацией. Главная страница обеспечит вас ссылками на множество различной информации, руководства и указания, которые позволят глубже взглянуть на интересующие вас темы.
Note: Теперь мы рекомендуем вам прочитать руководство по отладке. Умение хорошо отлаживать — абсолютная необходимость при написании скриптов. Мы также рекомендуем вам пользоваться списком предписанных переменных, который поможет вам в выполнении определенных задач, а писать скрипты станет намного легче и быстрее. |
Также смотрите:
- Продвинутый функционал
Введение
Lua — это скриптовый язык программирования такой. Быстрый, легкий и удобный.
Изначально Lua создавался как язык программирования баз данных. Фактически, все программирование на Lua сводится к различным манипуляциям с таблицами. Таблицы — это краеугольный камень философии Lua. Таблица — это набор данных, где каждому уникальному ключу соответствует значение. Простой синтаксис и легкость встраивания Lua в приложение обеспечили Lua широкое распростанение в среде игроделов (откуда родом я сам). Во многих игровых компаниях при приеме на работу знание Lua уже MUST.
Теперь небольшой вводный курс в Lua.
Типы данных
nil — ничего, означающее отсутствие какого либо значения
boolean — булевская переменная, может принимать значения true или false
number — число
string — строка
function — функция (да-да, это таже тип данных!)
userdata — специальный тип данных, позволяющий хранить в Lua данные из С (фактически это указатель void*)
thread — сопрограмма Lua (позволяет организовать превдо-многопоточность)
table — таблица — ассоциативный массив (набор пар ключ-значение), причем в качестве и ключа и значения может выступать любой тип данных (угу, и функция тоже может быть ключом!)
Lua — язык с динамической типизацией, то есть тип переменной устанавливается не заранее, как, например, в С, а в момент присвоения переменной значения.
Примеры:
var = true — var — переменная типа boolean
var = 1 — теперь var число
var = «string» — теперь var строка
var = function(a, b) return a + b end — а теперь var функция, которая принимает два параметра и возвращает их сумму
var = coroutine.create(var) — а теперь var сопрограмма
var = {} — а тепеь var таблица
var = nil — а теперь… теперь нет var!
Два символа — означают начало однострочного комментария.
Область видимости переменных
Переменные могут быть локальными или глобальными. Локальные переменные объявляются с ключевым словом local и определены внутри блока. При выходе за пределы блока значение локальной переменной становится nil. Блоком является либо файл, либо последовательность выражений между ключевыми словами do и end.
Пример:
local x = 10 — локальная переменная х
Переменная становитя глобальной после первого присвоения ей значения.
Пример:
y = 20 — глобальня переменная y
Функции
Итак, мы выяснили, что функция является типом данных. То есть, функции можно возвращать из функций.
Пример:
function outer()
function inner()
return 1
end
return inner
end
local i = outer() — значение переменной i равно функции inner
local j = i() — вызываем функцию i, значение переменной j равно 1
Функции могут принимать несколько значений. Если при вызове функции последние значения опущены, им присваивается nil. Параметры функции являются локальными переменными внутри функции.
Пример:
function f(x, y, z) — определение функции
end
f() — вызов функции. x, y, z равны nil
f(1) — вызов функции. x = 1, y, z равны nil
f(1, 2) — вызов функции. x = 1, y = 2, z равно nil
f(1, 2, 3) — вызов функции. x = 1, y = 2, z = 3
f(1, 2, 3, 4) — вызов функции. x = 1, y = 2, z = 3, 4 лишний параметр
Функции могут возвращать несколько значений.
Пример:
function f()
return 1, 2, 3 — функция возвращает три числа
end
local x, y, z = f() — вызов функции. x = 1, y = 2, z = 3
Переменная _ используется в Lua для замены ненужной переменной.
Например, из функции f() нам нужно получить только третий параметр:
local _, _, z = f() — z = 3
«Замороженные» переменные (upvalues)
Функция может обращаться к локальным переменным, которые определены вне ее. Если при вызове функции локальная переменная уже вышла из области видимости и была разрушена, то функция пользуется тем значением внешней локальной переменной (external local variable), которое было на момент определения функции.
Пример:
local x = 10 — установили значение x
function f()
return x + 1 — на момент создания функции значение переменной x равно 10
end
x = 20 — изменили x
local y = f() — значение у равно 21, локальная переменная х продолжает существовать.
Изменим время жизни локальной переменной x:
do — начало блока
local x = 10 — установили значение x
function f()
return x + 1 — на момент создания функции значение переменной x равно 10
end
end — конец блока, переменной x больше нет
local y = f() — значение у равно 11, хотя переменная х больше не существует
Функции, кстати, тоже могут быть локальными:
local f = function(x, y, z) — да, функцию можно объявить и так, это ведь всего лишь один из типов данных.
return z, y, x — хе-хе, неплохая функция swap получилась, верно?
end
Некоторые особенности при вызове функций
Например, есть две функции:
function f1()
return 1, 2
end
function f2(x, y)
return x + y
end
Поскольку первая функция возвращает два значения, а вторая принимает два значения, то функции могут быть вызваны так:
loсal x = f2( f1() ) — значение x равно 3
Задачка посложнее:
function f1()
return 1, 2, 3
end
function f2(x, y, z)
return x + y + z
end
local x = f2(f1()) — x = 1 + 2 + 3
Теперь так:
local x = f2(4, f1()) — x = 4 + 1 + 2
Понятно, что f1 дополняет последние параметры f2 нужным числом своих параметров. Однако это работает, только если вызов f1 стоит последним в списке параметров. Что будет если вызвать функции так:
local x = f2(f1(), 4, 5) — x = 1 + 4 + 5
В этом случае из f1 будет взят только первый параметр. А если так:
local x = f2(f1(), 4) — x = 1 + 4 + nil — ошибка!
Да, третьим параметром будет nil, что при выполнении математической операции сгенерирует ошибку.
Переменное число параметров
В Lua, как и в С, в функцию можно передавать переменное число параметров. Синтаксис также похож:
function f(x, y, …)
end
Все параметры, скрытые за … Lua упаковывает в локальную таблицу arg. У таблицы arg есть поле n, содержащее число переданных аргументов. Для примера, напечатаем все переданные в функцию аргументы:
function f(…)
for i = 1, arg.n do
print(arg[i])
end
end
f(1, 2, 3, «Вася») — выведет соответственно 1, 2, 3, «Вася»
Таблицы
Таблица — это набор пар ключ-значение. Ключом в таблице может быть любое значение, кроме nil. Значением поля в таблице так же может быть любое значение, кроме nil. Cоздание таблицы:
local t = {}
Заполним ее различными типами данных:
t[1] = 10
t[«Вася»] = «осел»
t[3] = true
local function f()
return 1
end
t[f] = «функция»
t[«функция»] = f
t.me = t — тоже самое, что t[«me»] = t — ссылка на самого себя
print(t[«функция»]()) — > 1
Таблица может быть проинициализирована при создании:
local t = {1, 2, 3, x = 5, [«Вася»] = «осел»}
Массивы
Если при инициализации таблицы ключи не были указаны, то Lua сама присваивает значениям ключи, начиная с 1.
Внимание! Индексация массивов в Lua начинается с 1, не с 0!
local t = {3, 4, 5}
print(t[1], t[2], t[3]) — > 3, 4, 5
Важный момент: пока возможно, Lua внутри себя таблицу хранит как массив, а не как хэш — таблицу. Соответственно доступ к элементам таблицы происходит почти так же быстро, как в массивах Си. Поэтому без особой нужды не стоит превращать массив в хэш. Для того, чтобы не нарушать структуру при добавлении и удалении элементов массива стоит пользоваться библиотекой Lua table.
local t = {1, 2, 3, 4, 5}
table.insert(t, 6) — добавляет элемент в конец массива. Теперь t = {1, 2, 3, 4, 5, 6}
table.insert(t, 0, 1) — вставляет элемент по индексу, сдвигая оставшиеся элементы массива. Теперь t = {0, 1, 2, 3, 4, 5, 6}
table.remove(t, 3) — удаляет из таблицы элемент по индексу 3 и сдвигает оставшиеся элементы. Теперь t = {0, 1, 3, 4, 5, 6}
Получение размера массива выполняется оператором #:
local count = #t
Оператор # возвращает целое число n, такое, что t[n] не nil, и t[n + 1] равно nil. Другими словами оператор #, возвращает максимальный индекс непрерывной последовательности ключей от начала массива. Соответственно, для таблицы:
local t = {1, [100] = 2}
print(#t) — > 1, поскольку t[1] не nil, а t[1 + 1] равно nil.
Для массива, в котором значения хранятся одно за другим, оператор # вернет количество элементов в массиве.
Обход элементов таблицы
В общем случае узнать, сколько элементов хранится в таблице Lua, кроме как обойдя все элементы таблицы, нельзя. Исключение — это массивы и оператор # для определения длины массива. Массив обходится обычным циклом for:
local t = {1, 2, 3, «Вася»}
for i = 1, #t, 1 do
print(t[i])
end
— > 1
— > 2
— > 3
— > Вася
Обычные таблицы обходятся циклом for с итератором:
local t = {1, 2, x = 4, y = 5, [«Вася»] = «осел»}
for key, value in pairs(t) do
print(key, value)
end
— > 1 1
— > 2 2
— > y 5
— > x 4
— > Вася осел
Как видно из печати, порядок элементов в таблице отличается от порядка, в котором значения помещали в таблицу, поскольку внутри хэш-таблицы ключи сортируются по некоему признаку.
Методы
Рассмотрим следующую конструкцию (попытка сымитировать ООП):
local t = {x = 1}
function t:fun()
print(self.x)
end
t:fun()
— > 1
Что мы здесь сделали?
1. Создали таблицу t;
2. В таблице t завели поле x и назначили ему значение 1;
3. В таблице t завели функцию fun;
4. Внутри функции fun обратились к таблице через специальную переменную self (на самом деле self это первый скрытый параметр у функции fun. Вызывая функцию как t:fun() , мы а самом деле сделали то же самое, что и t.fun(t))
5. Вызвали у таблицы (уже почти объекта!) t метод fun;
На что все это похоже? Это похоже на ООП. Есть, объект, есть метод объекта, есть вызов метода объекта. Чего нет? Нет возможности создавать объекты определенного типа (например типа t). То есть и объект, и метод существуют в единственном экземпляре. Для того, чтобы создать объект типа tt, который ведет себя так же, как объект типа t, нам придется повторить всю процедуру с самого начала:
local tt = {x = 2}
function tt:fun()
print(self.x)
end
function checkMetod(x)
x:fun()
end
checkMetod(t)
checkMetod(tt)
—> 1
—> 2
Есть ли выход из сложившейся ситуации? Да, есть. Но о нем мы поговорим после того, как рассмотрим метатаблицы.
Метатаблицы.
Метатаблицы позволяют переопределить поведение встроенных типов. Для таблиц это можно сделать прямо в Lua, а для других типов придется это делать через С.
Метатаблицы схожи с конструкцией operator() в C++. Рассмотрим, например, как можно складывать две таблицы (в самом языке такая операция не предусмотрена).
— первый операнд
local data1 = {x = 1}
— второй операнд
local data2 = {x = 2}
— создаем таблицу
local meta = {}
— в таблице определяем специальное поле __add (оператор +)
function meta.__add(op1, op2)
return op1.x + op2.x
end
— устанавливаем метатаблицу первому операнду
setmetatable(data1, meta)
— проверяем
print(data1 + data2) —> 3
Итак, что мы сделали? Создали два объекта, каждый из которых содержит поле х. Затем создали обычную таблицу meta. В ней определили функцию со специальным именем __add. Затем установили метатаблицу первому операнду.
С тем же успехом мы могли бы установить эту метатаблицу и второму операнду. Когда Lua обнаруживает операцию над значением, то сначала ищется подходящий обработчик в метатаблице первого операнда, и если там нет, то ищется обработчик в метатаблице второго операнда. Если обработчика и там нет, то генерируется ошибка.
Вот список операций, обработчики которых могут быть переопределены в метатаблице:
Обработчик | Оператор Lua | Примечание |
---|---|---|
__add(op1, op2) | + | сложение |
__sub(op1, op2) | — | вычитание |
__mul(op1, op2) | * | умножение |
__div(op1, op2) | / | деление |
__mod(op1, op2) | % | деление по модулю |
__pow(op1, op2) | ^ | возведение в степень |
__unm(op) | — | унарный минус |
__concat(op1, op2) | .. | конкатенация (склейка) |
__len(op) | # | унарный оператор взятия длины |
__eq(op1, op2) | == | оператор «равно» |
__lt(op1, op2) | < | оператор «меньше» |
__le(op1, op2) | <= | оператор «меньше либо равно» |
__index(op, key) | [] | оператор обращения по ключу вызовется если, например, вызвать local x = data1[1] |
__newindex(op, key, value) | [] | оператор присвоения нового значения по ключу если сделать эту функцию пустой, то таблица станет «readonly», то есть в нее нельзя будет добавить новое поле со значением вызовется если, например, вызвать data1[1] = 1 |
__call(op, …) | () | оператор вызова функции вызовется если, например, вызвать data1(1, 2, 3) в обработчик __call первым параметром придет операнд (в нашем случае data1), а следующими параметрами будут те параметры, что стояли в скобках (в нашем случае 1, 2, 3). |
Как видно, все очень просто.
ООП
С помощью метатаблиц можно реализовать некое подобие объектно-ориентированного программирования.
Попробуем это сделать. Сначала создадим базовый класс (обычная таблица):
Base = {}
В ней создадим поле field:
Base.field = «text»
и две функции для работы с этим полем:
function Base:setField(value)
self.field = value
end
function Base:getField()
return self.field
end
Обратите внимание на двоеточие перед именем метода — первым параметром в метод будет передаваться self, то есть сам объект класса (см. сюда), по аналогии с С++ это this, только в С++ this передается в метод неявно.
Итак, базовый класс создан. Это шаблон, по которому должны создаваться объекты данного типа.
Создадим объект типа Base:
local base = {}
Как видно, пока это обычная таблица. Установим у этой таблицы новую метатаблицу.
setmetatable(base, {__index = Base})
Вуаля! Теперь самая обычная таблица стала объектом типа Base. Что это значит? Согласно правилам, описанным в предыдущем пункте Метатаблицы, оператор __index в метатаблице вызывается всякий раз, когда осуществляется поиск в таблице по ключу. А что есть вызовы «методов» и обращение к «членам класса», как не поиск поля в таблице?
Сначала ищутся поля в самой таблице. Если поле не найдено, в метатаблице ищется метод __index. Если __index является функцией, вызывается эта функция. Если __index является таблицей, то поиск поля осуществляется в этой таблице. Эти вызовы делаются рекурсивно до тех пор, пока метатаблица либо __index не станет равным nil. С учетом вышесказанного становится понятным, почему следующий вызов дает такой результат:
print(base:getField()) —> text
Сначала был произведен поиск поля getField в таблице base. Ничего не найдено. Далее в таблице base ищется метатаблица. Найдено. В ней ищется поле __index. Найдено. Поле __index является таблицей. В таблице __index (а это на самом деле ссылка на таблицу Base) ищется поле getField. Найдено. Это функция. Поскольку мы вызывали base:getField(), то в функцию getField первым параметром передается таблица base (см. сюда). Далее внутри функции getField повторяется аналогичный поиск для поля field. В итоге печатается первоначально заданное значение этого поля «text».
Изменим значение этого поля:
base:setField(‘aaa’)
print(base:getField()) —> aaa
Теперь мы хотим наследоваться от класса Base. Наследование выполняется аналогично созданию объекта класса Base:
Child = {}
setmetatable(Child, {__index = Base})
Переопределим в Child метод getField(), фактически, делаем этот метод виртуальным:
function Child:getField()
return ‘zzz’
end
Создадим объект типа Child:
local child = {}
setmetatable(child, {__index = Child})
print(child:getField()) —> zzz
А как же быть, если мы хотим вызвать для Child метод родительского класса? А очень просто:
child:setField(‘ooo’)
print(Base.getField(child)) —> ooo
Модули
Модули — это один из способов грамотно организовать большой проект в Lua. Это что-то типа namespace в С++.
Для начала рассмотрим наивную попытку организовать модули самостоятельно. Сделать это нетрудно. Достаточно создать файл, где будут созданы глобальные таблицы с именами модулей, и далее в программе обращаться к этим глобальным таблицам.
Например:
файл modules.lua:
Module1 = {}
Module2 = {}
файл module1.lua:
Module1.var1 = 1
function Module1.fun1(a, b)
return a + b + Module1.var1
end
аналогично файл module2.lua:
Module2.var1 = 2
function Module2.fun1(a, b)
return a + b — Module2.var1
end
файл запуска приложения main.lua:
dofile(‘modules.lua’)
dofile(‘module1.lua’)
dofile(‘module2.lua’)
print(Module1.fun1(1, 2)) —> 4
print(Module2.fun1(1, 2)) —> 1
В общем, все очевидно. Неудобств несколько — необходимость внутри файла модуля (например, в module1.lua) при обращении к переменным модуля все время ссылаться на глобальную таблицу с именем модуля (Module1.var1), вероятность засорения глобального пространства имен именами временных переменных (написав, например, вместо local x = 1 просто x = 1), ну и еще есть какие-нибудь недостатки, включая главный — рукотворность этой
схемы.
Теперь рассмотрим, что нам предлагает Lua. Модули создаются глобальной функцией module(name). Эта функция проверяет, существует ли глобальная таблица с именем name. Если нет, то такая таблица создается. Затем эта таблица помещается в глобальную таблицу package.loaded[name] = table. Функция module(name) также устанавливает в созданной таблице поля t._NAME = name, t._M = t и t._PACKAGE = полное имя модуля минус последний компонент (если имя модуля составное). Имя модуля может состоять из нескольких частей, разделенных точками. В этом случае функция module создает (или использует, если уже существуют) таблицы для каждого компонента. Например вызов module(a.b.c) создаст глобальную таблицу a, в ней таблицу b, а в таблице b создаст таблицу с. Кроме того, функция module делает еще одну важную вещь — устанавливает созданную таблицу как новый environment (окружение). Это значит, что все последующие обращения к глобальным переменным (до конца блока, обычно до конца текущего файла) будут направляться в новую таблицу.
Загрузка модулей
Чтобы модулем можно было пользоваться, его надо загрузить. Для этого нужно вызвать глобальную функцию require(name). Эта функция начинает с просмотра таблицы package.loaded, чтобы определить, модуль name уже загружен или еще нет. Если модуль name загружен, то require возвращает значение package.loaded[name]. Если же нет, то require ищет загрузчик модуля.
<Далее идет почти буквальный перевод мануала. Переводил тщательно, чтобы самому все понять :)>
Загрузчики модулей хранятся втаблице package.loaders. Каждое поле этой таблицы является функцией поиска. Когда функция require ищет модуль, то она вызывает эти функции по очереди. Функция поиска может вернуть либо другую функцию (загрузчик модуля), либо строку, которая разъясняет, почему модуль не был найден (либо nil, если ей нечего сказать). Lua инициализирует таблицу package.loaders четырьмя функциями.
Первая функция ищет загрузчик в таблице package.preload.
Вторая функция ищет загрузчик Lua-библиотек, используя пути, прописанные в package.path. Путь — это последовательность шаблонов, разделенных точкой с запятой. В каждый шаблон функция поиска будет подставлять имя модуля, а затем будет пытаться открыть файл с именем, полученным из шаблона. Например, если
package.path = «./?.lua;./?.lc;/usr/local/?/init.lua»
то для загрузки модуля foo Lua будет пытаться открыть следующие файлы (порядок поиска совпадает с порядком задания шаблонов):
./foo.lua, ./foo.lc, and /usr/local/foo/init.lua.
Третья функция ищет загрузчик С-библиотек, используя пути, прописанные в package.cpath. Например, если
package.cpath = «./?.so;./?.dll;/usr/local/?/init.so»
то загрузчик будет пытаться открыть следующие файлы (порядок сохранен):
./foo.so, ./foo.dll, and /usr/local/foo/init.so.
Первая найденная библиотека динамически линкуется с программой. Затем загрузчик пытается найти С функцию внутри библиотеки, чтобы использовать ее для загрузки. Имя этой С-функции должно быть «luaopen_» + копия имени модуля, причем в имени модуля все точки должны быть заменены на подчеркивания. Кроме того, если в имени модуля есть дефис, то весь префикс, включая знак дефис, должен быть удален. Например, если имя модуля a.v1-b.c, то имя функции должно быть luaopen_b_c.
Четвертая функция пытается использовать загрузчик «все в одном». Эта функция использует шаблоны для загрузки С-библиотек, чтобы найти библиотеку для корневого модуля. Например, для модуля a.b.c эта функция будет искать С-библиотеку модуля a. Если такая библиотека найдена, то внутри нее ищется функция для загрузки субмодулей. В нашем примере такая функция должна иметь имя luaopen_a_b_c. Такая организация позволяет упаковывать несколько субмодулей внутри одной библиотеки, используя для загрузки каждого субмодуля отдельную функцию.
Уф, насилу перевел!
Теперь быстрее к самому вкусному — ООП на модулях!
ООП на модулях
В С++ это довольно привычно, располагать каждый класс в отдельном файле (обычно в двух, ИмяКласса.h и ИмяКласса.cpp). В Lua, поверьте, это тоже удобно :).
Проект удобно организовать следующим образом:
.
..
<luamodules> — папка
<cmodules> — папка
start.lua
…
Пути к Lua-библиотекам и С-библиотекам (загрузчики) можно прописать двумя различными способами.
Первый способ:
В первых двух строчках файла start.lua написать:
package.path = ‘./luamodules/?.lua’ — пути к Lua библиотекам
package.cpath = ‘./cmodules/?.dll’ — пути к С библиотекам
Второй путь:
Использовать системные переменные.
Создать файл start.bat в котором написать следующее:
set LUA_PATH=.luamodules?.lua;
set LUA_CPATH=.cmodules?.dll
lua.exe start.lua
а в самом файле start.lua уже ничего не писать.
Итак, повторим наш учебный проект, но теперь с использованием модулей.
Первый модуль назовем Factory. Он будет создавать объекты классов и устанавливать метатаблицы.
Создадим файл Factory.lua (мы еще помним, что при поиске модуля Factory в шаблон package.path будет подставляться имя модуля?). Сохраним его в папке ./luamodules. В этом файле напишем следующее:
local base = _G
module(‘Factory’)
function setBaseClass(class, baseClass)
base.assert(baseClass.mtab)
base.setmetatable(class, baseClass.mtab)
end
function create(class, …)
local w = {}
setBaseClass(w, class)
w:construct(base.unpack(arg))
return w
end
В начале файла мы видим магическую строку
local base = _G
Зачем она? Функция module, если вспомнить вышесказанное о ней, во время своего выполнения переключает локальный контекст. Это означает, что после ее вызова, если ничего не предпринять, доступ к глобальным переменным Lua будет закрыт. Поэтому мы сохраняем специальную глобальную переменную Lua _G (_G._G = _G) в локальной переменной base. Теперь доступ к глобальным переменным осуществляется через base.имя_переменной (в нашем случае это base.assert, base.setmetatable и base.unpack).
Так, прочный фундамент мы заложили, приступим к надстройке. Создадим файл ./luamodules/Base.lua
local base = _G
module(‘Base’)
mtab = { __index = _M }
local Factory = base.require(‘Factory’)
function new()
return Factory.create(_M)
end
function construct(self)
base.print(‘Base created!’)
self.field = ‘text’
end
function setField(self, field) — метод получения значения поля field
self.field = field
end
function getField(self) — метод установки значения поля field
return self.field
end
Проясним некоторые места:
local Factory = base.require(‘Factory’)
Для работы нам будет нужен модуль Factory.
Внимание!
Для избежания ошибок с циклическими зависимостями модулей, все функции require нужно вызывать после выполнения функции module!
mtab = { __index = _M }
Вспоминаем про функцию module — _M — это ссылка на саму таблицу Base.
mtab = { __index = _M }
Таблица mtab будет установлена как метатаблица у нового объекта класса Base при вызове метода create() модуля Factory. Это означает, что при поиске полей внутри объекта класса Base, если поле будет не найдено, то поиск будет осуществляться в таблице, на которую ссылается поле метатаблицы __index (см. метатаблицы).
function new()
return Factory.create(_M)
end
Для удобства. Конечно, в прикладном коде можно вызвать и
local base = Factory.create(Base)
но мне кажется что вызов
local base = Base.new()
короче и очевидней.
function construct(self)
base.print(‘Base created!’)
end
Эта функция будет вызвана из метода create() модуля Factory. Фактически, это конструктор класса.
Все методы класса имеют первым параметром ссылку на объект класса self.
Напишем тестовый скрипт ./start.lua:
package.path = ‘./luamodules/?.lua’ — пути к Lua библиотекам
package.cpath = ‘./cmodules/?.dll’ — пути к С библиотекам
require(‘Base’)
local base = Base.new()
print(base:getField())
base:setField(1)
print(base:getField())
Запускаем его на выполнение.
lua.exe start.lua
Должны получить:
Base created!
text
1
Отлично! Двигаемся дальше. Создадим файл ./luamodules/Child.lua
local base = _G
module(‘Child’)
mtab = { __index = _M }
local Factory = base.require(‘Factory’)
local Base = base.require(‘Base’)
Factory.setBaseClass(_M, Base) — устанавливаем Base как базовый класс
function new(param1, param2) — передаем параметры в конструктор
return Factory.create(_M, param1, param2)
end
function construct(self, param1, param2) — конструктор с параметрами
Base.construct(self) — вызов конструктора базового класса
base.print(‘Child created!’, param1, param2)
end
function getField(self) — переопределяем метод
return ‘zzz’
end
Модернизируем немного ./start.lua, добавив в него строки:
require(‘Child’)
и
local child = Child.new(1, 2)
print(child:getField())
child:setField(1)
print(child:getField())
Запускаем его на выполнение.
lua.exe start.lua
Должны получить:
Base created!
text
1
Base created!
Child created! 1 2
zzz
zzz
Завершающие штрихи. Как создасть статический член класса? Очень просто. Внутри модуля объявить его либо как
member = 1
либо (предпочтительнее, поскольку более очевидно чего вы хотите)
_M.member = 1
Соответственно, статический метод класса будет без первого параметра self:
function staticFun(param1, param2)
end
Здравствуйте. В этом уроке мы познакомимся с таким программированием, как Lua. Данный учебник будет написан на простом и понятном языке, чтобы не усложнять жизнь начинающим скриптерам. В введении в курс Lua я буду сравнивать аналогичные функции с SA-MP Pawn.
Введение в скриптинг MTA Lua
Содержание:
1) Client’n’Server Sides (2 стороны сервера – C-Сторона и S-Сторона) – Введение
2) Иерархическое строение сервера (meta.xml)
3) Администрирование (ACL)
4) Аргументы игрока (source, thePlayer, getLocalPlayer())
5) Вывод (Окончание учебника)
6) Дополнительный материал
Введение
Наша статья прямо связана с сетевой игрой для GTA San Andreas – Multi Theft Auto. Начну с того, что в MTA Lua вам предоставлены 2 стороны – Server (связана с функциями и действиями в основном с игроками и игровым миром) и Client (связана с функциями и действиями, не связанными с игроком). На S-Стороне пишут в основном системы, которые непосредственно связаны с игроком, например система команд. На C-Стороне пишут диалоговые окна (GUI), изображения на экран (dxDraw). Про dxDraw могу сказать то, что он немного похож на TextDraw из SA-MP, но имеет функцию загрузки любого выбранного вами изображения (на примере рабочего стола в моей системе Lunix OS).
Иерархическое строение сервера (meta.xml)
Сервер МТА имеет иерархическое строение, а именно один каталог принадлежит другому, а тот в свою очередь принадлежит третьему. Главным каталогом является папка или архив с названием ресурса (не имеет значения, как её назвать) в папке /MTA San Andreas Ver/server/mods/deathmatch/resources/
в котором в свою очередь обязательно должен находится файл meta.xml, где хранятся данные о ресурсе (тип, название ресурса, автор, версия, дочерние файлы (изображения, модели, текстуры, lua файлы ресурса, и другое)).
В МТА присутствует функция отделения карты/ресурса от главного запущенного (им является ресурс с пометкой «Gamemode» в файле meta.xml), следовательно, можно выгружать и загружать ресурс или карту прямо на сервер, не перезагружая при этом сервер.
Так-же тут присутствует возможность использовать функции из других ресурсов, например есть 2 ресурса – «admin» (система администрирования) и «TvoyRes». Вашим ресурсом является «TvoyRes». Вы можете прямо использовать функции из ресурса «admin», ничего не подгружая (кроме директории ресурса в meta.xml)
Администрирование (ACL)
Поговорим об администрировании сервера MTA.
В МТА администратор ресурса зовётся resourceRoot (некоторая связь с администрированием систем Linux). Главными resourceRoot’ами являются командная строка запуска сервера, и игровая консоль (вызывающаяся на символ «~» при английской раскладке, или клавишей F8). Игровая консоль от командной строки сервера отличаются тем, что в консоли можно вводить команды ресурсов (в командной строке нельзя), и входить в режим resourceRoot в консоли не нужно.
Давайте сравним типы администрирования Rcon (SA-MP) и resourceRoot, или просто Root (MTA):
— В SA-MP Rcon может войти любой игрок, зная определённый единственный пароль для входа.
— В МТА Root могут войти только зарегистрированные на сервере пользователи, имеющие привилегии, указанные в специальном файле.
Это показывает то, что в МТА по стандартным установкам сервера последних версий уже имеется система аккаунтов, нежели в SA-MP, которую, если нужно, можно написать.
В МТА так же имеется система уровней администрирования:
— Everyone (Default) – зарегистрированные аккаунты, они сюда после регистрации не попадают, их нужно записывать самому.
— Moderator – модератор, с ограниченным набором команд.
— SuperModerator – модератор, имеющий больше привилегий.
— Admin – администратор, который имеет доступные привилегии модератора и супер-модератора с добавлением некоторых полномочий.
— RPC – связан с доступом в интернет, имеет только функцию callRemote, которая связана с загрузкой деталей из интернета.
— MapEditor – имеющий доступ к редактированию карты (если загружен скрипт редактирования (с сервера, не имеющий отношения к Map Editor в меню игры))
— raceACLGroup – многие знают, что МТА с начала существования начинала с серверов, связанных с гонками (нежели SA-MP был полон DM серверов), эта группа связана с администрированием Race ресурсов (думаю данная группа не важна).
— Console – resourceRoot всего сервера, имеющий все полномочия вместе взятых групп.
По сравнению с SA-MP, в МТА в сервер встроены команды для администраторов.
Использование функций ACL (администрирования) в скриптинге выглядит не так просто, потому что для того, чтобы найти администратора на сервере через скрипт, нужно ввести поиск полномочий пользователя. Например, функция поиска объекта, имеющего доступ к чему-либо (только S-Сторона):
hasObjectPermissionTo
(object, what_is_to, bool)
И функция поиска объекта в ACL группах (поиск по группам администрирования) (только S-Сторона):
isObjectInACLGroup
(object, acl_group)
Рассмотрим эти функции на примере:
if
hasObjectPermissionTo
(source,
“function.addBan”
,
true
)
then
ВАШ КОД
end
Эта функция проверит, доступна ли у игрока source доступ к функции addBan (доступен администраторам и выше)
if
isObjectInACLGroup
(
“user.”
..
getPlayerName
(source),
aclGetGroup
(
“Admin”
))
then
ВАШ КОД
end
Данный код найдёт пользователя в группе Admins. Так как в файле администраторов игрок прописывается через user.name, то и поиск его тоже надо сделать через user. Функция getPlayerName покажет имя игрока, и, следовательно, исходный запрос будет выглядеть как user.name. Функция aclGetGroup ищет по группе администрирования, указанной в аргументе. Важно вводить группу такой, какая она есть (Admin, SuperModerator, Console, MapEditor), соблюдая регистр.
Это конечно выглядит и понимается сложней, чем то же самое в SA-MP:
if
IsPlayerAdmin
(playerid){ КОД }
return
1;
Аргументы игрока (source, thePlayer, getLocalPlayer())
Раз дело пошло о аргументе Source, расскажу о нём.
В МТА есть несколько аргументов на поиск игрока. Самым основным является source – он доступен для обеих сторон (S- и C- сторон), и определяет любого «раздражителя» (например, если объект выполняет действие с source, то в выполнении source’ом является именно этот объект, но не какой-либо левый игрок). Так же присутствует аргумент thePlayer, определяющий игрока, работает в S-Стороне. Ещё присутствует функция getLocalPlayer(), определяющая игрока в пределах C-Стороны.
Вывод (Окончание учебника)
На этом я заканчиваю данный урок. В заключении я добавлю, что МТА Lua является упрощённым языком, и пишется проще, чем SA-MP Pawn, но данный язык усложнён своими функциями, но именно они упрощают вид языка.
Дополнительный материал
Для тех кто думает переходить с Pawn в Lua (я всеми руками за) я могу предложить переменную, которая оставит вам привычный playerid.
Для S-Стороны:
playerid = source
Для C-Стороны:
playerid =
getLocalPlayer
()
Данный код можно писать в начале ресурса
Как редактор ресурсов, советую вам использовать MTA Script Editor.
Обновлено: 05.03.2023
Это руководство призвано исполнить две цели: обучить разработке скриптов с нуля и восполнить все пробелы в знаниях о Lua скриптинге под MoonLoader. В нём освещены все аспекты разработки — от самого простого до продвинутых возможностей и приемов, то есть оно подходит как и для начинающих скриптеров, так и для программистов с опытом. Предварительное прочтение старого руководства не требуется, здесь есть вся необходимая и более актуальная информация.
А если вы всё ещё не знакомы с MoonLoader, то сейчас самое время ознакомиться.
Ну что, приступим?
Перед продолжением необходимо установить сам MoonLoader, желательно через программу установки и с дополнительными инструментами:
1. Скачайте установщик MoonLoader последней версии и запустите, следуйте шагам программы установки
2. На странице выбора компонентов отметьте нужные скрипты (рекомендуется выбрать все)
3. Если вы намерены делать скрипты для SA:MP, выберите SAMP.Lua и отдельно установите SAMPFUNCS
4. Это не обязательно, но не помешает установить и MoonAdditions — эту библиотеку используют некоторые скрипты и она неплохо расширяет стандартные возможности скриптинга
5. Выберите установку расширения для Notepad++, если вы будете использовать программу Notepad++ для редактирования Lua скриптов
Для лёгкой и удобной работы с кодом Lua скриптов вам понадобится настроить для себя среду разработки. Для работы с Lua достаточно любого текстового редактора, но какой-нибудь блокнот Windows для этого подходит совсем плохо, поэтому лучше использовать специализированные программы. На текущий момент полная поддержка MoonLoader есть в Atom и Notepad++, помимо этого есть пользовательские дополнения для Visual Studio Code и Sublime Text.
После установки этого пакета вам будет предложено установить дополнительные пакеты, соглашаемся и забываем про это.
Проект по-умолчанию.
В меню File выбираем пункт Open Folder. и указываем путь до папки moonloader, после этого она откроется как проект.
Кодировка по-умолчанию.
Для установки кодировки по-умолчанию при создании нового скрипта переходим в настройки всё тем же сочетанием Ctrl + , и выбираем пункт Core. В поле File Encoding выбираем Cyrillic (Windows-1251).
Проект по-умолчанию.
Как и в Atom, здесь есть возможность показа меню проекта, а точнее «Папка как Проект». В меню «Файл» выбираем пункт «Открыть Папку как Проект» и указываем путь к папке «moonloader».
Кодировка по-умолчанию.
Над лентой выбираем пункт Опции и переходим в Настройки. В меню слева выбираем пункт Новый документ и в разделе кодировки ставим флажок на список, в котором выбираем кодировку Windows-1251
MoonLoader основан на языке программирования Lua, знание хотя бы основ которого обязательно для дальнейшей работы. Поскольку Lua очень популярен в среде разработки игр и других сферах, по нему полно учебных материалов. Ниже приведено несколько ресурсов, позволяющих изучить Lua от корки до корки. Не стоит пренебрегать этими уроками даже более опытным разработчикам, зачастую можно найти для себя много нового. Имейте в виду, что в MoonLoader используется LuaJIT — Lua 5.1 с некоторыми фичами Lua 5.2 и своими дополнениями, поэтому некоторые мелочи из этих материалов будет неприменимы в MoonLoader.
После установки среды разработки, изучения основ Lua и ознакомления с документацией MoonLoader, можно приступать от теории к практике. Давайте сначала рассмотрим самое основное, на всякий случай.
Глобальная область
Глобальная область — это основное тело скрипта, т.е. всё, что находится вне функций. В основном глобальная область используется для указания директив, подключения модулей, объявления каких-либо глобальных переменных и функций. Она выступает первым этапом загрузки скрипта, код из неё выполняется один раз после загрузки скрипта (а скрипты загружаются почти сразу же после запуска игры) и не может быть приостановлен. Основная работа со скриптом производится в потоке main.
Пример: Загружаем библиотеку VKEYS, записываем моё имя в переменную myName, объявляем функцию main.
Задержки и потоки
Задержка в скрипте позволяет приостанавливать исполнение кода на указанное количество миллисекунд или на один кадр. В ML для осуществления задержек используется функция wait(int time), её аргумент time принимает следующие значения:
-1 — устанавливает непрерывное ожидание, использование допустимо только в main
0 — задержка на один кадр
остальные значения указывают задержку в миллисекундах
Пример: Запущенный скрипт будет в вечном ожидании
Использование задержек недопустимо в callback-функциях. Для создания задержки в такой функции используйте создание потока:
Потоки — это сопрограммы, которые могут быть созданы в процессе работы скрипта для параллельного выполнения задач. Потоки в MoonLoader служат для выноса определенных действий за рамки основного потока, а так же для создания задержек выполняемого кода там, где это невозможно реализовать стандартными методами.
Для запуска потока используются функции lua_thread.create и lua_thread.create_suspended.
Пример: создаётся два потока для параллельного вывода текста в лог
Директивы
Скрипты для MoonLoader могут содержать о себе некоторую информацию и иметь определённые свойства исполнения — и то, и другое задаётся с помощью директив. Директивы — это обычные функции, предназначенные для указания информации о скрипте и изменения его поведения. Их принято обозначать в самом начале скрипта.
Все параметры, задающиеся директивами, можно получить из любого скрипта, обратившись к соответствующим полям класса LuaScript.
Пример:
В этом примере показаны не все директивы, за полным списком обращайтесь к соответствующей странице на вики.
Колбэки (функции обратного вызова) выступают реакцией на действие и по концепции очень похожи на события, но у них есть два отличия: первое — колбэк всегда регистрируется явно, чаще путём вызова функции с передачей функции-колбэка в качестве аргумента, второе — он всегда связан с какой-либо сущностью (командой, идентификатором и т.п.), т.е. будет вызван только если возникшее событие касается связанной сущности (например, колбэк команды будет вызван при вводе только одной команды, а не каждой, как это было бы с событием). Примером функции с колбэком является downloadUrlToFile, принимающая последним аргументом callback-функцию.
В колбэках, как и в событиях, нельзя использовать задержки.
Пример:
Использование библиотек
Библиотеки, либо модули всячески дополняют стандартный набор возможностей новыми и позволяют использовать в скриптах готовые инструменты для разработчиков. Модули делятся на стандартные и сторонние. Стандартные включены в дистрибутив MoonLoader и не требуют отдельной установки.
Все модули располагаются в директории moonloader/lib/ и устанавливаются туда же. Подключение библиотек осуществляется при помощи функции require, которая в качестве аргумента принимает название файла. Чаще всего подключение модулей производится в начале скрипта, но может быть выполнено в любом месте. Каждый модуль загружается отдельно для каждого скрипта.
Пример: Подключим модуль «vkeys», позволяющий работать с виртуальными клавишами.
Работа с модулями не ограничивается стандартным набором, часто приходится иметь дело со сторонними модулями — такие модули не поставляются вместе с MoonLoader и требуют ручную установку. Примерами таких модулей являются Dear ImGui и SAMP.Lua.
Вы можете создать собственный модуль и использовать его в своих скриптах. Благодаря этому вам будет легче оказывать им поддержку, скрипты станут чище и компактнее, а повторяющегося кода будет намного меньше.
Помимо этой возможности в MoonLoader присутствует система импорта, позволяющая использовать работающий скрипт как модуль с общим доступом — об этом и о создании модулей будет сказано позже.
Настоятельная рекомендация: никогда не публикуйте свои работы вместе со стандартными библиотеками или с изменениями в сторонних библиотеках — это может привести к проблемам у пользователей.
Несмотря на обилие информации в теме, знать всё невозможно, поэтому чаще заглядывайте на Wiki, а также не забывайте о теме Вопросы на Lua скриптингу, где вам смогут помочь при возникновении сложностей.
Больше информации именно по Lua лучше искать в поисковиках, язык довольно простой и если эта тема вам никак не помогла — стоит поискать более углубленные уроки.
- Добавляйте информацию о скрипте при помощи директив
- Соблюдайте единый стиль кода
- Соблюдайте табуляцию (отступы)
- Отделяйте блоки кода логически: пустые строки между функциями и блоками переменных, пробелы между блоками кода, осуществляющими логически завершённое действие и т.д.
- Называйте переменные и функции внятными именами
- Комментируйте неочевидные участки кода
Современные решения
С момента релиза ML прошло уже довольно много времени и, конечно, многое поменялось, так, например, вместо предопределенных переменных playerPed и playerHandle стоит использовать PLAYER_PED и PLAYER_HANDLE соответственно.
Помимо стандартного рендеринга, для создания сложных меню можно использовать фреймворк Dear ImGui. Для удобной обработки сетевого трафика SA:MP есть библиотека SAMP.Lua. Библиотека SA Memory для прямого взаимодействия со структурами игры. И MoonAdditions, добавляющая множество интересных функций.
vkeys — стандартный модуль, хранящий все ID и имена виртуальных клавиш. Так уж вышло, что этот модуль изначально не входил в состав MoonLoader и вместо него все константы загружались из модуля moonloader глобально, но со временем выяснилось, что это было плохим решением и поэтому коды клавиш были перенесены в отдельный модуль с немного другой реализацией. Но константы в старом модуле пришлось оставить для совместимости со старыми скриптами и теперь их использование оттуда считается устаревшим. Библиотека vkeys тут приведена в качестве примера, кроме неё были и другие нововведения, приведшие к устареванию старых решений.
Поэтому, если вы занимаетесь активной разработкой, всегда обращайте внимание на список изменений в обновлениях и пользуйтесь новейшими инструментами.
Упрощение процесса установки скриптов
«Да закинь вот эти файлы в папку CORE в папке SOURCE та что в папке с либами где под папкой IT хранится SCORE» — Бррр, чтобы подобное не случалось и ваш собеседник не впадал в ступор, старайтесь упростить установку до максимума — чтобы можно было просто скопировать все составляющие мода в одну папку. То есть соберите один архив так, чтобы неопытный пользователь мог свободно его установить или приложите инструкцию, если процесс установки сложнее стандартного. Чем установка проще, тем лучше и для вас, и для пользователя.
Компиляция скриптов
Во многих других языках программирования выполнение компиляции необходимо для запуска приложения на целевой машине, но в Lua компиляция не требуется — скрипты загружаются из исходного кода без дополнительных манипуляций. Однако, компиляция Lua скриптов возможна и чаще всего применяется для сокрытия исходного кода от любопытных глаз. Чаще всего это применяют для продаваемых скриптов, где защита этого самого скрипта — дело первостепенной важности. Не стоит злоупотреблять этой возможностью и прятать каждый свой скрипт под замок.
Для компиляции Lua скриптов под MoonLoader v.026 и выше скачайте интерпретатор LuaJIT v2.1.0-beta3, распакуйте архив в любое место и перетаскивайте lua-файл на compile.bat, рядом создастся luac-файл — это и есть скомпилированный скрипт. Для компиляции скриптов под более старые версии MoonLoader, вам понадобится LuaJIT 2.0.4.
С основными принципами разработки вы теперь знакомы и при этих знаниях сможете выполнить большинство задач, однако некоторые задачи требуют применения специальных техник. Давайте рассмотрим некоторые из них.
Создание модулей
Модули делятся на два типа: Lua и DLL. Lua-модули пишутся, как вы уже могли догадаться, на языке Lua и в результате представляют из себя привычные Lua-скрипты, только с некоторыми особенностями.
Давайте рассмотрим пример простого модуля, назовём его example:
TheChampGuess | Уроки Lua SAMP
Коля Руснак
TheChampGuess | Уроки Lua SAMP запись закреплена
Hellllloooo
Дарья Щур
Александр Адаев
Николай Фролов
TheChampGuess | Уроки Lua SAMP запись закреплена
Сам скрипт будет прикреплен к посту, а также в документе. Кроме того, вы можете ознакомиться с базовой версией функции, с которой и была написана оптимизированная версия текущего скрипта. За предварительную оптимизацию спасибо главному администратору данного сообщества — Vlad Tsurkan
Пока что не знаю, буду ли снимать видео по данному скрипту или нет, если что пишите в комменты. Скрипт имеет функционал включения/выключения, а также смены пола персонажа для отыгровок.
Діма Колодич
Как писать луа скрипты самп
Обучение Lua — Руководство для начинающих по написанию скриптов
Изучайте основы как работает Lua
Это первое руководство в этой серии.
1,617 | уникальных посетителей |
37 | добавили в избранное |
Это руководство предназначено для тех, у кого ограниченный опыт работы с LUA. Мы рассмотрим основы того, как оформлять код, строительные блоки для Вас, чтобы создавать более сложный код и предоставим некоторые примеры. Руководство написано так, чтобы сразу применять его на практике. Поэтому Вам следует открыть Tabletop Simulator и Ваш редактор LUA, чтобы следовать дальше.
Это первое руководство в этой серии. Второй – это Изучение Lua Подробнее. Третий представляет собой набор полезных функций под названием Learning Lua Functions.
Когда Вы сохраняете свои скрипты в Tabletop, он будет использовать Ваше последнее сохранение, а затем загрузит в него скрипты. Поэтому для любого скрипта, который Вы намереваетесь написать, Вам нужно будет сделать следующее:
- Подготовьте стол так, как Вы этого хотите.
- Сохраните стол.
- Загрузите стол.
Не забудьте сохранить/загрузить, а затем открыть скрипт в Atom или перейти в Host>Scripting в Tabletop Simulator, чтобы начать.
Global.lua — это скрипт, который является частью файла сохранения. Именно здесь мы будем работать над большей частью этого руководства. При новом сохранении он всегда начинается с некоторого текста, сохраненного в редакторе. Просто удалите его, мы его не будем использовать.
Также можно писать скрипты и прикреплять их к объектам вместо Global. Таким образом, если Вы сохраните объект, то сохраниться и его LUA скрипт. Вы можете выполнять большинство функций с использованием скрипты Global или объекта, но мы будем работать в Global.
Часто используемая функция, встроенная в Tabletop Simulator — onload(). Эта функция запускается каждый раз при загрузке скрипта (например, если нажата кнопка Отменить(Undo) / Повторить(Redo), а также во время загрузки сохранений).
Замечу, что все переменные вне функций также инициализируются всякий раз при загрузке скрипта.
Итак, давайте начнем с его использования, чтобы запустить функцию, которую мы создадим. Функции должны начинаться с строчной буквы и не содержать пробелов. Мы будем использовать exampleFunction.
Теперь наш скрипт, когда он загружается, попытается запустить функцию с именем exampleFunction. Но мы еще не писали! Итак, теперь мы создадим нашу собственную функцию сразу после завершения функции onload.
Extra Credit: Когда Вы создаете свою собственную функцию, Вы также можете передавать переменные вместе с ней для их использования функцией. Другой способ написать наше начальное упражнение:
function onload() exampleFunction(‘Hello, World.’) end function exampleFunction(passedString) print(passedString) end
Мы создали переменную для представления строки (passString), а затем напечатали то, что содержалось в этой переменной.
Чтобы повлиять и на объект, сначала мы должны идентифицировать его в LUA. Существует несколько способов сделать это, например, идентифицировать элементы, которые подбираются или отпускаются игроками, находить объекты в скриптовых зонах и многое другое. Мы будем идентифицировать эти объекты по их GUID.
GUID – это уникальный идентификатор, который имеет каждый сгенерированный элемент в игре. Даже два одинаковых элемента будут иметь разные GUID. Чтобы найти GUID объекта, щелкните по нему правой кнопкой мыши и перейдите к Scripting. Если Вы нажмете на GUID, он скопирует его в буфер обмена. GUID всегда является строкой, поэтому не забывайте, что строки всегда в кавычках. Давайте создадим несколько переменных с идентификаторами GUID наших объектов. ОБРАТИТЕ ВНИМАНИЕ: Ваши идентификаторы GUID будут отличаться от моих.
object1_GUID = ‘195868’ object2_GUID = ‘333365’ checker_GUID = ‘7dc60d’
Затем, создадим переменные для представления наших объектов. Используем функцию onLoad(), чтобы создание происходило при загрузке скрипта. Все эти имена переменных, которых мы делали, должны начинаться со строчной буквы и не содержать пробелов, но, кроме этого, Вы можете свободно сами составлять имена переменных. Используйте такие имена, чтобы было понятно, что объект из себя представляет. Я буду использовать object1, object2 и checker для представления моих Объектов. Функция, которую мы будем использовать для идентификации, будет getObjectFromGUID(строка). Мы помещаем GUID в место для строки.
function onload() object1 = getObjectFromGUID(object1_GUID) object2 = getObjectFromGUID(object2_GUID) checker = getObjectFromGUID(checker_GUID) end
Теперь нам нужно каким-то образом манипулировать этими объектами. Мы дадим им имена. В onload() после определения наших объектов мы будем использовать функцию setName(string). Обратите внимание, что setName, как и другие функции объекта, должна быть привязана к объекту. В противном случае скрипт не поймет, имя какого объекта мы хотим изменить. Строкой в setName будет то, что мы установили для имени.
object1.setName(‘Object1’) object2.setName(‘Object2’) checker.setName(‘That Stupid Checker’)
Если бы вы хотели, то нет причин, по которым вы не могли бы написать для шашки это, так:
function onload() getObjectFromGUID(‘7dc60d’).setName(‘That Stupid Checker’) end
Причина, по которой я не одобряю это ученикам является частично эстетическим выбором, и частично для ясности чтения кода. Вы хотите, чтобы кому-то было легко понять Ваш код, и как только вы начнете делать что-то более сложное, чем изменение имени Объекта, может становиться ОЧЕНЬ трудно понять, что происходит. Это улучшает восприятие кода при внесений правок в будущем.
- click_function = Строка —Имя вызываемой функции.
- function_owner = Объект —Объект или Global, где находится функция.
- label = Строка —Название на кнопке.
- position = Таблица —Координаты X, Y и Z, для которых отображается кнопка, от центра объекта, к которому он присоединен.
- rotation = Таблица —Поворот по осям X, Y и Z в градусах относительно объекта, к которому он привязан.
- width = Число —Ширина кнопки по отношению к масштабу объекта.
- height = Число —Высота кнопка по отношению к масштабу объекта.
- font_size = Число —Размер шрифта на кнопке относительно масштаба ее объекта.
- scale = Число —Масштаб кнопки по отношении к ее объекту.
Таблицы в LUA — это динамический гетерогенный ассоциативный массив, то есть множество пар (ключ-значение). Вы можете хранить все что угодно внутри таблицы и ссылаться на нее позже в таблице по имени или по номеру индекса (в LUA нумерация индексов в таблице начинаются с 1). Все таблицы обозначаются фигурными скобками <>. Мы создадим таблицу прямо под тем, где мы установили наши GUID, а затем заполнили ее записями для использования с функцией createButton(table). Название, которое мы выбираем для нашей таблицы это button_parameters.
button_parameters = <> button_parameters.click_function = ‘buttonClicked’ button_parameters.function_owner = nil button_parameters.label = ‘Press Me’ button_parameters.position = <0,0.8,0>button_parameters.rotation = button_parameters.width = 500 button_parameters.height = 500 button_parameters.font_size = 100
Теперь мы имеем параметры, то есть таблицу со значениями. Используем функцию объекта, чтобы создать кнопку на шашке. Запишите это внутри функции onload().
function buttonClicked() print(‘Learning is fun. Sort of.’) —Обучение — это весело. Вроде. end
Повторно нажимайте его, потому что, конечно, вы это сделаете.
button_parameters = < click_function=’buttonClicked’, function_owner=nil, label=’Press Me’, position=<0,0.8,0>, rotation=, width=500, height=500, font_size=100 >
EXTRA CREDIT: Это идеальный момент для начала игры с разными вещами, которые вы можете делать с объектами. Перейдите на страницу «Объект» в Knowledge Base и попробуйте материал. Двигайте объекты, заставляйте их переключаться на позиции, менять их цвета, что бы вы ни думали.
EXTRA CREDIT: Кроме того, при каждом нажатии кнопки функция click_function запускается с двумя параметрами. Первая — это ссылка на объект, в частности ссылка на объект, к которому привязана кнопка. Второй — это цвет (например, «Blue» — синий) в строчном формате цвета игрока, который нажал на кнопку.
if CONDITION then —Активируется если условие было истинно end
Вы также можете добавить к нему «else», так что если утверждение ложно, вместо этого происходит что-то ДРУГОЕ (else). Обратите внимание, что я добавил комментарии, используя два минуса подряд. Компилятор игнорирует что-либо на линии после —.
if CONDITION then —Активируется если условие было истинно else —Активируется если условие было ложно end
Мы попробуем несколько из них. Удалите текущее содержимое в Вашей функции buttonClicked(). Теперь введите в эту функцию следующие утверждения:
if 5 > 6 then print(«5 is greater than 6») —5 больше, чем 6 end if 6 > 5 then print(‘6 is greater than 5’) —6 больше, чем 5 end if 5 == 0 then print(«Five is equal to ZERO?!») —Пять равно НУЛЮ?! else print(«No, five isn’t equal to zero.») —Нет, пять не равно нулю. end
Еще раз удалите все скрипты внутри функции buttonClicked(). Мы собираемся создать новую переменную, а затем изменить ее. Новая переменная будет булевского типа. Булевские значения могут быть только true, false. Булевские значения всегда записываются маленькими буквами. Во-первых, мы создадим нашу переменную под нашим идентификатором GUID объектов и шашки.
Затем, в buttonClicked, мы установим некоторую логику, чтобы проверить, истинно ли значение trueOrFalse. Если оно истинно, то будет печатать, что это Истина, и переключит его на Ложь. Если кнопка снова нажата, будет печатать, что это Ложь, и переключит значение на Истина.
if trueOrFalse then print(‘trueOrFalse was true.’) —trueOrFalse была истина. trueOrFalse = false else print(‘trueOrFalse was false.’) —trueOrFalse была ложна. trueOrFalse = true end
Мы могли бы также написать это так «if trueOrFalse == true then», но это необязательно. Помните, что оператору IF нужно передать булевское значение. И так как trueOrFalse уже является одним из таких, мы можем отпустить «== true».
Цикл — это секция кода, которая могут запускаться несколько раз. Это один из более сложных элементов, которые Вы будете использовать в LUA. Они часто идут со таблицами, позволяя запускать код для каждой записи в таблице.
Какой вывод будет после нажатия на кнопку:
Какой вывод будет после нажатия на кнопку:
Это ещё один тип – ipairs. Pairs нужны для таблиц без числовых ключей, а ipairs нужны для таблицы с последовательными числовыми ключами (массивы). ipairs идет по порядку, когда pairs может идти в любом порядке.
Чтобы написать скрипт непосредственно в объект, щелкните на него правой кнопкой мыши, перейдите в Scripting и выберите Lua Editor (если Вы используете Atom, это откроет для него окно в Atom).
Когда вы пишете код здесь, это похоже на global. За исключением случаев, когда Вам нужно ссылаться на объект, частью которого является скрипт, вы просто пишете «self». Чтобы создать кнопку на себе, вы должны использовать self.createButton(table_of_paramiters).
Надеюсь, что это введение в LUA помогло Вам лучше понять некоторые основные механики написания скриптов.
Помните, что в Knowledge Base есть информация обо всех функциях, которые входят в состав Tabletop Simulator. Это и базовая практика с if, else, then и for позволит Вам выполнить большинство всего, что Вы хотите. Удачи.
[Samp gamemode creation] Обзор языка программирования PAWN (часть 1)
Итак, сегодня я буду рассказывать как я пишу мод на SAMP. Для начала разберёмся на каком языке пишут скрипты и моды для SAMP. Моды и скрипты в SAMP пишутся на языке PAWN. Pawn — это С-подобный скриптовый язык (как и lua) но, в отличии от lua, в Pawn скрипты именно компилируются,в байт код для запуска на абстрактной машине, а не интерпретируются как в Lua. Скажите — ну и что это даёт? А даёт это многое, например: компилятор pawn ещё до выполнение скрипта проверяет на наличие ошибок, и поэтому у вас никогда не будет внезапных ошибок в программе, также ещё скорость работы скрипта больше чем в том-же Lua, потому-что код скомпилирован в сразу понятный для машины код. Что такое pawn мы разобрались.
Давайте разберёмся с средой разработки, если в lua мы могли писать скрипты хоть в блокноте, то теперь нам нужна полноценная среда разработки.
1. Pawno — Очень простой редактор, в есть необходимый минимум чтобы писать скрипты на pawn.
Плюсы:
+ Малый размер (768 кб)
+ Идёт сразу с Samp server
+ Встроенный список функций из всех инклудов
+ Сразу есть все паблики и функции samp
Минусы:
— Подсветка синтаксиса сделана чисто для галочки (всего два цвета подсветки синий и чёрный )
— На windows начиная с vista надо запускать от имени админа
— На больших скриптах может вылетать
2. Notepad++ (с плагином nppexec) — Самый популярный редактор скриптов. Поддерживает плагины, и также множество языков программирования
Плюсы:
+ Расширяемость
+ Нормальная подсветка синтаксиса
+ Авто-табуляция кода
+ Удобная навигация по коду (можно сразу перейти к другой строке, и есть карта документа)
Минусы:
— Для поддержки pawn надо много чего настраивать.
— Нету Встроенного списока функций
Настройка плагина NppExec:
1. Выберите plugin manager
2. Откроется окно, ищём Nppexec, выбираем галочкой, жмём install, перезапускаем.
3. Должно появится в меню пункт, жмём
4. Откроется окно
вписываем туда код:
5. Нажимаем ok и компиляция начнётся
Но давайте перейдём к написанию программы «hello world!» Как я и сказал у меня samp вариант Pawn. Вот как выглядит hello world в pawn:
Компилируем:
Как видим, всё прошло успешно и компиляция завершена.
Рекомендуемые комментарии
Полезные ресурсы
Голосование
Информация
О нас
У нас играют и пишут программы как новички так и опытные программисты. А самые продвинутые участники нашего коллектива даже разрабатывают собственные авторские моды и аддоны, ресурспаки, репозитории, мощнейшие библиотеки и операционные системы.
11 комментариев
Читайте также:
- Где делать смазку для полозьев клондайк
- Как сделать гаечный ключ в industrial craft 2
- Я что похож на абонента soda luv
- Journey to the savage planet где лежат сохранения
- Записи шакала far cry 2 где
Итак, сегодня я буду рассказывать как я пишу мод на SAMP. Для начала разберёмся на каком языке пишут скрипты и моды для SAMP. Моды и скрипты в SAMP пишутся на языке PAWN. Pawn — это С-подобный скриптовый язык (как и lua) но, в отличии от lua, в Pawn скрипты именно компилируются,в байт код для запуска на абстрактной машине, а не интерпретируются как в Lua. Скажите — ну и что это даёт? А даёт это многое, например: компилятор pawn ещё до выполнение скрипта проверяет на наличие ошибок, и поэтому у вас никогда не будет внезапных ошибок в программе, также ещё скорость работы скрипта больше чем в том-же Lua, потому-что код скомпилирован в сразу понятный для машины код. Что такое pawn мы разобрались.
Давайте разберёмся с средой разработки, если в lua мы могли писать скрипты хоть в блокноте, то теперь нам нужна полноценная среда разработки.
1. Pawno — Очень простой редактор, в есть необходимый минимум чтобы писать скрипты на pawn.
Плюсы:
+ Малый размер (768 кб)
+ Идёт сразу с Samp server
+ Встроенный список функций из всех инклудов
+ Сразу есть все паблики и функции samp
Минусы:
— Подсветка синтаксиса сделана чисто для галочки (всего два цвета подсветки синий и чёрный)
— На windows начиная с vista надо запускать от имени админа
— На больших скриптах может вылетать
2. Notepad++ (с плагином nppexec) — Самый популярный редактор скриптов. Поддерживает плагины, и также множество языков программирования
Плюсы:
+ Расширяемость
+ Нормальная подсветка синтаксиса
+ Авто-табуляция кода
+ Удобная навигация по коду (можно сразу перейти к другой строке, и есть карта документа)
Минусы:
— Для поддержки pawn надо много чего настраивать.
— Нету Встроенного списока функций
Настройка плагина NppExec:
1. Выберите plugin manager
2. Откроется окно, ищём Nppexec, выбираем галочкой, жмём install, перезапускаем.
3. Должно появится в меню пункт, жмём
4. Откроется окно
вписываем туда код:
cd $(CURRENT_DIRECTORY) "Путь до pawncc.exe" "$(FILE_NAME)" -; -(
5. Нажимаем ok и компиляция начнётся
Но давайте перейдём к написанию программы «hello world!» Как я и сказал у меня samp вариант Pawn. Вот как выглядит hello world в pawn:
main(){ print("hello world!");}
Компилируем:
Как видим, всё прошло успешно и компиляция завершена.
Вот как выглядела бы ошибка:
С компиляцией разобрались, теперь нам надо запустить сервер, запускаем сервер и видем наше сообщение:
Теперь хотелось-бы чтобы например: hello world писалось не в консоль сервра, а например игроку в чат. Для этого нужно использовать include, да-да как и в си или c++ pawn поддерживает include и константы #define, и даже команды пре-процесса #pragma. Теперь, давайте подключим include к нашему скрипту для того, чтобы подключить include надо в начале скрипта написать #include <a_samp>, тем самым мы подключили include для работы функций samp. Теперь мы можем создать код в нашем скрипте:
public OnPlayerConnect(playerid) // Создаём паблик чтобы при подключении игрока что-то происходило{ SendClientMessage(playerid, -1, "hello world"); // Функция отправки сообщения return 1; // функция должна что-то возвращать}
Playerid — Ид игрока которому мы будем отправлять сообщение (в данном случае игроку который подключился к серверу)
-1 — Цвет сообщения (белый)
«hello world» — Строка которая будет отправляться.
Запускаем сервер, заходим в игру и видим наше сообщение:
Ну вот и всё это был весь мой обзор языка pawn. ВНИМАНИЕ! Я некого не собираюсь учить (я сам учусь) это был просто мини-обзор языка Pawn. Потому-что никто на форуме не знает этот замечательный язык программирования. Если бы он был в OpenComputers я бы был рад!