现在的位置: 首页 > 综合 > 正文

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

2017年11月26日 ⁄ 综合 ⁄ 共 30262字 ⁄ 字号 评论关闭

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

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 aren’t as common as they were in the glory days. In case you’re in the unfortunate position of not having played a Beat Em Up game before, check out some of these great games:

If you’ve seen these games, you will notice that they all involve single protagonists beating up a horde of weak foes in every way possible. Hence the term: Beat Em Up.

In this tutorial, you will learn how to make a simple Beat Em Up game for the iPhone, with stylish pixel art. In the process, you’ll learn how to keep track of animation states, hit boxes, add a d-pad, add simple enemy AI, and much more!

Note that this is an intermediate tutorial, so if you are new to Cocos2D, you should go through (at the least) the How
to Make a Simple iPhone Game With Cocos2D
 and How
to Make a Tile-Based Game with Cocos2D tutorial
 tutorials before proceeding.

Keep on reading to get your fists bloody and start kicking some butt!

Getting Started: ARC-Enabled Cocos2D

With iOS 5, Apple introduced ARC, or Automatic Reference Counting, which means that as a developer, you no longer have to handle the task of retaining and releasing memory. Instead, the compiler will do this for you automatically. This makes iOS development
much easier, and makes your code much more stable by eliminating a lot of memory leaks that can potentially crop up.

Unfortunately, the standard Cocos2D templates do not create projects that are ARC-compliant. So to get started, you will modify a standard Cocos2D project to enable ARC.

Grab the latest version of Cocos2D 2.x (this tutorial uses stable 2.0), unarchive the package, and install the templates by entering the following command in the Terminal, from the folder that contains the extracted files:

./install-templates.sh -f -u

Start up Xcode and create a new project with the iOS\Cocos2D v2.x\Cocos2D iOS template and name itPompaDroid.
Leave the Company Identifier as is, but remember to set the Device Family to iPhone. TapNext,
and then save the project to a folder of your choice.

Xcode

Note: Are you wondering why you named this project PompaDroid? Well, the
main character in this game has a funky haircut in the Pompadour
hairstyle
, and he’s about to beat up on a bunch of droids. My apologies to all the Android devs out there! ;]

The next step is to enable ARC for Cocos2D. Converting the Cocos2D framework itself to ARC will take a lot of time and unnecessary code modifications. So instead, you’re going to inform the compiler which components aren’t using ARC (in this case, the Cocos2D
library code) and which components are using ARC (all of your application source).

Select your project root in the Project Navigator (the left sidebar), and select PompaDroid from
the Targets section. Select the Build Phases tab and expand the Compile Sources group. Now you need to tell the compiler which Objective-C classes aren’t using ARC by entering -fno-objc-arc in
the Compiler Flags column.

To do this, select all files ending with .m by holding the Shift or Command Keys while
you click on the files, and then tap the Enter key. Type -fno-objc-arc in the window
that pops up, then press Enter again. Do this until all .m files, excluding IntroLayer.m, have this flag.

It should look like this:

Arc Compiler Flags

Next, go to the Build Settings tab, search for the row that says Objective-C Automatic Reference Counting (you can simply search for ARC in the search box to find the row), and set it to YES.
Then, in the Summary tab, make sure that your Deployment Target is at least 5.0 or higher.

Build and run your code to see if everything is OK so far. You should get the default template’s Hello World screen:

Hello World

Congratulations! Your Cocos2D project is now ARC-enabled. :]

The Game Scene

All right, now that you have the basic Hello World Template working, it’s time to create your Game Scene. The Game Scene will be responsible not only for showing you what’s happening within the game, but also for handling the game mechanics. Ray often calls
this the Action Layer.

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

Next, create two more files the same as above, but this time make them subclasses of CCLayer.
Name the first one GameLayer, and the second one HudLayer.

Finally, delete HelloWorldLayer.m and HelloWorldLayer.h,
as you won’t be needing them anymore. Be sure to select Move to Trash when deleting
the files, so that they don’t linger in your project folder.

Your project files should now look similar to this:

Game Scene Files

If you try to build your project now, you will encounter an error where IntroLayer.m cannot find HelloWorldLayer.h. To fix this, make the following changes to IntroLayer.m:

//remove this line
#import "HelloWorldLayer.h"
 
//add this line at the top
#import "GameScene.h"
 
//replace the method makeTransition:(ccTime)dt with this
-(void)makeTransition:(ccTime)dt {
	[[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:1.0 scene:[GameScene node] withColor:ccWHITE]];
}

The AppDelegate starts the game with IntroLayer.m, and it’s the Intro Layer’s responsibility to switch to the next scene after showing the splash image. Since HelloWorldLayer is now no longer part of the team, all you did above is put GameScene in its place.

Now switch over to GameScene.h and do the following:

//add to top of the file
#import "GameLayer.h"
#import "HudLayer.h"
 
//add before @end (but after @interface)
@property(nonatomic,weak)GameLayer *gameLayer;
@property(nonatomic,weak)HudLayer *hudLayer;

Quickly switch to GameScene.m and add the following inside the @implementation:

-(id)init {
    if ((self = [super init])) {
        _gameLayer = [GameLayer node];
        [self addChild:_gameLayer z:0];
        _hudLayer = [HudLayer node];
        [self addChild:_hudLayer z:1];
    }
    return self;
}

This is pretty basic stuff. You made one instance of GameLayer and one instance of HudLayer, and you added them to GameScene. Additionally, their z-value (a.k.a. z-order) dictates the order in which these two layers will be drawn on the scene, with the lower-valued
z being drawn first.

As you may have guessed from the names, GameLayer will contain game elements such as the characters and the stage, while HudLayer will contain display elements such as the Directional Pad. HudLayer has to be drawn last because it has to be on top of everything.

Note: To those experienced with Objective-C, the above code might look
pretty weird and may raise alarm. For one thing, you never declared instance variables, and you never synthesized properties. What gives?

In the latest version of Xcode, properties are now automatically synthesized, and they also get their own instance variable, complete with accompanying underscore before the name, all without you having to write a line of code. For more information, check out
Chapter 2 in iOS
6 by Tutorials
, “Modern Objective-C Syntax”.

Pretty neat, right? Just remember that if you do things this way, the instance variable stays private to that class, such that even its subclasses won’t be able to access it directly.

There are still a few exceptions to this rule, but unless you specifically get warning or error messages from Xcode, you can assume that the above method is allowed (as long as you’re using the latest Xcode).

Time to check out the results. Build and run your code, and you will see…

Blank Scene

Nothing. What did you expect? :P

Loading the Tiled Map

It’s time to put some content into the scene. You will start with the item that appears behind everything else: the Tiled Map. I’m going to assume you already know your way around creating Tiled Maps. If this isn’t the case, please refer to the How
to Make a Tile-Based Game with Cocos2D tutorial
 before proceeding.

Ready? First, download the resource kit for the tutorial from here and
extract it, which includes some lovely pixel art made by Hunter
Russell
!

Drag the Sprites folder into the Resources group
of your project. Make sure that Copy items into destination group’s folder is checked, Create
groups for any added folders
 is selected, and that thePompaDroid target
is selected under Add to targets. Tap Finish when you’re sure everything is set correctly.

Xcode

The Sprites folder contains several files, but you’ll mainly be working with two files:

  • pd_tiles.png: the tile piece images used in the tiled map.
  • pd_tilemap.tmx: the actual tile map loaded into Cocos2D.

Open up pd_tilemap.tmx using Tiled,
and you will see the entire map for the game.

Tiled Map Editor

There are a few important things to keep in mind here:

  1. There are two layers: Wall and Floor.
    Turn off the Opacity for each layer to see what each one is composed of, and you will notice that the
    fourth row of tiles from the bottom is comprised of tiles from both layers
    . Layers can be useful for a lot of things, and in this case, they’re being used to create variations of tiles by combining two tiles in one spot.
  2. Each tile is 32×32 in size.
  3. The walkable floor tiles are in the first 3 rows from the bottom.

Let’s get coding again. Go to GameLayer.h and add the following variable inside the
curly braces after the @interface:

CCTMXTiledMap *_tileMap;

Switch to GameLayer.m and add these methods inside the @implementation:

-(id)init {
    if ((self = [super init])) {
        [self initTileMap];
    }
    return self;
}
 
-(void)initTileMap {
    _tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"pd_tilemap.tmx"];
    for (CCTMXLayer *child in [_tileMap children]) {
        [[child texture] setAliasTexParameters];
    }
    [self addChild:_tileMap z:-6];
}

initTileMap creates the tiled map based on the .tmx file you provided. It then iterates over all members of the tiled map and calls setAliasTexParameters. This method ensures that anti-aliasing is turned off for all the textures used in the tiled map, so that
you can retain the pixel-like style of the textures even when the map is scaled. For more info on this, please refer to the Introduction
to Pixel Art for Games tutorial
.

Finally, you add the tile map to the layer. Keep in mind that the z-value of the tiled map is -6. You want to make sure that the tiled map is always behind everything, so from now on, there shouldn’t be any children added to GameLayer with a z-value lower than
-6.

Before you build and run, visit AppDelegate.m and search for this line:

if( ! [director_ enableRetinaDisplay:YES] )

And change it to this:

if( ! [director_ enableRetinaDisplay:NO] )

You’re disabling Retina support, since there isn’t a separate set of images for the Retina display. This doesn’t mean that the game won’t work on Retina devices, but rather, it will simply scale up all images you provided for bigger resolutions. Without this,
the tiled map would have just taken up half of the screen on Retina devices.

Now, build and run, and you should see the tiled map displayed on the screen:

The Tiled Map

Creating the Pompadoured Hero

In most 2D side-scrolling games, characters have different animations to represent different types of actions. This presents us with a problem: how do you know which animation to play at which time?

For this Beat Em Up game, you are going to implement one possible solution to this problem using astate
machine
.

A state machine, as the name may or may not imply, is simply something that changes behavior by switching states. A single state machine can have only one state at a time, but it can transition from one state to another.

To understand this better, imagine the base character for the game, and list the things he can do:

  1. Stay Put/Idle
  2. Walk
  3. Punch

Then list the requirements for each:

  1. If he is idle, then he cannot be walking or punching.
  2. If he is walking, then he cannot be idle or punching.
  3. If he is punching, then he cannot be idle or walking.

Expand this list to include two more actions: getting hurt, and dying, and you have five actions in
total:

Action States

Given the above, you could say that the character, if he were a state machine, would switch between an Idle State, a Walking State, a Punching State, a Hurt State, and a Dead State.

To have a complete flow between the states, each state should have a requirement and a result.
The walking state cannot suddenly transition to the dead state, for example, since your hero must first go through being hurt in order to reach the state of being dead.

The result of each state also helps solve the problem presented earlier. For the game’s implementation, when you switch between states, the character will also switch between animations.

OK, that’s enough theory for now. It’s time to continue coding!

The first step is to create a base template for a sprite that is able to switch between actions.

Note: For the purposes of this tutorial, since each action represents
a state, the words action and state will be used interchangeably.

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

Go to ActionSprite.h, and add the following before the @end:

//actions
@property(nonatomic,strong)id idleAction;
@property(nonatomic,strong)id attackAction;
@property(nonatomic,strong)id walkAction;
@property(nonatomic,strong)id hurtAction;
@property(nonatomic,strong)id knockedOutAction;
 
//states
@property(nonatomic,assign)ActionState actionState;
 
//attributes
@property(nonatomic,assign)float walkSpeed;
@property(nonatomic,assign)float hitPoints;
@property(nonatomic,assign)float damage;
 
//movement
@property(nonatomic,assign)CGPoint velocity;
@property(nonatomic,assign)CGPoint desiredPosition;
 
//measurements
@property(nonatomic,assign)float centerToSides;
@property(nonatomic,assign)float centerToBottom;
 
//action methods
-(void)idle;
-(void)attack;
-(void)hurtWithDamage:(float)damage;
-(void)knockout;
-(void)walkWithDirection:(CGPoint)direction;
 
//scheduled methods
-(void)update:(ccTime)dt;

The above code declares the basic variables and methods you need to create an ActionSprite. Separated by section, these are:

  • Actions: these are the CCActions (Cocos2D actions) to be executed for each state. The
    CCActions will be a combination of executing sprite animations and other events triggered when the character switches states.
  • States: holds the current action/state of the sprite, using a type named ActionState
    that you will define soon.
  • Attributes: contains values for the sprite’s walk speed for movement, hit/health points
    for getting hurt, and damage for attacking.
  • Movement: will be used later to calculate how the sprite moves around the map.
  • Measurements: holds useful measurement values regarding the actual image of the sprite.
    You need these values because the sprites you will use have a canvas size that is much larger than the image contained inside.
  • Action methods: you won’t call the CCActions (from the actions section above) directly.
    Instead, you will be using these methods to trigger each state.
  • Scheduled methods: anything that needs to run constantly at certain intervals, such
    as updates to the Sprite’s position and velocity, will be put here.

Before going to ActionSprite.m, you need to define the ActionState type used above.
For the sake of uniformity and ease-of-use, I personally like to keep all my definitions in one file. You will be doing just that in this tutorial.

Hit Command-N and create a new file with the iOS\C and C++\Header File template and name it Defines.

Open Defines.h, and add the following after #define PompaDroid_Defines_h:

// 1 - convenience measurements
#define SCREEN [[CCDirector sharedDirector] winSize]
#define CENTER ccp(SCREEN.width/2, SCREEN.height/2)
#define CURTIME CACurrentMediaTime()
 
// 2 - convenience functions
#define random_range(low,high) (arc4random()%(high-low+1))+low
#define frandom (float)arc4random()/UINT64_C(0x100000000)
#define frandom_range(low,high) ((high-low)*frandom)+low
 
// 3 - enumerations
typedef enum _ActionState {
    kActionStateNone = 0,
    kActionStateIdle,
    kActionStateAttack,
    kActionStateWalk,
    kActionStateHurt,
    kActionStateKnockedOut
} ActionState;
 
// 4 - structures
typedef struct _BoundingBox {
    CGRect actual;
    CGRect original;
} BoundingBox;

To explain briefly:

  1. These are some convenience macros so that you don’t need to type the long versions later on. For example, instead of having to type [[CCDirector
    sharedDirector] winSize]
     every time you want to check the screen’s dimensions, you just need to type SCREEN.
  2. These are a few convenience functions that return a random integer or float value, and will be useful later on.
  3. Here you define ActionState, which is an enumeration of the different states that the ActionSprite can have. These are simply integers, with values from 0 to 5, but the enumeration names make your code much more readable.
  4. Here you define a structure named BoundingBox, which will be used later for collision
    handling. You add it now so that you don’t need to go back to Defines.h ever again during the course of this tutorial.

You want Defines.h to be present in every file that you work on from now on, but it’s
a hassle to have to enter #import “Defines.h” in each of these files. The quick solution
is to include it in the pre-compiled header file.

In your Project Navigator, expand Supporting Files, open Prefix.pch,
and then do the following:

//add after #import <Foundation/Foundation.h>
#import "Defines.h"

That’s it. Build and run your code, and you should see this:

PompaDroid!

No? Oh, that’s right. Just teasing. :]

You probably just got the same blank result as before, but with a few warnings about having incomplete implementations because you haven’t yet put anything in ActionSprite.m. Leave it as is for now, and skip forward to getting your hero up on the screen.

Switch to GameLayer.h and add the following instance variable:

CCSpriteBatchNode *_actors;

Now switch to GameLayer.m and add the following to init:

//add inside if ((self = [super init]))
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"pd_sprites.plist"];
        _actors = [CCSpriteBatchNode batchNodeWithFile:@"pd_sprites.pvr.ccz"];
[_actors.texture setAliasTexParameters];
[self addChild:_actors z:-5];

You load all the frames in the Sprite Sheet you added earlier into your Resources, and create a CCSpriteBatchNode out of it. This batch node will later on be the parent of all your sprites. It has a higher z-value than the CCTMXTiledMap object, since you want
it to appear in front of the tiled map.

Note: Batch nodes are very effective when drawing multiple animated sprites.
You can skip the usage of batch nodes if you want, but it may have an impact on performance, especially when there are a lot of sprites on the scene.

Time to create the Hero class. 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 Hero.

Add this at the top of Hero.h:

#import "ActionSprite.h"

Switch to Hero.m, and add the following inside the @implementation:

-(id)init {
    if ((self = [super initWithSpriteFrameName:@"hero_idle_00.png"])) {
        int i;
 
        //idle animation
        CCArray *idleFrames = [CCArray arrayWithCapacity:6];
        for (i = 0; i < 6; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_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]];
 
        self.centerToBottom = 39.0;
        self.centerToSides = 29.0;
        self.hitPoints = 100.0;
        self.damage = 20.0;
        self.walkSpeed = 80;
    }
    return self;
}

You create the hero character with an initial idle sprite frame, prepare a CCArray containing all of the other sprite frames belonging to the idle animation, and create the CCAction that plays this animation.

This animation will simply change the displayed sprite frame for the hero at the chosen interval, in this case 1.0/12.0, which means 12 frames per second.

After that, you place some initial values for the hero’s attributes, including the measurements from the center of the sprite to the sides and bottom. To better understand what these two measurements are for, take a look at the following diagram:

Content Size

Each frame of the hero sprite was created on a 280×150 pixel canvas, but the actual hero sprite only takes up a fraction of that space. Each CCSprite has a property named contentSize, which gives you the size of the frame. It’s useful for cases wherein the
sprite takes up the whole frame, but very useless for this case, so it’s a good idea to store these two measurements so that you can position the sprite (or the hero in this case) easily.

If you’re wondering why the sprites have all that extra empty space, it’s because the sprites might be drawn in different ways for each animation, and some might require more space than the others. This way, you have the extra space to draw each sprite as needed,
instead of having the different sprites for an animation (or sprites from different animations) be different sizes.

Switch to GameLayer.h and add the following:

//add to top of file
#import "Hero.h"
 
//add inside @interface
Hero *_hero;

Next, switch to GameLayer.m and do the following:

//add inside if ((self = [super init])), right after [self addChild:_actors z:-5];
[self initHero];
 
//add this method inside the @implementation
-(void)initHero {
    _hero = [Hero node];
    [_actors addChild:_hero];
    _hero.position = ccp(_hero.centerToSides, 80);
    _hero.desiredPosition = _hero.position;
    [_hero idle];
}

You create an instance of Hero, add it to the batch node, and set its position. You call idle so
that the hero goes to the idle state and runs the idle animation. Here you can see one use of the _centerToSidesproperty:
to place the hero at the edge of the screen to be fully visible.

But you haven’t yet created the idle method. Go to ActionSprite.m and
add the following inside the @implementation:

-(void)idle {
    if (_actionState != kActionStateIdle) {
        [self stopAllActions];
        [self runAction:_idleAction];
        _actionState = kActionStateIdle;
        _velocity = CGPointZero;
    }
}

The idle method can only run if the ActionSprite isn’t already in the idle state. When it is triggered, it executes the idle action, changes the current action state to kActionStateIdle,
and zeroes out the velocity.

Build and run, and you should now see your hero doing his idle animation:

Idle Hero

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!

Where To Go From Here?

Here is the final
project
 with all of the code from the above tutorial.

So far, you’ve created an ARC-enabled Cocos2D 2.0 project, displayed a TMX Tiled Map, created a state machine-based sprite that supports idle and attack actions, and finally, you created your very own reusable D-pad implementation using delegation and protocols.
Not bad for only the first part, huh?

Stay tuned for Part
2
 of the tutorial, where you’ll complete the game by adding movement, collision, enemies, and simple AI.

In the meantime, if you have any questions or comments about anything you’ve done so far, please join the forum discussion below!

This is a post by iOS Tutorial Team Member Allen
Tan
, an iOS developer and co-founder atWhite
Widget
. You can also find him on  and Twitter.

抱歉!评论已关闭.