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

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.

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.

Entity Factory

Up to this point, you added the code to create an entity directly inside the HelloWorldLayer class. But usually when using an Entity System it's a lot easier to create a central class to create "templates" of entities that you can then customize.

For example, the central class would create a "basic Zap monster" for you with the appropriate components set, and then you can modify values for the particular Zap monster you want. This makes it easier for you to decide what components are attached to each entity at a central location.

We will call this central "entity making class" an entity factory. Create a new file in the Framework group using the Objective-C class template. Name the new class EntityFactory, and make it a subclass of NSObject.

Then replace EntityFactory.h with the following:

@class Entity;
@class EntityManager;
@class CCSpriteBatchNode;

@interface EntityFactory : NSObject

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

- (Entity *)createHumanPlayer;
- (Entity *)createAIPlayer;
- (Entity *)createQuirkMonster;

@end

And replace EntityFactory.m with the following:

#import "EntityFactory.h"
#import "cocos2d.h"
#import "EntityManager.h"
#import "RenderComponent.h"
#import "HealthComponent.h"

@implementation EntityFactory {
    EntityManager * _entityManager;
    CCSpriteBatchNode * _batchNode;
}

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

- (Entity *)createHumanPlayer {
    CCSprite * sprite = [[CCSprite alloc] initWithSpriteFrameName:@"castle1_def.png"];
    [_batchNode addChild:sprite];
    
    Entity * entity = [_entityManager createEntity];
    [_entityManager addComponent:[[RenderComponent alloc] initWithNode:sprite] toEntity:entity];
    [_entityManager addComponent:[[HealthComponent alloc] initWithCurHp:200 maxHp:200] toEntity:entity];
    return entity;
}

- (Entity *)createAIPlayer {
    CCSprite * sprite = [[CCSprite alloc] initWithSpriteFrameName:@"castle2_def.png"];
    [_batchNode addChild:sprite];
    
    Entity * entity = [_entityManager createEntity];
    [_entityManager addComponent:[[RenderComponent alloc] initWithNode:sprite] toEntity:entity];
    [_entityManager addComponent:[[HealthComponent alloc] initWithCurHp:200 maxHp:200] toEntity:entity];
    return entity;
}

- (Entity *)createQuirkMonster {
    CCSprite * sprite = [[CCSprite alloc] initWithSpriteFrameName:@"quirk1.png"];
    [_batchNode addChild:sprite];
    
    Entity * entity = [_entityManager createEntity];
    [_entityManager addComponent:[[RenderComponent alloc] initWithNode:sprite] toEntity:entity];
    [_entityManager addComponent:[[HealthComponent alloc] initWithCurHp:5 maxHp:5] toEntity:entity];
    return entity;
}

@end

This is the same code you wrote earlier, just brought into this helper class, so that anyone who has access to the EntityFactory can easily create game objects of any given "template". It's also a convenient place if you want to read these "template" settings out of a file instead!

There's also a new method in here to create a Quirk monster, as a test.

Now let's put it to use. Go back to HelloWorldLayer.m and make the following changes:

// Add this import to the top of the file
#import "EntityFactory.h"

// Add this private instance variable
EntityFactory * _entityFactory;

// Replace addPlayers with this:
- (void)addPlayers {
    
    CGSize winSize = [CCDirector sharedDirector].winSize;
    _entityManager = [[EntityManager alloc] init];
    _healthSystem = [[HealthSystem alloc] initWithEntityManager:_entityManager];
    _entityFactory = [[EntityFactory alloc] initWithEntityManager:_entityManager batchNode:_batchNode];
    
    _aiPlayer = [_entityFactory createAIPlayer];
    RenderComponent * aiRender = (RenderComponent *) [_entityManager getComponentOfClass:[RenderComponent class] forEntity:_aiPlayer];
    if (aiRender) {
        aiRender.node.position = ccp(winSize.width - aiRender.node.contentSize.width/2, winSize.height/2);
    }
    
    _humanPlayer = [_entityFactory createHumanPlayer];
    RenderComponent * humanRender = (RenderComponent *) [_entityManager getComponentOfClass:[RenderComponent class] forEntity:_humanPlayer];
    if (humanRender) {
        humanRender.node.position = ccp(humanRender.node.contentSize.width/2, winSize.height/2);
    }    
        
}

// Replace quirkButtonTapped: with this:
- (void)quirkButtonTapped:(id)sender {
    NSLog(@"Quirk button tapped!");
    
    [[SimpleAudioEngine sharedEngine] playEffect:@"spawn.wav"];
    
    Entity * entity = [_entityFactory createQuirkMonster];
    RenderComponent * render = (RenderComponent *) [_entityManager getComponentOfClass:[RenderComponent class] forEntity:entity];
    if (render) {        
        CGSize winSize = [CCDirector sharedDirector].winSize;
        float randomOffset = CCRANDOM_X_Y(-winSize.height * 0.25, winSize.height * 0.25);
        render.node.position = ccp(winSize.width * 0.25, winSize.height * 0.5 + randomOffset);
    }
}

This modifies the code to use the new entity factory - this way, there's a nice central place that defines what components are on each "template." It's not a big deal right now, but later when you want each system to be able to create objects, you'll be able to use the entity factory to do this rather than duplicating the same information in multiple places.

Finally, add this helper macro to the bottom of Supporting Files\Prefix.pch:

#define CCRANDOM_X_Y(__X__, __Y__) (((__Y__) - (__X__)) * (arc4random() / (float)0xffffffff) + (__X__))

Build and run, and now you can tap the quirk button to spawn some Quirks!

Spawning Monsters

Contributors

Over 300 content creators. Join our team.