Closed JayhawkZombie closed 6 years ago
Thanks for the detailed report.
For what it's worth, you're using the bytecode API exactly as how I'd imagined someone might. I very intentionally exposed a buffer containing raw bytecode for exactly this scenario, so no, you're not using it incorrectly at all. I'm reluctant to impose a particular serialization methodology, because games and game engines tend to do things a particular way, as you mentioned with Unreal.
Feel free to post any additional feedback, suggestions, or your general experience in using the library.
@JamesBoer Semi-related: Is it possible to store and restore the state of an unfinished script? As in, saving the memory and instruction pointer?
If not, what might be the best approach to avoid losing state through coroutines, i.e. when a game is saved and quit? For example, think some game corp releasing a modding SDK.
Maybe passing an option to forbid the usage of wait would work? In that case it'd probably best to turn those global parameters into local parameters, though...
I had considered this early in Jinx's design, but decided that having save/restore functionality at such a low level wasn't a feature I felt like tackling. In my own game, I simply write any changed state back to native functions for storage.
This would be a pretty major task to undertake. I can't promise anything, but I'll give it some thought and see how feasible something like this would be.
@JamesBoer Thanks. Don't worry about my case though, I probably won't keep state in the vm at all.
Out of interest: In your own game, how do you synchronize the coroutine back to the restored state? I mean, is there some obvious way better than a massive amount of branches?
Also, call me paranoid, but a big, fat disclaimer about running untrusted bytecode being a bad idea probably wouldn't hurt your documentation.
@chack05 Mostly I use Jinx for scripted in-game events. There's actually no way to save manually. The game just autosaves at fixed locations and occasionally after major events. I don't worry about restoring the world state precisely. Instead, the world will just respawn agents and let them restart their behaviors.
For simple cases, when I just need to fire a script off once, this is handled automatically by my game engine, if I set a world trigger to "persistent" and "one-shot", it remembers it's state without any intervention. So, that handles about 95% of the cases for me. Each world trigger has an "event" script and a "post-event" script. The script runs until whatever arbitrary success I decide upon, such as the destruction of some specific spawns, for instance, and then the post-event script runs, which may contain some other events (like a small cutscene), or sometimes just a "save" function.
There are some cases, though, for instance, when I want a trigger to change behavior based on the state of the world, like if you've progressed to a specific point in the story. I have a simple set of functions which can set or get arbitrary data via a central "game state" repository. I just have to identify it by object and property names, and then get or set a variable. Since these are string-based names, it makes it easy to set a value in one place in the game via a script, then check it somewhere else in a different script, and change the behavior accordingly.
So, it's fairly rudimentary, but it's been working pretty well so far.
@JamesBoer Thank you for the insight.
This isn't so much of an "issue" as it is a suggestion (or here's a way I've used this?).
Since Jinx compiles down to byte-code and we can directly access that byte-code, I've tried to think of a way of precompiling scripts.
I use cereal for serializing my game objects into archives, and it has a portable binary format that, as far as I've seen, doesn't seem to have any issues with endianness errors.
Now it's not the nicest solution, but it works as a proof on concept.
A class called
BytecodeInstance
stores compiled byte-code and serializes/deserializes it to/from binary archives, respectively.The byte-code is just saved exactly as it is in memory, as an array of
uint8_t
elements.It saves the length, too, of course. And cereal adds some bookkeeping data, so it's not exactly the same size as just the byte-code, but the bookkeeping data is minimal.
And a
BytecodeArchive
class maintains a collection ofBytecodeInstance
s, and creates new archives from byte-code.This is all assuming I didn't completely botch the usage of your API, but it hasn't crashed yet.
For testing these precompiled scripts, I registered 2 functions, in two different libraries.
I had to generate the precompiled scripts once, so I did so by just using the same process one would for compiling scripts from source, then archiving the byte-code, then finally serializing the byte-codes into a binary archive on disk.
After that point, I can just load it up easily (assuming I still added the native functions like above):
After that, the main program looks like:
Maybe it sounds trivial, or not essential, but it drastically speeds up loading times when booting up the engine.
The binary archives are, for the most part, unreadable, but most literal strings remain in-tact.
For the example above, the following is generated (if viewed as a hex dump) for the binary archive (for a single copy of the 4 scripts):
(where your signature "JINX" header is sitting inconspicuously on the first and second line
004a 494e 5800
, and at the beginning of the byte-code for every script saved).Looking at it as UTF-8 instead of a hex dump (in Notepad):
It looks different in Sublime. Go figure. I can shove that through a base64 encoder and obfuscate it, but that may be more hassle than its worth. There is no compression, I should add. It's just saved in raw binary.
Since Jinx stresses heavy testing, I tested compiling:
To avoid performance drops from too many allocations, I used Boost's memory pools (I did not change Jinx's custom allocator with global params - it always got slower when I did, so I decided to just leave it be).
400 scripts
From Source: 6.17897 seconds
From Precompiled Byte-Code: 0.025639 seconds
Archive Size: 218KB
Overall: 0.4% as much time.
1400 scripts
From Source: 21.8528 seconds
From Precompiled Byte-Code: 0.084868 seconds
Archive Size: 757KB
Overall: 0.3% as much time.
Anyway, to end a long post, I thought I'd share how I'm packaging scripts so that I only have to compile my scripts once.
Added benefit: If there's a bug in a script, all I have to do is remake the .bytecode file and patch that instead of the entire executable.
As for the strings remaining readable, I initially got the inspiration for serializing everything into game object archives from Unreal Engine. I've studied relatively closely how their serialization works, and many of their literal strings remain in-tact when serialized into shipped content (you can also see the names of the member variables of their classes in the object archives). So I imagine it's not an issue most people are really that concerned with.