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

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 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 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.

Stationary Walk

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 10x100 tiles total, but only 3x100 for the floor.
  • tileSize: this contains the dimensions of each tile, 32x32 pixels in this particular case.

GameLayer also makes a lot of references to ActionSprite’s two measurement values, centerToSides, and centerToBottom, 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.

Move 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 the tile-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.

Move Towards the End

Allen Tan

Contributors

Allen Tan

Author

Over 300 content creators. Join our team.