fleabitdev / glsp

The GameLisp scripting language
https://gamelisp.rs/
Apache License 2.0
395 stars 13 forks source link

Call for suggestions: Hotloading #10

Open fleabitdev opened 4 years ago

fleabitdev commented 4 years ago

Opening this issue to gather suggestions for how best to implement hotloading (that is: editing a game's source code, saving it, and seeing the changes reflected in a running game without needing to restart it).

GameLisp is extremely late-bound, which is both a blessing and a curse in this case. A blessing, because a lot of code should "just work" if we tweak bind-global! slightly and then run load again with the modified source file. A curse, because the user is free to make changes which have ridiculous knock-on effects, like redefining the defclass macro or the load function, or storing a reference to a class in a local variable, which is captured by a closure, which is stored in a Lib...

I'm struggling to decide whether to aim for a universal, highly-correct solution, or aim for a much simpler solution which will unpredictably fail sometimes. I chose the second option when designing compilation, and I'm quite happy with the result.

It would be easy enough to detect when a hot load call attempts to do something more complicated than calling bind-global!, in which case we could emit a warning, or fall back to reloading the entire source tree, or force a restart. In other words, even if naive hot-loading fails 5% of the time, we could make it a graceful failure rather than an unpleasant, confusing one.

I'm considering two options for dealing with dangling references:

Also relevant: #8, #9

fleabitdev commented 4 years ago

@baszalmstra, one of the developers of Mun, suggested that I look into how hot reloading of games is handled in the Lua ecosystem. I've found some very useful information there.

Similar to my thoughts above, it looks like hot reloading in Lua is usually achieved by simply re-running a source file to re-initialize various globals. Because Lua is late-bound, this tends to work very well without any special intervention from the programmer. There are three common corner cases:

I can think of four additional corner cases in GameLisp which aren't present in Lua:

Finally, small syntax errors while editing code are so common that I would consider error-recovery to be an essential feature. However, if an error interrupts hot-reloading partway through reloading a game's source tree, the codebase might be left in an incoherent state. I can see two possible solutions: (1) cache all globals and restore their old values if an error occurs, or (2) encourage the programmer to pause the game and display a "please try again" prompt when an error occurs during hotloading.

phoe commented 4 years ago

A curse, because the user is free to make changes which have ridiculous knock-on effects, like redefining the defclass macro or the load function

Common Lisp user here, explaining how we approach that issue.

The CL standard explicily states that trying to un/redefine any of the built-in standard functionality results in undefined behavior - so the implementation is allowed to permit this, signal an error, break in subtle ways, or outright crash. I think that this approach works well for CL as a language that is meant to empower the programmer: if the user is brave enough to redefine built-in functionality, they better know what they are doing, since they are on their own at this moment.

As for load-time object identity, Common Lisp implementations also handle the issue of loading compiled files (called FASLs in the CL world) and resolving load-time references to other objects. I'm not very knowledgeable on that topic. This part is not fully standardized and each implementation does it slightly differently; perhaps other lispers will be able to provide more information, if required.

cedric-h commented 4 years ago

I've had a great deal of success with hotloading using GameLisp as it exists right now, by simply returning an arr of classes from my entry.glsp, and then having entities in my game which supply the name of the class they'd like to dictate their behavior. If a change in the files are detected, the code is re-evaluated, and a new list of classes is returned. The classes are defined locally using let-class, so simply calling glsp::load("entry.glsp") is enough to get completely fresh versions. I avoid globals and def* entirely. Instances of objects which have behavior dictated by their class hold a Root<Obj> which points to an instance of their class. When the entry.glsp is reloaded, entities which have classes associated with their behavior are iterated through, and if a class with the same name can be found, a new instance of it is associated with the entity. (Though classes can override this behavior with their meth reload, which is passed the new class and returns an instance of it to replace the instance of the old class that's associated with the entity.)

(let-class Inchworm
    (field heading)

    (init (ent)
        (= @heading (rand-vec2 1.0 1.0)))

    (const static-update (fn ()
        (when (< 0.001 (rand 1.0))
            (let plant (rand-select ..(instances-of 'GrassClump))
                 new-worm (spawn-instance 'Inchworm))
            (= [new-worm 'pos] [plant 'pos]))))

    (meth update (ent)
        (= @heading (.norm (.+ (rand-vec2 0.1 0.1) @heading)))
        (.move ent (.* @heading 0.001))))