-
Notifications
You must be signed in to change notification settings - Fork 0
System
The Genode System module contains core components that serve as your application bootstrappers and framework-wide classes such as Gx::Exception. The System module in Genode has a different concept from the SFML System module, which encapsulates the low-level operating system features.
At the time of writing, the System module has two core components: Gx::Application and Gx::Context.
This class represents your application (or game) in a high-level perspective. It is an abstract class that encapsulates the traditional game loop and presents it as application lifecycles and delegates functions. This class also manages an instance of Gx::Context and Gx::SceneDirector internally.
Here are the following lifecycle methods that Gx::Application provides that you can override:
-
Boot(): Called when the application is about to start. Use this method to initialize global dependencies, states and resources. -
Update(double delta): Called every frame. Use this method to perform your game logic. -
Render(RenderSurface&, RenderStates) const: Called every frame. Use this method to render your objects. -
Shutdown(): Called when the application is about to exit. Use this method to stop operations and destroy external or manually managed resources. You also need to return the application exit code. You can return0to indicate the game was run successfully.
In addition to the lifecycle methods, there are several event delegates that you can implement too:
-
OnWindowCreated(sf::RenderWindow&): Called when the main application window was created or re-created. -
OnFocusChanged(bool): Called when the main application window focus state was changed. -
OnResized(const sf::Vector2u&): Called when the main application window was resized. -
OnInputReceived(const sf::Event&): Called when foreign events from the main application window have been polled. -
OnClose(): Called when the main application window is about to close, returntrueto proceed with the close event; otherwise, returnfalseto cancel it.
Boot and Shutdown are pure virtual functions, so your application must implement them. On top of that, Gx::Application implements Gx::RenderSurface that interconnects with the main sf::RenderWindow directly. See the Render Surface concept for more details.
Note
Only a single application instance can exist.
Starting another application instance via Start() will throw Gx::Exception.
Important
The application class is designed to run on a single thread with the following execution order in each frame: Input Polling -> Update -> Render. Without calling Start, the application will not be bootstrapped correctly, and manually calling these operations will lead to unexpected behavior.
The application class is designed to handle only one window at a time. Furthermore, SFML currently has a limitation that prevents you from changing the window state once a render window is created. The framework will re-create the window at any point should you change the window state via SetWindowState() method.
Currently, you cannot use the Gx::Application class if you wish to write a multi-threaded application or work with multiple windows simultaneously.
Here's what a minimum example of Gx::Application implementation would look like:
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
{
// Do some initialization here!
// Load the texture and sprite
m_texture = sf::Texture("/some/path/to/the/texture.png");
m_sprite = Gx::Sprite(texture);
}
void Update(double delta)
{
// Move the sprite by 10px to the right every 5 seconds
m_elapsed += delta;
if (m_elapsed >= 5000)
{
m_sprite.Move({10, 0});
m_elapsed -= 5000;
// Reset position when it reaches a certain point
if (m_sprite.GetPosition().x >= 500)
{
m_sprite.SetPosition({0, 0});
}
}
}
RenderStates Render(RenderSurface& surface, RenderStates states) const
{
surface.Render(m_sprite, states);
return states;
}
private:
double m_elapsed;
sf::Texture m_texture;
Gx::Sprite m_sprite;
};To start the application, simply instantiate your application class and call the Start() function.
#include "MyAwesomeGame.hpp"
int main(int argc , char** argv)
{
auto myGame = MyAwesomeGame(
"My Awesome Game", // Title bar string
sf::VideoMode({1920, 1080}), // sf::VideoMode to initialize the window
sf::View({0, 0, 1920, 1080}), // Default view of the application
false // Fullscreen mode
);
// Start is a thread-blocking function, and it returns the exit code of the application propagated from `Shutdown()`
return myGame.Start();
}The Genode system context is a lightweight yet powerful IoC Container class for bootstrapping, managing class dependencies, and performing dependency injection. While several great IoC solutions that are much more powerful and flexible are available, Gx::Context aims to be a lightweight/small-footprint container with a simple implementation, non-intrusive, and intuitive interface to use.
Dependency injection is a fancy phrase that essentially means this: class dependencies are "injected" into the class via the constructor or, in some cases, "setter" methods. But what does "bootstrapping" mean? It generally means registering whatever components your game needs to run. This could be a resource manager that is globally shared across your game components, a fresh instance of the physic engine, or an instance of loaded game configuration—you name it.
Here's an overview of Gx::Context in action looks like:
struct GameConfig
{
unsigned int FrameLimit;
bool AccessibilityEnabled;
// Some other configuration that your game needs..
};
class MyGameEntity : public Gx::Renderable, public Gx::Updatable
{
public:
// This is where the dependencies are injected to your class. Hence, dependency injection
MyGameEntity(GameConfig& config, Gx::AudioMixer& mixer) :
m_config(config),
m_mixer(mixer)
{
}
protected:
RenderStates Render(RenderSurface& surface, RenderStates states) const
{
// Render your game objects here..
// Render additional things related to accessibility
if (m_config.AccessibilityEnabled)
{
// Render stuff that relates to accessibility here..
}
}
void Update(const double delta)
{
// Update your game objects here
// Perform additional logic when accessibility is enabled
if (m_config.AccessibilityEnabled)
{
// Accessibility logic here..
}
}
private:
GameConfig& m_config;
Gx::AudioMixer& m_mixer;
};
// Here's where the magic happens:
int main(int argc, char** argv)
{
// Create the context
Gx::Context context();
// Define how we can provide Game Config instance when requested
context.Provide<GameConfig>([] (auto& ctx)
{
auto config = std::make_unique<GameConfig>();
// Add logic to load your game config here..
// Use Context in `ctx` parameter if the GameConfig requires another dependency
return config;
}, Gx::Context::Scope::Shared); // GameConfig instance will be shared across different contexts
// Create our entity based on the context object
// Notice that we don't have to configure Gx::AudioMixer. It is automatically bootstrapped when needed!
auto entity = context.Create<MyGameEntity>();
}Gx::Context can auto-resolve dependency with no configuration. The Gx::Context class can also resolve dependencies that have nested dependencies declared on their constructors, enabling you to build a complex dependency hierarchy that can be configured in any order with minimum configuration.
Caution
Do not inject Gx::Context to your class directly. Avoid resolving dependencies directly from the context instance in your class as much as possible. This will obscure the dependencies of your class and is often considered an anti-pattern.
Note
Gx::Application instance comes with an empty context that can be retrieved via GetContext().
Use this context as the root application bootstrapper context in your game.
As mentioned, the context class does not require configuration, particularly for concrete classes. It will automatically create the object and resolve the dependencies for you recursively. However, the dependency can be bootstrapped manually by explicitly calling the Provide function.
Consider the following structures:
struct InputSystem {};
struct MovementSystem {
MovementSystem(InputSystem& input) : m_input(&input) {}
InputSystem* m_input;
};The following code illustrates how to bootstrap the above structures:
auto context = Gx::Context();
context.Provide<MovementSystem>();
context.Provide<InputSystem>();Tip
You can register your type out-of-order.
The object will be lazily created when you call Require<T>() or Create<T>(), and the container will resolve its dependency recursively auto-magically.
Important
The registrant type needs at least one public constructor. The context will use the constructor with the least number of parameters.
If the public constructor with the least number of parameters has one or more overloads, the container will fail to resolve that type, and a runtime error will be thrown when retrieving the object by reference. You must manually bootstrap the dependency by binding the type with builder before resolving such type.
Additionally, the type must never accept smart pointers in the constructor, as the context handles the lifetime of dependencies. For bootstrapping an interface type or a type with a protected constructor, see Binding interface below.
If your class constructor requires an interface type, you must bind the interface before retrieving or using the type as a dependency from the context. Use As<T>() to bind your interface type with a concrete type when calling Provide<T>().
Consider the following structures:
class IInputSystem
{
public:
virtual void Input(sf::Event& ev) = 0;
};
struct InputSystem : IInputSystem
{
protected:
virtual void Input(sf::Event& ev) {}; // do something
};
struct MovementSystem {
MovementSystem(InputSystem& input) : m_input(&input) {}
InputSystem* m_input;
};The following code illustrates how to register the interface type above:
auto context = Gx::Context();
context.Provide<IInputSystem>(context.As<InputSystem>());
context.Provide<MovementSystem>(); // OptionalImportant
A runtime error will be thrown if you try to resolve a type that depends on an unregistered interface type.
Providing interface type with default Provide<T>() function will yield a compile-time error.
Regardless of whether the specified type is concrete or interface type, you can specify how the context should construct the object by defining a custom builder when you call Provide<T>().
auto context = Gx::Context();
context.Provide<IInputSystem>([] (auto& ctx)
{
// You can simply call `ctx.Create<InputSystem>()` here if the dependencies are concrete types
return std::make_unique<InputSystem>( // return as a std::unique_ptr
ctx.Require<KeyboardSystem>(),
ctx.Require<MouseSystem>()
);
});Use Require<T>() to resolve the object inside the container. The method will try to call the Provide<T>() function internally to attempt to bootstrap the specified type if it is not registered within the container.
auto context = Gx::Context();
// Create or retrieve MovementSystem from the container
// If the type is not registered, the container will automatically register the type for you.
auto& movementSystem = context.Require<MovementSystem>();
// Use the pointer overload if you don't want the container to register the type automatically.
// If the type is not registered, it will return `nullptr` instead.
auto lifeSystem = context.Require<LifeSystem*>();
assert(lifeSystem == nullptr, "Life System is not registered within the context!");Use Create<T>() to create a new fresh instance of the specified type. If the type requires one or more dependencies, it will be automatically resolved from the context or bootstrapped as needed, and those newly bootstrapped dependencies will be inserted and bound to the current Gx::Context instance.
// Create function will return a std::unique_ptr. Ensure you have provided the type previously if the type is an interface type!
auto lifeSystem = context.Create<LifeSystem>();Each Provide<T>() method overload has an optional Scope parameter, allowing you to choose between Scope::Local and Scope::Shared to control how you want to bind the object's lifetime with the current instance of the Gx::Context. By default, the Provide<T>() function uses Scope::Local, which makes each initial Require<T>() call creates a new instance of T in every Gx::Context instance.
Using scopes allows finer-grained lifetime control, where all types bootstrapped as local contexts are unique within a given scope. This allows singleton-like behavior within a scope, but multiple object instances can be created across scopes.
A new scope can be created by calling Capture() on a context instance:
auto context = Gx::Context();
context.Provide<FooBar>(Scope::Local); // Specifying `Scope::Local` is optional
context.Provide<SharedFooBar>(Scope::Shared); // Bootstrap a shared instance
auto& sharedInstace1 = context.Require<SharedFooBar>();
auto& instance1 = context.Require<FooBar>();
auto& instance2 = context.Require<FooBar>();
// The context itself is the scope
assert(&instance1 == &instance2);
{
// Create a new scope
auto scope = context.Capture();
auto& sharedInstance2 = scope.Require<SharedFooBar>();
auto& instance3 = scope.Require<FooBar>();
auto& instance4 = scope.Require<FooBar>();
// Instances should be equal inside a scope
assert(&instance3 == &instance4);
// Instances should not be equal across scopes
assert(&instance1 != &instance3);
// Shared instances should be equal across scopes
assert(&sharedInstance1 == &sharedInstance2);
}Caution
Any type bootstrapped and resolved in the parent context after calling Capture is not propagated to the scope instances, even if you use Scope::Shared!
Copyright © 2024 - CXO2
This open-source library is licensed under the zlib/libpng license.