Skip to content
CXO2 edited this page Dec 16, 2024 · 5 revisions

Overview

The Scene Graph module is the highest-level abstraction in Genode. It contains the core building block components for your game logic.

Almost every other module class in Genode can be heavily incorporated with the usage of SceneGraph classes that allow you to choose the right amount of abstraction based on your requirements. Thus, it is highly recommended that you explore all other modules' documentation first before reading further.

Important

Genode SceneGraph module employ Inheritance over Composition as its current implementation. It relies on virtual inheritance to describe the overall scene hierarchy and provides convenience functionalities. This may introduce performance overhead when dealing with thousands or millions of objects, especially if they have complex hierarchies.

If you wish to use the Composition over Inheritance approach such as ECS, you should override the Update and Render functions in your Gx::Scene implementation to update and render your objects by yourself and avoid adding node objects to the scene directly via AddChild. You also shouldn't use the Container based functions when doing this.

You will still have to pay the price of virtual inheritance for using the Gx::Scene class because the class uses it. However, the cost should be negligible because it will be the only class that uses virtual inheritance, at least from the Genode side.

Node

The Gx::Node class represents a fundamental building block in the scene graph hierarchy. As a core structural unit, a node object can be considered a container that can hold other nodes and has transformation properties—hence, it implements Gx::Transformable.

This parent-child structure simplifies complex scene composition. For instance, attaching multiple nodes to another single node object allows you to logically group related nodes that can be manipulated easily, such as applying transformations (like position, rotation, scale) that cascade down the hierarchy.

Caution

The nodes hold non-owning pointers of their children, and you are responsible for ensuring the lifetime of objects in the hierarchy. This allows you to modify hierarchy (such as removing a child from a node and adding it to a different node) without having to construct and destroy node objects.

Warning

Gx::Node by default is conditionally copyable: it can only be copied if it has no child. The copy constructor will throw Gx::InvalidOperationException if you're trying to copy a node that has children. This is because it is not trivial to perform a deep clone of a node object and its hierarchy without imposing an intrusive interface such as a clonable interface.

Rather than making a copy, you should write a function or a factory class to re-create an object from scratch based on the node properties. For this reason, exercise extra caution when instantiating a node object via Gx::ResourceManager::Instantiate!

Important

In Genode, a node object is not strictly a renderable element. They can be objects with no visual properties but are still transformable. For example, you can create an audio listener as a node object. This object then would use the transform properties to represent the position of the audio listener in 2D space.

Scene

The Gx::Scene class represents a logical container and organizational root for all elements of a particular segment of your game. In Genode, all these elements are expressed as the Gx::Node object. Everything in the Scene should be a node object. In fact, the Gx::Scene itself implements the Gx::Node class.

The scene objects are typically managed by a Gx::SceneDirector instance that either comes from Gx::Application or you can manually create and manage a scene director instance. Alternatively, it is also perfectly fine to manage the scene objects manually or create a "scene director" implementation on your own. See the scene director section to learn more about how your game interacts with your scenes. However, it is strongly recommended that you first understand how Gx::Scene works as described below.

The following code illustrates a typical implementation of the Gx::Scene class.

class AnotherScene : public Gx::Scene { /* implement additional scene here */ };

class MyGameScene : public Gx::Scene
{
public:
    // Use System Context to inject a pre-configured resource manager that represents a resource manager that shared across the scenes
    // Note that you shouldn't perform scene setup here; use the `Initialize` function instead
    MyGameScene(Gx::ResourceManager& sharedResources) : 
        m_sharedResources(sharedResources)
    {
    }
  
    ~MyGameScene() = default; // Same advice as the constructor: use the `Finalize` function instead

protected:
    // Initialize the scene using `Initialize` function
    void Initialize() override
    {
        // Load a sprite from a file
        auto& sprite = m_localResources.AddFromFile<Gx::Sprite>("/some/path/to/sprite/metadata.json");

        // Load an animation from the shared resource manager
        // The file will not be loaded if the shared resources already have the animation
        auto& animation = m_sharedResources.AddFromFile<Gx::Animation>("/some/path/to/animation/metadata.json");
        animation.Reset(); // Reset animation state to initial just in case there were leftover dirty states when it was being used in the other scenes.

        // Both of these entities implement `Gx::Node` and can be added to the scene directly and conveniently
        AddChild(sprite, animation);

        // Run a fade-out tween via Gx::TaskContainer
        Run<Gx::Fade>(sprite, 000, sf::seconds(5.f)); // completely fade-out the sprite in 5 seconds

        // Use GetDirector().Present() or GetDirector().Dismiss() to navigate to another scene or go back to the previous scene, respectively
        // The following code illustrates navigating to another scene when the animation finishes and go back to the previous scene if it is interrupted
        animation.SetAnimationCallback([this] (Gx::Animation& it)
        {
            if (it.GetState() == Gx::Animation::AnimationState::Completed)
            {
                GetDirector().Present<AnotherScene>();
            }
            if (it.GetState() == Gx::Animation::AnimationState::Stopped)
            {
                if (GetDirector().Dismiss())
                {
                    std::cout << "Going back to the previous scene";
                }
                else
                {
                    std::cout << "Previous scene is not available: the current scene is the first running scene";
                }
            }
        });
    }

    // Note: you don't have to render or update your objects manually here
    //       your scene will automatically do it for you. See "Container" section for more details

private:
    Gx::ResourceManager& m_sharedResources;
    Gx::ResourceManager m_localResources; // will be destroyed when the scene is no longer active
};

class SpriteLoader : public Gx::ResourceLoader<Gx::Sprite> { /* implement the loader here */ };
class AnimationLoader : public Gx::ResourceLoader<Gx::Animation> { /* implement the loader here */ };

class MyGame : public Gx::Application
{
protected:
    void Boot() override
    {
        // Register resource loaders to make resource manager works properly
        Gx::ResourceLoaderFactory<Gx::Sprite, SpriteLoader>();
        Gx::ResourceLoaderFactory<Gx::Animation, AnimationLoader>();

        // You can provide the shared resource manager here, but that would be optional unless you have to configure it
        // GetContext().Provide<Gx::ResourceManager>();

        // .. Or you can scope it using Shared scope, this allows the Scene to retrieve this resource manager instance via scene context
        GetContext().Provide<Gx::ResourceManager>(Gx::Context::Scope::Shared);

        // Note that the Scene construction will always use the root Context
        // However, the Scene itself will hold the captured (scoped) Context created from the root Context

        // Finally, activate the scene by using the provided scene director
        GetSceneDirector().Present<MyGameScene>();
    }

    int Shutdown() override
    {
        return 0;
    }
};

Lifecycle

Gx::Scene has several important lifecycle functions. None of these are pure virtual abstract functions; however, it is highly recommended that the core lifecycle functions be used in any scenario to prevent undefined behaviors and unexpected errors.

  • Initialize: Called when the scene is activated by the scene director. Use this to initialize your high-level scene logic states and resources.
  • Render: Called when the scene is being rendered.
  • Update: Called when the scene is being updated.
  • Input: Called when the scene receives user input.
  • OnAppFocusChanged: Called when the window focus is changed
  • OnAppResized: Called when the window is resized
  • OnAppClose: Called when the window is about to close. Return false to cancel the close event.
  • Finalize: Called when the scene is about to be deactivated. Use this to clean up objects that the scene manually manages.

Important

Using the Initialize and Finalize functions is strongly recommended instead of constructor and destructor.

Typical functions such as GetDirector, GetContext, and Require<T> will likely not be available in the constructors because the core components have not been initialized when your scene is constructed. Also, some resources and components are not guaranteed to be alive in the destructor either.

Note that the windowing event is for the window associated with the scene via Gx::SceneDirector and Gx::Application. If the Scene were managed by a Gx::SceneDirector that is not bound to a Gx::Application instance, these events wouldn't be triggered unless you manually propagate the event to the Gx::SceneDirector.

In addition, you can define a custom Initialize function that accepts any number of parameters with any type. You can invoke this initializer function via Gx::SceneDirector::Present.

class MyScene : public Gx::Scene
{
public:
    // Important: this needs to be public!
    void Initialize(const std::string& message) 
    {
        std::cout << message;
    }
};

// Usage:
Gx::SceneDirector& director = // ...
director.Present<MyScene>("this is the message");

Important

This custom Initialize function must use public modifier. Otherwise, the Present call will cause a compile error.
The default parameterless Initialize function will still be called regardless of the existence of the custom initializer function. However, use only one initialize function at a time for the best maintainability.

Caution

All the parameters' values passed to the Present are captured and copied; it is not perfectly forwarded, and thus, all parameters must be copyable.
This is because the current scene will be destroyed after calling Present. It makes little sense to forward objects from a scene that would soon be destroyed.

Container

The SceneGraph module provides built-in containers that simplify the parent-child processing of a node object. These containers are abstract classes that implement Gx::Node and can be used as a building block to create a custom node object. By default, the Gx::Scene class implements all of these containers.

Important

This is where the Inheritance over Composition truly starts; it works by heavily relying on virtual inheritance. You should avoid combining these containers in your entity or using these containers' functions via Gx::Scene if you wish to roll out your own Composition over Inheritance implementation, such as ECS.

Note

These containers do not explicitly check and process the children recursively. You must ensure that the children (particularly the ones that may have nested children) implement the appropriate containers to make every node properly visited and processed.

Renderable Container

The Gx::RenderableContainer represents a container of renderable objects. Rendering the container will render all container children that implement Gx::Renderable in back-to-front order.

Gx::RenderableContainer& container = //...
Gx::Sprite sprite = // ...
Gx::Text text = // ...

// Add sprite and text into the container
container.AddChild(sprite, text);

// Render the container
Gx::RenderSurface& surface = // ...
surface.Render(container); // This will render the sprite and then the text

Updatable Container

The Gx::UpdatableContainer represents a container of updatable objects. Updating the container will update all container children that implement Gx::Updatable in back-to-front order.

Gx::UpdatableContainer& container = //...
Gx::Animation animation = // ...
Gx::Updatable& updatable = // ...

// Add animation and updatable into the container
container.AddChild(animation, updatable);

// Update the container
container.Update(delta); // This will update the animation and then the updatable

Inputable Container

The Gx::InputableContainer represents a container of objects that can listen to user input. Propagating input events to the container will fire the input events to all container children that implement Gx::Inputable in back-to-front order.

Gx::InputableContainer& container = //...
Gx::Inputable& input1 = // ...
Gx::Inputable& input2 = // ...

// Add inputables object to the container
container.AddChild(input1, input2);

// Fire the input event
sf::Event& ev = // ...
container.Input(ev); // This will trigger the input callback of the input1 and then the input2

Render Batch Container

The Gx::RenderBatchContainer represents a container batch of renderable objects. Rendering the container will render all container children that implement Gx::Renderable in a single batch. This container implements Gx::RenderableContainer and Gx::UpdatableContainer.

Warning

The container impose rendering limitations that are subject to the Sprite Batch limitations.

Gx::RenderBatchContainer& container = //...
Gx::Sprite sprite = // ...
Gx::Text text = // ...

// Add sprite and text into the container
container.AddChild(sprite, text);

// Render the container
Gx::RenderSurface& surface = // ...
surface.Render(container); // This will render the sprite and the text in batch

Task Container

The Gx::TaskContainer represents a non-copyable container of task objects. Unlike all the other containers, it manages the Gx::Task objects rather than the Gx::Node objects. In addition, Gx::TaskContainer manages the lifetime of its task children.

Gx::TaskContainer implements Gx::Updatable, and it needs to be updated every frame to work correctly.

Important

The container won't manage task objects added via AddChild. Gx::Task class is not designed to work with Gx::Node simultaneously.

Starting a task

Use Run<T> to create and start a task. The template parameter represents the type of Gx::Task. You will need to specify parameters matching the public constructor parameters of the given task type; they will be forwarded using perfect forwarding to the task constructor.

The created task is stored internally within the task container and will continue to exist as long as it is in the Initial or Running state. The Run<T> function returns std::weak_ptr<T>; use this to determine whether the task is running and exists within the container.

Gx::TaskContainer& container = // ...
Gx::Colorable& colorable = // ...

// Create and start fade tween via container
std::weak_ptr<Gx::Fade> fade = container.Run<Gx::Fade>(colorable, 255, sf::seconds(5.f));

// ... sometimes later: check if fade is still alive
if (auto ptr = fade.lock())
{
    // Task is still alive; you can do something with it
    ptr->Reset(); // reset the fade tween while it is still running
}
else
{
    // Task is no longer alive; it's either stopped or completed
}

You can also provide a temporary task that the container will copy.

Gx::TaskContainer& container = // ...
Gx::Colorable& colorable = // ...

// The specified task will be copied, so it must be a copy-constructible type!
container.Run(Gx::Fade(colorable, 255, sf::seconds(5.f));

Alternatively, you can provide a reference for tasks that you manage manually. The task object will not be destroyed but removed from the container once it is stopped or completed.

Gx::TaskContainer& container = // ...
Gx::Colorable& colorable = // ...

// Create and manage your own task
auto fade = Gx::Fade(colorable, 255, sf::seconds(5.f);

// The specified task will be referenced but not destroyed when it no longer running
container.Run(fade);

Caution

When using this overload, you are responsible for ensuring the task is alive while running inside the container. Otherwise, it is undefined behavior, and memory corruption may occur.

Important

The task that started via the Run call will not updated immediately and will be updated in the next Update() frame. Therefore, you should exercise extra caution when altering the task states as it may affect the initial values of task states for certain types of tasks.

See Tween documentation for more details.

Stopping tasks

You can stop a task directly from the task std::weak_ptr; The task will removed in the next Update cycle of Gx::TaskContainer. However, you can stop it via the container by using a reference

Gx::TaskContainer& container = // ...
Gx::Colorable& colorable = // ...

// Create and manage your own task
auto fade = Gx::Fade(colorable, 255, sf::seconds(5.f);
container.Run(fade);

// ...

// Stop the task via container
container.Stop(fade);

You can also stop all running tasks by calling StopAll.

Scene Director

The Gx::SceneDirector represents a class that manages the scene construction and destruction. It has one active scene at a time. The Gx::SceneDirector implements Gx::Renderable, Gx::Updatable, and Gx::Inputable. When you render, update, or delegate input events to the director, it will be delegated to the currently active scene.

The Gx::SceneDirector will only keep one scene object alive at a time. The scene object is created whenever you activate a scene via the Present or Dismiss function. If another scene is presented beforehand, it will be destroyed. Scene object creation is based on the deserializer specified during the scene registration. The registration itself could be implicitly called by the Gx::SceneDirector when possible.

Using director with application

By default, Gx::Application creates and manages a Gx::SceneDirector for you. The application instance automatically forwards the lifecycle call events such as Render, Update, and Input.

Use GetSceneDirector() to get the scene director instance from the application instance.

class MyAwesomeGame : public Gx::Application
{
public:
    // Application constructor
    MyAwesomeGame(std::string title, const sf::VideoMode& mode, const sf::View& view, const bool fullScreen, const sf::ContextSettings& settings) :
        Gx::Application(std::move(title), mode, view, fullScreen, settings)
    {
    }

protected:
    void Boot() override 
    {
        // Implement your game initialization here..

        // ...

        // You can Register scenes and Present the initial scene here
        GetSceneDirector().Present<MyAwesomeScene>();
    }

    // The rest of your application implementation..
}

Creating director

You don't have to create Gx::SceneDirector manually when you're using Gx::Application. Use the director provided by the application class, which can be retrieved by calling GetSceneDirector().

If you're not using the application class, create the director by specifying Gx::RenderSurface to the constructor. It will be used when a scene needs to get or set a sf::View from the actual surface.

Gx::RenderSurface& surface = // ...
auto director = Gx::SceneDirector(surface);

Note

An internal System Context will be used instead of the application context when a scene director is created this way.

Registering Scene

Typically, you don't need to register your scene manually because the director will attempt to register the scene when you try Present it if it is not registered. However, you can manually register your scene by calling Register.

The registration tells the scene director how a scene should be constructed. The default Register function will assume the scene to be created using either the System Context provided by the associated application or a context managed by the scene director internally.

This means the framework will automatically inject the dependency that the scene needs via the constructor.

class MyScene : public Gx::Scene 
{
public:
    // Dependencies are injected automatically by the system context when a scene is registered implicitly by default or registered using the default Register overload
    MyScene(Gx::ResourceManager& sharedResources)
    {
        // Do something with shared resources here..
        // Note: do not initialize your resource here directly, see Lifecycle for more details
    }

    // The rest of scene implementation goes here..
};

Gx::SceneDirector director = // ...

// Register the scene using default register overload
director.Register<MyScene>();

// Additionally, you may configure the system context here before presenting the scene
// If you're using application class, do this in the `Boot()` function and use `GetContext()` to retrieve the context
// Otherwise, use `director.GetContext()` to get the system context managed by the scene director

// Internally, the scene will be created using a System Context `Create<T>` function when it is presented

In some cases, you may wish to specify the scene instantiation manually or to make your scene deserializable from a file or other sources that can be loaded via a Resource Loader. In these cases, you can register your scene via the Register overload that accepts a scene deserializer function with the Gx::ResourceContext parameter.

class MyScene : public Gx::Scene { /* scene implementation here.. */ };

Gx::SceneDirector director = // ...

// Register the scene
director.Register<MyScene>([] (const Gx::ResourceContext& ctx)
{
    // Create your scene here, for example, use resource loader:
    std::unique_ptr<Gx::ResourceLoader<MyScene>> mySceneLoader = Gx::ResourceLoaderFactory::CreateLoader<MyScene>();

    // Return scene as Gx::ResourcePtr
    mySceneLoader->LoadFromFile("/path/to/myscene/metadata.json", ctx);
});

Important

Using the default Register will prevent the scene header from using class forwarding of its dependencies regardless of whether the parameter is a pointer or reference. The Gx::SceneDirector needs to determine whether the scene is constructible via System Context. Thus, it is impossible to do so when one or more dependencies are forwarded because they're not concrete types.

Presenting a Scene

Use the Present<T> function to create and activate a scene. The scene object will be made based on how it was registered. Present has several overloads that could affect how the scene is created and initialized.

When a scene is presented, it will be pushed to a scene history stack internally. This will allow you to go back to the previously presented scene without knowing the actual scene.

Default present overload

You can call the Present<T> function without any parameters. When a scene is presented this way, the Gx::ResourceContext::Default is passed to the scene deserializer.

class MyScene : public Gx::Scene { /* scene implementation here.. */ };

SceneDirector& director = // ...
director.Present<MyScene>();

Present using resource context parameter

You can also specify a custom Gx::ResourceContext when presenting a scene. The specified resource context will be forwarded to the scene deserializer.

The specified Gx::ResourceContext is captured and copied to the scene history stack.

class MyScene : public Gx::Scene { /* scene implementation here.. */ };

SceneDirector& director = // ...
Gx::ResourceContext& context = // ...

director.Present<MyScene>(context);

Present using scene initializer parameters

if you have a scene with a custom Initialize function, you can pass custom parameters when calling Present. The parameters are captured and copied to the scene history stack when you do so.

See the Scene Lifecycle section for more details.

class MyScene : public Gx::Scene
{
public:
    void Initialize(std::string message)
    {
        std::cout << "received message: " << message.c_str();
    }

    // Implement the rest of the scene..
};

SceneDirector& director = // ...
director.Present<MyScene>("hello world");

Using a resource context + scene initializer parameters

Finally, you can specify resource context and scene initializer parameters when calling Present<T>. Make sure the context is positioned first in the arguments.

Both Gx::ResourceContext and the initializer parameters are captured and copied to the scene history stack. Therefore, the specified Gx::ResourceContext must be copyable.

class MyScene : public Gx::Scene
{
public:
    void Initialize(std::string message)
    {
        std::cout << "received message: " << message.c_str();
    }

    // Implement the rest of the scene..
};

SceneDirector& director = // ...

// Register the scene
director.Register<MyScene>([] (const Gx::ResourceContext& ctx)
{
    // Create your scene here. For example, use resource loader:
    std::unique_ptr<Gx::ResourceLoader<MyScene>> mySceneLoader = Gx::ResourceLoaderFactory::CreateLoader<MyScene>();

    // Return scene as Gx::ResourcePtr
    mySceneLoader->LoadFromFile("/path/to/myscene/metadata.json", ctx);
});

Gx::ResourceContext& context = // ...
director.Present<MyScene>(context, "hello world");

Dismissing a Scene

You can return to the previous scene by calling the Dismiss function. When dismissing a scene, the active scene will be erased from the history stack. The Dismiss function returns a boolean indicating whether the director returned to the previous scene successfully. It will only return false when the history stack is empty, meaning there is no scene to return to.

When the currently active scene is dismissed successfully, the previous scene will be constructed and initialized in the same manner when they're presented. For example, if the scene was presented using a custom Gx::ResourceContext and initializer parameters, the scene will be presented using those parameters again that were captured during the Present<T> call when the scene is activated via the Dismiss call.

Like Present<T>, the dismiss function has multiple overloads.

Default dismiss overload

Call Dismiss() to go back to the previous scene.

SceneDirector& director = // ...

// Dismiss the current active scene and go back to the previous scene if available
// The scene deserializer and scene initializer will be replayed with the same parameters if they were specified during `Present<T>` call
director.Dismiss();

Dismiss using custom resource context and/or scene initializer parameters

Similar to the Present<T> counterpart, you can call Dismiss with a custom resource context or initializer parameters or both. However, to specify initializer parameters, you must provide the scene type to dismiss by calling Dismiss<T> instead.

Using Dismiss<T> will also allow you to return to a specific scene. This will remove any subsequent scenes between the specified and currently active scenes. The scene history stack is preserved, and the director will do nothing when the specified scene type does not exist in the scene history stack.

SceneDirector& director = // ...

// Dismiss the current active scene and construct the previous scene with a custom resource context
Gx::ResourceContext& context = // ...
director.Dismiss(context);

// ... Or using initializer parameters to initialize the previous scene with new parameters
director.Dismiss<MyPreviousScene>("hello world", 3.14f, true);

// ... You can also jump back straight to a specific scene using the template Dismiss function
director.Dismiss<MyFirstScene>(); // using default initializer

// ... Finally you can use both a custom resource context and initializer parameters
director.Dismiss<MyGameScene>(context, "hello world", 3.14f, true);