Entity Component System-basierte Engine

9

Hinweis: Ich programmiere dies in Javascript, aber es sollte größtenteils sprachunabhängig sein.

Ich denke darüber nach, meinen Motor auf einen ECS-basierten umzustellen.

Ich habe die Grundidee ( Hinweis: Das ist falsch, siehe meine Antwort ):

Entitäten sind Spielobjekte.
Komponenten sind Teile der Funktionalität ( reactToInput()) oder des Zustands ( position), die an Entitäten "geklebt" werden können.
Systeme haben eine Liste von Entitäten, die sie verwalten und aktualisieren.

Aber ich bin mir nicht ganz sicher, ob ich die Implementierung und einige Details bekomme ...

Frage: Kann ein System mit verschiedenen Arten von Entitäten arbeiten? Normalerweise gebe ich das Beispiel einer Klasse an, die Scenein meiner Engine aufgerufen wird , und sie wird jetzt auch diesem Zweck dienen. Eine Szene ist ein Container aller Objekte, die gerendert, aktualisiert, das Rendering (Lichter) und möglicherweise in Zukunft sogar 2DSoundEmitterObjekte beeinflusst werden können. Es verfügt über eine übergeordnete Benutzeroberfläche, sodass sich der Benutzer nicht um den Typ des Objekts scene.add()und all diese Dinge kümmern muss .

Mir ist klar, dass das Sceneein System sein könnte. Es nimmt Entitäten auf, speichert sie und kann dann ihre Aktualisierungsmethoden aufrufen und möglicherweise sogar einige Statusänderungen vornehmen. Aber es gibt ein Problem: Wie ich oben beschrieben habe, Scenekönnen verschiedene Arten von Objekten gefüttert werden ! Was soll ich beispielsweise in einer Situation tun, in der eine Szene sowohl renderbare Objekte ("Drawables") als auch Lichter enthält? Sollte ich Entitäten vor der Interaktion typüberprüfen lassen? Oder sollte ich es auf einer noch niedrigeren Ebene lösen: Erstellen Sie eine LightSourceKomponente, die zu jedem Objekt hinzugefügt werden kann, und das Licht wäre nur eine Einheit mit LightSourceund PositionKomponenten. Ist das akzeptabel?

Ist es auch eine gute Praxis, weiterhin konventionelle Vererbung und traditionelle Klassen zu verwenden? Zum Beispiel kann ich einfach nicht herausfinden, was mein Rendererwäre! Es ist kein System, da seine einzige Funktion darin besteht, eine Kamera und eine Szene aufzunehmen, alles zu rendern und Effekte (wie Schatten) anzuwenden. Es verwaltet auch den Kontext, die Breite und die Höhe des Spiels, macht Übersetzungen ... Aber es ist immer noch kein System!

Bearbeiten: Könnten Sie vielleicht Ressourcen verknüpfen, die Sie im ECS gefunden haben? Ich habe Probleme, gute zu finden.

jcora
quelle
2
Anstatt die Antwort auf dieser Seite erneut zu veröffentlichen, gebe ich einfach diesen Link: gamedev.stackexchange.com/questions/23533/… Entität sollte nicht abgeleitet werden, Unterschiede zwischen Entitäten sollten durch Komponenten erreicht werden. Im Allgemeinen benötigen Sie eine Schnittstelle für jedes Hauptsystem (Rendering, Physik, Netzwerk, Eingabe, Audio usw.). Ich habe meinen Renderer so eingerichtet, dass die Szene nach renderbaren Entitäten abgefragt wird und der Szenenmanager dann jede Entität, auf der sich eine Renderkomponente befindet, nach ihren Renderinformationen fragt.
Nic Foster
1
Komponentendesign auf dem T = Machine-Blog (da Sie nach einem guten gefragt haben)
John McDonald
Code und Diskussion eines Entity Frameworks: gamadu.com/artemis
Patrick Hughes
@ JohnMcDonald, ich habe einen Kommentar zu diesem Artikel geschrieben, obwohl er auf Moderation wartet. Sie können es hier sehen: t-machine.org/index.php/2007/12/22/… . Ich bin "Yannbane".
JCora
Außerdem beschreibt @NicFoster, der Artikel, mit dem John auf T = Machine verlinkt hat, etwas anderes als Ihre Antwort ... Für diesen Dave haben Entitäten keine Liste von Komponenten, sie sind nur ein Name. Wie "flsjn304" - das ist eine Einheit. Es wird "irgendwo" gespeichert. Und ich muss die Sache noch einmal lesen, um zu verstehen, ob er tatsächlich Komponenten in Systemen aufbewahrt , was mir sehr seltsam erscheint!
JCora

Antworten:

6

Lassen Sie mich sehen, ob ich durch den Versuch, als Web- / UI-JS-Entwickler zu verstehen, hilfreich sein kann. Gehen Sie auch nicht zu weit in der Sprachunabhängigkeit. Viele Muster, die in anderen Sprachen erstellt wurden, sind es wert, studiert zu werden, können jedoch aufgrund ihrer Flexibilität in JS sehr unterschiedlich angewendet werden oder sind aufgrund der Formbarkeit der Sprache nicht unbedingt erforderlich. Sie könnten einige Möglichkeiten nutzen, wenn Sie Ihren Code so schreiben, dass JS dieselben Grenzen hat wie eine klassischere OOP-orientierte Sprache.

Denken Sie beim Faktor "OOP nicht verwenden" zunächst daran, dass JavaScript-Objekte im Vergleich zu anderen Sprachen wie Knetmasse sind und Sie sich wirklich alle Mühe geben müssen, um einen Alptraum für ein Kaskadenvererbungsschema zu erstellen, da JS keine Klasse ist -basiert und Compositing kommt viel natürlicher dazu. Wenn Sie ein albernes Klassen- oder Prototyp-Hand-Me-Down-System in Ihrem JS implementieren, sollten Sie es fallen lassen. In JS verwenden wir Verschlüsse, Prototypen und geben Funktionen wie Süßigkeiten weiter. Es ist widerlich und schmutzig und falsch, aber auch kraftvoll, prägnant und so mögen wir es.

Vererbungsintensive Ansätze werden in Design Patterns tatsächlich als Anti-Pattern formuliert, und das aus gutem Grund für alle, die Klassen oder klassenähnliche Strukturen im Wert von mehr als 15 Stufen durchlaufen haben, um herauszufinden, wo zum Teufel die kaputte Version einer Methode ist kam herein von kann dir sagen.

Ich weiß nicht, warum so viele Programmierer dies lieben (besonders Java-Leute, die aus irgendeinem Grund JavaScript schreiben), aber es ist schrecklich, unleserlich und völlig unbrauchbar, wenn man es an Übermaß gewöhnt. Vererbung ist hier und da in Ordnung, aber in JS nicht wirklich notwendig. In Sprachen, in denen es sich um eine verlockendere Abkürzung handelt, sollte sie eher abstrakteren Architekturproblemen vorbehalten sein als wörtlicheren Modellierungsschemata wie dem Frankensteining einer Zombie-Implementierung durch eine Vererbungskette, die ein BunnyRabbit enthielt, weil es zufällig funktionierte. Das ist keine gute Wiederverwendung von Code. Es ist ein Alptraum für die Instandhaltung.

Als JS-Entwickler erscheinen mir Entity / Component / System-basierte Engines als System / Muster, um Designprobleme zu entkoppeln und dann Objekte für die Implementierung auf einer sehr detaillierten Ebene zusammenzusetzen. Mit anderen Worten, ein Kinderspiel in einer Sprache wie JavaScript. Aber lassen Sie mich sehen, ob ich das zuerst richtig mache.

  • Entität - Die spezifische Sache, die Sie entwerfen. Wir sprechen mehr in Richtung der Eigennamen (aber natürlich nicht wirklich). Nicht 'Scene', sondern 'IntroAreaLevelOne'. IntroAreaLevelOne befindet sich möglicherweise in einer SzeneEntity-Box, aber wir konzentrieren uns auf etwas Bestimmtes, das sich von anderen verwandten Dingen unterscheidet. Im Code ist eine Entität eigentlich nur ein Name (oder eine ID), der an eine Reihe von Dingen gebunden ist, die implementiert oder eingerichtet werden müssen (die Komponenten), um nützlich zu sein.

  • Komponenten - Arten von Dingen, die eine Entität benötigt. Dies sind allgemeine Substantive. Wie WalkingAnimation. Innerhalb von WalkingAnimation können wir spezifischer werden, wie "Shambling" (gute Wahl für Zombies und Pflanzenmonster) oder "ChickenWalker" (ideal für Robotertypen mit umgekehrten Gelenken). Hinweis: Ich bin mir nicht sicher, wie sich das vom Rendern eines solchen 3D-Modells abkoppeln könnte - also vielleicht ein Mistbeispiel, aber ich bin eher ein JS-Profi als ein erfahrener Spieleentwickler. In JS würde ich den Mapping-Mechanismus mit den Komponenten in dieselbe Box setzen. Eigene Komponenten sind wahrscheinlich logisch und eher eine Roadmap, die Ihren Systemen sagt, was zu implementieren ist, wenn Systeme überhaupt benötigt werden (bei meinem Versuch, ECS zu verwenden, sind einige Komponenten nur Sammlungen von Eigenschaftssätzen). Sobald eine Komponente eingerichtet ist, wird sie

  • Systeme - Das echte Programmfleisch ist da. KI-Systeme werden gebaut und verknüpft, Rendering wird erreicht, Animationssequenzen werden erstellt usw. Ich überlasse diese und überlasse sie hauptsächlich der Vorstellungskraft, aber im Beispiel System.AI nimmt eine Reihe von Eigenschaften auf und spuckt eine Funktion aus, die wird verwendet, um dem Objekt Ereignishandler hinzuzufügen, die letztendlich in der Implementierung verwendet werden. Das Wichtigste an System.AI ist, dass es mehrere Komponententypen abdeckt. Sie könnten alle KI-Dinge mit einer Komponente aussortieren, aber dies zu tun, bedeutet, den Sinn der Granularisierung der Dinge falsch zu verstehen.

Beachten Sie die Ziele: Wir möchten es Nicht-Designern einfach machen, eine Art GUI-Oberfläche einzubinden, um verschiedene Arten von Dingen zu optimieren, indem wir Komponenten innerhalb eines für sie sinnvollen Paradigmas maximieren und aufeinander abstimmen, und wir möchten davon wegkommen Beliebte beliebige Codeschemata, die viel einfacher zu schreiben sind als zu ändern oder zu warten.

Also in JS vielleicht so etwas. Spielentwickler sagen mir bitte, wenn ich es schrecklich falsch verstanden habe:

//I'm going with simple objects of flags over arrays of component names
//easier to read and can provide an opt-out default
//Assume a genre-bending stealth assassin game

//new (function etc... is a lazy way to define a constructor and auto-instantiate
var npcEntities = new (function NpcEntities(){

    //note: {} in JS is an object literal, a simple obj namespace (a dictionary)
    //plain ol' internal var in JS is akin to a private member
    var default={ //most NPCs are humanoids and critters - why repeat things?
        speedAttributes:true,
        maneuverAttributes:true,
        combatAttributes:true,
        walkingAnimation:true,
        runningAnimation:true,
        combatAnimation:true,
        aiOblivious:true,
        aiAggro:true,
        aiWary:true, //"I heard something!"
        aiFearful:true
    };

    //this. exposes as public

    this.zombie={ //zombies are slow, but keep on coming so don't need these
        runningAnimation:false,
        aiFearful:false
    };

    this.laserTurret={ //most defaults are pointless so ignore 'em
        ignoreDefault:true,
        combatAttributes:true,
        maneuverAttrubtes:true, //turning speed only
    };
    //also this.nerd, this.lawyer and on and on...

    //loop runs on instantiation which we're forcing on the spot

    //note: it would be silly to repeat this loop in other entity collections
    //but I'm spelling it out to keep things straight-forward.
    //Probably a good example of a place where one-level inheritance from
    //a more general entity class might make sense with hurting the pattern.
    //In JS, of course, that would be completely unnecessary. I'd just build a
    //constructor factory with a looping function new objects could access via
    //closure.

    for(var x in npcEntities){

        var thisEntity = npcEntities[x];

        if(!thisEntity.ignoreDefaults){

            thisEntity = someObjectXCopyFunction(defaults,thisEntity);
            //copies entity properties over defaults

        }
        else {
            //remove nonComponent property since we loop again later
            delete thisEntity.ignoreDefaults;
        }
    }
})() //end of entity instantiation

var npcComponents = {
    //all components should have public entityMap properties

    //No systems in use here. Just bundles of related attributes
    speedAttributes: new (function SpeedAttributes(){
        var shamblingBiped = {
            walkingAcceleration:1,
            topWalking:3
        },
        averageMan = {
            walkingAcceleration:3,
            runningAcceleration:4,
            topWalking: 4,
            topRunning: 6
        },
        programmer = {
            walkingAcceleration:1,
            runningAcceleration:100,
            topWalking:2
            topRunning:2000
        }; //end local/private vars

        //left is entity names | right is the component subcategory
        this.entityMap={
            zombie:shamblingBiped,
            lawyer:averageMan,
            nerd:programmer,
            gCostanza:programmer //makes a cameo during the fire-in-nursery stage
        }
    })(), //end speedAttributes

    //Now an example of an AI component - maps to function used to set eventHandlers
    //functions which, because JS is awesome we can pass around like candy
    //I'll just use some imaginary systems on this one

    aiFearful: new (function AiFearful(){
        var averageMan = Systems.AI({ //builds and returns eventSetting function
            fearThreshold:70, //%hitpoints remaining
            fleeFrom:'lastAttacker',
            tactic:'avoidIntercept',
            hazardAwareness:'distracted'
        }),
        programmer = Systems.AI({
            fearThreshold:95,
            fleeFrom:'anythingMoving',
            tactic:'beeline',
            hazardAwareness:'pantsCrappingPanic'
        });//end local vars/private members


         this.entityMap={
            lawyer:averageMan,
            nerd:averageMan, //nerds can run like programmers but are less cowardly
            gCostanza:programmer //makes a cameo during the fire-in-nursery stage
        }
    })(),//and more components...

    //Systems.AI is general and would get called for all the AI components.
    //It basically spits out functions used to set events on NPC objects that
    //determine their behavior. You could do it all in one shot but
    //the idea is to keep it granular enough for designers to actually tweak stuff
    //easily without tugging on developer pantlegs constantly.
    //e.g. SuperZombies, zombies, but slightly tougher, faster, smarter
}//end npcComponents

function createNPCConstructor(npcType){

    var components = npcEntities[npcType],

    //objConstructor is returned but components is still accessible via closure.

    objConstructor = function(){
        for(var x in components){
            //object iteration <property> in <object>

            var thisComponent = components[x];

            if(typeof thisComponent === 'function'){
                thisComponent.apply(this);
                //fires function as if it were a property of instance
                //would allow the function to add additional properties and set
                //event handlers via the 'this' keyword
            }
            else {
                objConstructor.prototype[x] = thisComponent;
                //public property accessed via reference to constructor prototype
                //good for low memory footprint among other things
            }
        }
    }
    return objConstructor;
}

var npcBuilders= {}; //empty object literal
for (var x in npcEntities){
    npcConstructors[x] = createNPCConstructor(x);
}

Jetzt können Sie jederzeit mit einem NPC bauen npcBuilders.<npcName>();

Eine grafische Benutzeroberfläche kann in die Objekte npcEntities und Komponenten eingebunden werden und es Designern ermöglichen, alte Entitäten zu optimieren oder neue Entitäten zu erstellen, indem sie einfach Komponenten mischen und abgleichen (obwohl es dort keinen Mechanismus für nicht standardmäßige Komponenten gibt, sondern spezielle Komponenten im laufenden Betrieb hinzugefügt werden könnten Code, solange es eine definierte Komponente dafür gab.

Erik Reppen
quelle
Wenn ich mir das sechs Jahre später anschaue, bin ich mir nicht sicher, ob ich meine eigene Antwort verstehe. Könnte dies verbessert werden?
Erik Reppen
1

Ich habe Entity Systems in den Artikeln gelesen, die freundliche Leute in den Kommentaren zur Verfügung gestellt haben, aber ich hatte immer noch einige Zweifel, also stellte ich eine andere Frage .

Zunächst einmal waren meine Definitionen falsch. Entitäten und Komponenten sind nur dumme Datenhalter, während Systeme alle Funktionen bereitstellen .

Ich habe genug gelernt, um den größten Teil meiner Frage hier zu behandeln, also werde ich sie beantworten.

Die SceneKlasse, über die ich gesprochen habe, sollte kein System sein. Es sollte jedoch ein zentraler Manager sein, der alle Entitäten enthalten, Nachrichten erleichtern und möglicherweise sogar Systeme verwalten kann. Es kann auch als eine Art Fabrik für Entitäten fungieren, und ich habe beschlossen, es so zu verwenden. Es kann jeden Entitätstyp annehmen, muss diese Entität dann jedoch einem geeigneten System zuführen (das aus Leistungsgründen keine Typprüfungen durchführen sollte, es sei denn, sie sind bitweise).

Ich sollte bei der Implementierung eines ES kein OOP verwenden, schlägt Adam vor, aber ich finde keinen Grund, keine Objekte mit Methoden für Entitäten und Komponenten zu haben, nicht nur dumme Dateninhaber.

Das Rendererkann einfach als System implementiert werden. Es würde eine Liste von zeichnbaren Objekten führen und die draw()Methode ihrer Renderkomponente alle 16 ms aufrufen.

jcora
quelle
1
"Entitäten und Komponenten sind nur dumme Dateninhaber, während Systeme alle Funktionen bereitstellen." "Rufen Sie die draw () -Methode ihrer Renderkomponente auf." Sie sind immer noch verwirrt. Außerdem besiegt eine "draw" -Methode den Zweck des Rendering-Systems insgesamt. Ich verstehe auch nicht, warum Ihr Szenendiagramm nicht Teil des Renderers sein kann. Es ist nur ein praktisches Werkzeug. Sie können Ihre "zeichnbare" Komponente immer als Knoten implementieren. Es ist einfach unnötig, das Szenendiagramm für mehr als die Szene verantwortlich zu machen, und ich bin sicher, dass das Debuggen ein Chaos sein wird.
Dreta
@dreta, der Renderer (derzeit keine ES-Implementierung der Engine) führt Transformationen, Kameraänderungen und Alpha-Inhalte durch und zeichnet in Zukunft verschiedene Effekte, die GUI und Schatten. Es schien nur natürlich, dieses Zeug zu gruppieren. Sollte die Szene nicht für die Erstellung einer Speichereinheit verantwortlich sein? Oder sollte etwas anderes sie speichern? Der Erstellungsteil besteht wahrscheinlich nur aus ein paar Zeilen, in denen vom Benutzer bereitgestellte Komponenten zusammengefasst werden. Er "erstellt" überhaupt nichts, sondern nur Instanzen.
JCora
Nicht jedes Objekt kann gerendert werden, nicht jedes Objekt kann kollidieren oder einen Ton abgeben. Mit Ihrem Szenenobjekt führen Sie eine extreme Kopplung durch. Warum? Das Schreiben und Debuggen wird nur mühsam sein. Entitäten werden verwendet, um ein Objekt zu identifizieren, Komponenten enthalten Daten und Systeme arbeiten mit den Daten. Warum sollten Sie all das zusammenführen, anstatt über geeignete Systeme wie RenderingSystem und SoundSystem zu verfügen, und diese Systeme nur dann stören, wenn eine Entität über alle erforderlichen Komponenten verfügt?
Dreta
1
Schattenwurf wird normalerweise an Lichtquellen angehängt. Sie können jedoch einfach eine Komponente "CastsShadow" erstellen und beim Rendern dynamischer Objekte danach suchen. Wenn Sie 2D ausführen, ist dies nur ein grundlegendes Problem bei der Bestellung. Ein einfacher Maleralgorithmus löst dieses Problem für Sie. TBH, du machst dir zu früh Sorgen. Sie werden es herausfinden, wenn es Zeit ist, es zu tun, und Sie haben nur das im Kopf. Im Moment verwirren Sie sich nur. Sie können nicht hoffen, beim ersten Mal alles richtig zu machen, es wird einfach nicht passieren. Sie werden diese Brücke überqueren, wenn Sie dazu kommen.
Dreta
1
"Entitäten und Komponenten sind nur dumme Dateninhaber, während Systeme alle Funktionen bereitstellen." Nicht unbedingt. Sie sind in den Ansätzen einiger Leute. Aber nicht andere. Schauen Sie sich die Unity-Engine an - das gesamte Verhalten liegt in den Komponenten.
Kylotan
-2

Einführung in das Abhängigkeitsmanagement 101.

In diesem Kurs wird davon ausgegangen, dass Sie über Grundkenntnisse in Abhängigkeitsinjektion und Repository-Design verfügen.

Die Abhängigkeitsinjektion ist nur eine ausgefallene Möglichkeit für Objekte, miteinander zu sprechen (über Nachrichten / Signale / Delegierte / was auch immer), ohne direkt gekoppelt zu sein.

Es geht um den Satz: " newist Kleber."

Ich werde dies in C # demonstrieren.

public interface IEntity
{
    int[] Position { get; }
    int[] Size { get; }
    bool Update();
    void Render();
}

public interface IRenderSystem
{
    void Draw(IEntity entity);
}

public interface IMovementSystem
{
    bool CanMoveLeft();
    void MoveLeft();
    bool CanMoveRight();
    void MoveRight();
    bool CanMoveUp();
    void MoveUp();
    bool CanMoveDown();
    void MoveDown();
    bool Moved();
    int[] Position { get; set; }
}

public interface IInputSystem
{
    string Direction { get; set; }
}

public class Player : IEntity
{
    private readonly IInputSystem _inputSystem;
    private readonly IMovementSystem _movementSystem;
    private readonly IRenderSystem _renderSystem;
    private readonly int[] _size = new[] { 10, 10 };

    public Player(IRenderSystem renderSystem, IMovementSystem movementSystem, IInputSystem inputSystem)
    {
        _renderSystem = renderSystem;
        _movementSystem = movementSystem;
        _inputSystem = inputSystem;
    }

    public bool Update()
    {
        if (_inputSystem.Direction == "Left" && _movementSystem.CanMoveLeft())
            _movementSystem.MoveLeft();
        if (_inputSystem.Direction == "Right" && _movementSystem.CanMoveRight())
            _movementSystem.MoveRight();
        if (_inputSystem.Direction == "Up" && _movementSystem.CanMoveUp())
            _movementSystem.MoveUp();
        if (_inputSystem.Direction == "Down" && _movementSystem.CanMoveDown())
            _movementSystem.MoveDown();

        return _movementSystem.Moved();
    }

    public void Render()
    {
        if (_movementSystem.Moved())
            _renderSystem.Draw(this);
    }

    public int[] Position
    {
        get { return _movementSystem.Position; }
    }

    public int[] Size
    {
        get { return _size; }
    }
}

Dies ist ein Basissystem für Eingabe, Bewegung und Rendern. Die PlayerKlasse ist in diesem Fall die Entität und die Komponenten sind die Schnittstellen. Die PlayerKlasse weiß nichts über die Interna, wie eine konkrete IRenderSystem, IMovementSystemoder IInputSystemArbeit. Die Schnittstellen bieten jedoch die Möglichkeit Player, Signale zu senden (z. B. Draw auf dem IRenderSystem aufrufen), ohne davon abhängig zu sein, wie das Endergebnis erzielt wird.

Nehmen Sie zum Beispiel meine Implementierung von IMovementSystem:

public interface IGameMap
{
    string LeftOf(int[] currentPosition);
    string RightOf(int[] currentPosition);
    string UpOf(int[] currentPosition);
    string DownOf(int[] currentPosition);
}

public class MovementSystem : IMovementSystem
{
    private readonly IGameMap _gameMap;
    private int[] _previousPosition;
    private readonly int[] _currentPosition;
    public MovementSystem(IGameMap gameMap, int[] initialPosition)
    {
        _gameMap = gameMap;
        _currentPosition = initialPosition;
        _previousPosition = initialPosition;
    }

    public bool CanMoveLeft()
    {
        return _gameMap.LeftOf(_currentPosition) == "Unoccupied";
    }

    public void MoveLeft()
    {
        _previousPosition = _currentPosition;
        _currentPosition[0]--;
    }

    public bool CanMoveRight()
    {
        return _gameMap.RightOf(_currentPosition) == "Unoccupied";
    }

    public void MoveRight()
    {
        _previousPosition = _currentPosition;
        _currentPosition[0]++;
    }

    public bool CanMoveUp()
    {
        return _gameMap.UpOf(_currentPosition) == "Unoccupied";
    }

    public void MoveUp()
    {
        _previousPosition = _currentPosition;
        _currentPosition[1]--;
    }

    public bool CanMoveDown()
    {
        return _gameMap.DownOf(_currentPosition) == "Unoccupied";
    }

    public void MoveDown()
    {
        _previousPosition = _currentPosition;
        _currentPosition[1]++;
    }

    public bool Moved()
    {
        return _previousPosition == _currentPosition;
    }

    public int[] Position
    {
        get { return _currentPosition; }
    }
}

MovementSystemkann seine eigenen Abhängigkeiten haben und das Playerwürde nicht einmal interessieren. Mithilfe von Schnittstellen kann eine Spielstatusmaschine erstellt werden:

public class GameEngine
{
    private readonly List<IEntity> _entities;
    private List<IEntity> _renderQueue; 

    public GameEngine()
    {
        _entities = new List<IEntity>();
    }

    public void RegisterEntity(IEntity entity)
    {
        _entities.Add(entity);
    }

    public void Update()
    {
        _renderQueue = new List<IEntity>();
        foreach (var entity in _entities)
        {
            if(entity.Update())
                _renderQueue.Add(entity);
        }
        // Linq version for those interested
        //_renderQueue.AddRange(_entities.Where(e => e.Update()));
    }

    public void Render()
    {
        foreach (var entity in _renderQueue)
        {
            entity.Render();
        }
    }
}

Und das ist der Beginn eines schönen Spiels (das auch auf Einheiten getestet werden kann).

Und mit ein paar Ergänzungen und etwas Polymorphismus:

public interface IEntity
{
}

public interface IRenderableEntity : IEntity
{
    void Render();        
}

public interface IUpdateableEntity : IEntity
{
    void Update();
    bool Updated { get; }
}

public interface IRenderSystem
{
    void Draw(IRenderableEntity entity);
}

// new player class
public class Player : IRenderableEntity, IUpdateableEntity
{
    private readonly IInputSystem _inputSystem;
    private readonly IMovementSystem _movementSystem;
    private readonly IRenderSystem _renderSystem;
    private readonly int[] _size = new[] { 10, 10 };

    public Player(IRenderSystem renderSystem, IMovementSystem movementSystem, IInputSystem inputSystem)
    {
        _renderSystem = renderSystem;
        _movementSystem = movementSystem;
        _inputSystem = inputSystem;
    }

    public void Update()
    {
        if (_inputSystem.Direction == "Left" && _movementSystem.CanMoveLeft())
            _movementSystem.MoveLeft();
        if (_inputSystem.Direction == "Right" && _movementSystem.CanMoveRight())
            _movementSystem.MoveRight();
        if (_inputSystem.Direction == "Up" && _movementSystem.CanMoveUp())
            _movementSystem.MoveUp();
        if (_inputSystem.Direction == "Down" && _movementSystem.CanMoveDown())
            _movementSystem.MoveDown();
    }

    public bool Updated
    {
        get { return _movementSystem.Moved(); }
    }

    public void Render()
    {
        if (_movementSystem.Moved())
            _renderSystem.Draw(this);
    }

    public int[] Position
    {
        get { return _movementSystem.Position; }
    }

    public int[] Size
    {
        get { return _size; }
    }
}

public class GameEngine
{
    private readonly List<IEntity> _entities;
    private List<IRenderableEntity> _renderQueue; 

    public GameEngine()
    {
        _entities = new List<IEntity>();
    }

    public void RegisterEntity(IEntity entity)
    {
        _entities.Add(entity);
    }

    public void Update()
    {
        _renderQueue = new List<IRenderableEntity>();
        foreach (var entity in _entities)
        {
            if (entity is IUpdateableEntity)
            {
                var updateEntity = entity as IUpdateableEntity;
                updateEntity.Update();
            }

            if (entity is IRenderableEntity)
            {
                var renderEntity = entity as IRenderableEntity;
                _renderQueue.Add(renderEntity);
            }
        }
    }

    public void Render()
    {
        foreach (var entity in _renderQueue)
        {
            entity.Render();
        }
    }
}

Wir haben jetzt ein primitives Entitäts- / Komponentensystem, das auf aggregierten Schnittstellen und loser Vererbung basiert.

Dustin Kingen
quelle
1
Dies ist so gegen das Komponentendesign :) Was würden Sie tun, wenn ein Player Sounds erzeugen soll und ein anderer nicht?
Kikaimaru
@Kikaimaru Pass in einem ISoundSystem, das keinen Sound wiedergibt. dh tut nichts.
Dustin Kingen
3
-1, nicht weil es sich um schlechten Code handelt, sondern weil er für die komponentenbasierte Architektur überhaupt nicht relevant ist - tatsächlich versuchen die Komponenten, die Verbreitung solcher Schnittstellen zu vermeiden.
Kylotan
@Kylotan Ich denke mein Verständnis muss falsch sein.
Dustin Kingen