#nah its a user scriptable game so if the

1 messages · Page 1 of 1 (latest)

neon bridge
#

What does your user script look like, can they be swapped out at runtime, and how do they get executed?

#

I'm only loosely following conversations around here, but I'm curious since I'm also making a game that's heavily user scripting based and have to worry about performance (especially so for my game since it's targeting low end mobile), thought it'd be nice to compare our approaches and exchange some notes.

#

Ah not sure how Discord threads work, in case you don't see it @soft yew

soft yew
#

The scripting I made language looks somewhat like javascript. It can be updated at runtime, since it's a game where you write scripts to control tanks, it spawns 1 tank per script, ive exposed a number if built in functions to the scripting language that people can call in the script for setting speed and turning and shooting etc. The scripts are tokenized then compiled into a set of nested closures that represent their abstract syntax tree. I do a bit of additional constant folding while the compile runs to collapse some of the closures into just their result but it could ve improved more

#

If the scripts contain a start and an update function, it binds them to the start and update of the unity monobehaviour that is attached to the tank

soft yew
#

Upside of the limited datatypes means everything can be stack allocated so it's impossible to allocate heap memory with my currently available scripting syntax which is nice from a performance standpoint

neon bridge
#

Ah I see.

#

Mine is a bit different, the audience of my game will likely be very novice programmers (and perhaps programmers that got into programmer solely because of the game) so I'd like it to offer many possible scripting languages especially the popular ones, so that they can still use all of the knowledge and toolings of these existing popular languages, rather than having to learn an exotic one.

#

So my design being the script format is an already parsed AST, and the game compiles that AST into bytecode and run them through a bytecode interpreter with Burst.
On the user side, they would have different tooling for each language (currently only JS/TS) which will compile their idiomatic code in that language into the AST (with some limitations)

soft yew
#

Ah thats fair, I wanted something that I had total control over so I can do fun things like tie game mechanics to instruction count, or be able to break out of infinit loops or stack overflows safely, etc.

#

Also this isn't like a big project, just a fun little one to pit the devs at my company against each other by having them write tank scripts lol

neon bridge
#

Ah I see, yeah that makes lots of sense then.

#

I do wonder how the performance goes, it seems like you've put a great deal of optimization into it.

soft yew
#

It works well with only 2 tanks in the scene casting roughly 300 rays each I get 1200 fps, though as the number of tanks goes up that goes down reasonably quickly since it's not multi threaded

#

But I can still run like 30 tanks at around 60 fps so meh

neon bridge
#

Oh I meant the scripting.

soft yew
#

Ah, yeah I got rid of all memory allocation, I pool the context object to recycle them, etc

#

The first version of the compiler was running my benchmark script at 90 fps, now it runs the same script at 280 fps

neon bridge
#

Nice.

#

What's the script like?

soft yew
#

I'll upload it to paste bin or something, sec

#

my benchmark is 4 of those scripts running controlling 4 tanks on the scene

#

its not a super well optimized script, just something i threw togeather while building the scripting engine for the game. So there are a lot of improvements that can be made from a logic/efficiency standpoint. I just keep using that one since i can compare performance improvements it with old builds using it

neon bridge
#

Yeah good to use the same benchmark script across all builds.

soft yew
#

with a il2cpp build and 4 of those scripts i get like 480 fps, so its performance is good enough imo, now the only things i think i can do to improve it are to add some form of multi threading support, but thats annoying to do with the scripts need to be able to call functions that can do raycasts and read components off of the raycast targets

#

and apart from that, i could potentially look at more ways to fold instructions down, currently it does a simple folding style where it attempts to execute a ast branch without a context, and if its able to without any exceptions being thrown, it knows it can fold that branch down into just a function that returns the result of that branch

#

the downside of this is, all function calls currently have to be written to the context, and you cant currently const fold something that needs access to the context, so what id need to do is to setup a sort of placeholder system, where while it compiles everything, functions are written to a placeholder table that can be used instead of the context so it can directly inline the function lookup and call(instead of having to pull it from the context dictionary each call) and potentially fold it out entirely if the body of the function is all const-foldable.

neon bridge
#

Interesting.

#

In mine, optimizations like constant folding happen both on the compiler side (the tooling that compiles user script of a certain language like JS, to the AST format the game can read) and also on the AST side.
So given an AST node of shape If(true, branchA, branchB) that can completely collapse to just branchA, and a node And(true, dynamicValueA, dynamicValueB, true, true, false, dynamicValueC) it can collapse to And(dynamicValueA, dynamicValueB, false).

soft yew
#

ah yeah mine is a full source to code compiler, it doesn't make any sort of intermediary file so all optimizations are done while it parses the source and builds the 'closure-tree'

#

i also dont bother pruning reserved words during tokenization, and instead treat all words as identifiers, and leave it up to the context to determin if its a reserved word or not and throw an error if its compiled using teh debugging flag, so not super traditional in that regard as well

neon bridge
#

Another optimizations that I've found to be extremely effective, is a readonly memory region that's only writable before the game starts.
Before the game starts, a preprocess callback in the user script is called, which user script can write to that memory region however they want (for example, calculating constants that are only known at runtime, like object size which depends on screen size). Once the game starts, that memory region becomes readonly and cannot be modified.
Because of that constraint, all AST nodes that read from that memory region can be collapsed to just a constant node, and then more constant folding can happen on top of it.
This doesn't just affect simple stuffs, but also think of things like game wide settings that cannot be changed once game started. For example a spectator mode where you cannot play and can only watch, knowing it cannot be change all AST nodes like If(spectatorMode, spectateLogic, participantLogic) can all be collapsed, which means most game wide settings have zero branching cost at runtime.

soft yew
#

only the expression cost of the check is done for branching, but if that can be folded out, then it is, there is no cost for the bodies of the functions if they are not being reached