Introduction to Component Based Architecture in Games

This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer. When you’re making a game, you need to create objects to represent the entities in your games – like monsters, the player, bullets, and so on. When you first get started, you might think the most logical thing is […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 6 of this article. Click here to view the first page.

Creating the Component Class and a Subclass

Next create a new group under EntitySystem called Components.

Then add a new file to the Components group using the Objective-C class template. Name the new class Component, and make it a subclass of NSObject.

And then you're done :] This is a completely empty class, all the interesting stuff will be in subclasses.

Again, this is a class that is optional and you don't even really need. I like having it for a bit of type safety though.

Let's create two subclass for now so you can see some examples. Add a new file to the Components group using the Objective-C class template. Name the new class RenderComponent, and make it a subclass of Component.

Replace the contents of RenderComponent.h with the following:

#import "Component.h"
#import "cocos2d.h"

@interface RenderComponent : Component

@property (strong) CCSprite * node;

- (id)initWithNode:(CCSprite *)node;

@end

And replace RenderComponent.m with the following:

#import "RenderComponent.h"

@implementation RenderComponent

- (id)initWithNode:(CCSprite *)node {
    if ((self = [super init])) {
        self.node = node;
    }
    return self;
}

@end

As you can see, this class is literally just data - along with a convenience method to initialize the data. According to the Entity System design, this class should have no code to actually operate upon the data - that is the job of the systems, which you'll develop later.

Let's see another example. Add a new file to the Components group using the Objective-C class template. Name the new class HealthComponent, and make it a subclass of Component.

Replace the contents of HealthComponent.h with the following:

#import "Component.h"

@interface HealthComponent : Component

@property (assign) float curHp;
@property (assign) float maxHp;
@property (assign) BOOL alive;

- (id)initWithCurHp:(float)curHp maxHp:(float)maxHp;

@end

And replace HealthComponent.m with the following:

#import "HealthComponent.h"

@implementation HealthComponent

- (id)initWithCurHp:(float)curHp maxHp:(float)maxHp {
    if ((self = [super init])) {
        self.curHp = curHp;
        self.maxHp = maxHp;
        self.alive = YES;
    }
    return self;
}

@end

Again, just data and a convenience method.

Creating the Entity Manager

Next you need to create an object that acts as the "database", where you can look up entities, get their list of components, and so on. In this game, you'll call this class the Entity Manager.

Add a new file to the Framework group using the Objective-C class template. Name the new class EntityManager, and make it a subclass of NSObject.

Open EntityManager.h and replace the contents with the following:

#import "Entity.h"
#import "Component.h"

@interface EntityManager : NSObject

- (uint32_t) generateNewEid;
- (Entity *)createEntity;
- (void)addComponent:(Component *)component toEntity:(Entity *)entity;
- (Component *)getComponentOfClass:(Class)class forEntity:(Entity *)entity;
- (void)removeEntity:(Entity *)entity;
- (NSArray *)getAllEntitiesPosessingComponentOfClass:(Class)class;

@end

And replace EntityManager.m with the following:

#import "EntityManager.h"

@implementation EntityManager {
    NSMutableArray * _entities;
    NSMutableDictionary * _componentsByClass;
    uint32_t _lowestUnassignedEid;
}

- (id)init {
    if ((self = [super init])) {        
        _entities = [NSMutableArray array];
        _componentsByClass = [NSMutableDictionary dictionary];
        _lowestUnassignedEid = 1;
    }
    return self;
}

@end

This just initializes the data structures you'll be using for the EntityManager. You'll have a list of all the entities (which remember are just integers!), then you'll have a dictionary that contains a list of each type of components (RenderComponents, HealthComponents, etc). Finally, you'll keep track of the lowest unassigned entity ID to use.

Next add this method to generate a new entity ID:

- (uint32_t) generateNewEid {
    if (_lowestUnassignedEid < UINT32_MAX) {
        return _lowestUnassignedEid++;
    } else {
        for (uint32_t i = 1; i < UINT32_MAX; ++i) {
            if (![_entities containsObject:@(i)]) {
                return i;
            }
        }
        NSLog(@"ERROR: No available EIDs!");
        return 0;
    }
}

As you can see, it simply returns the next highest number - until it wraps around to the maximum integer, in which case it looks through the list of entities for an available entity ID.

Next add this method to create a new Entity:

- (Entity *)createEntity {
    uint32_t eid = [self generateNewEid];
    [_entities addObject:@(eid)];
    return [[Entity alloc] initWithEid:eid];
}

This is as simple as generating a new entity ID and adding it to the list. It returns the wrapper Entity object for convenience.

Next add this method to add a component to an entity, and a method to get a component for an entity of a particular class:

- (void)addComponent:(Component *)component toEntity:(Entity *)entity {
    NSMutableDictionary * components = _componentsByClass[NSStringFromClass([component class])];
    if (!components) {
        components = [NSMutableDictionary dictionary];
        _componentsByClass[NSStringFromClass([component class])] = components;
    }    
    components[@(entity.eid)] = component;
}

- (Component *)getComponentOfClass:(Class)class forEntity:(Entity *)entity {
    return _componentsByClass[NSStringFromClass(class)][@(entity.eid)];
}

The addComponent method first looks inside the dictionary of components by class, where the key is the name of the class (in string version), and the value is a second dictionary. The second dictionary's key is the entity ID, and the value is the component itself. The getComponentOfClass simply reverses this lookup.

Note: With this approach, there can only be one instance of a component per entity. You may have a game where you want to have multiple instances of a component per entity (such as two guns) - if that's the case, you can change these data structures a bit so instead of the value of the second dictionary being a component, it would be a list of components instead.

Next add this method to remove an entity:

- (void)removeEntity:(Entity *)entity {
    for (NSMutableDictionary * components in _componentsByClass.allValues) {
        if (components[@(entity.eid)]) {
            [components removeObjectForKey:@(entity.eid)];
        }
    }
    [_entities removeObject:@(entity.eid)];
}

This simply removes all components for an entity from the data structures, then removes the entity itself.

Finally, add this helper method:

- (NSArray *)getAllEntitiesPosessingComponentOfClass:(Class)class {
    NSMutableDictionary * components = _componentsByClass[NSStringFromClass(class)];
    if (components) {
        NSMutableArray * retval = [NSMutableArray arrayWithCapacity:components.allKeys.count];
        for (NSNumber * eid in components.allKeys) {
            [retval addObject:[[Entity alloc] initWithEid:eid.integerValue]];
        }
        return retval;
    } else {
        return [NSArray array];
    }
}

This is a helper method to find all the entities that have a particular component. Think of it as a "select * from component_name" query.

And that's it for the Entity Manager for now. There's only a few more steps before you're ready to try this out!

Creating the Systems

So far you've created the entity part of the entity system (Ientity, Component, and Component subclasses), along with the database part (EntityManager). Now it's time to create the system part - i.e. the code that actually does something useful!

Inside the EntitySystem group, create a new subgroup called Systems.

Then add a new file to the Systems group using the Objective-C class template. Name the new class System, and make it a subclass of NSObject.

Then replace System.h with the following code:

@class EntityManager;

@interface System : NSObject

@property (strong) EntityManager * entityManager;

- (id)initWithEntityManager:(EntityManager *)entityManager;

- (void)update:(float)dt;

@end

And replace System.m with the following:

#import "System.h"

@implementation System

- (id)initWithEntityManager:(EntityManager *)entityManager {
    if ((self = [super init])) {
        self.entityManager = entityManager;
    }
    return self;
}

- (void)update:(float)dt {   
}

@end

So you can see the base System subclass is pretty simple - it has a reference to the entity manager, and an update method.

Now let's create a subclass that will be responsible for a) figuring out when an object is alive or dead, and doing some basic logic upon death, and b) drawing the health bar to the screen.

To do this, add a new file to the Systems group using the Objective-C class template. Name the new class HealthSystem, and make it a subclass of NSObject.

Open HealthSystem.h and replace the contents with the following:

#import "System.h"

@interface HealthSystem : System

- (void)draw;

@end

Then open HealthSystem.m and replace the contents with the following:

#import "HealthSystem.h"
#import "EntityManager.h"
#import "HealthComponent.h"
#import "RenderComponent.h"
#import "SimpleAudioEngine.h"

@implementation HealthSystem

- (void)update:(float)dt {
    
    // 1
    NSArray * entities = [self.entityManager getAllEntitiesPosessingComponentOfClass:[HealthComponent class]];
    for (Entity * entity in entities) {
        
        // 2
        HealthComponent * health = (HealthComponent *) [self.entityManager getComponentOfClass:[HealthComponent class] forEntity:entity];
        RenderComponent * render = (RenderComponent *) [self.entityManager getComponentOfClass:[RenderComponent class] forEntity:entity];
        
        // 3
        if (!health.alive) return;
        if (health.maxHp == 0) return;
        if (health.curHp <= 0) {
            [[SimpleAudioEngine sharedEngine] playEffect:@"boom.wav"];
            health.alive = FALSE;
            
            // 4
            if (render) {            
                [render.node runAction:
                 [CCSequence actions:
                  [CCFadeOut actionWithDuration:0.5],
                  [CCCallBlock actionWithBlock:^{
                     [render.node removeFromParentAndCleanup:YES];
                     [self.entityManager removeEntity:entity];
                 }], nil]];
            } else {
                [self.entityManager removeEntity:entity];
            }
        }
    }    
}

@end

There's a good bit of code here, so let's go over it section by section:

  1. Uses the helper method you wrote earlier to get all of the entities that have HealthComponents associated with them.
  2. For each of these entities, looks up the HealthComponent and tries to see if there's a RenderComponent too. The HealthComponent is guaranteed (since you just searched for it), but the RenderComponent might be nil.
  3. This is the same code that was in the previous version of MonsterWars - no change here.
  4. If the object has died, checks to see if there's a render node. If there is, it fades out the node, then removes it from the entity manager (and screen). Otherwise it just immediately removes it from the entity manager.

Next add the draw method:

- (void)draw {    
    NSArray * entities = [self.entityManager getAllEntitiesPosessingComponentOfClass:[HealthComponent class]];
    for (Entity * entity in entities) {

        HealthComponent * health = (HealthComponent *) [self.entityManager getComponentOfClass:[HealthComponent class] forEntity:entity];
        RenderComponent * render = (RenderComponent *) [self.entityManager getComponentOfClass:[RenderComponent class] forEntity:entity];        
        if (!health || !render) continue;
        
        int sX = render.node.position.x - render.node.contentSize.width/2;
        int eX = render.node.position.x + render.node.contentSize.width/2;
        int actualY = render.node.position.y + render.node.contentSize.height/2;
        
        static int maxColor = 200;
        static int colorBuffer = 55;
        float percentage = ((float) health.curHp) / ((float) health.maxHp);
        int actualX = ((eX-sX) * percentage) + sX;
        int amtRed = ((1.0f-percentage)*maxColor)+colorBuffer;
        int amtGreen = (percentage*maxColor)+colorBuffer;
        
        glLineWidth(7);
        ccDrawColor4B(amtRed,amtGreen,0,255);
        ccDrawLine(ccp(sX, actualY), ccp(actualX, actualY));
    }    
}

This starts out just as last time, except this time to draw the health bar both the health component and render component are required (like the "key" discussed earlier), so it bails if these are not there.

Otherwise, the rest of this code is just the same as it was in the previous version of Monster Wars.

So overall, notice that this one system works primarily on the HealthComponent data, but it uses aspects of the RenderComponent data to get its job done. This is quite typical when looking at systems!

Contributors

Over 300 content creators. Join our team.