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

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. In this two-part tutorial series, you’ll learn how to make a cool Beat Em Up Game for the iPhone, using Cocos2D 2.0! Lately, Beat Em Up games […] By Allen Tan.

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

The Punch Action

This wouldn't be much of a Beat Em Up game if the hero were idle the whole time – that killer pompadour won’t take out enemies on it’s own – so the next thing you'll do is make him punch.

The structure is already there, so this will be very straightforward, and similar to creating the idle action.

First, you prepare the sprite frames and CCAction. Go to Hero.m and add the following:

//add after the idle action inside if ((self = [super initWithSpriteFrameName:@"hero_idle_00.png"]])
        // attack animation
        CCArray *attackFrames = [CCArray arrayWithCapacity:3];
        for (i = 0; i < 3; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_attack_00_%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];

There are some differences here compared to before. The attack animation runs faster than the idle animation – at 24 frames per second. Plus, the attack action only animates the attack animation once, and quickly switches back to the idle state by calling idle.

Go to ActionSprite.m and add the new method:

-(void)attack {
    if (_actionState == kActionStateIdle || _actionState == kActionStateAttack || _actionState == kActionStateWalk) {
        [self stopAllActions];
        [self runAction:_attackAction];
        _actionState = kActionStateAttack;
    }
}

Here you see a few more restrictions to the attack action than the idle action. The hero can only attack if his previous action was idle, attack, or walk. This is to ensure that the hero will not be able to attack when he is being hurt, or when he's dead.

After the initial checks, the action state is changed to kActionStateAttack, and the attack action is executed.

To trigger the attack method, go to GameLayer.m and do the following:

// add to init, inside if ((self = [super init]))
self.isTouchEnabled = YES;

//add this method inside the @implementation
-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [_hero attack];
}

You simply map the attack action of the hero to a touch on the screen. This will make him attack every time the screen is touched.

Build and run, and try tapping the screen a couple of times.

Hero Attack

Creating an 8-Directional D-Pad

The next logical thing to do is to make the hero move around the map. Originally, Beat Em Ups were made with console gaming in mind, and most console games used directional pads to control the player characters. So, for this tutorial, you will make your own virtual 8-directional D-pad to move the hero. Hooray!

Start by creating the D-pad class. Hit Command-N and create a new file with the iOS\Cocos2D v2.x\CCNode Class template. Make it a subclass of CCSprite and name it SimpleDPad.

Open SimpleDPad.h and add the following:

//add before @interface
@class SimpleDPad;

@protocol SimpleDPadDelegate <NSObject>

-(void)simpleDPad:(SimpleDPad *)simpleDPad didChangeDirectionTo:(CGPoint)direction;
-(void)simpleDPad:(SimpleDPad *)simpleDPad isHoldingDirection:(CGPoint)direction;
-(void)simpleDPadTouchEnded:(SimpleDPad *)simpleDPad;

@end

//add right after the @interface SimpleDPad : CCSprite and before the opening curly bracket
<CCTargetedTouchDelegate>

//add inside the @interface within the curly brackets
float _radius;
CGPoint _direction;

//add after closing curly bracket but before the @end
@property(nonatomic,weak)id <SimpleDPadDelegate> delegate;
@property(nonatomic,assign)BOOL isHeld;

+(id)dPadWithFile:(NSString *)fileName radius:(float)radius;
-(id)initWithFile:(NSString *)filename radius:(float)radius;

There are a lot of declarations above, but here are the important ones:

  • radius is simply the radius of the circle formed by the D-pad.
  • direction is the current direction being pressed. This is a vector with (-1.0, -1.0) being the bottom left direction, and (1.0, 1.0) being the top right direction.
  • delegate is the delegate of the D-pad, which is explained in detail below.
  • isHeld is a Boolean that returns YES as long as the player is touching the D-pad.

For the SimpleDPad, you are going to use a coding design-pattern called Delegation. It means that a delegate class (other than SimpleDPad) will handle some of the tasks started by the delegated class (SimpleDPad). At certain points that you specify, SimpleDPad will pass on responsibility to the delegate class, mostly when it comes to handling any game-related stuff.

This keeps SimpleDPad ignorant of any game logic, thus allowing you to re-use it in any other game that you may want to develop!

This simple diagram of what happens when the D-pad is touched should help you visualize the plan:

D-Pad Delegation

When SimpleDPad detects a touch that is within the radius of the D-pad, it calculates the direction of that touch, and sends a message to its delegate indicating the direction. Anything else after that is not the concern of SimpleDPad.

To enforce this pattern, SimpleDPad needs to at least know something about its delegate, specifically, the methods to be called to pass the direction value to the delegate. This is where another design pattern comes in: Protocols.

Go back to the code above, and look at the section inside the @protocol. This defines methods that any delegate of SimpleDPad should have, and acts sort of like an indirect header file for the delegate class. In this way, SimpleDPad enforces its delegate to have the three specified methods, so that it can be sure that it can call any of these methods whenever it wants to pass something onto the delegate.

Actually, SimpleDPad itself follows a protocol, as can be seen in this bit of code:

<CCTargetedTouchDelegate>

The above is a protocol for making a touch-enabled class capable of claiming touches, disallowing any other class from receiving that touch. When the SimpleDPad is touched, it should claim the touch so that GameLayer won't be able to. Remember that when GameLayer is touched, the hero will perform his attack action, and you don't want that happening when you touch the D-pad.

Now switch to SimpleDPad.m and add the following:

//add inside the @implementation
+(id)dPadWithFile:(NSString *)fileName radius:(float)radius {
    return [[self alloc] initWithFile:fileName radius:radius];
}

-(id)initWithFile:(NSString *)filename radius:(float)radius {
    if ((self = [super initWithFile:filename])) {
        _radius = radius;
        _direction = CGPointZero;
        _isHeld = NO;
        [self scheduleUpdate];
    }
    return self;
}

These’s nothing new here – only a bunch of initialization methods.

Still in the same file, add the following methods:

-(void)onEnterTransitionDidFinish {
    [[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self priority:1 swallowsTouches:YES];
}

-(void) onExit {
    [[[CCDirector sharedDirector] touchDispatcher] removeDelegate:self];
}

-(void)update:(ccTime)dt {
    if (_isHeld) {
        [_delegate simpleDPad:self isHoldingDirection:_direction];
    }
}

The first two methods register and remove SimpleDPad as a delegate class that swallows (or claims) touches it receives, while the update method constantly passes on the direction value to the delegate, as long as SimpleDPad is being touched.

Don't leave SimpleDPad.m just yet! You still need to add the following methods:

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint location = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]];
    
    float distanceSQ = ccpDistanceSQ(location, position_);
    if (distanceSQ <= _radius * _radius) {
        //get angle 8 directions
        [self updateDirectionForTouchLocation:location];
        _isHeld = YES;
        return YES;
    }
    return NO;
}

-(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint location = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]];
    [self updateDirectionForTouchLocation:location];
}

-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
    _direction = CGPointZero;
    _isHeld = NO;
    [_delegate simpleDPadTouchEnded:self];
}

-(void)updateDirectionForTouchLocation:(CGPoint)location {
    float radians = ccpToAngle(ccpSub(location, position_));
    float degrees = -1 * CC_RADIANS_TO_DEGREES(radians);
    
    if (degrees <= 22.5 && degrees >= -22.5) {
        //right
        _direction = ccp(1.0, 0.0);
    } else if (degrees > 22.5 && degrees < 67.5) {
        //bottomright
        _direction = ccp(1.0, -1.0);
    } else if (degrees >= 67.5 && degrees <= 112.5) {
        //bottom
        _direction = ccp(0.0, -1.0);
    } else if (degrees > 112.5 && degrees < 157.5) {
        //bottomleft
        _direction = ccp(-1.0, -1.0);
    } else if (degrees >= 157.5 || degrees <= -157.5) {
        //left
        _direction = ccp(-1.0, 0.0);
    } else if (degrees < -22.5 && degrees > -67.5) {
        //topright
        _direction = ccp(1.0, 1.0);
    } else if (degrees <= -67.5 && degrees >= -112.5) {
        //top
        _direction = ccp(0.0, 1.0);
    } else if (degrees < -112.5 && degrees > -157.5) {
        //topleft
        _direction = ccp(-1.0, 1.0);
    }
    [_delegate simpleDPad:self didChangeDirectionTo:_direction];
}

Don’t be intimidated by the long if-else statement; it actually is very simple once broken down:

  • ccTouchBegan: checks if the touch location is inside the D-pad's circle. If yes, it switches on the isHeld Boolean, and triggers an update of its direction value. It also returns YES to claim the touch.
  • ccTouchMoved: simply triggers an update of its direction value every time the touch is moved.
  • ccTouchEnded: switches off the isHeld Boolean, centers the direction, and notifies its delegate that the touch has ended.
  • updateDirectionForTouchLocation: calculates the location of the touch against the center of the D-pad, assigns the correct value for direction based on the resulting angle, and passes the value onto the delegate. The angle values, in degrees, may look weird to you, since the common misconception is that 0 degrees means north. Actually, 0 degrees in mathematics is east of the circle, becoming positive in the counterclockwise direction. Since you multiply the angle by -1, it becomes positive in the clockwise direction, which is what I'm used to working with.

OK, that's the D-pad class implemented. But now you need to add it to your game and use it. And the D-pad needs to be on top of everything else, so it has to be put in the Hud.

Open HudLayer.h and add the following:

//add to top of file
#import "SimpleDPad.h"

// add after the closing curly bracket but before the @end
@property(nonatomic,weak)SimpleDPad *dPad;

Switch over to HudLayer.m and add the following method:

-(id)init {
    if ((self = [super init])) {
        _dPad = [SimpleDPad dPadWithFile:@"pd_dpad.png" radius:64];
        _dPad.position = ccp(64.0, 64.0);
        _dPad.opacity = 100;
        [self addChild:_dPad];
    }
    return self;
}

The above code instantiates a SimpleDPad instance and adds it to HudLayer. Right now, GameScene handles both the GameLayer and HudLayer, but there may be cases wherein you want to access the HudLayer directly from GameLayer.

Go to GameLayer.h and do the following:

//add to top of file
#import "SimpleDPad.h"
#import "HudLayer.h"

//add in between @interface GameLayer : CClayer and the opening curly bracket
<SimpleDPadDelegate>

//add after the closing curly bracket and the @end
@property(nonatomic,weak)HudLayer *hud;

You just added a weak reference to a HudLayer instance within GameLayer. You also made GameLayer follow the protocol created by SimpleDPad.

Time to connect things within GameScene! So go to GameScene.m and do the following:

//add to init inside if ((self = [super init])) right after [self addChild:_hudLayer z:1]
_hudLayer.dPad.delegate = _gameLayer;
_gameLayer.hud = _hudLayer;

The code simply makes GameLayer the delegate of HudLayer's SimpleDPad, and also connects HudLayer to GameLayer.

Build and run, and you should now see the cool D-pad made by Vicki on the screen.

D-Pad

Don't try pressing the D-pad just yet. Doing so will crash the game, since you haven't implemented the protocol methods. That will come in the second part of this tutorial!

Allen Tan

Contributors

Allen Tan

Author

Over 300 content creators. Join our team.