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!
Antworten:
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 (
Penguin
erbt vonFlightlessBird
erbt vonBird
erbt vonAnimal
erbt von ...) Geben Sie an, welche Eigenschaften diese Klasse hat. BeispielsweisePenguin
könnte Ihre Klasse eine Instanz einerEatsFish
Komponente enthalten, diese jedoch nichtCanFly
.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.
quelle
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
Level
Klasse (es ist ein Spiellevel = Raum = Karte), die einBrick
Objekt (eine Entität) enthält. DasLevel
weiß, dass die Ausgangsposition desBrick
am Ort4.0f, 8.0f
(X, Y) ist und seine Geschwindigkeit ist0.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 eineBrick
Entität und setzt unmittelbar danach (bevor ein Verhalten aufgerufen wird) die Position derBrick
auf die ursprüngliche.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: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 fortfahrenOnMessage()
.Ü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.
quelle
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?
quelle