• entries
    743
  • comments
    1924
  • views
    580107

Scripting

Sign in to follow this  
Aardvajk

98 views

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 }

#double
65535

#offset
start:
setax loadpcx
call
outn
outnl
end

loadpcx:
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.
Sign in to follow this  


2 Comments


Recommended Comments

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.

Share this comment


Link to comment
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].

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now