
Creating a game before your eyes - part 3: We fasten the scripting language to Unity (UniLua)

As promised, I continue to share with you those technical details that we encounter in the process of creating our game.
This time, let's talk about a language for writing in-game scripts.
In this article I will explain why Lua, and not a self-made bike. Why in general the game may need a scripting language. What subtleties are there when screwing this case to Unity and I will show how this is done using UniLua integration as an example.
I must say right away that to the latest information on the Internet there are almost zero, and half of this zero is in Chinese. So, you can say - keep exclusive.
Why do we need scripts?
In our game, we need to show a variety of scripted scenes.
I will give a typical example of a quest. The character enters the store and sees that there is a robbery. A picture showing bandits holding a bat at the temple of a frightened seller is shown. Then some kind of dialogue is shown. Then we see how our character approaches the mess and a window appears for choosing an action - to help the seller and distribute to racketeers or fit in with them.
Obviously, here you need to move the sprites, change animations for them, show the player different dialogs and pictures ... There are not many options here - either hardcode each quest, or try to script this thing.
Obviously, hardcoding such things is not at all hard.
Why Lua?
Actually, initially there was a choice between own bicycle and Lua.
It would seem that from the first approach the language does not require much and you can write your own. Call yourself teams in order and that’s it. But if you think a little deeper ... Will the script events be related to the parameters of the game? For example, a previously killed NPC should not appear in scenes. Or something else like that. And this already means some conditions, triggers, etc.
As a result, the parser of a "simple language" can result in a very complex contraption, which will need to parse heaps of logical expressions, etc. etc.
Without thinking twice, it was decided to use someone else's and proven. Lua. Perhaps there are other languages as well ... but it is Lua that I see constantly in other games. In the same World of Warcraft, mods were written in this strange language, where indexing starts with one.
So, again, - it was decided to use a solution verified by others.
Integration in Unity
This is where the first fun begins. The first library that implements Lua in Unity, which you will find, will look good. But if you dig deeper, it turns out that it uses some specific .Net methods, which, for example, are not available on mobile phones (and, possibly, on some other platforms).
And we would like a library that would be supported everywhere (just in case) and preferably still completely with the sources, and not in a closed DLL.
Rummaging through the internet, we found a free creation of Chinese programmers - UniLua . Full sorts and works everywhere.
It is good for everyone except that the docks are incredibly meager and partially written in Chinese.
Oh well, we have the source! And the brain ... =) Download, drop the UniLua folder into plugins (so as not to be recompiled every time) and go.
We call the Lua script from C #
Everything is relatively simple here:
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-скрипт
You can try to run. If no one cursed, then everything is fine. The empty script succeeded.
Calling C # Functions from Lua
Now we need to learn to steer at least some of this script. Obviously, we need to learn how to call C # code from Lua.
Let's write a method that simply writes a parameter to the log:
private int L_Trace(ILuaState s)
{
Debug.Log("Lua trace: " + s.L_CheckString(1)); // читаем первый параметр
return 1; // так надо
}
As you can see, we used the class
ILuaState
. This is where all the input parameters are stored (which we want to transfer from Lua and we need to return the result there. Please note! The result in Lua is returned not through return
, but through s.PushInteger()
, s.PushString()
etc. The function is written. Now it needs to be connected to Lua.
private int OpenLib(ILuaState lua)
{
var define = new NameFuncPair[] // структура, описывающая все доступные методы (интерфейс Lua -> C#)
{
new NameFuncPair("trace", L_Trace),
};
lua.L_NewLib(define);
return 1;
}
Next, after creating the _lua object, we need to add a connection to this library description:
_lua.L_OpenLibs();
_lua.L_RequireF("mylib", OpenLib, true);
Done! Now you can do this:
string lua_script = @"
local lib = require ""mylib""
lib.trace(""Test output"")
";
It would seem that all? But no. Now the hardest part.
Yield
With a little thought, you can understand that our Lua script should not be executed continuously. There will obviously be pauses, waiting for the end of some kind of animation, pressing a key, etc. That is, the script should return control back to the sharps, and then, at some point, continue.
It was here that I broke many copies. An explanatory description of how to do this was very difficult to find (and that was for another library).
The first thing we will need is to run the script not by Call, but through a separate thread:
//_status.Call(0, 0); это нам больше не нужно. вместо этого пишем:
_thread = _lua.NewThread();
_status = _thread.L_LoadString(lua_script);
_thread.Resume(null, 0);
Now imagine that in C # we wrote the function “wait for the animation to finish” (
L_WaitForAnimationStop
), which we call from Lua. The implementation here may be different, then I will describe the general principle. In this function, we need to hang some kind of callback at the end of this animation, and most importantly, together
return 1
we must do this: private int L_WaitForAnimationStop(ILuaState s)
{
// здесь добавляем нужные callback'и и т.п.
_temp_state = s; // сохраняем ILuaState в приватный член класса
return s.YieldK(s.GetTop(), 0, null); // указываем Lua, что оно должно отдать управление шарпам
}
And directly in the callback, we will need to continue executing the script from the place where it stopped
if (_temp_state.GetTop() > 0) _thread.Resume(null, 0);
That's all. Now a script like:
lib.trace("starting")
lib.wait_for_animation_stop()
lib.trace("stopped")
after it will
lib.wait_for_animation_stop()
pause and continue only when you want it (i.e., in the above case, call callback, which will do it Resume()
).What has been achieved
Using the above method, as well as shamanism to simulate OOP , it was possible to achieve the following syntax:
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")
The script creates two character sprites, moves the first to the “workout” point, the second to the “fridge” point, then waits for both to finish their movement, and only then writes “finished ok”.
From the documentation, I can only advise Lua 5.2 Reference Manual , where all these shamanisms are described, albeit a little for another implementation.
All articles in the series:
- Idea, vision, choice of setting, platform, distribution model, etc.
- Shaders for styling pictures under CRT / LCD
- We fasten a scripting language to Unity (UniLua)
- Shader for fade in by palette (a la NES)
- Subtotal (prototype)
- Let's talk about PR indie games
- 2D animations in Unity ("like flash")
- Visual scripting of cut scenes in Unity (uScript)