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 5 of 7 of this article. Click here to view the first page.

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!

Putting it All Together

It's almost time to try this out - you just need to add some code to put everything together!

Open HelloWorldLayer.m and import these headers at the top of the file:

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

Also define these instance variables:

EntityManager * _entityManager;
HealthSystem * _healthSystem;
Entity * _aiPlayer;
Entity * _humanPlayer;

Then implement addPlayers as follows:

- (void)addPlayers {
    
    CGSize winSize = [CCDirector sharedDirector].winSize;
    _entityManager = [[EntityManager alloc] init];
    _healthSystem = [[HealthSystem alloc] initWithEntityManager:_entityManager];
    
    // Create AI
    CCSprite * aiSprite = [[CCSprite alloc] initWithSpriteFrameName:@"castle2_def.png"];
    aiSprite.position = ccp(winSize.width - aiSprite.contentSize.width/2, winSize.height/2);
    [_batchNode addChild:aiSprite];
    
    _aiPlayer = [_entityManager createEntity];
    [_entityManager addComponent:[[RenderComponent alloc] initWithNode:aiSprite] toEntity:_aiPlayer];
    [_entityManager addComponent:[[HealthComponent alloc] initWithCurHp:200 maxHp:200] toEntity:_aiPlayer];
    
    // Create human
    CCSprite * humanSprite = [[CCSprite alloc] initWithSpriteFrameName:@"castle1_def.png"];
    humanSprite.position = ccp(humanSprite.contentSize.width/2, winSize.height/2);
    [_batchNode addChild:humanSprite];
    
    _humanPlayer = [_entityManager createEntity];
    [_entityManager addComponent:[[RenderComponent alloc] initWithNode:humanSprite] toEntity:_humanPlayer];
    [_entityManager addComponent:[[HealthComponent alloc] initWithCurHp:200 maxHp:200] toEntity:_humanPlayer];
        
}

As you can see, now adding a game object is a matter of creating an Entity (i.e. integer), and then adding a bunch of components to it.

Note: Adam Martin pointed out that you might want to make a static initializer for each component so you can call something like [HealthComponent healthWithCurHp:200 maxHp:200] instead of the alloc/init method shown above. Since you're creating components so often, this little bit of savings adds up. In addition, it's even better if you use C++ structures for components instead of objects, since it saves you time in implementaiton.

Note: Adam Martin pointed out that you might want to make a static initializer for each component so you can call something like [HealthComponent healthWithCurHp:200 maxHp:200] instead of the alloc/init method shown above. Since you're creating components so often, this little bit of savings adds up. In addition, it's even better if you use C++ structures for components instead of objects, since it saves you time in implementaiton.

Finally, implement update: and draw as follows:

- (void)update:(ccTime)dt {
    [_healthSystem update:dt];
    
    // Test code to decrease AI's health
    static float timeSinceLastHealthDecrease = 0;
    timeSinceLastHealthDecrease += dt;
    if (timeSinceLastHealthDecrease > 1.0) {
        timeSinceLastHealthDecrease = 0;
        HealthComponent * health = (HealthComponent *) [_entityManager getComponentOfClass:[HealthComponent class] forEntity:_aiPlayer];
        if (health) {
            health.curHp -= 10;
            if (health.curHp <= 0) {
                [self showRestartMenu:YES];
            }
        }
    }
    
}

- (void)draw {
    [_healthSystem draw];
}

Every tick, it gives each system time to update - which right now is just the health system. There is also some test code in there to decrease the AI's health every second, just so you can see something happening.

And that's it - you can finally perform your first test of a basic Entity System framework! Build and run, and you'll see the castles appear on the screen, with their health rendering appropriately:

Entity System Test

And to see one of the benefits of the entity system, comment out the lines that add the HealthComponents to the AI and Human. Build and run, and the game will still work fine - the castles just won't have health bars! Then put them back when you're done.

Contributors

Over 300 content creators. Join our team.