
Как и обещал — продолжаю делиться с вами теми техническими деталями, которые встречаются нам в процессе создания нашей игры.
На этот раз поговорим о языке для написания внутриигровых скриптов.
В этой статье я расскажу, почему именно Lua, а не самописный велосипед. Зачем вообще игре может понадобится скриптовый язык. Какие тонкости есть при прикручивании этого дела к Unity и покажу как это делается на примере интеграции UniLua.
Сразу скажу, что к последнему информации в интернете почти что ноль, и половина этого нуля — на китайском. Так что, можно сказать, — держите эксклюзив.
Зачем нам скрипты?
В нашей игре у нас есть необходимость показывать разнообразные скриптованные сценки.
Приведу типичный пример квеста. Персонаж заходит в магазин и видит, что там идет ограбление. Показывается картинка, изображающая бандитов, держащих биту у виска испуганного продавца. Затем показывается какой-то диалог. Потом мы видим, как наш персонаж подходит к заварушке и появляется окно выбора действия — помочь продавцу и раздать рэкетирам или вписаться за них.
Очевидно, что здесь нужно двигать спрайты, менять им анимации, показывать игроку разные диалоги и картинки… Вариантов тут не много — либо хардкодить каждый квест, либо попытаться это дело заскриптовать.
Очевидно, что хардкодить такие штуки — вообще не тру.
Почему Lua?
Собственно, изначально был выбор между собственным велосипедом и Lua.
Казалось бы, с первого приближения язык многого не требует и можно написать собственный. Вызывай себе команды по порядку и все. Но если подумать поглубже… Будут ли события скрипта связанны с параметрами игры? Например, убитый раньше NPC не должен появляться в сценках. Или еще что-то такое. А это уже означает какие-то условия, триггеры и т.п.
В результате парсер «простенького языка» может вылиться в весьма сложную штуковину, которой надо будет парсить кучи логических выражений и т.п. и т.д.
Недолго думая, было решено использовать чужое и проверенное. Lua. Возможно, есть еще и другие языки… но именно Lua я вижу постоянно в других играх. В том же World of Warcraft моды писались именно на этом странном языке, где индексация начинается с единицы.
Так что, опять-таки, — было принято решение использовать проверенное другими решение.
Интеграция в Unity
Здесь начинается первое веселье. Первая же библиотека, реализовывающаяя Lua в Unity, которую вы найдете — будет выглядеть хорошо. Но если копнуть глубже, то окажется, что она юзает какие-то специфичные методы .Net, которые, например, недоступны на мобилах (а, возможно, и каких-то других платформах).
А нам бы хотелось библиотеку, которая бы поддерживалась везде (на всякий случай) и желательно еще полностью с исходниками, а не в закрытой DLL’ке.
Покопавшись в инете, мы нашли бесплатное творение китайских программистов — UniLua. Полные сорцы и работает везде.
Оно всем хорошо кроме того, что доки невероятно скудны и частично написаны на китайском.
Ну да ладно, у нас же есть исходники! И мозг… =) Качаем, закидываем папку UniLua в плагины (чтобы не перекомпилировалось каждый раз) и вперед.
Вызываем Lua-скрипт из C#
Тут все сравнительно просто:
using UniLua; private ILuaState _lua; // через этот объект будет производится работа с Lua private ThreadStatus _status; // объект для работы с конкретным скриптом ... _lua = LuaAPI.NewState(); // создаем string lua_script = ""; // сюда можно писать код на Lua _status = _lua.L_LoadString(lua_script); // загружаем скрипт if (_status != ThreadStatus.LUA_OK) { Debug.LogError("Error parsing lua code"); } _status.Call(0, 0); // запускаем Lua-скрипт
Можно попробовать запустить. Если никто не ругнулся — значит все хорошо. Пустой скрипт успешно выполнился.
Вызов функций C# из Lua
Теперь надо научиться рулить хоть чем-то из этого скрипта. Очевидно, нам нужно научиться вызывать код на C# из Lua.
Напишем метод, который просто пишет параметр в лог:
private int L_Trace(ILuaState s) { Debug.Log("Lua trace: " + s.L_CheckString(1)); // читаем первый параметр return 1; // так надо }
Как видите, мы использовали класс ILuaState. Именно там хранятся все входные параметры (которые мы захотим передать из Lua и именно туда нужно возвращать результат. Обратите внимание! Результат в Lua возвращается не через return, а через s.PushInteger(), s.PushString() и т.п.
Функция написана. Теперь ее надо подключить к Lua.
private int OpenLib(ILuaState lua) { var define = new NameFuncPair[] // структура, описывающая все доступные методы (интерфейс Lua -> C#) { new NameFuncPair("trace", L_Trace), }; lua.L_NewLib(define); return 1; }
Далее, после создания объекта _lua, нам нужно добавить подключение этого описания библиотеки:
_lua.L_OpenLibs(); _lua.L_RequireF("mylib", OpenLib, true);
Готово! Теперь можно сделать так:
string lua_script = @" local lib = require ""mylib"" lib.trace(""Test output"") ";
Казалось бы, все? Но нет. Теперь самое сложное.
Yield
Немного подумав, можно понять, что наш скрипт на Lua не должен выполняться непрерывно. В нем явно будут паузы, ожидание окончания какой-то анимации, нажатия клавиши и т.п. То есть скрипт должен возвращать управление обратно шарпам, а потом, в какой-то момент — продолжаться.
Именно здесь я сломал множество копий. Толковое описание, как это сделать было очень трудно найти (и то было для другой библиотеки).
Первое, что нам нужно будет — это запускать скрипт не Call’ом, а через отдельный поток:
//_status.Call(0, 0); это нам больше не нужно. вместо этого пишем: _thread = _lua.NewThread(); _status = _thread.L_LoadString(lua_script); _thread.Resume(null, 0);
Теперь представим себе, что мы на C# написали функцию «подождать окончания анимации» (L_WaitForAnimationStop), которую вызываем из Lua. Реализация тут может быть разная, то я опишу общий принцип.
В этой функции нам нужно повесить на окончание этой анимации какой-то callback, и самое главное — ввместо return 1 мы должны сделать так:
private int L_WaitForAnimationStop(ILuaState s) { // здесь добавляем нужные callback'и и т.п. _temp_state = s; // сохраняем ILuaState в приватный член класса return s.YieldK(s.GetTop(), 0, null); // указываем Lua, что оно должно отдать управление шарпам }
А непосредственно в callback’е — нам нужно будет продолжить выполнение скрипта с места, где он остановился
if (_temp_state.GetTop() > 0) _thread.Resume(null, 0);
Вот и все. Теперь скрипт типа:
lib.trace("starting") lib.wait_for_animation_stop() lib.trace("stopped")
после lib.wait_for_animation_stop() приостановится и продолжится только когда вы этого захотите (т.е. в вышеописанном случае — вызовите callback, который и сделает Resume()).
Чего удалось добиться
С помощью вышеописанного метода, а также шаманства для имитации ООП, удалось добиться такого синтаксиса:
local ch1 = CharacterGfx() ch1.create("char_0") local ch2 = CharacterGfx() ch2.create("char_1") ch1.moveto("workout") ch2.moveto("fridge") ch2.wait_move_finish() ch1.wait_move_finish() vh.trace("finished ok")
Скрипт создает два спрайта персонажей, двигает первого к точке «workout», второго — к точке «fridge», потом ждет, когда оба закончат свое движение, и только потом пишет «finished ok».
Из документации могу посоветовать только Lua 5.2 Reference Manual, где все эти шаманства описаны, хоть и немного для другой реализации.
ссылка на оригинал статьи http://habrahabr.ru/post/211576/
Добавить комментарий