defun-games / claylib

A Common Lisp 2D/3D game toolkit built on top of Raylib 4.5.
zlib License
71 stars 4 forks source link

Possibly easier use of multiple scenes #18

Closed mjkalyan closed 2 years ago

mjkalyan commented 2 years ago

An attempt at addressing https://github.com/defun-games/claylib/issues/17

We export game-scene and switch-scene, which uses a new special variable *scene* to track the current scene. Simply setfing *scene* and doing the loading/unloading at the beginning of a game loop iteration didn't seem like the best way to switch scenes, that's why there is the switch-scene convenience function. It allows you to fully switch scenes mid-loop.

As mentioned in 97d107a52549c10b0fc3d89de87ab676aa83c9e5, this is vulnerable to changes in *SCENE* whether by nested do-game-loop calls or otherwise.

Also this is not addressing potential loads inside a loop. If we don't want to load a scene entirely up-front, then we have to either

  1. load it smartly using set-up-scene (not exported yet) at some point before we need it (there's potential to do so in a non-blocking way here) so that switch-scene does less work, or
  2. let it get loaded on demand by switch-scene

It is probably safe to push the tear-down-scene call in switch-scene to another thread unless the scene's %free is :now and it really must be done now.

Let me know what you're thinking @shelvick. I changed core example 2 to use this new method so you can check that out.

mjkalyan commented 2 years ago

So, as it is, this PR folds with-scene logic into do-game-loop. You can see how it affects core example 2:

@@ -43,16 +46,13 @@
 (defun main ()
   (with-window (:title "raylib [core] example - basic screen manager")
     (do-game-loop (:livesupport t
+                   :scene *logo*
                    :vars ((scenes `(,*logo* ,@(alexandria:circular-list *title* *gameplay* *ending*)))
                           (frame-count 0)))
       (incf frame-count)
       (when (or (and (next-screen) (not (eql (car scenes) *logo*)))
                 (and (eql (car scenes) *logo*) (> frame-count (* 2 *target-fps*))))
-        (setf scenes (cdr scenes)))
-      (let ((scene (car scenes)))
-        (with-scene scene (:free :later)
-          (with-drawing (draw-scene-all scene)))))
+        (setf scenes (cdr scenes))
+        (switch-scene (car scenes)))
+      (with-drawing (draw-scene-all *scene*)))

And we control which scenes are freed when in make-scene. So in this case we let the default :free :now work on the *logo* scene, which is only used once, while using :free :later for the cyclic scenes.

My thoughts on the changes so far:

shelvick commented 2 years ago
  • I'm curious about whether we ever want multiple scenes drawn at the same time

Maybe. Remember that a scene is really just an encapsulation of variables and CLOS objects. There's nothing special about *scene* other than convenience, and there's nothing stopping the user from calling set-up-scene on a different scene or drawing objects directly from that scene. The problem you run into with multiple scenes is the same problem I brought up about with-scenes -- namespace collision. So basically you're choosing between multiple scenes (but having to call scene-object on everything) or having a single scene but you can have the convenience of exposing the variables automatically, as you mentioned in your third point. Personally, I prefer the latter.

  • maybe it makes sense to represent a UI as a scene

Fun fact: There's no loading involved with Raygui (or at least I don't think there is; haven't gotten far enough to be 100% sure). Most likely direction at this point is we'll do the same thing re: creating an object hierarchy. Maybe the GUI will use scenes or maybe it will be a slightly different abstraction but there should not be any asset-like classes involved except indirectly, like this example. I sure wish it had a cheatsheet though, ugh.

  • It kinda makes sense to let do-game-loop expose the scene objects (what with-scene-objects does) of the active *scene* automatically but I'm really not sure of the implications of this.

Yes for a single scene, no for multiple scenes. The implications would be that you can't reuse object names in your game logic, but I'd hope users wouldn't be trying to do that anyway.

shelvick commented 2 years ago
  • I'd probably change switch-scene to add-scene and rem-scene, and turn *scene* into *scenes*. This probably also means that *scenes* should contain names for the scenes like their assets/object lists.

Well... I'd be remiss if I didn't mention another approach. Use your multiple scenes technique, give each scene a unique name (so *scenes* is a hash table), and then you can still expose the variables and ensure they are unique by concatenating each name to be like level2-monster5. That has the potential to be kind of messy but also kind of awesome.

mjkalyan commented 2 years ago

So basically you're choosing between multiple scenes (but having to call scene-object on everything) or having a single scene but you can have the convenience of exposing the variables automatically, as you mentioned in your third point. Personally, I prefer the latter.

Okay, that makes sense to me! We'll go that route then.

give each scene a unique name (so *scenes* is a hash table), and then you can still expose the variables and ensure they are unique by concatenating each name to be like level2-monster5

I elided the details but this is basically what I was thinking. We can keep it in the back of our heads for now.

Maybe the GUI will use scenes or maybe it will be a slightly different abstraction

Good to know, I should look into raygui more.

mjkalyan commented 2 years ago

To complete before merge:

We want with-scene-objects to handle multiple scenes, so we're going to want multiple scenes loaded at once.

If I understand @shelvick's intention (from #19), I think one approach that fits is having do-game-loop track a list of active (loaded/should-be-loaded) *scenes* (instead of *scene*). do-game-loop would initially call set-up-scene on all the scenes in *scenes*, and eventually tear them all down. Then we detect differences in *scenes* and rectify them (setting up/tearing down) at the start of a loop iteration.

But... It also seems fine to just let the user track a scene list of there own, e.g.

(let ((my-scenes (list *level-1* *level-2*)))
  (mapcar #'set-up-scene my-scenes)
  (with-scene-objects my-scenes
    (do-game-loop ...))) ;; I can decide to set up or tear down more scenes as I please

or, provide with-scenes to ease this a bit

(with-scenes my-scenes
  (with-scene-objects my-scenes
    (do-game-loop ...)))

We can still set up and tear down scenes in the loop as long as with-scenes doesn't have a problem with tearing down scenes that are already torn down.

Since the problem we were trying to address was just better handling of multiple scenes and we're not doing any binding stuff in do-game-loop, I don't think we even need *scene* or switch-scene at all.

shelvick commented 2 years ago

I think one approach that fits is having do-game-loop track a list of active (loaded/should-be-loaded) *scenes* (instead of *scene*). do-game-loop would initially call set-up-scene on all the scenes in *scenes*, and eventually tear them all down. Then we detect differences in *scenes* and rectify them (setting up/tearing down) at the start of a loop iteration.

This seems straightforward enough to me. Is there any reason not to do it?

We can still set up and tear down scenes in the loop as long as with-scenes doesn't have a problem with tearing down scenes that are already torn down.

It shouldn't. There may still be bugs but in general I was pretty careful to check for NIL's and all that in the free methods.

Since the problem we were trying to address was just better handling of multiple scenes and we're not doing any binding stuff in do-game-loop, I don't think we even need *scene* or switch-scene at all.

Agreed. add-scene and rem-scene could still be convenient though even if all they do is push/pop the list and call set-up-scene/tear-down-scene. Actually, we don't need those either if we're doing that in do-game-loop, do we?

mjkalyan commented 2 years ago

Actually, we don't need those either if we're doing that in do-game-loop, do we?

Correct, assuming we use the beginning-of-iteration approach. But add-scene/rem-scene would probably be faster (and easier to write) than checking each iteration for changes in a list of *scenes*. But again, we would only take this approach with a *scene* or *scenes* variable.

Instead, I think we go forwith-scenes to do an initial setup and a catch-all tear down, then allow users to manage additional scenes as they please with set-up-scene/tear-down-scene.

mjkalyan commented 2 years ago

Alright, I switched core example 2 to with-scenes. If you like this, @shelvick, then I'll update all the examples and we can start to merge this.

shelvick commented 2 years ago

At first I wasn't sure how I felt about with-scenes instead of just doing it in the game loop. After seeing the example code I think it makes sense. It's a good separation and still easy enough to handle more scenes within the loop if you want to.

Onward and upward! :shipit: