How To Make A Side-Scrolling Beat Em Up Game Like Scott Pilgrim with Cocos2D – Part 2

This is a post by iOS Tutorial Team Member Allen Tan, an iOS developer and co-founder at White Widget. You can also find him on Google+ andTwitter.

Welcome back to the second (and final) part of our Beat Em Up game tutorial series!

If you followed the first part, then you’ve already created the hero and have the D-pad controller present in the game.

You will pick up where you left off, and by the end will have completed your very own Beat Em Up game.

This part is exciting, in that you will see the results of much of what you do on the screen. Just to mention a few items: you will add movement, scrolling, collision, enemies, AI, and some polish with music and sound effects!

Before you start, make sure that you have a copy of the project from Part 1, either by going through the first tutorial, or by downloading the finished project.

Don’t forget to grab a copy of the resource kit if you haven’t already, as it contains some stuff that you haven’t used yet.

Let’s get back to doing what we do best – beating up on androids! :]

Moving the Hero

In the last section of Part 1, you created a D-pad controller and displayed it onscreen. But at the moment, pressing the D-pad crashes the game instead of moving the player. Let’s remedy this quickly!

The first step is to create a movement state for the hero.

Go to Hero.m and add the following:

//add after the attack action inside if ((self = [super initWithSpriteFrameName:@"hero_idle_00.png"]])
// walk animation
CCArray *walkFrames = [CCArray arrayWithCapacity:8];
for (i = 0; i < 8; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_walk_%02d.png", i]];
    [walkFrames addObject:frame];
}
CCAnimation *walkAnimation = [CCAnimation animationWithSpriteFrames:[walkFrames getNSArray] delay:1.0/12.0];
self.walkAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:walkAnimation]];

This should be familiar to you by now. You add new frames for the walking animation, and create the walk action in a similar way to how you created the idle action.

Switch to ActionSprite.m and add the following method:

-(void)walkWithDirection:(CGPoint)direction {
    if (_actionState == kActionStateIdle) {
        [self stopAllActions];
        [self runAction:_walkAction];
        _actionState = kActionStateWalk;
    }
    if (_actionState == kActionStateWalk) {
        _velocity = ccp(direction.x * _walkSpeed, direction.y * _walkSpeed);
        if (_velocity.x >= 0) self.scaleX = 1.0;
        else self.scaleX = -1.0;
    }
}

This checks to see if the previous action was idle, then it changes the action to walk, and runs the walk animation, but if the previous action was already a walk action, then it simply changes the velocity of the sprite based on the walkSpeed value.

The method also checks the left/right direction of the sprite, and flips the sprite accordingly by switching the value of scaleX between -1 and 1.

To connect the hero’s walk action to the D-pad, you must turn to the delegate of the D-pad: GameLayer.

Switch to GameLayer.m and implement the methods enforced by the SimpleDPadDelegate protocol:

-(void)simpleDPad:(SimpleDPad *)simpleDPad didChangeDirectionTo:(CGPoint)direction {
    [_hero walkWithDirection:direction];
}
 
-(void)simpleDPadTouchEnded:(SimpleDPad *)simpleDPad {
    if (_hero.actionState == kActionStateWalk) {
        [_hero idle];
    }
}
 
-(void)simpleDPad:(SimpleDPad *)simpleDPad isHoldingDirection:(CGPoint)direction {
    [_hero walkWithDirection:direction];
}

You trigger the hero’s move method every time the SimpleDPad sends a direction, and trigger the hero’s idle method every time the touch on SimpleDPad stops.

Build and run, and try moving the hero using the D-pad.

All right, he’s walking! Wait a minute… he’s not actually moving… what gives?

Take a look walkWithDirection: again, and you’ll notice that it doesn’t do anything except change the velocity of the hero. Where is the code for changing the hero’s position?

Changing the hero’s position is the responsibility of both ActionSprite and GameLayer. An ActionSprite never really knows where it is located on the map. Hence, it doesn’t know when it has reached the map’s edges. It only knows where it wants to go – the desired position. It is GameLayer’s responsibility to translate that desired position into an actual position.

You already declared a CGPoint named desiredPosition for ActionSprite. This is the only position value that ActionSprite should be working with.

Go to ActionSprite.m and add the following method:

-(void)update:(ccTime)dt {
    if (_actionState == kActionStateWalk) {
        _desiredPosition = ccpAdd(position_, ccpMult(_velocity, dt));
    }
}

This method is called every time the game updates the scene, and it updates the desired position of the sprite only when it is in the walking state. It adds the value of velocity to the current position of the sprite, but before that, velocity is multiplied by delta time so that the time interval is factored into the equation.

Multiplying by delta time makes the hero move at the same rate, no matter the current frame rate. Position + Velocity * Delta Time really just means move x and y (velocity) points each second (1 dt).

Note: This way of integrating position is called Euler’s integration. It’s known for being an approach that’s easy to understand and implement, but not one that is extremely accurate. But since this isn’t a physics simulation, Euler’s integration is close enough for your purposes.

Switch to GameLayer.m and do the following:

//add inside if ((self = [super init])) right after [self initTileMap];
[self scheduleUpdate];
 
//add these methods inside the @implementation
-(void)dealloc {
    [self unscheduleUpdate];
}
 
-(void)update:(ccTime)dt {
    [_hero update:dt];
    [self updatePositions];
}
 
-(void)updatePositions {
    float posX = MIN(_tileMap.mapSize.width * _tileMap.tileSize.width - _hero.centerToSides, MAX(_hero.centerToSides, _hero.desiredPosition.x));
    float posY = MIN(3 * _tileMap.tileSize.height + _hero.centerToBottom, MAX(_hero.centerToBottom, _hero.desiredPosition.y));
    _hero.position = ccp(posX, posY);
}

You schedule GameLayer’s update method, which acts as the main run loop for the game. Here you will see how GameLayer and ActionSprite cooperate in setting ActionSprite’s position.

At every loop, GameLayer asks the hero to update its desired position, and then it takes that desired position and checks if it is within the bounds of the Tiled Map’s floors by using these values:

  • mapSize: this is the number of tiles in the Tiled Map. There are 10×100 tiles total, but only 3×100 for the floor.
  • tileSize: this contains the dimensions of each tile, 32×32 pixels in this particular case.

GameLayer also makes a lot of references to ActionSprite’s two measurement values, centerToSides, andcenterToBottom, because if ActionSprite wants to stay within the scene, its position shouldn’t go past the actual sprite bounds. (Remember that the canvas for the sprites you’re using is much bigger than the actual sprite area, in at least some cases.)

If the position of ActionSprite is within the boundaries that have been set, GameLayer gives the hero its desired position. If not, GameLayer asks the hero to stay in its current position.

Note: The MIN function compares two values, and returns the lower value, while the MAX function returns the higher of two values. Using these two in conjunction clamps a value to a minimum and maximum number. Cocos2D also comes with a convenience function for CGPoints that is similar to what you just did: ccpClamp.

Build and run, and you should now be able to move your hero across the map.

You’ll soon find, though, that there’s one more issue that needs attention. The hero can walk past the right edge of the map, such that he vanishes from the screen.

You can set the tiled map to scroll based on the hero’s position by plugging in the method found in thetile-based game tutorial.

Still in GameLayer.m, do the following:

//add this in update:(ccTime)dt, right after [self updatePositions];
[self setViewpointCenter:_hero.position];
 
//add this method
-(void)setViewpointCenter:(CGPoint) position {
 
    CGSize winSize = [[CCDirector sharedDirector] winSize];
 
    int x = MAX(position.x, winSize.width / 2);
    int y = MAX(position.y, winSize.height / 2);
    x = MIN(x, (_tileMap.mapSize.width * _tileMap.tileSize.width)
            - winSize.width / 2);
    y = MIN(y, (_tileMap.mapSize.height * _tileMap.tileSize.height)
            - winSize.height/2);
    CGPoint actualPosition = ccp(x, y);
 
    CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
    CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
    self.position = viewPoint;
}

This code centers the screen on the hero’s position, except in cases where the hero is at the edge of the map.

For a complete explanation of how this works, please refer to the previously mentioned tile-based game tutorial.

Build and run. The hero should now be visible at all times.

Bring on the Droids

While being able to walk around is fun, walking around a huge empty corridor can become pretty boring, pretty fast for your hero. It’s up to you to give him some company. :]

You already have a base model for the sprite: ActionSprite. You can reuse that to make computer-controlled characters for the game.

This part of the tutorial will move rather quickly, since it’s very similar to how the hero was created.

Hit Command-N and create a new file with the iOS\Cocos2D v2.x\CCNode Class template. Make it a subclass of ActionSprite and name it Robot.

Go to Robot.h and add this line at the top:

#import "ActionSprite.h"

Switch to Robot.m and add this method:

-(id)init {
    if ((self = [super initWithSpriteFrameName:@"robot_idle_00.png"])) {
        int i;
 
        //idle animation
        CCArray *idleFrames = [CCArray arrayWithCapacity:5];
        for (i = 0; i < 5; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_idle_%02d.png", i]];
            [idleFrames addObject:frame];
        }
        CCAnimation *idleAnimation = [CCAnimation animationWithSpriteFrames:[idleFrames getNSArray] delay:1.0/12.0];
        self.idleAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:idleAnimation]];
 
        //attack animation
        CCArray *attackFrames = [CCArray arrayWithCapacity:5];
        for (i = 0; i < 5; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_attack_%02d.png", i]];
            [attackFrames addObject:frame];
        }
        CCAnimation *attackAnimation = [CCAnimation animationWithSpriteFrames:[attackFrames getNSArray] delay:1.0/24.0];
        self.attackAction = [CCSequence actions:[CCAnimate actionWithAnimation:attackAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
 
        //walk animation
        CCArray *walkFrames = [CCArray arrayWithCapacity:6];
        for (i = 0; i < 6; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_walk_%02d.png", i]];
            [walkFrames addObject:frame];
        }
        CCAnimation *walkAnimation = [CCAnimation animationWithSpriteFrames:[walkFrames getNSArray] delay:1.0/12.0];
        self.walkAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:walkAnimation]];
 
        self.walkSpeed = 80;
        self.centerToBottom = 39.0;
        self.centerToSides = 29.0;
        self.hitPoints = 100;
        self.damage = 10;
    }
    return self;
}

As with the hero, the above code creates a robot with three actions: idle, attack and walk. It also fills in the two measurement values: centerToBottom and centerToSides.

Notice that the robot’s attributes are weaker compared to the hero. This is logical, since otherwise your hero will never be able to defeat the robots. :]

Let’s jump straight to filling the game with a lot of these robots. You want a brawl, don’t you?

Switch to GameLayer.h and add the following property:

@property(nonatomic,strong)CCArray *robots;

Now switch to GameLayer.m and do the following:

//add to top of file
#import "Robot.h"
 
//add inside if ((self = [super init])) right after [self initHero];
[self initRobots];
 
//add this method inside the @implementation
-(void)initRobots {
    int robotCount = 50;
    self.robots = [[CCArray alloc] initWithCapacity:robotCount];
 
    for (int i = 0; i < robotCount; i++) {
        Robot *robot = [Robot node];
        [_actors addChild:robot];
        [_robots addObject:robot];
 
        int minX = SCREEN.width + robot.centerToSides;
        int maxX = _tileMap.mapSize.width * _tileMap.tileSize.width - robot.centerToSides;
        int minY = robot.centerToBottom;
        int maxY = 3 * _tileMap.tileSize.height + robot.centerToBottom;
        robot.scaleX = -1;
        robot.position = ccp(random_range(minX, maxX), random_range(minY, maxY));
        robot.desiredPosition = robot.position;
        [robot idle];
    }
}

You just did the following:

  • Created an array of 50 robots, and added them to the batch node.
  • Used the random functions you created in Defines.h to randomly place the 50 robots across the tile map’s floors. You also made sure that no robots are placed at the starting point by making the minimum random value bigger than the screen’s width.
  • Made each robot perform its idle action.

Build and run, and walk the hero around until you see robots on the map.

Try walking around a bit in an area with robots, and you’ll notice that there’s something really wrong with how the robots are drawn. According to the current perspective, if the hero is below a robot, then he should be drawn in front of the robot, not the other way around.

For things to be drawn in the right sequence, you need to explicitly tell the game which objects to draw first. You should already know how to do this – think back to how you made the tile map appear behind everything else.

Figured it out? If your answer was z-order/z-value, then you’re 100% correct!

To refresh your memory, take a look at how the sprite batch node and tile map were added to the scene in GameLayer.m:

//you set the z-value of tileMap lower than actors, so anything drawn in actors appears in front of tileMap
[self addChild:_actors z:-5];     //this is your CCSpriteBatchNode
[self addChild:_tileMap z:-6];    //this is your CCTMXTiledMap

Now take a look at how the hero and the robots were added:

[_actors addChild:_hero];
[_actors addChild:robot];

There are two differences:

  1. The SpriteBatchNode and the CCTMXTiledMap were both added as direct children of GameLayer, while the hero and the robots were added as children of the CCSpriteBatchNode. GameLayer is responsible for drawing the CCSpriteBatchNode and CCTMXTiledMap in the proper sequence, while it is the CCSpriteBatchNode’s responsibility to draw the hero and the robots in the correct sequence within itself.
  2. You didn’t explicitly assign a z-value to the hero and the robots. By default, the object added last will have a higher z-value than the previous objects – and that’s why all the robots are drawn in front of the hero.

To fix the broken drawing sequence, you need to handle the z-order dynamically. Every time a sprite moves across the screen vertically, its z-order should be changed. The higher a sprite is on the screen, the lower its z-value should be.

Still in GameLayer.m, do the following:

//add this method inside the @implementation
-(void)reorderActors {
    ActionSprite *sprite;
    CCARRAY_FOREACH(_actors.children, sprite) {
        [_actors reorderChild:sprite z:(_tileMap.mapSize.height * _tileMap.tileSize.height) - sprite.position.y];
    }
}
 
//add this method inside update, right after [self updatePositions]
[self reorderActors];

Every time the sprite positions are updated, this method makes the CCSpriteBatchNode reorder the z-value of each of its children, based on how far the child is from the bottom of the map. As the child goes higher, the resulting z-value goes down.

Note: Each CCNode has its own property named zOrder, but changing this won’t give you the same effect as calling reorderChild from the parent. It’s the parent’s responsibility to draw its children in order, so it should also be the parent’s responsibility to set the order of its children.

Build and run, and the drawing sequence should now be correct.

Punch-a-Bot: Collision Detection

Well, your hero’s now got company. But there’s still nothing much for him to do. And if this is a Beat Em Up game, then he should be able to beat somebody up, right? Time to lay down the hurt on these robots!

For the hero to be able to punch and actually hit the robots, you need to implement a way of detecting collisions. And for this particular tutorial, you will create a very simple collision detection system using rectangles. In this system, you will define two rectangles/boxes for each character:

  • Hit box: this will represent the body of the sprite.
  • Attack box: this will represent the hand of the sprite.

If the attack box of one ActionSprite collides with the hit box of another, then a collision occurs. This distinction between the two rectangles will help you decide who hit whom.

You already defined a structure for your collision box in Defines.h. For reference, here it is again:

typedef struct _BoundingBox {
    CGRect actual;
    CGRect original;
} BoundingBox;

Each bounding box has two rectangles: the actual, and the original.

  1. The original rectangle is the rectangle local to each individual sprite, and never changes once it is set. Think of it as the internal location of the bounding box as the sprite sees it.
  2. The actual rectangle, on the other hand, is the rectangle as it is located in world space. As the sprite moves, so does the actual rectangle. Think of it as the location of the bounding box as the GameLayer sees it.

Go to ActionSprite.h and add the following:

@property(nonatomic,assign)BoundingBox hitBox;
@property(nonatomic,assign)BoundingBox attackBox;
 
-(BoundingBox)createBoundingBoxWithOrigin:(CGPoint)origin size:(CGSize)size;

The above creates two bounding boxes for ActionSprite: the hit box, and the attack box, as discussed above. It also sets up a factory method for a bounding box, which simply creates a BoundingBox structure given an origin and size.

Switch to ActionSprite.m and add the following methods:

-(BoundingBox)createBoundingBoxWithOrigin:(CGPoint)origin size:(CGSize)size {
    BoundingBox boundingBox;
    boundingBox.original.origin = origin;
    boundingBox.original.size = size;
    boundingBox.actual.origin = ccpAdd(position_, ccp(boundingBox.original.origin.x, boundingBox.original.origin.y));
    boundingBox.actual.size = size;
    return boundingBox;
}
 
-(void)transformBoxes {
    _hitBox.actual.origin = ccpAdd(position_, ccp(_hitBox.original.origin.x * scaleX_, _hitBox.original.origin.y * scaleY_));
    _hitBox.actual.size = CGSizeMake(_hitBox.original.size.width * scaleX_, _hitBox.original.size.height * scaleY_);
    _attackBox.actual.origin = ccpAdd(position_, ccp(_attackBox.original.origin.x * scaleX_, _attackBox.original.origin.y * scaleY_));
    _attackBox.actual.size = CGSizeMake(_attackBox.original.size.width * scaleX_, _attackBox.original.size.height * scaleY_);
}
 
-(void)setPosition:(CGPoint)position {
    [super setPosition:position];
    [self transformBoxes];
}

The first method creates a new BoundingBox and is there to assist subclasses of ActionSprite in creating their own bounding boxes.

The second method, transformBoxes, updates the origin and size of the actual measurements of each bounding box, based on the sprite’s position and scale, and the local origin and size of the bounding box. You take the scale into consideration because it determines the direction the sprite is facing. A box located on the right side of the sprite will flip to the left side when the scale is set to -1.

Switch to Hero.m and create the new bounding boxes:

//add inside if ((self = [super initWithSpriteFrameName)) after self.centerToSide
// Create bounding boxes
self.hitBox = [self createBoundingBoxWithOrigin:ccp(-self.centerToSides, -self.centerToBottom) size:CGSizeMake(self.centerToSides * 2, self.centerToBottom * 2)];
self.attackBox = [self createBoundingBoxWithOrigin:ccp(self.centerToSides, -10) size:CGSizeMake(20, 20)];

Likewise, switch to Robot.m and add the following:

//add inside if ((self = [super initWithSpriteFrameName)) after self.centerToSide
// Create bounding boxes
self.hitBox = [self createBoundingBoxWithOrigin:ccp(-self.centerToSides, -self.centerToBottom) size:CGSizeMake(self.centerToSides * 2, self.centerToBottom * 2)];
self.attackBox = [self createBoundingBoxWithOrigin:ccp(self.centerToSides, -5) size:CGSizeMake(25, 20)];

You now have the hit and attack boxes for the hero and the robots. If you were to visualize the boxes, they would look something like this:

Whenever an attack box (red) intersects with a hit box (blue), a collision occurs.

Before writing the code that checks for this intersection of bounding boxes, you must first make sure that ActionSprite can react properly to being hit. You’ve already coded the idle, attack, and walk actions, but still haven’t created the hurt and death actions.

Time to bring on the pain! Go to ActionSprite.m and add the following methods:

-(void)hurtWithDamage:(float)damage {
    if (_actionState != kActionStateKnockedOut) {
        [self stopAllActions];
        [self runAction:_hurtAction];
        _actionState = kActionStateHurt;
        _hitPoints -= damage;
 
        if (_hitPoints <= 0.0) {
            [self knockout];
        }
    }
}
 
-(void)knockout {
    [self stopAllActions];
    [self runAction:_knockedOutAction];
    _hitPoints = 0.0;
    _actionState = kActionStateKnockedOut;
}

As long as the sprite is not dead, getting hit will switch its state to hurt, execute the hurt animation, and subtract the right amount of damage from the sprite’s hit points. If the hit points fall below 0, then the knocked out (death) action occurs.

To complete these two actions, you still have to retrofit both the Hero and Robot classes with their respective hurt and death actions.

Go to Hero.m first and add the following inside init (below the existing animation blocks):

//hurt animation
CCArray *hurtFrames = [CCArray arrayWithCapacity:8];
for (i = 0; i < 3; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_hurt_%02d.png", i]];
    [hurtFrames addObject:frame];
}
CCAnimation *hurtAnimation = [CCAnimation animationWithSpriteFrames:[hurtFrames getNSArray] delay:1.0/12.0];
self.hurtAction = [CCSequence actions:[CCAnimate actionWithAnimation:hurtAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
 
//knocked out animation
CCArray *knockedOutFrames = [CCArray arrayWithCapacity:5];
for (i = 0; i < 5; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_knockout_%02d.png", i]];
    [knockedOutFrames addObject:frame];
}
CCAnimation *knockedOutAnimation = [CCAnimation animationWithSpriteFrames:[knockedOutFrames getNSArray] delay:1.0/12.0];
self.knockedOutAction = [CCSequence actions:[CCAnimate actionWithAnimation:knockedOutAnimation], [CCBlink actionWithDuration:2.0 blinks:10.0], nil];

Switch to Robot.m and add the following inside init (again, below the existing animation blocks):

//hurt animation
CCArray *hurtFrames = [CCArray arrayWithCapacity:8];
for (i = 0; i < 3; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_hurt_%02d.png", i]];
    [hurtFrames addObject:frame];
}
CCAnimation *hurtAnimation = [CCAnimation animationWithSpriteFrames:[hurtFrames getNSArray] delay:1.0/12.0];
self.hurtAction = [CCSequence actions:[CCAnimate actionWithAnimation:hurtAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
 
//knocked out animation
CCArray *knockedOutFrames = [CCArray arrayWithCapacity:5];
for (i = 0; i < 5; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_knockout_%02d.png", i]];
    [knockedOutFrames addObject:frame];
}
CCAnimation *knockedOutAnimation = [CCAnimation animationWithSpriteFrames:[knockedOutFrames getNSArray] delay:1.0/12.0];
self.knockedOutAction = [CCSequence actions:[CCAnimate actionWithAnimation:knockedOutAnimation], [CCBlink actionWithDuration:2.0 blinks:10.0], nil];

It’s the same old stuff. You create the hurt and death actions in the same way as you created the other actions. The hurt action transitions to the idle action when it finishes, and the death action makes the sprite blink after it animates. (In classic Beat Em’ Up style.)

Switch over to GameLayer.m to officially add collision handling:

//add this inside ccTouchesBegan, right after [_hero attack];
if (_hero.actionState == kActionStateAttack) {
    Robot *robot;
    CCARRAY_FOREACH(_robots, robot) {
        if (robot.actionState != kActionStateKnockedOut) {
            if (fabsf(_hero.position.y - robot.position.y) < 10) {
                if (CGRectIntersectsRect(_hero.attackBox.actual, robot.hitBox.actual)) {
                    [robot hurtWithDamage:_hero.damage];
                }
            }
        }
    }
}

The above code detects collisions in three easy steps:

  1. Check if the hero’s state is attack, and if the robot’s state is anything but knocked out.
  2. Check if the hero’s position and the robot’s position are only 10 points apart vertically. This indicates that they are standing on the same plane.
  3. Check if the attack box of the hero intersects with the hit box of the robot by using theCGRectIntersectsRect function.

If all of these conditions are passed, then a collision occurs, and the robot’s hurt action is executed. The hero’s damage value is passed in as a parameter, so that the method knows just how many hit points it has to subtract.

Build and run, and get punching!

When Robots Attack: Simple AI

You’re punching, punching and they all fall down! But they never attack back? What fun is that?

Completing the game should require both a winning and losing condition. Currently, you can kill all the robots on the map, and nothing will happen. You want the game to end when either all the robots are wiped out, or the hero dies.

You certainly won’t have the hero dying if the robots just stand around acting like punching bags. :] To make them move and use the actions that you created for them, you need to develop a simple AI (Artificial Intelligence) system.

The AI that you will create is based on decisions. You will give each robot a chance to decide on a course of action at specific time intervals. The first thing that they need to know is when they get make this choice.

Go to Robot.h and add this property:

@property(nonatomic,assign)double nextDecisionTime;

Switch to Robot.m and initialize this property:

//add inside if ((self = [super initWithSpriteFrameName])) in init
_nextDecisionTime = 0;

This property is named to indicate its purpose – it holds the next time at which the robot can make a decision.

Switch to GameLayer.m and add the following:

-(void)updateRobots:(ccTime)dt {
    int alive = 0;
    Robot *robot;
    float distanceSQ;
    int randomChoice = 0;
    CCARRAY_FOREACH(_robots, robot) {
        [robot update:dt];
        if (robot.actionState != kActionStateKnockedOut) {
            //1
            alive++;
 
            //2
            if (CURTIME > robot.nextDecisionTime) {
                distanceSQ = ccpDistanceSQ(robot.position, _hero.position);
 
                //3
                if (distanceSQ <= 50 * 50) {
                    robot.nextDecisionTime = CURTIME + frandom_range(0.1, 0.5);
                    randomChoice = random_range(0, 1);
 
                    if (randomChoice == 0) {
                        if (_hero.position.x > robot.position.x) {
                            robot.scaleX = 1.0;
                        } else {
                            robot.scaleX = -1.0;
                        }
 
                        //4
                        [robot attack];
                        if (robot.actionState == kActionStateAttack) {
                            if (fabsf(_hero.position.y - robot.position.y) < 10) {
                                if (CGRectIntersectsRect(_hero.hitBox.actual, robot.attackBox.actual)) {
                                    [_hero hurtWithDamage:robot.damage];
 
                                    //end game checker here
                                }
                            }
                        }
                    } else {
                        [robot idle];
                    }
                } else if (distanceSQ <= SCREEN.width * SCREEN.width) {
                    //5
                    robot.nextDecisionTime = CURTIME + frandom_range(0.5, 1.0);
                    randomChoice = random_range(0, 2);
                    if (randomChoice == 0) {
                        CGPoint moveDirection = ccpNormalize(ccpSub(_hero.position, robot.position));
                        [robot walkWithDirection:moveDirection];
                    } else {
                        [robot idle];
                    }
                }
            }
        }
    }
 
    //end game checker here
}

Now that is one long snippet of code! Don’t worry, soon it will all be clear.

Let’s take the above code section-by-section. For each robot in the game:

  1. You keep a count of how many robots are still alive. A robot is considered alive as long as its state is not knocked out (dead). This will be used later on to determine whether or not the game should end.
  2. You check if the current application time went past the robot’s next decision time. If it did, then it means that the robot needs to make a new decision. CURTIME is a shortcut macro you defined in Defines.h.
  3. You check if the robot is close enough to the hero so that its punches have a chance to connect with the hero. If so, then the robot makes a random choice of whether to face the hero and punch, or to remain idle.
  4. If the robot decides to attack, you check for collisions in the same way you did before for the hero’s attack. This time, the roles of the hero and the robot are reversed.
  5. If the distance between the robot and the hero is less than the width of the screen, then the robot gets to decide to either move towards the hero, or remain idle. The robot moves based on the normal vector produced by both the hero’s position, and the robot’s position. The normal vector is like the distance between the two, but with the value clamped from -1.0 to 1.0. Or rather, it is the x-y coordinate version of the angle between the hero and the robot.

Every time a robot makes a decision, its next decision time is set to a random time in the future. In the meantime, he continues executing whatever actions he started running in the last decision time.

Still in GameLayer.m, do the following:

//add inside update, right before [self updatePositions];
[self updateRobots:dt];
 
//add inside the updatePositions method, right after _hero.position = ccp(posX, posY);
// Update robots
Robot *robot;
CCARRAY_FOREACH(_robots, robot) {
    posX = MIN(_tileMap.mapSize.width * _tileMap.tileSize.width - robot.centerToSides, MAX(robot.centerToSides, robot.desiredPosition.x));
    posY = MIN(3 * _tileMap.tileSize.height + robot.centerToBottom, MAX(robot.centerToBottom, robot.desiredPosition.y));
    robot.position = ccp(posX, posY);
}

Here, you make sure that the Robot AI method you created earlier is called every game loop. It also loops through each robot and moves them based on their desired position.

Build and run, and face the robotic menace from down the corridor!

Play the game until you beat all the robots, or until the hero dies, and you’ll see that the game gets stuck. If you followed my previous tutorial on making a game like Fruit Ninja, you probably know that I like to end tutorial games by simply showing a button that allows you to restart everything. So let’s do that here as well!

Still in GameLayer.m, do the following:

//add to top of file
#import "GameScene.h"
 
//add these methods inside @implementation
-(void)endGame {
    CCLabelTTF *restartLabel = [CCLabelTTF labelWithString:@"RESTART" fontName:@"Arial" fontSize:30];
    CCMenuItemLabel *restartItem = [CCMenuItemLabel itemWithLabel:restartLabel target:self selector:@selector(restartGame)];
    CCMenu *menu = [CCMenu menuWithItems:restartItem, nil];
    menu.position = CENTER;
    menu.tag = 5;
    [_hud addChild:menu z:5];
}
 
-(void)restartGame {
    [[CCDirector sharedDirector] replaceScene:[GameScene node]];
}

The first method creates and shows a Restart button that, when pressed, triggers the second method. The latter just commands the director to replace the current scene with a new instance of GameScene.

Look back at updateRobots: above, and you will see that I left two placeholder comments in there, like this:

//end game checker here

Do the following in updateRobots::

//add this in place of the FIRST placeholder comment
if (_hero.actionState == kActionStateKnockedOut && [_hud getChildByTag:5] == nil) {
    [self endGame];
}
 
//add this in place of the SECOND placeholder comment
if (alive == 0 && [_hud getChildByTag:5] == nil) {
    [self endGame];
}

Both of these if statements check for game-ending conditions. The first one checks if the hero is still alive right after having been hit by a robot. If he’s dead, then the game ends.

The second one checks if all the robots are dead. If they are, then the game also ends.

There’s one funky check happening here, where the HudLayer looks to see if it has a child with a tag value of 5. And you might be wondering – what is that all about?

Look back at endGame above, and you will see that the End Game menu has a tag value of 5. Since this checker runs in a loop, it needs to make sure that the End Game menu has not previously been created. Otherwise, it will keep on creating new End Game menu items every chance it gets.

Build and run. Have fun beating up those pesky robots!

I was down for the count before I could take a screen shot! X_X

Gratuitous Music and Sound Effects

The game plays pretty nicely, but it isn’t very satisfying when you sock it to a robot without audio feedback. And some background music might not hurt either. I can assure you, I’m not going to leave you hanging without some fun music and sound effects!

For this game, you’ll be using background music made by Kevin MacLeod of Incompetech, and also some 8-bit sound effects that I created using the neat bfxr utility.

You can add the music and sound effects into the game in just a few simple steps. :]

Drag the Sounds folder from the resource kit (the one you downloaded and extracted in Part 1 of this tutorial) into the Resources group of your project. Make sure that Copy items into destination group’s folder is checked, and that Create groups for any added folders is selected.

Switch to GameLayer.m and do the following:

//add to top of file
#import "SimpleAudioEngine.h"
 
//add inside if ((self = [super init])) in init
// Load audio
[[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:@"latin_industries.aifc"];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"latin_industries.aifc"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_hit0.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_hit1.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_herodeath.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_botdeath.caf"];

Switch over to ActionSprite.m and do the following:

//add to top of file
#import "SimpleAudioEngine.h"
 
//add inside the first if statement (before the second if statement) of the hurtWithDamage method
int randomSound = random_range(0, 1);
[[SimpleAudioEngine sharedEngine] playEffect:[NSString stringWithFormat:@"pd_hit%d.caf", randomSound]];

Switch to Hero.m, and do the following:

//add to top of file
#import "SimpleAudioEngine.h"
 
//add this method inside the @implementation
-(void)knockout {
    [super knockout];
    [[SimpleAudioEngine sharedEngine] playEffect:@"pd_herodeath.caf"];
}

Finally, switch to Robot.m and do the following:

//add to top of file
#import "SimpleAudioEngine.h"
 
//add this method inside the @implementation
-(void)knockout {
    [super knockout];
    [[SimpleAudioEngine sharedEngine] playEffect:@"pd_botdeath.caf"];
}

The above code preloads the music and sound effects in GameLayer, and plugs in the appropriate sound effect for each action event.

You’re done! Build, run, and play until you drop!

Where To Go From Here?

Here is the final project with all the code from the complete tutorial.

You may have been through both parts of this tutorial and have the bodies hitting the floor, but you’ve only seen the tip of the iceberg! There is still a lot more ground to be covered when it comes to making a full-fledged Beat Em Up Game.

An Exciting Announcement

If you’re craving more Beat Em Up action, I have good news for you – I will soon be releasing a Beat Em Up Starter Kit here at raywenderlich.com!

You’re probably wondering just how awesome it will be. As a teaser, here’s a list of what you can expect to find and learn in the kit:

  • More actions! 3-hit combos, jumping, running
  • Combination actions: jumping attack, running attack
  • Adding Life Bars
  • Animated 8-directional D-pad
  • Animated D-pad buttons
  • State machine applied to game events, and battle events with enemy spawning!
  • Multiple levels, and using PLIST files for setting up events
  • Creating different types of enemies using the sprite color tinting technique
  • Boss enemy: Pompadour VS Mohawk! (cue dramatic music)
  • More advanced enemy AI
  • Much better collision system using circles instead of rectangles
  • Adjusting collision circles based on actions, and drawing these circles onto the screen for debugging
  • weapon for your Pompadoured Hero: the Gauntlet
  • Destructible tiled map objects
  • Visible damage numbers and hit explosions
  • …and much more!

Whoa, that’s a lot of stuff! If you’re interested in the Beat Em Up Game Starter Kit, make sure you’re signed up for our monthly newsletter – we’ll announce it there when it comes out! :]

Update 4/8/13: Good news – the Beat Em Up Game Starter Kit is now available! Check it out on theraywenderlich.com store.

It’s gonna be epic – but in the meantime, if you have any questions, comments, or suggestions about this tutorial, please join the forum discussion below :]

你可能感兴趣的:(cocos2d-x)