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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Introduction to Component Based Architecture in Games
55 mins
- Introducing MonsterWars
- A Shooting Castle: Overview
- A Shooting Castle: Implementation
- Drawbacks of Object Oriented Game Architecture
- Introduction to Component Based Architecture
- Component Based Architecture Approaches
- Object for Each Component with Message Passing
- Entity System Approach
- Creating the Entity Class
- Creating the Component Class and a Subclass
- Creating the Entity Manager
- Creating the Systems
- Putting it All Together
- Entity Factory
- Moving Monsters
- Where To Go From Here?
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!
Moving Monsters
Let's try making our monsters move now - that will be a good example of adding another component and another system.
Let's start with the Component. Create a new file in the Components group using the Objective-C class template. Name the new class MoveComponent, and make it a subclass of Component.
Then replace MoveComponent.h with the following:
#import "Component.h"
@interface MoveComponent : Component
@property (assign) CGPoint moveTarget;
@property (assign) CGPoint velocity;
@property (assign) CGPoint acceleration;
@property (assign) float maxVelocity;
@property (assign) float maxAcceleration;
- (id)initWithMoveTarget:(CGPoint)moveTarget maxVelocity:(float)maxVelocity maxAcceleration:(float)maxAcceleration;
@end
And MoveComponent.m with the following:
#import "MoveComponent.h"
@implementation MoveComponent
- (id)initWithMoveTarget:(CGPoint)moveTarget maxVelocity:(float)maxVelocity maxAcceleration:(float)maxAcceleration {
if ((self = [super init])) {
self.moveTarget = moveTarget;
self.velocity = CGPointZero;
self.acceleration = CGPointZero;
self.maxVelocity = maxVelocity;
self.maxAcceleration = maxAcceleration;
}
return self;
}
@end
As usual, this is just a data class with the information related to movement. Note that this system takes an "input variable" - the target that the object should move to.
Next, create a new file in the Systems group using the Objective-C class template. Name the new class MoveSystem, and make it a subclass of System.
Then replace MoveSystem.m with the following:
#import "MoveSystem.h"
#import "EntityManager.h"
#import "MoveComponent.h"
#import "RenderComponent.h"
@implementation MoveSystem
- (CGPoint)arriveEntity:(Entity *)entity withMoveComponent:(MoveComponent *)move renderComponent:(RenderComponent *)render {
CGPoint vector = ccpSub(move.moveTarget, render.node.position);
float distance = ccpLength(vector);
float targetRadius = 5;
float slowRadius = targetRadius + 25;
static float timeToTarget = 0.1;
if (distance < targetRadius) {
return CGPointZero;
}
float targetSpeed;
if (distance > slowRadius) {
targetSpeed = move.maxVelocity;
} else {
targetSpeed = move.maxVelocity * distance / slowRadius;
}
CGPoint targetVelocity = ccpMult(ccpNormalize(vector), targetSpeed);
CGPoint acceleration = ccpMult(ccpSub(targetVelocity, move.velocity), 1/timeToTarget);
if (ccpLength(acceleration) > move.maxAcceleration) {
acceleration = ccpMult(ccpNormalize(acceleration), move.maxAcceleration);
}
return acceleration;
}
- (CGPoint)separateEntity:(Entity *)entity withMoveComponent:(MoveComponent *)move renderComponent:(RenderComponent *)render {
CGPoint steering = CGPointZero;
NSArray * entities = [self.entityManager getAllEntitiesPosessingComponentOfClass:[RenderComponent class]];
for (Entity * otherEntity in entities) {
if (otherEntity.eid == entity.eid) continue;
RenderComponent * otherRender = (RenderComponent *) [self.entityManager getComponentOfClass:[RenderComponent class] forEntity:otherEntity];
CGPoint direction = ccpSub(render.node.position, otherRender.node.position);
float distance = ccpLength(direction);
static float SEPARATE_THRESHHOLD = 20;
if (distance < SEPARATE_THRESHHOLD) {
direction = ccpNormalize(direction);
steering = ccpAdd(steering, ccpMult(direction, move.maxAcceleration));
}
}
return steering;
}
- (void)update:(float)dt {
NSArray * entities = [self.entityManager getAllEntitiesPosessingComponentOfClass:[MoveComponent class]];
for (Entity * entity in entities) {
MoveComponent * move = (MoveComponent *) [self.entityManager getComponentOfClass:[MoveComponent class] forEntity:entity];
RenderComponent * render = (RenderComponent *) [self.entityManager getComponentOfClass:[RenderComponent class] forEntity:entity];
if (!move || !render) continue;
CGPoint arrivePart = [self arriveEntity:entity withMoveComponent:move renderComponent:render];
CGPoint separatePart = [self separateEntity:entity withMoveComponent:move renderComponent:render];
CGPoint newAcceleration = ccpAdd(arrivePart, separatePart);
// Update current acceleration based on the above, and clamp
move.acceleration = ccpAdd(move.acceleration, newAcceleration);
if (ccpLength(move.acceleration) > move.maxAcceleration) {
move.acceleration = ccpMult(ccpNormalize(move.acceleration), move.maxAcceleration);
}
// Update current velocity based on acceleration and dt, and clamp
move.velocity = ccpAdd(move.velocity, ccpMult(move.acceleration, dt));
if (ccpLength(move.velocity) > move.maxVelocity) {
move.velocity = ccpMult(ccpNormalize(move.velocity), move.maxVelocity);
}
// Update position based on velocity
CGPoint newPosition = ccpAdd(render.node.position, ccpMult(move.velocity, dt));
CGSize winSize = [CCDirector sharedDirector].winSize;
newPosition.x = MAX(MIN(newPosition.x, winSize.width), 0);
newPosition.y = MAX(MIN(newPosition.y, winSize.height), 0);
render.node.position = newPosition;
}
}
@end
This is a big block of code, but I'm not going to review it because it is the same code as we covered in the previous AI tutorial, except it has been converted to use the Entity System, in the manner we discussed already in this tutorial. Take a look and make sure it makes sense to you.
Next, let's add this new Component to the Quirk template. Open EntityFactory.m and make the following changes:
// Add to top of file
#import "MoveComponent.h"
// Add to bottom of createQuirkMonster before the return
[_entityManager addComponent:[[MoveComponent alloc] initWithMoveTarget:ccp(200, 200) maxVelocity:100 maxAcceleration:100] toEntity:entity];
Finally, set up the new MoveSystem in HelloWorldLayer.m:
// Add to top of file
#import "MoveSystem.h"
// Add new private instance variable
MoveSystem * _moveSystem;
// Add in addPlayers, right after creating the healthSystem
_moveSystem = [[MoveSystem alloc] initWithEntityManager:_entityManager];
// Add in update, right after calling update on the healthSystem
[_moveSystem update:dt];
And that's it! Build and run, and spawn a few Quirks, and they will move toward their target (right now set to 200, 200):
Want to try something really fun? See how easy it is to make your castle move. Inside EntityFactory.m, add this to the bottom of createAIPlayer, right before the return statement:
[_entityManager addComponent:[[MoveComponent alloc] initWithMoveTarget:ccp(400, 200) maxVelocity:100 maxAcceleration:100] toEntity:entity];
Build and run, and you have a moving castle! (Maybe it would be cool if you converted the castle into a big robot, that moved up and down!)