wareya |
Posted on 19-01-29, 01:45 (revision 1)
|
Post: #32 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
Well, it's been well over five years since I first started getting into game engine programming, and I guess it's finally official: I didn't just pull off the "make your own game engine" meme. I didn't just pull off the "make your own game logic scripting language" meme. I made an insane high-level programming language with support for runtime procedural metaprogramming and a simple game engine around it. I took two stabs at making the language before, but both old versions were a lot worse than what I have now. So, here it is: https://github.com/wareya/gammakit https://github.com/wareya/magmakit https://streamable.com/p7xrd Gammakit is a ridiculous toy programming language that supports runtime code generation, lambdas, generators, first-class primitive data structures (arrays, dicts, maps), and coherent integration into the parent application (magmakit)'s design philosophy with custom types. It has support for GameMaker-like objects and wrapping the current scope with that of a given instance (i.e. with()), and also implicitly looping over all extant instances of a given object (i.e. GameMaker's specific with()). Scope looks just like lexical scope, but nested functions don't close over the scope they're defined in. Functions can be passed around in variables. Global variables are supported but have to be prefixed with "global."; global functions exist and don't need to be prefixed. There are "subroutine"-like functions which execute as though they were code written inline where they were called (i.e. they can access the parent stack frame). Primitive collections stored in variables can be modified with method-like function bindings, like myarray->remove(5). The language doesn't have a concept of references, except for the manually-managed IDs of instances. Yes, really. Gammakit is implemented without anything resembling a garbage collector, and without doing any analysis of what variables or values belong to what code. Everything is managed at runtime. The language's grammar has a declarative specification. The parser is a PEG-like recursive descent parser with support for backtracking. The compiler compiles to bytecode, which is executed in an ad-hoc virtual machine. There are built-in binding functions like "print", but the parent application doesn't have to provide them to the interpreter. Code can be generated at runtime using parsing and compilation bindings, resulting in a function-like value that you can store in a variable and pass around cheaply and call however many times you want. Did I mention that it supports generators? You know, semicoroutines? Those things that are super useful for AI scripts, visual novels, and virtual machines? And it's not JUST a toy. I have a proof-of-concept of the very beginnings of a game written in Magmakit, a toy game engine that adds bindings for things like sprite loading, drawing, and input, and has a mainloop. Let me link that streamable again. https://streamable.com/p7xrd THIS is what you get when you go "I don't like this. I want to make my own." instead of just making a game in an existing game engine. A dev art sprite jumping around on tiles. |
Screwtape |
Posted on 19-01-30, 04:38
|
Full mod
Post: #99 of 443 Since: 10-30-18 Last post: 1101 days Last view: 172 days |
> It has support for GameMaker-like objects and wrapping the current scope with that of a given instance (i.e. with()) ...so, dynamic scope? That's normally considered a downside for predictability reasons (changing the name of a variable in a function that calls another function that eventually calls a callback can break your program), but I guess it's got significant upsides for your specific domain? > There are "subroutine"-like functions which execute as though they were code written inline where they were called Can "break" in a subroutine break out of a loop in the caller? > The language doesn't have a concept of references, except for the manually-managed IDs of instances. So... automatic reference counting, or actual "I forgot to decref, therefore memory leak" and "I forgot to incref, therefore use-after-free"? > The parser is a PEG-like recursive descent parser with support for backtracking. Are you using a parser-combinator library like pest or nom, or doing the whole thing from scratch? If it's from scratch, do you use the packrat optimization? Do you have the cut operator to clean up the packrat cache? BTW, your readme is a bit broken, and is using double-bullets for nested lists. > var myf = compile_text("print(\"test\");"); You should totally adopt Rust's raw-string-literal syntax, it's simple to write and easy to parse. > for(var i = 0; i < max; i += 1) A language with first-class generators, but C-style for loops? :/ > myotherast = rewrite(myotherast, [](ast) { A language with first-class lambdas, but awkward C++ style syntax instead of nice Rust syntax? :/ > TODO: > - ternary operators You're probably sensing a theme here, but I've really enjoyed Rust's "everything is an expression" consistency over the traditional C "expressions and atatements are totally different, but this expression does basically the same thing as that statement". > - inheritance? how would it work? I've heard a lot of people talking about how entity-component-system libraries are much better for game development than traditional inheritance; it'd be neat to explore a gamedev-optimized language that natively used ECS for its data structures rather than inheritance. The ending of the words is ALMSIVI. |
wareya |
Posted on 19-01-30, 05:54 (revision 7)
|
Post: #35 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
> ...so, dynamic scope? That's normally considered a downside for predictability reasons (changing the name of a variable in a function that calls another function that eventually calls a callback can break your program), but I guess it's got significant upsides for your specific domain? To put it another way, with() blocks just have another source to search for variables for - the instance you give to with(). If it fails to find a variable in the current frame, it'll look in the instance you gave with(). Like JS's with. But unlike JS's with, it also loops over instances if you pass it an object ID instead of an instance ID. This sounds bad, but it dramatically simplifies very simple system-as-in-the-S-in-ECS style state update code. > Can "break" in a subroutine break out of a loop in the caller? Hey now, I'm not THAT crazy. The way they're implemented is that, when the interpreter searches for a variable, it can search outside from subroutine-like function frames, but not normal function frames. > So... automatic reference counting, or actual "I forgot to decref, therefore memory leak" and "I forgot to incref, therefore use-after-free"? "Instances" are manually managed with instance_create() and instance_kill(). Everything else is automatically managed and copied by value whenever it's assigned to a new variable or passed to a function. Both of these design choices mirror GameMaker; "instances" belong to the game world, everything else belongs to the current code. GameMaker only has floats and strings as primitive types though, not also collections like Gammakit does. > Are you using a parser-combinator library like pest or nom, or doing the whole thing from scratch? From scratch. > If it's from scratch, do you use the packrat optimization? Do you have the cut operator to clean up the packrat cache? I have no idea what that is, so probably not. EDIT: I looked packrat parsing up again and realized that it's conceptually similar to how the viterbi algorithm works, up until the part where it decides what parse to pick, so in theory I could probably do it. > BTW, your readme is a bit broken, and is using double-bullets for nested lists. I was using "-- ..." before when it was plain text, but it didn't work, so I just changed it to "- - ..." > You should totally adopt Rust's raw-string-literal syntax, it's simple to write and easy to parse. I might, but I want to handle multi-line string literals first. > A language with first-class generators, but C-style for loops? :/ There's for-each loops (i.e. "for ( varname in collection )"), but I don't have a range function to produce a collection, since it's more expensive, since I don't have lazy collections and for-each loops don't work on generators yet. (generators currently work like "while(generator) invoke generator;".) > A language with first-class lambdas, but awkward C++ style syntax instead of nice Rust syntax? :/ This is the only lambda syntax that makes sense for gammakit, since there aren't references anywhere and the compiler and runtime are too simplistic for me to want to do implicit captures of specific variables. > You're probably sensing a theme here, but I've really enjoyed Rust's "everything is an expression" consistency over the traditional C "expressions and atatements are totally different, but this expression does basically the same thing as that statement". The way the compiler is designed would make "everything is an expression" extremely fragile. I considered it a few times but put the thought on the back burner for "maybe if I make a less insane language some day". That reminds me - I did remember to make it so "and" and "or" short-circuit, thankfully. (Old versions of GameMaker don't.) > I've heard a lot of people talking about how entity-component-system libraries are much better for game development than traditional inheritance; it'd be neat to explore a gamedev-optimized language that natively used ECS for its data structures rather than inheritance. Inheritance is useful for the specific case of making pre-baked variants of objects that construct/destruct differently but have all the same "properties", rather than passing configuration data to the constructor or having separate factories for different variants. This is what Gang Garrison 2 uses inheritance for, for example. In general, since gammakit lets you store functions in variables (game maker forces you to use execute_string(), which is very slow), it would be realistic to avoid inheritance by ~pretending~ to have overloaded functions. This wouldn't help with variant-specific constructors/destructors, but it's there. You wouldn't be able to call the "parent's" version of those functions, though, which is why true inheritance might be useful. |
Sintendo |
Posted on 19-02-02, 13:32
|
Post: #5 of 17 Since: 10-29-18 Last post: 1802 days Last view: 1373 days |
Posted by wareyaI could've gone a lifetime without knowing that existed in JavaScript, and I've been writing Node.js code professionally for four years. |
wareya |
Posted on 19-02-02, 13:54 (revision 2)
|
Post: #39 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
Yeah, JS doesn't give you a whole lot of reason to use "with", and the way that JS objects work and how idiomatic JS looks make it very bad form to use it, just like, in general. |
wareya |
Posted on 19-02-02, 22:25 (revision 3)
|
Post: #40 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
Just realized that switching to packrat parsing would fix a design flaw in my current parser caused by backtracking being limited in how it can maneuver around failed grammar points (i.e. if a grammar point fails, it doesn't try looking for ambiguous rules inside the point that fails, it just tries the next alternative to the one that failed). Or, rather, packrat parsing would make it much easier to make my "backtracking" function correctly. No idea if I'll actually switch, but it's funny that the method with the faster worst case is also easier to implement "well". |
wareya |
Posted on 19-04-04, 17:31 (revision 2)
|
Post: #55 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
Got around to adding a ternary operator (each argument must be wrapped in parens because I don't want to worry about how its precedence works), slightly better syntax for indexing into dictionaries with literal strings (as though they're structs), and generators actually working with "for ( _ in _ )" loops now (before you had to truth-test and manually invoke them in a loop to have the same effect - and yes, you can manually invoke generators one step at a time). Change 2 (the one about dicts) is good: https://github.com/wareya/magmakit/commit/337c0b13bd71848a29d0833dcc57066deeacc45e Also, packrat parsing wouldn't actually fix by parser's design flaw. Apparently my parser's design flaw is just a normal and well-understood property of PEG grammars. |
wertigon |
Posted on 19-04-04, 20:42
|
Post: #28 of 205
Since: 11-24-18 Last post: 155 days Last view: 27 days |
Posted by wareya Wait, so if I read this right... You just made the emacs equivalent for game engines? Looks... Promising. Might check it out further once I get a new gaming/developer rig. :) |
wareya |
Posted on 19-04-05, 03:56
|
Post: #56 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
You're probably going to be a little disappointed. "Procedural metaprogramming" in this case amounts to asking for the AST corresponding to some string, changing that AST, then compiling it into a function object. Nowhere near as elegant as lisp macros, though it's still obviously very powerful. |
wareya |
Posted on 19-08-06, 08:28 (revision 2)
|
Post: #77 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
Magmakit's current "demo" is a trivial platformer with tiles where all collision is implemented using AABB sweep-distance-normal tracing and a continuous contact solver. It's very slow. Runs at a decent framerate with only a dozen or so tiles, but very slow. One of the unspoken design principles behind magmakit is that if a developer really needs to do something performance sensitive they should hack at the engine itself (magmakit) instead of doing it in gammakit code (which is slow) and hoping for the best. I began constructing the skeleton for a Real, Proper collision detection system, which, naturally, being the masochist I am, I started at the broadphase instead of just looping through every single static and dynamic object and laughing at how much faster Rust is than Gammakit, because I figured I might as well do some "hard" programming to remind myself how much it sucks and make myself play more video games or something (which I haven't been doing enough of lately). I decided to implement a BVH system with AABBs (so, a dynamic AABB tree), because querying a quadtree or grid requires keeping a hashset/btreeset of all known potentially-hit objects, and I figure BVHs are better because they don't have that problem (this is the full extent of the thought I put into it; I really just wanted to do something I haven't done before, and I've made quadtrees and grids before). Again, people seem to call the type of BVH I'm implemented a "dynamic AABB tree". The most straightforward explanation of dynamic AABB trees I could find is this blog post here: https://www.randygaul.net/2013/08/06/dynamic-aabb-tree/ - it's a bit skimpy on some of the details, so I'm not sure I'm going to implement everything properly (no, I'm sure that I won't), but it's concise and seems straightforward enough. After a couple hours of bashing on my keyboard, I have a working insert-only BVH system, or at least I think I do, there isn't really a formal specification of what BVHs really function like for me to check or anything like that. It turns out that BVH systems are very strongly heuristic-based, and changing things slightly can have a huge impact on the resulting arrangement of bounding volumes. I have an album of how my BVH grows as I add more tiles to it here: https://imgur.com/a/4cT4vTw It seems that building a BVH gradually will inevitably result in strange obviously non-optimal collections of bounding volumes, as wikipedia puts it "BVHs also naturally support inserting and removing objects without full rebuild, but with resulting BVH having usually worse query performance compared to full rebuild". It seems that the right way to avoid this is to completely rebuild the entire hierarchy after a while if the tree is too inefficient, but I'm not really interested yet. Maybe once everything else is in place. |
wareya |
Posted on 19-09-13, 16:36 (revision 1)
|
Post: #81 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
Spent most of two days changing gammakit to have a static lexical scope model rather than emulating lexical scope at runtime. The motivation was to try to bring down the runtime of a 5000-iterations "nbody" demo down below 0.32 seconds, which is the time that demo has on my computer when run in Game Maker 8.0's version of GML. Gammakit is superficially very similar to GML, but way, way, way more powerful, so GML is the most fitting comparison. After those two days of work, with many aspects of the language currently on very shaky ground, I'm happy to say that I managed to bring the 5000 iterations nbodies demo in gammakit down from 0.75 seconds to around 0.5. Yep. That's it. Gammakit performs zero real optimization in the process of converting source code to bytecode. There is no analysis of variable accesses or anything like that. Up next: trying to eliminate "ValRef", a mutable smart pointer that is probably itself another performance hole. |
wareya |
Posted on 19-09-14, 22:22
|
Post: #82 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
And so it is done. |
wareya |
Posted on 19-10-15, 20:33
|
Post: #96 of 100 Since: 10-30-18 Last post: 1781 days Last view: 1346 days |
I wrote a proper writeup of how gammakit was designed the other day. If you want to make your brain hurt, go ahead and give it a read. https://github.com/wareya/notes/blob/master/offtopic/gammakit.md |