Scripting

Published January 25, 2007
Advertisement
Scrap most of yesterday's post. Everything seems okay again.

So we're going with the original idea of a virtual machine for the scripting. I'm writing it as a seperate library module so I can maybe use it in future games as well.

The idea is that the vm executes assembly-style operations, quite efficiently through a big switch statement. However, the vm is initialised with a pointer to a static "service" function that, when the vm executes the svc instruction, gets called with the central register and an array of 32 integers passed as parameters.

As an example, here's a short virtual assembler script that "calls" service 22 from within the owning program, in this case hypothetically retrieving the player x position via register 3:

#byte #enum { end call ret svc	      setrx setax	      getrg setrg	      outn outnl }#double65535#offsetstart:	setax loadpcx	call	outn	outnl	endloadpcx:        setrx 22	svc	setax 3	getrg	ret#link#save "x.imp"


And the service function in the test driver program:

bool Service(int Code,int *Regs){	switch(Code)		{		case 22: Regs[3]=230375; return true;		}	return true;}


I'm not sure how clear all that is. Basically I can then write a compiler as a seperate command line program to compile virtually any simple languages I choose to design into "binary" files for the vm to execute.

As an example, if we assume the language supports a "service()" built-in method and a "register()" method to access the internal 32 ints array, you could create a "header" file for Udo a bit like:

int getplayerx(){    service(22); return register(3);}


then in the actual script for the level, have stuff like:

main(){    if(getplayerx()>2000)       {       activateID(200);       activateID(201);       }}


and so on, where activateID() is another function in the header file that sets a register to the ID passed in then calls a specific service.

So in the Udo codebase, all we do is include the vm and initialise it to a static Service function that just has to fulfill each service code by either placing values in the register array, or extracting values and triggering stuff in the program, or a combination of the two.

It will make more sense when I have some concrete examples. I reckon it will be fairly flexible. It just won't be able to directly call back functions within the owning application like other scripting languages can.

[EDIT]

Bit of progress. The compiled file now starts with a vector of ID/Address pairs that map numeric IDs to particular entry point addresses in the program segment.

This means that using the vm, you can call Vm.Execute(1023) which returns false if no such ID exists or will execute with the matching address as the entry point.

You can still use the call and ret instructions to call subroutines from anywhere else in the program segment. At the moment you need an end instruction to exit, but I'm wondering about using the ret instruction to act as the end instruction if the call stack is empty when the ret is encountered.

This would mean you could technically call one of the pre-registered entrypoint addresses as a subroutine from elsewhere in the script, if you so wanted, and it means one less instruction required (not that that matters really).

Actually, I better leave the explicit end instruction in place, so you can exit the script without having to unwind the call stack. But end can be expressly for that purpose, and all blocks of instructions will now end with a ret instruction regardless of whether they represent a subroutine or an entry-point block.

Seems sensible. We shall see.

Rambling aside, the point of all this is so that in the level editor I can assign script IDs to stuff. For example, a button you could press would have a ScriptID field I can set in the map editor, and when you press the button, Vm.Execute(ScriptID) can be called for the scripted responses to pressing the button.

There would be special case IDs, like 0 could be run at level startup, 1 could be run every frame and so on.

So I envisage the actual script might end up looking like this:

int getplayerx(){    service(22); return register(0);}activateID(int ID){    register(0)=ID; service(45);}procedure 0{    activateID(100);    activateID(101);}procedure 1{    if(getplayerx()>200) activateID(200);}


So there would be a difference from the compilers point of view between a function, which has a normal name and parameters and so on, and a procedure which has a numeric ID, no parameters and no return value and can only be called from externally.

[EDIT AGAIN]

I'm pretty sure from previous projects that the instruction set for the vm is complete. There are only 38 instructions, implemented in a big old switch statement like this:

bool CImp::CImplementation::Cycle(){	int Tx,Ty;	Mm(Ir);	switch(Ir)		{		case ic::end  : return false;		case ic::call : Cs << Pc; Pc=Ax; break;		case ic::ret  : if(Cs.Empty()) return false; Cs >> Pc; break;		case ic::svc  : return Service(Rx); break;		case ic::setrx: Mm(Rx); break;		case ic::setax: Mm(Ax); break;				case ic::movra: Rx=Ax; break;		case ic::movar: Ax=Rx; break;                  case ic::movrs: Mm(Tx); Tx=Sp-(Tx+sizeof(int))-Ds; break;				case ic::getrg: Rx=Reg[Ax]; break;		case ic::putrg: Reg[Ax]=Rx; break;				case ic::get  : Rx=Mm[Ds+Ax]; break;		case ic::put  : Mm[Ds+Ax]=Rx; break;		case ic::mpush: Ms << Rx; break;		case ic::mpop : Ms >> Rx; break;                  case ic::push : Mm[Sp]=Rx; Sp+=sizeof(int); break;                  case ic::popn : Mm(Tx); Sp-=Tx; break;                  case ic::poke : Mm(Tx); Mm[Sp-(Tx+sizeof(int))]=Rx; break;                  case ic::peek : Mm(Tx); Rx=Mm[Sp-(Tx+sizeof(int))]; break;				case ic::add  : Ms >> Ty >> Tx; Ms << Tx+Ty; break;		case ic::sub  : Ms >> Ty >> Tx; Ms << Tx-Ty; break;		case ic::mul  : Ms >> Ty >> Tx; Ms << Tx*Ty; break;		case ic::div  : Ms >> Ty >> Tx; Ms << Tx/Ty; break;		case ic::neg  : Ms >> Tx; Ms << -Tx; break;		case ic::eq   : Ms >> Ty >> Tx; Ms << int(Tx==Ty); break;		case ic::neq  : Ms >> Ty >> Tx; Ms << int(Tx!=Ty); break;		case ic::lt   : Ms >> Ty >> Tx; Ms << int(Txbreak
;
case ic::lteq : Ms >> Ty >> Tx; Ms << int(Tx<=Ty); break;
case ic::gt : Ms >> Ty >> Tx; Ms << int(Tx>Ty); break;
case ic::gteq : Ms >> Ty >> Tx; Ms << int(Tx>=Ty); break;

case ic::land : Ms >> Ty >> Tx; Ms << int(Tx && Ty); break;
case ic::lor : Ms >> Ty >> Tx; Ms << int(Tx || Ty); break;
case ic::lnot : Ms >> Tx; Ms << !Tx; break;

case ic::jmp : Mm(Tx); Pc=Tx; break;
case ic::jz : Mm(Tx); if(!Rx) Pc=Tx; break;

case ic::outn : Output(Rx); break;
case ic::outs : Output(Mm.Str(Ts+Rx)); break;
case ic::outnl: Output("\n"); break;
}

return true;
}




From writing compilers before, that should be sufficient to create a nice recursive sort of simple C-like language.

It might be worth revisiting later and adding in some optimisation instructions to replace common groups of instructions that the compiler spits out, but we shall see.

Going to write the compiler with Borland BCC55 since I find that easier to work with from the command line than the Microsoft compiler, and it doesn't actually need to interface with VS programs in any way.
Previous Entry GGE
Next Entry Scripting update
0 likes 2 comments

Comments

jman2050
I used a similar scripting system when I developed on my last project, Zelda Classic, except without services. Looking back, it probably would've been easier to implement something standard like LUA, but I can definitely say that the performance you get from making a low-level virtual machine (provided it's written correctly) is quite satisfactory.
January 25, 2007 04:03 PM
Aardvajk
Yeah, I'm not really too worried about the performance. I'm not sure if the service system will hit any problems really though, since it will be impossible to pass anything except ints in and out of the vm.

Still, I reckon it will serve as a good enough script engine for what I need for Udo.

Since it is a stack-based vm, it will be pretty trivial to write a recursive descent compiler for whatever scripting language I decide to go with. Just a question of building trees of polymorphic nodes out of expressions really.

I like this approach, because I could have two or three completely different languages, all with their own compilers, that output the virtual binary code for the vm. Not that I'm going to, but I could [smile].
January 25, 2007 04:54 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement