Создание игры на ваших глазах — часть 3: Прикручиваем скриптовый язык к Unity (UniLua)

soulburner 6 февраля 2014 в 12:17 38,3k

Как и обещал — продолжаю делиться с вами теми техническими деталями, которые встречаются нам в процессе создания нашей игры.

На этот раз поговорим о языке для написания внутриигровых скриптов.

В этой статье я расскажу, почему именно 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, где все эти шаманства описаны, хоть и немного для другой реализации.

Все статьи серии:
  1. Идея, вижен, выбор сеттинга, платформы, модели распространения и т.п
  2. Шейдеры для стилизации картинки под ЭЛТ/LCD
  3. Прикручиваем скриптовый язык к Unity (UniLua)
  4. Шейдер для fade in по палитре (а-ля NES)
  5. Промежуточный итог (прототип)
  6. Поговорим о пиаре инди игр
  7. 2D-анимации в Unity («как во флэше»)
  8. Визуальное скриптование кат-сцен в Unity (uScript)
Проголосовать:
+17
Сохранить: