
Как и обещал — продолжаю делиться с вами теми техническими деталями, которые встречаются нам в процессе создания нашей игры.
На этот раз поговорим о языке для написания внутриигровых скриптов.
В этой статье я расскажу, почему именно 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, где все эти шаманства описаны, хоть и немного для другой реализации.
Все статьи серии:
- Идея, вижен, выбор сеттинга, платформы, модели распространения и т.п
- Шейдеры для стилизации картинки под ЭЛТ/LCD
- Прикручиваем скриптовый язык к Unity (UniLua)
- Шейдер для fade in по палитре (а-ля NES)
- Промежуточный итог (прототип)
- Поговорим о пиаре инди игр
- 2D-анимации в Unity («как во флэше»)
- Визуальное скриптование кат-сцен в Unity (uScript)