How to manage game scenes ?

edited November 8 in Help request

Hello,

i'm looking for the most elegant way to manage game scenes ?

My first thought was to create a GameScene class and then create "MainMenuScene", "PlayScene" and "OptionsScene" classes and simply call current_scene.run() in my Run function.

But this solution felt ... hacky, so what's the official way to achieve this ?

Thanks :blush:

Comments

  • edited November 8

    Interesting timing as I am actually working on a video sample on that topic. The video isn't ready but the project is already available if you want to have a look: https://github.com/orx/sceneswap

    To quickly summarize, the scenes are just simple objects, there's no dedicated code to handle them, another object provides the transition between scenes as we store the current scene in the Runtime section with the Scene property.

    If you're curious the other two videos can be watched at

    Don't hesitate if you have any questions regarding the samples, they are a bit more advanced that beginner-level, focusing on the data/config-driven approach.

  • Thanks for the reply.

    I'll be honest, I understand close to nothing in your source.

    So here is what i've (or at least I guess i've) understood, please correct me if i'm wrong.

    orxConfig_PushSection("Main");
    orxInput_SetValue(orxConfig_GetListString("Transitions", 0), orxFLOAT_1);
    

    OK so I guess the orxConfig_GetListString function return ToSceneRed and the orxInput_SetValue simulate a keypress on the Key wich have ToSceneRed` as a value ? (KEY_1).

    Why do we do that instead of loading the first scene simply by createing SceneRed. It's the first one, we don't need transition ? And using input to change scenes isn't possible in a real project ?

    // Register update function
        orxClock_Register(orxClock_FindFirst(orx2F(-1.0f), orxCLOCK_TYPE_CORE), &Update, orxNULL, orxMODULE_ID_MAIN, orxCLOCK_PRIORITY_NORMAL);
    

    The orxClock_FindFirst is returning a clock and I supposed the second parameter means « give me the main clock ».
    For the orxClock_Register it register a callback method we give it the main clock, the function we want to call, but the last 3 parameters are blurry to me. is the module something thread related ? What are the priority and when do that matter ?

    for (orxS32 i = 0, iCount = orxConfig_GetListCount("Transitions"); i < iCount; i++)
    {
        const orxSTRING zTransition = orxConfig_GetListString("Transitions", i);
        if (orxInput_HasBeenActivated(zTransition))
        {
            orxObject_CreateFromConfig(zTransition);
            break;
        }
    }
    I image we just fetch the keybind for every possible transitions, if we press a key (eg.KEY_2) we will create the object [ToSceneYellow].
    
    And now it's when I don't understand anymore.
    
    So the object `[ToScene]` is kinda like an interface for all switching object.
    `LifeTime = track` mean the object is alive until the timeline is ended and `TrackList = @` probably mean self ? 
    
    So if we press the KEY_2 we spawn this object 
    

    [[email protected]]
    0 = > Get Runtime Scene, Object.AddTrack < MoveOut, > Object.Create SceneYellow, Object.AddTrack < MoveIn```

    0 = 0s i got it but the value of this key is giving me headache :lol: .

    The only thing I understand is Object.Create SceneYellow, pretty obvious name (and we used this command in the Beginner's Guide).

    I understand that MoveOutand MoveIn are track with contain animation (scroll?) to display the move out the scene and move in the new one. So Get Runtime Scene is probably something like "give me the current scene". But the whole syntax using <, > and ^ (in the [MoveOut] & [MoveIn]) is totally new to me and I don't have any idea on what that mean 🤔

    I think that's all, sorry for all the questions i'm still a beginner in game dev so I probably don't understand very basic concepts.

    Thanks I didn't took the [ToSceneBlue] track as an example :lol: but yeah basically all those symbol (,, <, >, ^, #) are super confusing. I tought the # symbol was used to chain thing but sometimes you use it and I don't understand why.

  • edited November 9

    @Vick said:
    Thanks for the reply.

    My pleasure!

    I'll be honest, I understand close to nothing in your source.

    No worries, as I mentioned those samples are more advanced and make the assumptions people are already familiar with orx itself.

    orxConfig_PushSection("Main");
    orxInput_SetValue(orxConfig_GetListString("Transitions", 0), orxFLOAT_1);
    

    OK so I guess the orxConfig_GetListString function return ToSceneRed and the orxInput_SetValue simulate a keypress on the Key wich have ToSceneRed` as a value ? (KEY_1).

    Right, in this case, we are using the same name for the input and the scene transition object. It's a choice to make the whole thing simpler, but it's just a mean for the user to trigger the transitions themselves and play around with them.
    In the end, the inputs here are not very relevant to the feature we want to showcase (scene swapping).

    Why do we do that instead of loading the first scene simply by createing SceneRed. It's the first one, we don't need transition ? And using input to change scenes isn't possible in a real project ?

    As stated above, the inputs is simply to let the user play around with the transitions, interrupting them and see what happens. We also use it for the first scene this way they can modify it the way they want to play around with the system, while not having them touch the code at all. For example, if they wanted to have the first scene be the spinning one, they would simply need to modify the Transitions property.

    // Register update function
        orxClock_Register(orxClock_FindFirst(orx2F(-1.0f), orxCLOCK_TYPE_CORE), &Update, orxNULL, orxMODULE_ID_MAIN, orxCLOCK_PRIORITY_NORMAL);
    

    The orxClock_FindFirst is returning a clock and I supposed the second parameter means « give me the main clock ».
    For the orxClock_Register it register a callback method we give it the main clock, the function we want to call, but the last 3 parameters are blurry to me. is the module something thread related ? What are the priority and when do that matter ?

    Correct, let's go over the last 3 parameters:

    • orxNULL -> it's a context that will be passed to the function you register, every time it's called from the clock module. In this case we do not need any extra info so we set the context to orxNULL
    • orxMODULE_ID_MAIN -> this can be used to freeze some of the registered functions if the corresponding module becomes unavailable. This is almost never used and you can simply pass orxMODULE_ID_MAIN as a user. Internal calls will use different module IDs, but that's not relevant unless you want to replace parts of the engine itself.
    • orxCLOCK_PRIORITY_NORMAL -> this one is actually somewhat relevant as it can help you find your place in the main loop sequence. Usually gameplay-related update functions will use either the normal or the low priority. When a clock is updated, it'll call the functions registered on it from the highest priority to the lowest one, ensuring some ordering in the overall sequence.
    for (orxS32 i = 0, iCount = orxConfig_GetListCount("Transitions"); i < iCount; i++)
    {
        const orxSTRING zTransition = orxConfig_GetListString("Transitions", i);
        if (orxInput_HasBeenActivated(zTransition))
        {
            orxObject_CreateFromConfig(zTransition);
            break;
        }
    }
    

    I image we just fetch the keybind for every possible transitions, if we press a key (eg.KEY_2) we will create the object [ToSceneYellow].

    Exactly, it's not really related to the scene swapping itself, just the way I chose for the users to interact with the whole process.

    And now it's when I don't understand anymore.

    So the object [ToScene] is kinda like an interface for all switching object.
    LifeTime = track mean the object is alive until the timeline is ended and TrackList = @ probably mean self ?

    I guess we could call it this way, some would call it template as well. The config system supports inheritance, you can inherit from an entire section or with a key by key approach. In this case, we want to make sure that all our transition objects, which will inherit from this section:

    • will use themselves to provide a timeline (ie. a list of tracks with TrackList = @, you can find more info about @ in the wiki or at the top of CreationTemplate.ini)
    • will get deleted as soon as the track defined above will be done running (LifeTime = track), making sure that when the transition's been orchestrated, the transition object will get deleted automatically

    So if we press the KEY_2 we spawn this object
    [[email protected]] 0 = > Get Runtime Scene, Object.AddTrack < MoveOut, > Object.Create SceneYellow, Object.AddTrack < MoveIn

    0 = 0s i got it but the value of this key is giving me headache :lol: .

    Yep, you'd need to check commands first. Those are a very simple way to interact with parts of the engine from either config (again, check the top of CreationTemplate.ini to see the two ways they can be called from the config data) or from the interactive console (press the backtick/tilde key to open it, quake-style).

    More info on that topic at:

    [Part 1/2]

  • edited November 9

    [Part 2/2]

    The only thing I understand is Object.Create SceneYellow, pretty obvious name (and we used this command in the Beginner's Guide).

    Yep, to summarize, this track executes commands at time = 0 second (ie. immediately upon the object's creation). The commands being:

    • > Get Runtime Scene -> We retrieve the config value from the section Runtime with the key Scene (that's where we store the current's scene GUID, which is a orxU64 value, cf the StoreScene track that runs on all the Scene objects) and push it on the result stack (that's the > symbol)
    • Object.AddTrack < MoveOut -> We pop the GUID from the stack (<), which is our current scene as retrieved above, and add a track on it called MoveOut (defined somewhere else in config)
    • > Object.Create SceneYellow -> We create SceneYellow and push its GUID on the stack (>)
    • Object.AddTrack < MoveIn -> We pop it from the stack (<) and add a track on it called MoveIn

    If I remember correctly, MoveOut moves the old scene outside of the camera's frustum to the left and MoveIn does the opposite, giving a sliding effect to this transition.

    I understand that MoveOutand MoveIn are track with contain animation (scroll?) to display the move out the scene and move in the new one. So Get Runtime Scene is probably something like "give me the current scene". But the whole syntax using <, > and ^ (in the [MoveOut] & [MoveIn]) is totally new to me and I don't have any idea on what that mean 🤔

    Yep, I think you'll find the info you need in the wiki (in the two links mentioned above). To summarize:

    • > means push the result of the commands on the stack
    • < means pop a parameter from the stack
    • ^ means replace with the GUID of the object that's running this command (you can conceptualize it as this or self)

    I think that's all, sorry for all the questions i'm still a beginner in game dev so I probably don't understand very basic concepts.

    No worries, don't hesitate with all the questions. Orx's approach is data-driven, which means a lot of the behaviors can be entirely driven from the config files (like being able to create a whole scene from a single object creation call). Even in the gamedev scene, this is not that common, at least not to that extent, so I completely understand if you find it confusing.
    That being said, everything we did here could be done in code as well, but this would have a few disadvantages:

    • you can't modify/tweak things without recompiling (which is annoying for programmers but almost a blocker for non-coder designers/artists/etc...)
    • you can't take advantage of the hot reloading features
    • you can't use the config override system which allows one to target multiple platforms, sometimes with wildly different setup like portrait vs landscape, without having to recompile

    In the last case, for example, by giving you an example, you could be testing the Android version of your game from your computer by simply modifying a single include line in config that would override a bunch of properties and would let you test a different platform natively, with the same codebase (no need for an emulator, better performances, etc...).

    Thanks I didn't took the [ToSceneBlue] track as an example :lol: but yeah basically all those symbol (,, <, >, ^, #) are super confusing. I tought the # symbol was used to chain thing but sometimes you use it and I don't understand why.

    Yep, that one is quite advanced. We actually modify the entire rendering pipeline by creating new viewports, having both scenes exist at the same time, render to off-screen textures and use a cross-fade shader to compose the whole thing during the transition, then finally restoring the default rendering pipeline, and cleaning all the intermediate things (viewports, off-screen textures, cameras, etc...) as well as the old scene.
    All in all it's quite concise for such dramatic changes, especially at runtime, but it's definitely not beginner's level, so I wouldn't pay too much attention to it at the moment. :)

    The # works for any config values, it's the element separator (ie. the value will be considered as a list and not as a single value), while the , separator is a command-only concept. I use both to achieve the formatting I wanted (# allows for continuing the list on the next line, while ,, not being a config concept, would be ignored and everything would need to be on the same line).
    Again, quite an advanced use, don't worry about it. :)

  • edited November 9

    Actually, you know what, let's dissect that ToSceneBlue and you'll see that in the end, even though it looks quite busy, it's also quite straightforward. :)

    [[email protected]]
    0               = >> Get Runtime Scene, Set Runtime Out <, Object.SetPosition < (0, 0, 200) #
                      >  Object.Create SceneBlue, Object.SetPosition < (0, 0, 100) #
                      >  Viewport.Get Viewport, Viewport.Delete <, Viewport.Create ViewportIn, Viewport.Create ViewportOut, Viewport.Create Viewport #
                      >  Object.Create SceneDissolve, Object.SetOwner < ^
    0.8             = >  Viewport.Get ViewportIn, Viewport.Delete <, > Viewport.Get ViewportOut, Viewport.Delete < #
                      >  Get Runtime Out, Object.Delete <, > Get Runtime Scene, Object.SetPosition < (0, 0, 0)
    

    So at time T = 0, we run 4 sets of commands, all separated by #. Each set of commands will then be split into sequential commands by the command module, using the , separator.

    First set:

    • >> Get Runtime Scene -> We retrieve the current scene and push it twice on the stack
    • Set Runtime Out < -> We pop it, and store it in the Runtime section under the key Out (for later use)
    • Object.SetPosition < (0, 0, 200) -> We pop it again (hence why we pushed it twice in the first place), and move it 200 units further on the Z axis, to make sure it's not seen by our main camera anymore, ie. it won't be rendered with our current rendering pipeline

    Second set:

    • > Object.Create SceneBlue -> We create the new scene, SceneBlue, and push it on the stack
    • Object.SetPosition < (0, 0, 100) -> we move it 100 units further on the Z axis, still out of view of our main camera

    We now have both our scenes, the old one around Z = 200 and the new one around Z = 100. If we left it at that, we'd see nothing on screen anymore.

    Third set:

    • > Viewport.Get Viewport -> We retrieve our current viewport named Viewport (yeah, that was original) and push it on the stack
    • Viewport.Delete < -> We delete it (now we don't have any rendering pipeline whatsoever left
    • Viewport.Create ViewportIn -> We create a new viewport called ViewportIn (and in its section, it'll define that it renders to a texture named TextureIn, that will be created at the correct size on the fly, with a camera called CameraIn, also created on the fly, and centered around Z = 100, ie. "seeing" our new scene)
    • Viewport.Create ViewportOut -> We create a new viewport called ViewportOut, same idea than with ViewportIn, but this one will render the old scene around Z = 200 into the texture TextureOut
    • Viewport.Create Viewport -> we recreate our original viewport. So why did we delete it in the first place? well, we want it to be the last, so that it can be rendered after TextureIn and TextureOut have been filled. The creation order does matter for viewports.

    If we stopped there, we'd get the new scene rendered to a texture called TextureIn, the old scene rendered to TextureOut, but still a black screen as we wouldn't be using those textures.

    Fourth set:

    • > Object.Create SceneDissolve -> We create an object called SceneDissolve. This object will use the main camera as parent, stretch itself to cover its entire frustum, and use a shader that will take both textures as inputs and do a cross-dissolve with them. We also push it on the stack.
    • Object.SetOwner < ^ -> We change its parent to the current object (ToSceneBlue), this way when ToSceneBlue gets deleted, so will SceneDissolve, and we won't have to clean it up manually.

    And now we have a nice effects rendered on screen, with a single object, SceneDissolve, seen by the main camera.

    We let 0.8 seconds go and now it's time to clean everything up with two sets of commands.

    First set:

    • > Viewport.Get ViewportIn -> We retrieve ViewportIn and push it on the stack
    • Viewport.Delete < -> We delete it, with it are also gone CameraIn and TextureIn, no more rendering of the new scene to an offscreen texture
    • > Viewport.Get ViewportOut -> We retrieve ViewportOut and push it on the stack (sounds familiar? ;))
    • Viewport.Delete < -> We delete it -> no more CameraOut and TextureOut

    Second set:

    • > Get Runtime Out -> We retrieve the content of the key Out in the section Runtime, which is where we saved our old scene at the very beginning. As the new scene has overridden the key Scene in that same section, we wouldn't be able to find it otherwise. We push it on the stack.
    • Object.Delete < -> We delete it, no more old scene, all objects contained in it have been deleted as well
    • > Get Runtime Scene -> We retrieve the new scene and push it on the stack.
    • Object.SetPosition < (0, 0, 0) -> We put our scene back at Z = 0, where the main camera can now render it as if nothing happened.

    And all done, the whole render pipeline is back to its old self, the new scene is rendered correctly and where we expect it in the world. :)

    And because the track's now over, ToSceneBlue gets deleted as well, due to the LifeTime = track property, and with it SceneDissolve disappears as well.

    I know I'm a bit biased, but I think this example is rather cool and illustrates how flexible orx can be, especially with touchy things like modifying render passes or creating/deleting entire scenes.

    Let's say now that for low end hardware you want a simple instant replacement (like with ToSceneRed), you can have a file called LowEnd.ini, and in it you'd redefine ToSceneBlue to not have this crazy track and simply behave like ToSceneRed. All you'd have to do is to load that config file, either from another config file or from code, and the entire transition system for that scene would now be different.

  • Hey @iarwain thanks for all those awesome informations.

    The commandnotes page and your explanations really help.

    One thing I still don't understand is how to make use of this. Ok now we switch between SceneYellow, SceneGreen, SceneCyan etc ... but in a real game I would need to define the content of the scene in my code ?

    Let's take a simple example, a sokoban game

    I have 5 scenes: MainMenuScene, GameScene, LevelEditorScene, PauseMenuScene.

    In my config I create some objects, all the scenes, PlayerObject, BoxObject, BoxOkObject, WallObject and GoalObject.

    The thing is, i'm in my MainMenuScene and I press start, so it switch to the GameScene. Now in my code I would probably like to do multiple things like :

    • Read a file (where my level is stored)
    • Create a lot of objects to "draw" my level in the right position
    • Handle Input for this specific scene. (because if you press start on this scene you open the pausescene not the gamescene).

    So in my code I still need to define my scenes using classes ? So I can have a Scene::update() method to handle all of this ?

    (I'm working on a sokoban example because it would be easier for me to show you with code)

    Thanks again for your help :smile:

  • Nothing prevents you from using classes if that's how you want to organize your code, all I'm saying is that it's a choice, not something mandatory. :)

    In the example of a Sokoban, why not load all the levels at once? Each level expressed in config form will probably be a handful of kb, having 1000 levels would then be a handful of mb and should be loaded in a split second. Unless you're targeting extremely low end devices (or you want to change the list of levels dynamically for some reason, like UGC content), I don't see why not loading them all at once.
    Your objects would already be children of the level itself. You can either use your own format or @sausage's Tiled exporter if you wanted to use Tiled as a level editor, for example. In the end, you'd create the entire level with a single call to orxObject_CreateFromConfig.

    Now that being said, I'd recommend checking Scroll (there are info on it in the wiki), which would bring you a thin C++ layer on top of orx that facilitates binding C++ classes to config sections as well as the handling of per-object events (like collisions or animations).

    Lastly, you can switch input sets to handle different situations, like pause menu vs game. You can also use classes of course, I tend to simply have an enum that tells me which Update function I should be using at the moment, as I don't usually have too many different states (main menu, in-game, pause, game over), things like this.
    For example, creating a Pause object could not only disable or pause the game scene as well as swap the current input set. And when exiting, it'd put the original set back and re-enable or un-pause the old scene, those could be done from tracks or in code, up to your preference.

    Again, you should go with the way you feel more comfortable, I tend to like having a minimal amount of classes, usually one for the game and one per type of entity (player, npc, weapon, etc...) and that's about it, I then usually put all the variations in config.

  • This is an interesting topic for me as well. I've been playing with orx for less than a week in the evenings after work to see what I can do, so probably premature for me to have much of a relevant opinion!

    I'm trying to work up a simple "space invaders" type game with a couple of levels and a boss. The idea is to exercise the various things I know I'd like to be able to do (ie menu, multiple levels, intro/outro for levels, etc) to see how it works in orx.

    So far I am getting convinced by the data driven approach. If you are used to coding behaviors yourself, it is hard to change. There is a learning curve to the orx system which I suppose will take some time to digest. My first impression is that is it very well thought out and implemented. I've had no problems at all with orx itself, only a few confusions of my own on how to use it. Without @iarwain and others here, it would indeed be a little difficult to navigate.

    For the scene handling, I'd like to ultimately go to something like what @iarwain is outlining above, though I haven't had time to really look at it yet. For my initial test though, I've done the scenes (levels?) in the way I'm familiar with.

    I make a SceneBase class with each level/scene as a subclass of that, containing an update function that can be called by the main orx update function. Each subclass has the ability to set the desired scene/level at any time, which is reacted to in the main orx update function. I can elaborate further if anyone is interested.

    In any case, looking forward to learning more. I'm really glad I stumbled onto orx.

  • edited November 10

    I know I'm advocating for the put-as-many-things-on-data-side approach as much as I can, but in the end, if you feel more comfortable with a code driven approach, or anywhere in-between, it's completely fine. You should be able to take the approach that feels the most comfortable to you without having to fight against the engine, and hopefully that's the case.

    I know someone recently used the config to bind different update function on the fly as well, that's an approach I never thought about and it turned out to work perfectly fine for them as much as I can tell.

    Lastly, if you want to use dedicated C++ classes, I really recommend to check Scroll as that's one of the big advantages it offers on pure orx.
    There's a ScrollObject based class, which can be derived and bound to config sections (it works with section inheritence as well). This way, you can simply bind your different scene classes to the different scene objects and those would get created as soon as the underlying orx object is created.

    To be fair, I almost never use orx without Scroll myself, unless it's for short tests and demonstrations. However for bigger projects, I'm always using Scroll.
    I still have on my todo list a task to add Scroll to the project init feature, I'll try to get it finished before the new year.

    There's an old multi-part tutorial at https://wiki.orx-project.org/en/tutorials/community/acksys/scroll0
    Some passages might be a bit outdated, but most of it should still be relevant.

  • Thanks! Scroll is next on my list to try out. Looks like a good match for how I am approaching things.

Sign In or Register to comment.