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?
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!)
Where To Go From Here?
Now that you have a firm understanding of how to make components and systems that use components, converting the rest of the project to the Entity System model is a fairly straightforward port - but time consuming.
It would take forever to explain each step in this tutorial, so instead I have done this for you. Go ahead and download the completed port and check it out. You can also look at the git history that comes with the project to see how I built it up one piece at a time.
Note I'm not convinced this is the most elegant way to do this, but it is a full working example of a simple component based game - which are in rare supply! :]
As I mentioned earlier, if any of you have ideas for how to make this project better, feel free to update the project and send me an updated version! As mentioned earlier, I'll post any alternative versions here so others can learn and benefit.
I hope this article has helped you learn the basics about component based architecture and get some ideas for how you might like to approach it with your games. Remember, the key is to prefer composition over inhertance - how you make that happen is a matter of style and preference! :]
Huge thanks to Adam Martin for tech reviewing this tutorial.
If you have any comments, questions, or suggestions for this tutorial, please join the forum discussion below!
This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer.