Skip to content

Core.Modules

Dennis Steffen edited this page Jan 4, 2026 · 1 revision

In the context of your project, ObjectManager modules are a design pattern used to group related service registrations into manageable, reusable components. Instead of cluttering your main entry point (like Program.cs or a Game class) with dozens of individual Register calls, you encapsulate that logic within a "Module" class.

Based on the code provided, there are three primary ways these modules are structured:

1. The Static Initialization Pattern (e.g., CoreModule)

This is the simplest form. It uses a static method to perform a batch of standard registrations that don't typically change.

  • Usage: Ideal for global services or framework-level setup.
  • Example:
// To use it, you just call the static method once
    CoreModule.Load();

2. The Fluent/Builder Pattern (e.g., InputModule)

This pattern is used when the module needs configuration before it is finalized. It uses method chaining to "build" the desired state.

  • Key components:
    • A Create() method to start.
    • Configuration methods that return this (the module instance).
    • A Setup() or Load() method to perform the actual registration in the ObjectManager.
    • Use WithAutoAssign() to automatically switch between devices. This is ideal for single player sessions. If you don't use this then you need to manually assign it to players based on profileid and playerid.

NOTICE Player ID start always from 1

  • Example:
InputModule<GameInput>.Create()
            .AddInputMapper(new GenericMapper<GameInput>(GlobalObjectManager.ObjectManager.Register(new RaylibKeyboardBinder<GameInput>()))
                .AddInputKeyboard(0, GameInput.Up, KeyboardKey.Up)
                .AddInputKeyboard(0, GameInput.Down, KeyboardKey.Down)
                .AddInputKeyboard(0, GameInput.Left, KeyboardKey.Left)
                .AddInputKeyboard(0, GameInput.Right, KeyboardKey.Right)
                .AddInputKeyboard(0, GameInput.Start, KeyboardKey.Enter)
                .AddInputKeyboard(0, GameInput.Action, KeyboardKey.Enter)
                .AddAxis(0, 1, GameInput.Left, GameInput.Right, GameInput.Up, GameInput.Down)
                .AddInputKeyboard(1, GameInput.Up, KeyboardKey.W)
                .AddInputKeyboard(1, GameInput.Down, KeyboardKey.S)
                .AddInputKeyboard(1, GameInput.Left, KeyboardKey.A)
                .AddInputKeyboard(1, GameInput.Right, KeyboardKey.D)
                .AddInputKeyboard(1, GameInput.Start, KeyboardKey.F)
                .AddInputKeyboard(1, GameInput.Action, KeyboardKey.F)
                .AddAxis(1, 1, GameInput.Left, GameInput.Right, GameInput.Up, GameInput.Down))
            .AddInputMapper(new ControllerInputMapper<GameInput>(new SDLControllerDeviceManager())
                .Map(GameInput.Start, ControllerInputEnum.AorCross)
                .Map(GameInput.Action, ControllerInputEnum.Start)
                .Map(GameInput.Down, ControllerInputEnum.DPadDown)
                .Map(GameInput.Left, ControllerInputEnum.DPadLeft)
                .Map(GameInput.Up, ControllerInputEnum.DPadUp)
                .Map(GameInput.Right, ControllerInputEnum.DPadRight)
                .SetDPadIsAxis(true))
            .WithAutoAssign()
            .Setup(); // This is where the ObjectManager.Add/Register calls happen

3. The Factory/Instance Pattern (e.g., ArcadeEmulatorModule)

This pattern combines a static factory method with instance-based configuration. It often takes external dependencies (like an ArcadeGame instance) to decide what services to register.

  • Usage: Ideal for complex systems that might be optional or have different modes (like "standalone" vs "server").
  • Example:
var arcade = ArcadeEmulatorModule.Load(myGame, mqttModule, standaloneMode: true);
    arcade.SetIntroScene<MyMenuScene>();

How to create your own Module

If you wanted to create a new module for something like a "Network System," here is how you would implement it following the existing conventions:

Example: NetworkModule.cs

using Meatcorps.Engine.Core.ObjectManager;
using Meatcorps.Engine.Core.Interfaces.Services;

namespace Meatcorps.Engine.Core.Modules;

public class NetworkModule
{
    private string _ip = "127.0.0.1";
    private int _port = 8080;
    private readonly ObjectManager.ObjectManager _manager;

    private NetworkModule(ObjectManager.ObjectManager? manager)
    {
        _manager = manager ?? GlobalObjectManager.ObjectManager;
    }

    public static NetworkModule Create(ObjectManager.ObjectManager? manager = null)
    {
        return new NetworkModule(manager);
    }

    public NetworkModule Configure(string ip, int port)
    {
        _ip = ip;
        _port = port;
        return this;
    }

    public void Load()
    {
        var service = new NetworkService(_ip, _port);
        
        // Register the specific class
        _manager.Register<NetworkService>(service);
        
        // Also register it as a background service if it implements IBackgroundService
        if (service is IBackgroundService background)
        {
            _manager.Add<IBackgroundService>(background);
        }
    }
}

Why use this approach?

  1. Readability: Your startup code becomes a high-level list of features (LoadCore, SetupInput, LoadNetwork) rather than a list of individual classes.
  2. Dependency Management: The module can handle the order of operations. For example, in InputModule.Setup(), it creates a PlayerInputRouter first because the InputManager depends on it.
  3. Interface Abstraction: Modules often register a concrete class (like PlayerInputRouter) under multiple interfaces (like IBackgroundService and IInputMapper), ensuring the rest of your app only sees the interface it needs.

Clone this wiki locally