Verhalten / Attribut GameComponent-Architektur

7

Ich bin ein Student und Spieleentwicklung ist ein kleines Hobby für mich. Ich schreibe ein neues Spiel und habe nach einem neuen Ansatz für die Entwicklung der Spiel-Engine gesucht, um sie flexibler zu gestalten (und mir eine solide Grundlage für zukünftige Bemühungen zu bieten). Ich habe ein paar Artikel darüber gefunden, wie ein besserer Ansatz darin besteht, Ihre Logik zu modellieren, anstatt sich auf eine tiefe Vererbung zu verlassen, um Spielentitäten darzustellen (was zu starken Wurzel- und Blattknoten in Ihrem Vererbungsbaum sowie in einigen Fällen zu einer Diamantvererbung führen kann) in Komponenten (noch besser, Verhalten und Attribute).

Hier ist eine Präsentation, die dies veranschaulicht: http://www.gdcvault.com/free/gdc-canada-09 "Theorie und Praxis der Spielobjektkomponente Architec ..."

Ich implementiere dies mit XNA und C #. Ich habe eine Entitätsklasse:

class Entity
{
    private Game1 mGame;
    private Dictionary<AttributeType, object> mAttributes;
    private Dictionary<BehaviorType, Behavior> mBehaviors;

    public Entity(Game game)
    {
        mGame = game;
        mAttributes = new Dictionary<AttributeType, object>();
        mBehaviors = new Dictionary<BehaviorType, Behavior>();
    }

    public Game GetGame()
    {
        return mGame;
    }

    //Attributes
    public void SetAttribute<T>(AttributeType attributeType, T value)
    {
        if (mAttributes.ContainsKey(attributeType))
        {
            mAttributes[attributeType] = value;
            OnMessage<T>(MessageType.ATTRIBUTE_UPDATED, attributeType, value);
        }
        else
        {
            mAttributes.Add(attributeType, value);
            OnMessage<T>(MessageType.ATTRIBUTE_CREATED, attributeType, value);
        }
    }

    public T GetAttribute<T>(AttributeType attributeType)
    {
        if (!mAttributes.ContainsKey(attributeType))
        {
            throw new KeyNotFoundException("GetAttribute: Attribute with type: " + attributeType.ToString() + " not found.");
        }
        return (T)mAttributes[attributeType];
    }

    public bool HasAttribute(AttributeType attributeType)
    {
        return mAttributes.ContainsKey(attributeType);
    }

    //Behaviors
    public void SetBehavior(BehaviorType behaviorType, Behavior behavior)
    {
        if (mBehaviors.ContainsKey(behaviorType))
        {
            mBehaviors[behaviorType] = behavior;
        }
        else
        {
            mBehaviors.Add(behaviorType, behavior);
        }
    }

    public Behavior GetBehavior(BehaviorType behaviorType)
    {
        if (!mBehaviors.ContainsKey(behaviorType))
        {
            throw new KeyNotFoundException("GetBehavior: Behavior with type: " + behaviorType.ToString() + " not found.");
        }
        return mBehaviors[behaviorType];
    }

    public bool HasBehavior(BehaviorType behaviorType)
    {
        return mBehaviors.ContainsKey(behaviorType);
    }

    public Behavior RemoveBehavior(BehaviorType behaviorType)
    {
        if (!mBehaviors.ContainsKey(behaviorType))
        {
            throw new KeyNotFoundException("RemoveBehavior: Behavior with type: " + behaviorType.ToString() + " not found.");
        }
        Behavior behavior = mBehaviors[behaviorType];
        mBehaviors.Remove(behaviorType);
        return behavior;
    }

    public void OnUpdate(GameTime gameTime)
    {
        foreach (Behavior behavior in mBehaviors.Values)
        {
            behavior.OnUpdate(gameTime);
        }
    }

    public void OnMessage<T>(MessageType messageType, AttributeType attributeType, T data)
    {
        foreach (Behavior behavior in mBehaviors.Values)
        {
            behavior.OnMessage<T>(messageType, attributeType, data);
        }
    }
}

Wobei "AttributeType" und "BehaviorType" nur Aufzählungen sind:

public enum AttributeType
{
    POSITION_2D,
    VELOCITY_2D,
    TEXTURE_2D,
    RGB_8888
}

Die Verhaltensklasse ist eine Superklasse aller anderen Verhaltensweisen. Verhaltensweisen sollen eigenständig und modular sein (z. B. könnte ich ein SpriteBatchRenderBehavior haben, das nur weiß, wie eine Entität in SpriteBatch gerendert wird, das in jedem neuen Spiel verwendet werden sollte, das dieses Framework verwendet).

abstract class Behavior
{
    //Reference to owner to access attributes
    private Entity mOwner;

    public Behavior(Entity owner)
    {
        mOwner = owner;
    }

    public Entity GetOwner()
    {
        return mOwner;
    }

    protected void SetAttribute<T>(AttributeType attributeType, T value)
    {
        mOwner.SetAttribute<T>(attributeType, value);
    }

    protected T GetAttribute<T>(AttributeType attributeType)
    {
        return (T)mOwner.GetAttribute<T>(attributeType);
    }

    public abstract void OnUpdate(GameTime gameTime);

    public abstract void OnMessage<T>(MessageType messageType, AttributeType attributeType, T data);
}

Eine Entität verfügt also über eine Reihe von beschrifteten Attributen (POSITION_2D, TEXTURE_2D usw.). Jedes Verhalten kann über get / set-Methoden auf die Attribute seines Besitzers zugreifen. Ich stoße auf ein Problem, bei dem Attribute aus meiner Entität zwischen Verhaltensweisen geteilt werden müssen (nicht sicher, ob dies tatsächlich ein Problem ist). Wenn ich ein Verhalten habe, das die Position einer Entität in der Spielwelt ändert, ändert es grundsätzlich das Positionsattribut der Entität. Ich füge dann das SpriteBatchDrawBehavior zur Entität hinzu, was von der Entität abhängt, die eine Position hat (damit sie weiß, wo die Entität gezeichnet werden soll). Mein Problem ist, wenn ich das Zeichenverhalten vor dem Bewegungsverhalten hinzufüge, gibt es keine Garantie dafür, dass die Entität noch ein Positionsattribut erhalten hat (in diesem Fall würde eine nicht gefundene Schlüsselausnahme ausgelöst).

Ich kann immer sicherstellen, dass die Entität eine Position hat, indem ich ein Verhalten hinzufüge, das garantiert, dass sie eine Position hat, bevor ich ein Zeichenverhalten hinzufüge (oder jedes andere Verhalten, das von diesem Attribut abhängt). Dies würde jedoch dazu führen, dass diese Verhaltensweisen so gekoppelt werden, wie sie sind sollte nicht sein.

Die andere Möglichkeit besteht darin, das Zeichenverhalten von einem Bewegungsverhalten zu erben, das von einem Positionsverhalten erbt (dies bringt uns jedoch zurück zu dem Problem der Diamantvererbung usw. und von untergeordneten Klassen, die für ihre übergeordneten Klassen keine oder nur eine geringe tatsächliche Relevanz haben als mit ähnlichen Abhängigkeiten).

Eine weitere Option besteht darin, mein Verhalten zu überprüfen: "Wenn mein Besitzer keine Position hat, erstellen Sie ein Positionsattribut für ihn". Dies wirft jedoch die Frage auf: Welcher Standardwert sollte für das neue Attribut ausgewählt werden ? etc etc. etc.

Irgendwelche Vorschläge? Übersehe ich etwas?

Vielen Dank!


quelle
1
Zu Ihrer Information: Sieht so aus, als würden Sie versuchen, C ++ in C # zu schreiben. Wir verwenden keine Getter wie GetGame (). Suchen Sie nach "Eigenschaften" und vereinfachen Sie Ihr Leben.
3Dave

Antworten:

5

Huch - als sie sagten, Sie sollten Ihre Entitäten in Komponenten modellieren, bedeuteten sie nicht, dass jede Entität zu einer Sammlung von enumbasierten Attributen wird!

Der Sinn des komponentenbasierten Designs besteht darin, dass Sie Ihre Klassen nicht wie bei vielen Spielen traditionell aus Komponentenklassen zusammensetzen , anstatt eine große Baum-Hierarchie von Klassen zu haben ( Penguinerbt von FlightlessBirderbt von Birderbt von Animalerbt von ...) Geben Sie an, welche Eigenschaften diese Klasse hat. Beispielsweise Penguinkönnte Ihre Klasse eine Instanz einer EatsFishKomponente enthalten, diese jedoch nicht CanFly.

Siehe hier und hier für eine bessere Beschreibung der Komponenten-basierte Design , als ich jemals geben könnte hoffen (Ich mag besonders dieses Artikels) .


Beachten Sie, dass die von Ihnen verknüpften Folien die Verwendung von Mix-Ins zum Modellieren von Komponenten vorgeschlagen haben. C # unterstützt keine Mehrfachvererbung wie C ++, daher können Sie Mix-Ins nicht wirklich "ausführen". Sie müssen Ihre Klassen auf normalere Weise zusammenstellen: mithilfe von Schnittstellen und / oder Kompositionen.

Beachten Sie auch, dass sie die Verwendung eines Messagingsystems unterstützen, das normalerweise als schlechtes Design angesehen wird (obwohl es weit verbreitet ist).

Da Sie erwähnt haben, dass Sie XNA verwenden, muss ich erwähnen, dass XNA eine integrierte Unterstützung für Dienste bietet . Dies löst die Instanziierungsprobleme, die Sie haben. Und was noch wichtiger ist, dies hilft wirklich dabei, Ihren Code sauber und getrennt zu halten ... aber aus irgendeinem gottlosen Grund scheint er in keinem XNA-Tutorial oder Buch erwähnt zu werden.

BlueRaja - Danny Pflughoeft
quelle
Ich möchte dieses Framework so portabel wie möglich halten. Würde das Erstellen meines Spiels mit XNA GameServices mein Spiel nicht an XNA binden? Oder ist das GameService-Framework einfach genug, um es selbst für die Verwendung auf anderen Plattformen (Android, iPhone usw.) zu implementieren? Außerdem zeigen sie in der Präsentation, auf die ich verwiesen habe, eine Integritätskomponente, die einen Verweis auf ein verwandtes Attribut vom Eigentümer der Komponente abruft: "GetAttribute (HEALTH_KEY);", weshalb ich mich dazu entschlossen habe, meine Attribute so zu modellieren.
@Travis: Wenn Sie es auf Android / iPhone portierbar machen möchten, ist C # keine gute Wahl. Vielleicht möchten Sie sich mit plattformübergreifenden mobilen Bibliotheken für Java oder C ++ befassen. Wenn Sie jedoch nur C # kennen, bleiben Sie dabei und sorgen Sie sich, dass Sie vorerst tatsächlich ein Spiel erstellen . Sorgen Sie sich später um die Portabilität - das Erstellen eines Spiels ist schwierig genug, ohne dass Sie eine neue Sprache lernen müssen!
BlueRaja - Danny Pflughoeft
Oh, haha, ich weiß, dass C # für die Portabilität sicherlich nicht das Beste ist, aber die in diesem Game-Engine-Design verwendeten Konzepte sind auf jede Plattform portierbar (worauf ich mich konzentriere). Ich habe ein ziemlich gutes Verständnis für das Schreiben von Spielen mit Android ndk, open gl es 2.0 und den c ++ directx 10-Bibliotheken. Ich schreibe dieses Spiel nur in C # und XNA, weil es für ein Microsoft Windows Phone-Ereignis auf dem Campus ist.
Können Sie näher erläutern, wie Sie Travis 'Code ändern würden? Ich habe immer noch Probleme, das komponentenbasierte Spieldesign zu verstehen, und die Artikel, auf die Sie verlinkt haben, sind die gleichen, die ich schon oft gesehen habe. Sie sind zu hoch und vage, um das Muster wirklich zu verstehen (für mich jedenfalls). Nach meinem Verständnis des komponentenbasierten Designs scheint Travis auf dem richtigen Weg zu sein. Können Sie genauer sagen, wo Ihrer Meinung nach ein Fehler aufgetreten ist und wie sein Code verbessert werden könnte?
Michael
1

Ich habe die Präsentation von GDC Canada von Marcin Chady einige Stunden vor dem Lesen Ihrer Frage gelesen und ich denke, dass Sie den Sinn der Attribute nicht verstanden haben. Ich persönlich (ich habe die Präsentation nicht geschrieben und weiß auch nicht, wie sie in Radical Entertainment wirklich funktioniert) denke, dass Sie die erforderlichen Attribute erstellen müssen, bevor Sie sie tatsächlich verwenden.

Das ChangePositionBehaviour und das RenderBehaviour verwenden beide das Position- Attribut, aber weder das erste noch das zweite initialisieren das Attribut. Sie müssen es zuerst initialisieren. Sie müssen der Entität mitteilen, wo sie sich befindet.

Lassen Sie mich es an einem einfachen Beispiel eines Breakout-Klons zeigen :

Sie haben eine LevelKlasse (es ist ein Spiellevel = Raum = Karte), die ein BrickObjekt (eine Entität) enthält. Das Levelweiß, dass die Ausgangsposition des Brickam Ort 4.0f, 8.0f(X, Y) ist und seine Geschwindigkeit ist 0.0f, 1.0f. Wenn es es nicht wusste, konnte es keine Ebene sein, da Sie die Positionen der Objekte in einer Ebenendefinition (Ebenendatendatei) angeben müssen. Also, was macht das Level? Es erstellt eine BrickEntität und setzt unmittelbar danach (bevor ein Verhalten aufgerufen wird) die Position der Brickauf die ursprüngliche.

Brick.SetAttribute<Vector2>(AttributeType.POSITION_2D, new Vector2(4, 8))
Brick.SetAttribute<Vector2>(AttributeType.VELOCITY_2D, new Vector2(0, 1))

Dann können Sie einfach fortfahren. Und wenn Sie das zeichnen müssen Brick, kennen Sie bereits die Position davon. Wenn Sie die Position ändern müssen (es ist ein beweglicher Stein, der sich von der linken Seite des Bildschirms zur rechten Seite und dann zurück bewegt), tun Sie einfach Folgendes:

Vector2 position = GetAttribute<Vector2>(AttributeType.POSITION_2D);
Vector2 speed = GetAttribute<Vector2>(AttributeType.VELOCITY_2D);
position += speed;
SetAttribute<Vector2>(AttributeType.POSITION_2D, position);

Ich hoffe du hast mich verstanden. Es ist ganz einfach: Das Verhalten hängt von Attributen ab , daher müssen die Attribute bereits festgelegt sein, bevor Sie mit OnUpdate()und fortfahren OnMessage().

Übrigens: (Da ich mit meinem Neuling-Ruf bei GameDev SE keinen Kommentar abgeben kann, füge ich diesen übrigen Text meiner Antwort hinzu.) Sie benötigen keine Mehrfachvererbung, daher verstehe ich wirklich nicht, wo die führende Antwort von ist BlueRaja - Danny Pflughoeft geht ... Die Theorie und Praxis der Präsentation der Spielobjektkomponenten- Architektur kann problemlos in C # sowie in jede andere OOP-Sprache umgewandelt werden. Und ich weiß nicht, was mit der Nachrichtenübermittlung falsch ist, da ich mir in diesem Beispiel keine andere mögliche Lösung vorstellen kann (Sie können die Verwendung von Nachrichten nicht vermeiden, wenn dieser Code meiner Meinung nach generisch sein soll). . Möglicherweise könnten Sie benutzerdefinierte Ereignishandler verwenden und anstelle von Nachrichten Ereignisargumente übergeben. Es könnte auch funktionieren.

TomsonTom
quelle
0

Mein Vorschlag wäre, eine Initialisierungsfunktion für die Verhaltensweisen zu haben. Wenn Sie der Entität das Verhalten hinzufügen, kann sie ihre Attribute hinzufügen. Bei der Initialisierung suchen Sie nach den benötigten Attributen und geben einen Fehler aus, wenn diese nicht vorhanden sind. Auf diese Weise können Sie sie alle in beliebiger Reihenfolge hinzufügen, aber sie suchen erst nach der Initialisierung nach den Attributen.

Alternativ können Sie Ihre OnMessage-Infrastruktur verwenden, um fehlende Attribute zu ignorieren, wenn der Entität ein Verhalten hinzugefügt wird, und dann auf alle erforderlichen Attribute zu warten, die in der OnMessage für das Verhalten hinzugefügt werden sollen. Wenn ein Attribut fehlt, wenn Sie versuchen, das Verhalten zu verwenden, können Sie entweder einen Fehler auslösen oder das Verhalten einfach nicht ausführen. Was auch immer für Ihr Spiel besser geeignet ist.

Hilft irgendetwas davon?

Smelch
quelle
Ich mag Ihre Idee, das Verhalten nach der Erstellung initialisieren zu lassen, um zu überprüfen, ob erforderliche Attribute vorhanden sind. Ich denke, ich könnte versuchen, das zu tun und zu sehen, wie es funktioniert. Ich hatte tatsächlich die gleiche Idee mit der Verwendung der OnMessage-Methoden, um Verhaltensweisen über das Hinzufügen neuer Attribute zu benachrichtigen. Es scheint jedoch, dass beide Lösungen immer noch von der Reihenfolge abhängen, in der die Komponenten hinzugefügt / initialisiert werden. Wenn beim Nachrichtensystem eine Komponente ein Attribut hinzufügt, wird eine einmalige Nachricht an alle Verhaltensweisen gesendet. Allerdings wurden der Entität möglicherweise noch keine relevanten Verhaltensweisen hinzugefügt, da die Nachricht fehlt.
@TravisSein Diejenigen, die nach dem Senden der Nachricht hinzugefügt wurden, sollten beim Hinzufügen nach dem Attribut suchen, wenn es vorhanden war, bevor es gut ist. Wenn es danach erstellt wird, wird die Nachricht angezeigt.