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

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):

Moving monsters with components

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.

Contributors

Over 300 content creators. Join our team.