Hello Handmade folks,
I am here to present you something that I thought should be already existing but, to my surprise, couldn't find it: a very simple scripting language for games: both easy to write in and easy to include in a program.
A little bit of history first, you can skip this if you want to get straight to the point:
I am currently writing a custom game engine and a game, and I needed to be able to script simple sequence of events: opening a door with a camera pointed at it, triggering an event when picking up an object...
At first I implemented a simple binary tree: every instruction always returned a boolean, and there could be a branch when a function returned "true" or "false".
That approach led to a few problems:
->Most functions didn't need to return either "true" or "false", so there were many unnecessary "false" pointers in memory
->This "code" was stored in json files, and the syntax quickly became unreadable when the script was a little bit complicated
Therefore, I end-up refactoring my "Virtual Machine" (that's a big word for such a simple thing x) ) and it ended-up working like a simplified CPU: the input was a list of instructions, and there were "jump" instructions inserted to do the branching. With this new implementation, the system won some power and flexibility, so I needed to be able to write code that took advantage of it, as well as get rid of my old json stuff.
At the same time, I searched for an existing scripting language I could include in my engine. I took two candidates I found in the book "Game Engine Architecture, Second Edition": Lua and Pawn. Both of these had two problems, for me:
->They were both overkill for what I wanted to achieve
->They both had their very own syntax, and I know from my experience (I used to work in a company where we ported old point&click games to mobile using an in-house C++ engine and Lua as its scripting language, in the same job I had to jump back-and-forth to Java and C# because I did some native Android programming while working on some Unity projects...) that I can easily get confused when switching language, and since I spent most of my time writing C/C++ for my engine, I wanted something that looked like C when scripting.
->Third "problem": I already had something kind-of working, so why not improve that instead?
Hopefully, I had some lex/yacc classes back in Computer Engineering school (ENSIIE, in France), so creating a new language didn't seem daunting to me, and I knew where to start.
I originally announced the language in
this forum post (in French)
The language's syntax looks something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | //Left lever's code
script onLeftLever()
{
lockPlayer()
{
playToggleMechanism(2);
launchToggleMechanism(0);
playCameraPath("camerapath0");
/*
If both doors are open
We open the gate
*/
if(isMechanismAtEnd(1) && isMechanismAtEnd(0))
{
launchToggleMechanism(4);
playCameraPath("camerapathFloorGrid");
}
else if(isMechanismAtEnd(4))
{
//Else, if the gate is open
//we shut it
launchToggleMechanism(4);
playCameraPath("camerapathFloorGrid");
}
}
}
|
Currently, it compiles to an easier-to-parse list of instructions in text form that looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | 1
21
onLeftLever 0
lockPlayer
playToggleMechanism 2
launchToggleMechanism 0
playCameraPath "camerapath0"
isMechanismAtEnd 1
isMechanismAtEnd 0
and
jumpIfFalse 5
launchToggleMechanism 4
playCameraPath "camerapathFloorGrid"
jump 6
isMechanismAtEnd 4
jumpIfFalse 4
launchToggleMechanism 4
playCameraPath "camerapathFloorGrid"
unlockPlayer
nop
|
That is the actual form that compile to "executable" instructions.
(I will call this "assembly" for future reference in this post).
The compiler's implementation is so simple it could have been a school project.
Currently, to use the VM, you have to:
->Declare all your engine's functions you want to be able to call from script, and put them in an array that holds some more info about them:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | #include "ceq/ceq.h"
CEQ_INSTRUCTION(g_launchToggleMechanism);
CEQ_INSTRUCTION(g_coroutineToggleMechanism);
CEQ_INSTRUCTION(g_toggleDoor);
CEQ_INSTRUCTION(g_toggleCollider);
CEQ_INSTRUCTION(g_launchCameraPath);
CEQ_INSTRUCTION(g_coroutineCameraPath);
CEQ_INSTRUCTION(g_lockPlayer);
CEQ_INSTRUCTION(g_unlockPlayer);
/*etc*/
ceq::ScriptFunctionInfo scriptFunctionsData[] =
{
{"launchToggleMechanism", g_launchToggleMechanism, NULL, NULL},
{"playToggleMechanism", g_launchToggleMechanism, g_coroutineToggleMechanism, NULL},
{"toggleDoor", g_toggleDoor, NULL, NULL},
{"toggleCollider", g_toggleCollider, NULL, NULL},
{"toggleMechanism", g_toggleMechanism, NULL, NULL},
{"launchCameraPath", g_launchCameraPath, NULL, NULL},
{"playCameraPath", g_launchCameraPath, g_coroutineCameraPath, NULL},
{"lockPlayer", g_lockPlayer, NULL, "unlockPlayer"},
{"unlockPlayer", g_unlockPlayer, NULL, NULL},
/*etc*/
};
|
(CEQ_INSTRUCTION is simply a macro that is either replaced by the function's prototype, or that can allow you to compile a stand-alone compiler in which you don't need to include the whole functions' definitions.)
->Define these functions (examples taken from my game's prototype, need some refinement):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | void g_lockPlayer(ceq::Context* context, void* data)
{
++(context->game->lockPlayer);
}
void g_unlockPlayer(ceq::Context* context, void* data)
{
if(context->game->lockPlayer > 0)
{
--(context->game->lockPlayer);
}
}
void g_isMechanismAtEnd(ceq::Context* context, void* data)
{
WE2016::Mechanism* mechanism = (WE2016::Mechanism*)data;
context->pushBoolToStack(context->game->isMechanismAtEnd(mechanism));
}
|
To use it in-game:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 | //"GAMECLASS" is the type of the "game" field in the VM's context that represents your running game
//you don't really have to use it if you don't need it, it will be "void" if you don't define it.
#define GAMECLASS MyGameStruct
#include "ceq/ceq.h"
//A list of compiled instructions
ceq::Instruction* instructions;
//The "VM"
ceq::Machine scriptMachine;
//In your implementation:
//This is required if you need to compile from source to "assembly"
#define _CEQ_COMPILER_IMPLEMENTATION_
#include "ceq/ceq_compiler.h"
#define _CEQ_IMPLEMENTATION_
#include "ceq/ceq.h"
//Pass your set of functions to the VM
//This will be used for "assembling"
scriptMachine.setAPI(scriptFunctionsData, ArrayCount(scriptFunctionsData));
//Currently, the only "compile" function takes an input file path as input and another file's path as output
//(flex/bison rely on FILE* as input/output streams)
//You can also compile up-front and only use the "assembly" file in your game
ceq_compiler::compileFile("source.ceq", "assembly.ceq", &scriptMachine);
//The "assembling" part is not yet covered here
//There, the "instructions" field defined above will be initialized with the number of instructions defined in the file (second line)
//There is also a list of scripts in the file, where each element simply is the index of "instructions" where the script begins
//At some point, you will need a way to assign parameters to the instructions that need them.
//Assigning parameters to an instruction looks something like this:
int testData = 5;
instruction->setData((void*)&testData, sizeof(int));
//Then, in your game's loop:
{
//Every time you need to trigger a script
scriptMachine.run(myTrigger.script, this);
//"this" will be assigned to the VM's context's "game" field explained earlier
//If you have some coroutines in your instruction set, you need to call this every frame:
scriptMachine.runCoroutines();
}
|
I tried to make its usage as simple as possible:
->To include the VM to your project, there is only one file to include, with at least one place where the "_CEQ_IMPLEMENTATION_" flag is defined
->Same when you want to include the compiler, with the flag "_CEQ_COMPILER_IMPLEMENTATION_"
->only 2 different functions need to be called in the game loop, one of them every time you need to launch a script, the other one once in the frame.
->I hope I made the initialization part hassle-free enough.
I did not really show the "assembling" here because my current implementation is too tied to my game, and I am not sure about the final form of my "assembly" files.
====================
Now let's talk about the features of this language.
This language was originally meant to execute simple sequences of events. As of now, there are basic features of other languages that I don't plan to add because I don't find an use for them in my work, unless my scripts become really hard to read:
->variable definitions and manipulations
->Retrieving a custom object/structure from the engine for later use
->Defining custom data structures
What I currently have is:
->Call engine functions
->The functions' parameters may be either integers or strings
->if/else statements
->boolean arithmetic
->infinite loops (which happen to be the same as Rust's "loop {}"
->A simple coroutine system
->What I call "block functions" (maybe this already exists as another name?):
Instead of writing:
| lockPlayer();
doLengthyStuff();
unlockPlayer();
|
And risking forgetting the "unlockPlayer()" call, you can require the "lockPlayer()" function to be called this way:
| lockPlayer()
{
doLengthyStuff();
}
|
So that the compiler will throw you an error if you try to call it like a "normal" function, and the actual call to "unlockPlayer()" will automatically be added at the end of lockPlayer's scope.
What I plan on adding:
->A function/macro system
->integer arithmetic
->while(){} and do{}while() loops
What dependencies I hope to get rid of:
->std::vector, used in the VM to store running coroutines
->various libc features in the compiler: FILE*, malloc/realloc/free, strdup, printf... some part of flex/bison's generated code (maybe I can get rid of these via options?), a lot in my own code, including itoa(I've read this one is not even standard).
I didn't put this on Github yet as it is far from finished and not clean enough, and I'm advancing on other parts of my engine/game at the same time. I'll do my best to release a version as soon as possible.
When I do, I will release both the lex/yacc files and the generated files, and the VM which is contained in a single file.
Thanks for your attention, and I hope I'll come back here soon for an update.