7
\$\begingroup\$

I am a college student, and game development is a little hobby for me. I am writing a new game and I have been looking for a new approach to developing the game engine as to make it more flexible (and allow to provide myself with a solid foundation for future endeavors). I found a few articles about how rather than relying on deep inheritance to represent game entities (Which can lead to heavy root and leaf nodes in your inheritance tree, as well as diamond inheritance in some cases), a better approach is to model your logic in components (Better yet, behaviors and attributes).

Here is a presentation that illustrates this: http://www.gdcvault.com/free/gdc-canada-09 "Theory and Practice of the Game Object Component Architec..."

I'm implementing this using XNA and C#. I have an Entity class:

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);
        }
    }
}

Where "AttributeType" and "BehaviorType" are just enums:

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

The behavior class is a super-class of all other behaviors; behaviors are meant to be self contained and modular (For example, I might have a SpriteBatchRenderBehavior which knows only how to render an entity to the SpriteBatch, which should be usable in any new game that uses this framework).

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);
}

So an entity has a bag of labeled attributes (POSITION_2D, TEXTURE_2D, etc), each behavior can access the attributes of their owner through get/set methods. I'm running into a problem where attributes from my entity need to be shared between behaviors (not sure if this IS actually a problem). Basically, if I have a behavior that changes the position of an entity in the game world, it modifies the position attribute of the entity. I then add the SpriteBatchDrawBehavior to the entity, which depends on the entity having a position (so it knows where to draw the entity). My problem is, if I add the draw behavior before the movement behavior, there is no guarantee that the entity will have acquired a position attribute yet (in which case a key not found exception would be thrown).

I can always make sure the entity has a position by adding a behavior that guarantees it WILL have a position before adding a draw behavior (or any other behavior that would depend on this attribute), however this would make these behaviors coupled in a way they shouldn't be.

The other option is to inherit the draw behavior from a movement behavior which inherits from a position behavior (but this brings us back to the problem with diamond inheritance, etc, and child classes that have little to no ACTUAL relevance to their parent classes, other than having similar dependencies).

One more option is to write a check in my behaviors like: "If my owner doesn't have a position, then make a position attribute for him", but then this raises the question: which default value should be chosen for the new attribute? etc etc.

Any suggestions? Am I overlooking something?

Thanks!

\$\endgroup\$
1
  • 1
    \$\begingroup\$ FYI: looks like you're trying to write C++ in C#. We don't use getters like GetGame(). Look up "properties" and simplify your life. \$\endgroup\$
    – 3Dave
    Jul 23, 2012 at 20:08

3 Answers 3

5
\$\begingroup\$

Yikes - when they said you should model your entities into components, they didn't mean make every entity a grab-bag of enum-based attributes!

The point of component-based design is that, rather than having one large tree-heirarchy of classes (Penguin inherits from FlightlessBird inherits from Bird inherits from Animal inherits from...), like many games traditionally do, you build your classes up from component-classes which specify what properties that class has. For example, your Penguin class might contain an instance of an EatsFish component, but wouldn't contain CanFly.

See here and here for a better description of component-based design than I could ever hope to give (I particularly like this article).


Note that the slides you linked to suggested using Mix-ins to model components. C# does not support multiple-inheritance like C++ does, so you can't really "do" mix-ins. You'll have to compose your classes the more normal way: using interfaces and/or composition.

Note also that they support using a messaging system, which is usually considered to be bad design (despite being widely used).

Finally, since you mentioned you're using XNA, I feel compelled to mention that XNA has built-in support for services. This will solve the instantiation problems you're having. And more importantly, this really helps keep your code clean and separated... but for some ungodly reason it never seems to be mentioned in any XNA tutorials or books.

\$\endgroup\$
4
  • \$\begingroup\$ I want to keep this framework as portable as it can possibly be; wouldn't creating my game using XNA GameServices sort of tie my game down to XNA? Or is the GameService framework simple enough to implement on my own for use on other platforms (android, iphone, etc). Also, in the presentation that I referenced they show a health component that grabs a reference to a related attribute from the owner of the component: "GetAttribute(HEALTH_KEY);", which is why I chose to model my attributes in such a way. \$\endgroup\$
    – Travis Sein
    Jan 13, 2012 at 22:27
  • \$\begingroup\$ @Travis: If you want to make it portable to android/iphone, C# is not a good choice. You might want to look into cross-platform mobile libraries for Java or C++. However, if C# is all you know, stick with that and worry about actually creating a game for now. Worry about portability later - creating a game is hard enough without having to learn a new language on top of it! \$\endgroup\$ Jan 13, 2012 at 22:39
  • \$\begingroup\$ Oh, haha I know that C# certainly isn't the best for portability, however the concepts used in this game engine design are portable to whatever platform (Which is what I'm focusing on). I have a pretty good grasp of writing games using android ndk, open gl es 2.0, and the c++ directx 10 libs, I'm just writing this game in C# and XNA because it's for a Microsoft Windows Phone event on campus. \$\endgroup\$
    – Travis Sein
    Jan 13, 2012 at 22:46
  • \$\begingroup\$ Can you elaborate on how you would change Travis's code? I'm still struggling to understand component-based game design, and the articles you linked to are the same ones I've seen many times before; they're too high level and vague to really help understand the pattern (for me anyway). From my understanding of component-based design, it seems like Travis is on the right track. Can you be more specific about where you think he went wrong and how his code could be improved? \$\endgroup\$
    – Michael
    Mar 2, 2012 at 7:02
1
\$\begingroup\$

I've read the presentation from GDC Canada by Marcin Chady a few hours before reading your question and I think that you didn't understand the point of attributes. I personally (I didn't write the presentation, nor I do know how it really works in Radical Entertainment) think that you must create the needed attributes before actually using them.

The ChangePositionBehaviour and RenderBehaviour both use the Position attribute, but neither the first one, nor the second one do initialize the attribute. You must initialize it first. You have to tell the entity where it's located.

Let me show you it on a simple example of a Breakout clone:

You've got a Level class (it's a game level = room = map), which contains Brick object (entity). The Level knows, that the initial position of the Brick is at location 4.0f, 8.0f (X, Y) and it's speed is 0.0f, 1.0f. If it didn't know it, it couldn't have been a level, because you must specify objects' locations in a level definition (level data file). So, what does the level do? It creates a Brick entity and right after that (before any behaviour is called) it sets the position of the Brick to the initial one.

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

Then, you can simply continue. And when you need to draw the Brick, you already know the position of it. When you need to change the position of it (it's a movable brick, which moves from the left side of the screen to the right side, and then back), you just do:

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

I hope you understood me. It's simple: the behaviours rely on attributes, so the attributes must be already set before you proceed to OnUpdate() and OnMessage().

By the way: (as I cannot comment with my newbie reputation on GameDev SE, I'm adding this btw text to my answer) You don't need any multiple inheritance, so I really don't understand where is the leading answer of BlueRaja - Danny Pflughoeft going... The Theory and Practice of Game Object Component Architecture presentation is fully transformable to C# without any problems, as well as to any other OOP language. And I don't know what's wrong with message passing, because in this example, there isn't any other possible solution I can think of (you can't avoid using messages if you want this code to be generic, in my opinion). Maybe you could use custom event handlers and instead of messages, you'd pass event arguments. It could work too.

\$\endgroup\$
0
\$\begingroup\$

My suggestion would be to have an initialization function for the behaviors. When you add the behavior to the entity, it can add its attributes. When you initialize, you search for attributes you need and raise an error if they aren't there. That way you can add them all in any order, but they won't actually look for the attributes until initialization.

Alternatively, you can use your OnMessage infrastructure to ignore missing attributes when a behavior is added to the entity, then listen for all needed attributes to be added in the OnMessage for the behavior. If an attribute is missing when you try to use the behavior you can either throw an error or simply not execute the behavior. Whichever is more appropriate for your game.

Does any of that help?

\$\endgroup\$
2
  • \$\begingroup\$ I like your idea of having the behaviors initialize after construction to check if required attributes exist. I think I might try doing that and see how it works out. I actually had the same idea with using the OnMessage methods to notify behaviors about the addition of new attributes; but it seems both of these solutions still depend on the order in which the components are added/initialized. With the message system, if a component adds an attribute, a one time message will be sent to all behaviors, however relevant behaviors may not have been added to the entity yet, missing the message. \$\endgroup\$
    – Travis Sein
    Jan 13, 2012 at 22:35
  • \$\begingroup\$ @TravisSein The ones added after the message is sent should check for the attribute when they are added, that way if it existed before it's good. If it's created after, it'll get the message. \$\endgroup\$
    – Smelch
    Jan 13, 2012 at 22:44

You must log in to answer this question.