Learning Cocos2D: Simple Collision Detection and Enemy AI
- Creating the Radar Dish and Viking Classes
- Creating the Viking Class
- Final Steps
- Summary
- Challenges
In the previous chapter you learned the basics of Cocos2D animations and actions. You also started building a flexible framework for Space Viking. In this chapter you go further and create the first enemy for Ole to do battle with. In the process you learn how to implement a simple system for collision detection and the artificial intelligence brain of the enemies in Space Viking.
There is a significant amount of code necessary in this chapter to drive the behavior of Ole and the RadarDish. Take your time understanding how these classes work, as they are the foundation and models for the rest of the classes in Space Viking.
Ready to defeat the aliens?
Creating the Radar Dish and Viking Classes
From just a CCSprite to a fully animated character, Ole the Viking takes the plunge from simple to advanced from here on out. In this section you create the RadarDish and Viking classes to encapsulate the logic needed by each, including all of the animations. The RadarDish class is worth a close look, as all of the enemy characters in Space Viking are modeled after it.
Creating the RadarDish Class
In this first scene, there is a suspicious radar dish on the right side of the screen. It scans for foreign creatures such as Ole. Ole needs to find a way to destroy the radar dish before it alerts the enemy robots of his presence. Fortunately, Ole knows two ways to deal with such problems: his left and right fists. Create the new RadarDish class in Xcode by following these steps:
- In Xcode, right-click on the EnemyObjects group.
- Select New File, choose the Cocoa Touch category under iOS and Objective-C class as the file type, and click Next.
- For the Subclass field, enter GameCharacter and click Next.
- Enter RadarDish for the filename and click Finish.
Open the RadarDish.h header file and change the contents to match the code in Listing 4.1.
Listing 4.1. RadarDish.h header file
// RadarDish.h // SpaceViking //#import <Foundation/Foundation.h>
#import "GameCharacter.h"
@interface
RadarDish :GameCharacter
{CCAnimation
*tiltingAnim;CCAnimation
*transmittingAnim;CCAnimation
*takingAHitAnim;CCAnimation
*blowingUpAnim;GameCharacter
*vikingCharacter; }@property
(nonatomic
,retain
)CCAnimation
*tiltingAnim;@property
(nonatomic
,retain
)CCAnimation
*transmittingAnim;@property
(nonatomic
,retain
)CCAnimation
*takingAHitAnim;@property
(nonatomic
,retain
)CCAnimation
*blowingUpAnim;@end
Looking at Listing 4.1 you can see that the RadarDish class inherits from the GameCharacter class and that it defines four CCAnimation instance variables. There is also an instance variable to hold a pointer back to the Viking character.
Listings 4.2, 4.3, and 4.4 show the contents of the RadarDish.m implementation file. The changeState and updateStateWithDelta time methods are crucial to understand, as they are the most basic versions of what you will find in all of the characters in Space Viking. While reading this code, keep in mind that the RadarDish is a simple enemy that never moves or attacks the Viking. The RadarDish does take damage from the Viking, eventually blowing up by moving to a dead state. Listing 4.2 covers the top portion of the RadarDish.m implementation file, including the changeState method. Open the RadarDish.m implementation file and replace the code so that it matches the contents in Listings 4.2, 4.3, and 4.4.
Listing 4.2. RadarDish.m implementation file (top portion)
// RadarDish.m // SpaceViking#import "RadarDish.h"
@implementation
RadarDish@synthesize
tiltingAnim;@synthesize
transmittingAnim;@synthesize
takingAHitAnim;@synthesize
blowingUpAnim; - (void
) dealloc{ [tiltingAnim release
]; [transmittingAnim release
]; [takingAHitAnim release
]; [blowingUpAnim release
]; [super dealloc
]; } -(void
)changeState:(CharacterStates
)newState { [self stopAllActions
];id
action =nil
; [self setCharacterState
:newState];switch
(newState) {case
kStateSpawning:CCLOG
(@"RadarDish->Starting the Spawning Animation"
); action = [CCAnimate actionWithAnimation
:tiltingAnim
restoreOriginalFrame
:NO
];break
;case
kStateIdle:CCLOG
(@"RadarDish->Changing State to Idle"
); action = [CCAnimate actionWithAnimation
:transmittingAnim
restoreOriginalFrame
:NO
];break
;case
kStateTakingDamage:CCLOG
(@"RadarDish->Changing State to TakingDamage"
);characterHealth
=characterHealth
- [vikingCharacter getWeaponDamage
];if
(characterHealth
<=0.0f
) { [self changeState
:kStateDead
]; }else
{ action = [CCAnimate actionWithAnimation
:takingAHitAnim
restoreOriginalFrame
:NO
]; }break
;case
kStateDead:CCLOG
(@"RadarDish->Changing State to Dead"
); action = [CCAnimate actionWithAnimation
:blowingUpAnim
restoreOriginalFrame
:NO
];break
;default
:CCLOG
(@"Unhandled state %d in RadarDish"
, newState);break
; }if
(action !=nil
) { [self runAction
:action]; } }
The changeState method is called when the RadarDish needs to transition between states. In the beginning of this chapter you were introduced to state machines, and the changeState method is what allows for transitions to different states in the miniscule "brain" of the RadarDish. The RadarDish brain can exist in one of four states: spawning, idle, taking damage, or dead. In the listings that follow, you will see that the RadarDish is initialized in the spawning state when it is created, and then through the updateStateWithDeltaTime method it will move through the four states.
When the updateStateWithDeltaTime determines that the RadarDish needs to change its state, the changeState method is called. Looking at Listing 4.2, you can recap what the switch state is doing as follows:
-
Spawning (kStateSpawning)
Starts up the RadarDish with the tilting animation, which is the dish moving up and down.
-
Idle (kStateIdle)
Runs the transmitting animation, which is the RadarDish blinking.
-
Taking Damage (kStateTakingDamage)
Runs the taking damage animation, showing a hit to the RadarDish. The RadarDish health is reduced according to the type of weapon being used against it.
- Dead (kStateDead)
The RadarDish plays a death animation of it blowing up. This state occurs once the RadarDish health is at or below zero.
The next section of the RadarDish implementation file is covered in Listing 4.3, showing the updateStateWithDeltaTime method.
Listing 4.3. RadarDish.m implementation file (middle portion)
-(void
)updateStateWithDeltaTime
:(ccTime)deltaTimeandListOfGameObjects
:(CCArray*)listOfGameObjects {if
(characterState == kStateDead)return
;// 1
vikingCharacter = (GameCharacter
*)[[self
parent] getChildByTag:kVikingSpriteTagValue];// 2
CGRectvikingBoudingBox
= [vikingCharacteradjustedBoundingBox
];// 3
CharacterStates vikingState = [vikingCharacter characterState];// 4
// Calculate if the Viking is attacking and nearby
if
((vikingState
==kStateAttacking
) && (CGRectIntersectsRect([self
adjustedBoundingBox], vikingBoudingBox))) {// 5
if
(characterState
!=kStateTakingDamage
) {// If RadarDish is NOT already taking Damage
[self
changeState:kStateTakingDamage];return
; } }if
(([self numberOfRunningActions
] ==0
) && (characterState
!= kStateDead)) { CCLOG(@"Going to Idle"
); [self
changeState:kStateIdle];// 6
return
; } }
Now let's examine the numbered lines of the code:
- Checks if the RadarDish is already dead. If it is, this method is short-circuited and returned. If the RadarDish is dead, there is nothing to update.
- Gets the Viking character object from the RadarDish parent. All of Space Viking's objects are children of the scene SpriteBatchNode, referred to here as the parent. The Viking in particular was added to the SpriteBatchNode with a particular tag, referred to by the constant kVikingSpriteTagValue. By obtaining a reference to the Viking object, the RadarDish can determine if the Viking is nearby and attacking the RadarDish. (Listing 4.3 contains the code that sets up the kVikingSpriteTagValue constant.)
- Gets the Viking character's adjusted bounding box.
- Gets the Viking character's state.
- Determines if the Viking is nearby and attacking. If the adjusted bounding boxes for the Viking and the RadarDish overlap, and the Viking is in his attack phase, the RadarDish can be certain that the Viking is attacking it. The call to changeState:kStateTakingDamage will alter the RadarDish animation to reflect the attack and reduce the RadarDish character's health.
- Resets the transmission animation on the RadarDish. If the RadarDish is not currently playing an animation, and it is not dead, it is reset to idle so that the transmission animation can restart.
The last part of the RadarDish.m implementation file is the longest but least complicated. There is an initAnimations method, which sets up all of the RadarDish animations, and an init method that initializes the RadarDish and sets up the starting values for the instance variables. Add the contents of Listing 4.4 to your RadarDish.m implementation file.
Listing 4.4. RadarDish.m implementation file (bottom portion)
-(void
)initAnimations
{ [self setTiltingAnim
: [self loadPlistForAnimationWithName
:@"tiltingAnim"
andClassName
:NSStringFromClass([self
class])]]; [self setTransmittingAnim
: [self loadPlistForAnimationWithName
:@"transmittingAnim"
andClassName
:NSStringFromClass([self
class])]]; [self
setTakingAHitAnim: [self
loadPlistForAnimationWithName:@"takingAHitAnim"
andClassName
:NSStringFromClass([self
class])]]; [self setBlowingUpAnim
: [self loadPlistForAnimationWithName
:@"blowingUpAnim"
andClassName:NSStringFromClass([self
class])]]; } -(id
) init {if
( (self
=[super
init]) ) {CCLOG
(@"### RadarDish initialized"
); [self initAnimations
];// 1
characterHealth =100.0f
;// 2
gameObjectType = kEnemyTypeRadarDish;// 3
[self
changeState:kStateSpawning];// 4
}return self
; }@end
The initAnimations method calls the loadPlistForAnimationWithName method you declared in the GameObject class. The name of the animation to load is passed along with the class name. Note the convenience method NSStringFromClass is used to get an NSString from the class name, in this case RadarDish. The class name is used to find the correct plist file for the object, since the plist files have a name corresponding to the class. The following occurs in the init method:
- Calls the initAnimations method, which sets up all of the animations for the RadarDish. The frame's coordinates and textures were already loaded and cached by Cocos2D when the texture atlas files (scene1atlas.png and scene1atlas.plist) were loaded by the GameplayLayer class.
- Sets the initial health of the RadarDish to a value of 100.
- Sets the RadarDish to be a Game Object of type kEnemyTypeRadarDish.
- Initializes the state of the RadarDish to spawning. Looking back at Listing 4.2, you can see that this starts the tilting animation, which is followed by the transmitting animation when the RadarDish moves from spawning to an idle state.
There is a little more work left before you can have this chapter's game running on your device. You need to add the Viking class and make some changes to the GameplayLayer class. It is important to understand how the updateStateWithDeltaTime and the changeState methods in RadarDish control the state of the AI brain. These same two methods are used to drive the brain of all of the other game characters, including Ole the Viking.