For fun, I've decided to write my own small, lightweight scripting language for describing motions of the noninteractive background objects in closure, and triggering animations and such under certain conditions. It won't be implemented into the game soon, but it's fun to write a mini compiler and interpreter for this for now.
This is sorta a mini-dev log for this.
First, I decided what I want the scripting language to do, and what it should look like.
I decided what it would look like, and I decided on c++ syntax, but each script is pretty much its own function
- only floats and ints
- operators: +-*/%, assignment: =, comparison: == != > >= < <=, grouping: ( )
- no dynamic allocation
- if statements, no while/for statements (for now)
- math functions: sin, cos, tan, sqrt (but easily able to add more if I so chose)
- pass the script pointers to data to access/modify through code
Essentially, each script will describe one algorithm.
so I can do stuff like this:
script: (ints A and B and OUTPUT are passed to the script from code)
int C = A * (B + 100)*sin(400 * A);
if(C > 200){
OUTPUT = 1;
} else {
OUTPUT = 2;
}
basic stuff pretty much.
Second, I decided on an instruction set for the virtual machine, and how it would operate. All I know about VMs is looking at flash bytecode and the small amount of assembly I know, other than that I'm pretty much making this up as I go along.
I'm going for a stack based thingy
so that means converting normal expressions like A + B * C to
B C * A +
it would push B and C on the stack, Multiply them, place the result on the stack (in place of B and C), put A on the stack, then add them, so after this is done the value on the stack is what you'd expect
Also, I don't know how normal VMs do this, but since I want floats and ints, i made one stack for floating point expressions and one stack for integer expressions, each expression chooses one or the other to work with (if the expression is only ints, it uses the int stack otherwise it uses the float stack and casts ints to float when it pushes onto the stack)
so I also have 3 classes of variable: local, outside/reference, and constant
and came up with this instruction set:
SETMODE, // sets the current stack (float or int)
POPINT, POPFLOAT, POPINTR, POPFLOATR, // moves a number from the current stack to a register, R = reference, C = constant
PUSHINT, PUSHFLOAT, PUSHINTR, PUSHFLOATR, PUSHINTC, PUSHFLOATC, // moves a number from the register to the current working stack
CALL, // calls a function with the current stacktop as a paramater, places value on stack
MULT, DIV, ADD, SUB, MOD, // operates on top 2 in stack and places return value in stack
JUMP, // sets program pointer
COMPEQ, COMPGR, COMPLT, COMPGREQ, COMPLTEQ, COMPNEQ, // compares top 2 on stack, places result in "result" boolean
SKIPIF, // skips next line if result is true
for CALL, it could have SIN, COS, TAN, or SQRT as a parameter
set, push, pop call, and jump take 1 argument, the rest take none
this works in my head but I don't know if you can get it through my shoddy explanation
Then I got a function that reads in a file and splits it into distinct tokens (converts "A=B+45*C;" into {"A", "=", "B", "+", "45", "*", "C", ";"}
for a sample from now on, I'm gonna do what I have for my test which is
A * sin(B) + (A*B*B); with A as an int and B as a float passed to it
so the first bit converts it to (using spaces to delimit tokens, internally it's an array of strings)
A * sin ( B ) + ( A * B * B ) ;
Next, I pull out one line at a time and send it to the "expression compiler" (strips out the ; and will do a few things like resolving variable names in the future too)
A * sin ( B ) + ( A * B * B )
Now I begin processing this array of tokens into something easier for the program to compile into opcodes, so seeing as I need to get it into the stack form shown above, I need to swap the order of tokens around, so first I resolve function names (which is sin in this case)
in stack form , sin(b) is "B SIN" which is push B on the stack, then call sin on the top of the stack, so that's simple, when I find a function name, I move it over to right in front of the corresponding closing parenthesis
A * ( B sin ) + ( A * B * B )
Next up is shuffling operators around. Now, for this, i treat parenthesis groupings as a single token, (i essentially pass through them during my swapping)
So I start by looking at the first one and incrementing till I find an operator (in this case the first operator is *
I then swap the operator and the next "token group" (which is the next token if there's no parenthesis)
A ( B sin ) * + ( A * B * B ) (currently looking at *, next token is +, swap that)
A ( B sin ) * ( A * B * B ) +
Next, I begin converting to opcodes, but, when I encounter a (, I remove it, and count forward till I find the corresponding ), remove that, and send the grouping in between to the expression compiler (recursion). So the following 2 expressions get compiled through the method above and placed in place:
B sin (already fine)
A * B * B (turns to)
A B * B *
so the final expression before translation is
A B sin * A B * B * +
Then it's simply just processing one token at a time and resolving names and symbols
My compiler outputs:
SETMODE FLOAT //the expression type is float, remember A was an int and B was a float
PUSHINTR A
PUSHFLOATR B
CALL SIN
MULT
PUSHINTR A
PUSHFLOATR B
MULT
PUSHFLOATR B
MULT
ADD
the R on the end of the push opcodes stands for "reference" since A and B are references to variables declared in the c++ program.
But, the opcodes look pretty much correct, and the compiler handles stacking parenthesis properly.
Up next: operator precedence. I can just insert parenthesis around multiplication division and modulus accordingly for now though, the thing will handle it correctly if I do that.