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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make A Side-Scrolling Beat Em Up Game Like Scott Pilgrim with Cocos2D – Part 2
40 mins
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:
- 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.
- 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.
- 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.
- 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.
- 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