Ga naar inhoud

Systemen Systeem

Doelstelling

Ik wou het mogelijk maken om code los te koppelen van individuele game objecten. Zodat code beter verdeeld en de Single Responsibility Principle van SOLID(SOLID, z.d.) beter toegepast kan worden.

Toegepaste oplossing

Het idee was om net zoals in het project voor de vorige een systeem maken waarmee je systemen kan maken. Die dan net zoals gameobjecten gewoon methods hebben zoals update en draw die elke frame worden opgeroepen, maar dat ze zelf niet een object zijn. Dit is juist op een zulk manier gedaan dat je zeker kan zijn dat er altijd maar maximaal één instantie van elke systeem bestaat. Die ook op een gemakkelijke manier overal beschikbaar is.

Ook heb ik overal de IGameLoopObject interface gebruikt, want dat bestond al in de template en heeft dezelfde methods als die ik al wou toevoegen. Dus nu is er meer mogelijk met elke systeem.

BaseSystem

Hier is de basis systeem, in het kort eigenlijk alleen een uitbreiding om de IGameLoopObject interface. Maar het is een echte class in plaats van ook een interface omdat ik niet wil dat elke class zomaar een systeem kan worden. Dat zou dan met composition gedaan moeten worden.

public abstract class BaseSystem : IGameLoopObject
{
    protected App app;
    protected SystemHandler systemHandler;

    public virtual void Initialize(App app, SystemHandler handler)
    {
        this.app = app;
        systemHandler = handler;
    }

    public virtual void LoadContent()
    { }

    public virtual void HandleInput(InputHelper inputHelper)
    { }

    public virtual void Update(GameTime gameTime)
    { }

    public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
    { }

    public virtual void Reset()
    { }
}

SystemHandler

Hier is het hart van de systemen, de echte systemen systeem. Het houd alle systemen bij en roept elke method ervoor op.

Ik heb voor de systemen een lose Dictionary en List gebruikt in plaats van een OrderedDictionary omdat het geen generic variant heeft. Wat het moeilijker maakt om mooie code ermee te schrijven.

Ook heb Queues gemaakt voor nieuwe systemen om ze te initialiseren en laden. Dit moest ik doen omdat het technisch gezien mogelijk is om nieuwe systemen toe te voegen nadat het spel al is begonnen. En deze methods kunnen niet zomaar gelijk opgeroepen worden omdat er speciale requirements aan kunnen liggen. Bijvoorbeeld het inladen van afbeelding kan alleen in de draw thread gedaan. Deze queues worden elke frame in update en draw weer geleegd.

public class SystemHandler : IGameLoopObject
{
    protected App app;

    protected Dictionary<Type, BaseSystem> systemsByType = new();
    protected List<BaseSystem> systemsInOrder = new();

    protected Queue<BaseSystem> systemsToInitialize = new();
    protected Queue<BaseSystem> systemsToLoad = new();

    public SystemHandler AddSystem<T>() where T : BaseSystem, new()
        => AddSystem(new T());

    public SystemHandler AddSystem<T>(T system) where T : BaseSystem
    {
        systemsByType.Add(system.GetType(), system);
        systemsInOrder.Add(system);

        systemsToInitialize.Enqueue(system);
        systemsToLoad.Enqueue(system);

        return this;
    }

Hier worden de systemen toegankelijk gemaakt. Het is de bedoeling dat je altijd op deze manier een systeem erbij haalt om te gebruiken. Maar voor al bestaande systemen heb ik de oude manier laten staan, zodat ik niet te veel conflicten zou creëren.

    public T? TryGetSystem<T>() where T : BaseSystem
        => (T?)systemsByType.GetValueOrDefault(typeof(T), null);

    public T GetSystem<T>() where T : BaseSystem
    {
        var gotSystem = systemsByType.TryGetValue(typeof(T), out var system);
        if (!gotSystem || system == null)
            throw new ArgumentNullException(nameof(T), $"Tried to get system '{typeof(T).FullName}', which wasn't added to the system handler.");

        return (T)system;
    }

Hieronder zijn dan alle lifetime methods. Samen met de losse methods die de initialize en load queues legen, die elke frame dus nog een keer worden opgeroepen.

    public void Initialize(App app)
    {
        this.app = app;
        InitializeSystems();
    }

    private void InitializeSystems()
    {
        foreach (var system in systemsToInitialize)
        {
            system.Initialize(app, this);
        }
        systemsToInitialize.Clear();
    }


    public void LoadContent()
    {
        LoadSystemContent();
    }

    private void LoadSystemContent()
    {
        foreach (var system in systemsToLoad)
        {
            system.LoadContent();
        }
        systemsToLoad.Clear();
    }


    public void HandleInput(InputHelper inputHelper)
    {
        foreach (var system in systemsInOrder)
            system.HandleInput(inputHelper);
    }

    public void Update(GameTime gameTime)
    {
        // initialize non-initialised systems added late
        InitializeSystems();

        foreach (var system in systemsInOrder)
            system.Update(gameTime);
    }

    public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
    {
        // load not yet loaded systems added late
        LoadSystemContent();

        foreach (var system in systemsInOrder)
            system.Draw(gameTime, spriteBatch);
    }

    public void Reset()
    {
        foreach (var system in systemsInOrder)
            system.Reset();
    }
}

Toepassingen

Ik heb gelijk het systemen systeem gebruikt en een paar klassen die al bijna systemen zijn systemen gemaakt. Bijvoorbeeld de InputHelper, alleen dat laat nog wel wat om over te wensen. Want het word ook in de HandleInput method als argument meegegeven.

Maar desondanks dat verschoonde het de structuur van de App basis van het spel al snel. Ook heb ik een deel ervan eruit gehaald en in zijn eigen system gezet hieronder, wat ook als een mooi voorbeeld dient van hoe dit veel overzichtelijker is.

public class WindowControlsSystem : BaseSystem
{
    public Keys[] closeKeys = [Keys.Escape];
    public Keys[] fullscreenKeys = [Keys.F11, Keys.F5];

    public override void HandleInput(InputHelper inputHelper)
    {
        if (closeKeys.Any(inputHelper.IsKeyPressed))
            app.Exit();

        if (fullscreenKeys.Any(inputHelper.IsKeyPressed))
            app.IsFullScreen = !app.IsFullScreen;
    }
}

Klassendiagram

classDiagram class IGameLoopObject { <<interface>> +HandleInput(InputHelper) +Update(GameTime) +Draw(GameTime, SpriteBatch) +Reset() } BaseSystem --|> IGameLoopObject BaseSystem --* SystemHandler class BaseSystem { <<abstract>> #App app #SystemHandler systemHandler +Initialize(App, SystemHandler) +LoadContent() } SystemHandler --|> IGameLoopObject class SystemHandler { #App app #Dictionary~Type, BaseSystem~ systemsByType #List~BaseSystem~ systemsInOrder #Queue~BaseSystem~ systemsToInitialize #Queue~BaseSystem~ systemsToLoad +AddSystem~T~() +AddSystem~T~(T system) +TryGetSystem~T~() T? +GetSystem~T~() T +Initialize(App) -InitializeSystems() +LoadContent() -LoadSystemContent() } AssetManager --|> BaseSystem InputHelper --|> BaseSystem StateManager --|> BaseSystem WindowControlsSystem --|> BaseSystem

Bronnen


Laatst geüpdatet: May 9, 2024
Gecreëerd: April 19, 2024