handmade.network » Forums » Work-in-Progress » ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
Guntha
14 posts
#15451 ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
4 months, 2 weeks ago Edited by Guntha on June 1, 2018, 11:03 a.m. Reason: typo in previous edit

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:
1
2
3
lockPlayer();
doLengthyStuff();
unlockPlayer();

And risking forgetting the "unlockPlayer()" call, you can require the "lockPlayer()" function to be called this way:
1
2
3
4
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.
Guntha
14 posts
#15549 ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
4 months ago

Quick update: I found the time to add while(){} and do{}while() loops (it was actually a matter of minutes since I had already implemented ifs and loop{}).
I might add an "#include"-like system to the list of features I want to include.

An info I forgot to mention in my first post: when I finished the first usable version of the language, I found that a few days before someone made a blog post about their own scripting language for their own engine:

https://www.kircode.com/post/desi...pting-language-for-my-game-engine

(This one looks somewhat closer to SCUMM than C).

It looks there is a market for simple and lightweight scripting languages, therefore I hope I can make this one available soon :)
wilberton
Andy Gill
18 posts
#15556 ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
4 months ago

Hey, this looks really cool. I definitely like the simplicity. Can you have script functions with more than one argument?

One thing I would consider is replacing flex/bison with your own parser. In the long run it's going to be the better option and it's not hard to do (check out the first few episodes of Per Vognsen's Bitwise project for an excellent introduction to writing a lexer+parser).
Guntha
14 posts
#15563 ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
4 months ago Edited by Guntha on June 14, 2018, 11:44 p.m. Reason: better english?

Hello wilberton, thanks for your feedback :)

Actually, yeah, there is no hard limit on the argument count, I have a few functions for my game that require several arguments, it just happened that the example I posted didn't feature any x(

I used flex & bison mostly as a starting point, I had to use them in school and knowing that these tools exist gave me confidence in my ability to create a language. But when including the generated parsers in my game, even though the generated code is pretty efficient in my opinion, I realized these tools weren't really fit for creating a library out of them... Especially if, in a large project, you need to include SEVERAL parsers generated with flex/bison: you can easily have several definitions of yylex() or yyparse() or any other function... For now, I'm getting away with it by enclosing the parsers' implementations in a namespace (even though I only have 1 such generated parser in my project... I did it just in case :p ):

1
2
3
4
5
6
7
#ifdef _CEQ_COMPILER_IMPLEMENTATION_
namespace ceq_compiler
{
#include "generated/ceq_yacc.cpp"
#include "generated/ceq_flex.cpp"
}
#endif


This can be slightly changed by some command-line options of flex and bison that allow you to remove some functions or rename some others... But this feels like patching a wooden leg. And the options of both tools are not coherent with each other x) There is also a licensing issue that is not very clear to me when using bison (as I understand it, you may redistribute the generated parser the way you want, as long as it isn't part of a parser generator project...)

In the long run, I think writing my own parser will be the way to go. A few days ago, I listened to the handmade dev show with Jonathan Blow, where he explained he only used parser generators as a tool to validate his grammar.
ratchetfreak
404 posts
#15585 ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
4 months ago Edited by ratchetfreak on June 19, 2018, 10:49 a.m.

When you go to implement math and other expressions do some research on register based and stack based interpreters.

Stack-based interpreters are easier to implement with shorter opcodes but you spend more time shuffling values on the stack to make each operation work. Though big one-liners can compile cleanly to stack based execution and will not need that much value shuffling.

Register based interpreters will end up executing fewer instructions but will have larger individual opcodes because they need to reference which registers they operate on. They are also harder to compile to. On modern hardware doing fewer dispatches will end up being a win over shorter opcodes.
Guntha
14 posts
#15588 ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
4 months ago

Hello ratchetfreak,

Currently, the boolean arithmetic is already stack-based, and I planned to stay on this track when I will be implementing integer arithmetic. However, currently, my compiler doesn't try to optimize stack usage.

Also, since I don't want the language to feature variable declaration and manipulation, I don't expect much shuffling going on. (As you say, it will only be big one-liners.)
I will research about register-based interpreters, in case I want to change my mind about this, or for a future feature that may require it :) Thanks.

By the way: these days I'm trying to find the best way to make a save/load system for a game project, and scripts are a part of the things I want to serialize: I need to save the currently-playing coroutine, as well as, maybe, the current state of the stack. I see 2 problems with this:
=>If I save the stack, I may have to save the type of each variable on it (currently it's only a dumb byte array) to be able to deal with endianness if a save file is shared between platforms with different endianness (even though the last existing mainstream big-endian platforms I know of are the PS3 and Xbox360).
=>To save the currently playing coroutine, the simplest thing to do would be to save the index of the instruction, but what if the script is patched and an old save is loaded with an executable running the new script, and the saved index doesn't fit with the same coroutine (or any coroutine at all)? The only sensible solutions I can think of are either forbidding saves while a coroutine is running, or manually creating a correspondence table each time a script is patched, to be able to replace the old index with the new one.

If you have any opinion on this, that could help :)
ratchetfreak
404 posts
#15591 ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
4 months ago

I'd limit when you can save, that helps with both issues

The big issue then is paused scripts that are waiting on an event. But if you don't allow scripts invocations to continue over multiple frames then there's no issue. Instead you force the scripts to follow a state machine with the state stored in the objects.
Guntha
14 posts
#16335 ceq - A Dead-Simple Scripting Language for games with Simple Scripting Needs
1 month ago

Hello,

Sorry for the lack of updates, I was recently busy with updating other parts of my engine, and now I found a full time job again. I still plan on adding the missing features and releasing a first version, but I don't know when that will happen.

I just randomly watched this video from Jonathan Blow and don't agree with the way he opposes "90's/2000's scripting languages" vs visual scripting languages (around 53:30).

His experience is way longer than mine, so his opinion is probably worthier than mine, but here's my experience anyway.

I've professionnally worked on projects that used Lua as a scripting language, others that used Unity's uScript and others that used Unreal Engine 4's blueprint. I was usually more on the engine side on these projects, so I had to debug them, and I also worked with scripters who worked with the 3 of them.

He says that visual scripting brings something because you don't have to be a programmer to use them. Except their logic is exactly the same as "text" scripting languages (or any procedural languages). You have branches, loops, variables, and have to do everything in the right order. You can see the flow of the program the same way and are allowed to write as much spaghetti-looking code as you can in text, except you have actual spaghetti drawn on screen.

There are several drawbacks I faced with visual scripting languages:
->You need to learn how to use their own debugging tools, and when something breaks in a built-in function, you have to step through unreadable generated code, if the system you use even lets you see that code.
->If you ever tried to refactor, or just move code, inside a blueprint, you noticed it's even more error-prone than in text form: you might miss some part of the code you want to move when box-selecting it, you have to rearrange the links (when in text, you don't have to cope with this notion of "link"), and then you have to try to rearrange the whole function to avoid seeing lines crossing each other or going in unintuitive directions, and get the function going pretty much in a straight line (going straight is "built-in" in text!)
->Version control (SVN in my case) doesn't know what to do with it because they're usually binary files. So the person working with one is required to lock it.
->Searching for something is less intuitive than in text, and again this requires a custom tool, when in text any text editor could do it
->In the line of both searching and debugging: when doing something illegal in Lua, you get the line number in the file at which something is wrong, either when loading or executing the script. After failing compiling a visual script, you have to look for boxes that are glowing red.
->Somehow, longer compilation times than the equivalent text script (I especially experienced it with uScript, where it first generates a C# script, then compiles that script). Not great for quick iteration. Also, for some reason, both uScript and blueprints don't allow script edition at runtime, when it was trivial to do in our engine using Lua.
->We didn't do modding, so that's more a question: how do you allow modding in a visually scripted language, without giving away all your development tools?

After a while using uScript, scripters missed working with Lua (or be allowed to use straight C#.).
Although, our integration of lua was far from optimal (we didn't even have coroutines for a long time, everything was callback-based), but iteration was pretty much instantaneous.

Now, there are some tools built in visual scripting tools that I would love to have for a text scripting language. For example, when executing a scripted function, you can see at the same time in the editor which path the code takes, and that would be super useful to have with text.

When he says "You still need to be a real programmer to program in a scripting language, and by that point, why not use a real programming language?", my answer is simple (but can be argued): a script file is easier to treat as a cross-platform asset than a compiled language source file or DLL.

I agree with later things he says, like when some languages (including Lua, by the way) allow you to use a variable without declaring it, or without specifying its type, making debugging harder. In my opinion, a scripting language should be more restrictive than a compiled language, not the other way around. Anyway, for now at least, ceq, won't have this problem: you won't have any variable, only constants =D.

Hopefully, next post will be an actual update post.