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+ and Twitter. 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 […] By Allen Tan.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

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:

Bounding Boxes

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 the CGRectIntersectsRect 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!

Punch-a-Bot

Allen Tan

Contributors

Allen Tan

Author

Over 300 content creators. Join our team.