An action game without some kind of enemy is not much of a game at all. Raiders will have three types of enemy sprites, each with different traits and intelligence. In this chapter, you will create those enemies and learn how to program some basic artificial intelligence that will make the game more challenging.
Enemy sprites are among the most important aspects of many games. Enemy sprites that have a bit of intelligence, or even character, make a game more interesting and fun to play, and can also make it more memorable and addictive.
In Raiders, you’ll create three types of enemy:
• The DumbSprite has little or no intelligence. These sprites simply drop down in a straight line and then return to their original starting points.
• The Diagonal Sprite flies on a predetermined path, but will use some intelligence when targeting missiles at the player. For example, it will gauge where the player currently is, and if he’s moving, try to estimate where he will be when the missile lands.
• The Kamikaze Sprite will hone in on the player with the intention of crashing into him, or getting close enough to ensure that the missile has a very good chance of hitting its target.
The EnemySprite class is actually inherited from PlayerSprite, as it needs to do many of the things that PlayerSprite does—such as track its current position—while using a different drawing method to a base sprite.
The EnemySprite also has a couple of extra properties: isStrafing
and enemyType
.
isStrafing
checks if the sprite is currently on a strafing run.
The enemyType
property is one of
typedef enum { kDumbSprite, kDiagonalSprite, kKamikazeSprite } EnemyTypes;
as defined in common.h. EnemySprite also has a method called startRun:
which starts a strafing run. It takes the player’s current position as a parameter.
A strafing run takes place on a predetermined path, which will be different depending on the enemyType. The sprite will either follow a path from point to point, or move in a single direction for a specific period of time. This code is run in drawPlayer
and the different path for each enemyType is called from calculatePathAtTime
. Specifics of each strafing run are described under the section for each sprite.
When a strafing run is finished, the sprite sends a message to the target of the StrafingFinishedDelegate
as defined:
@protocol StrafingFinishedDelegate<NSObject>
- (void)strafingFinished;
@end
Each enemy sprite has a property called missile
of type Sprite
. A sprite can have only one missile onscreen at once, s0 a BOOL property called isMissileActive
tracks enemy missiles. If isMissileActive
is YES, then you draw the missile.
Although each enemy type will have a distinct missile graphic, the missiles all behave the same way: falling straight down once fired.
updateScene
in AbstractSceneController has to be changed to check and draw active missiles:
- (void)updateScene {
if (spriteList != nil) {
for (Sprite *sprite in spriteList) {
if ([sprite isKindOfClass:[EnemySprite class]] && [(EnemySprite *)sprite isMissileActive])
[[(EnemySprite *)sprite missile] updateTransforms];
[sprite updateTransforms];
}
}
}
Simply put, the code checks each sprite in the current scene. If the sprite is an enemy sprite, it checks to see if that sprite has an active missile. If so, the code calls updateTransforms
on the missile and draws it.
In drawPlayer
for the enemy sprite, if the missile is still active, the y coordinate increases by one with each update, until the missile falls off the bottom of the screen and becomes inactive:
if (isMissileActive) {
missile_y++;
if (missile_y > 480)
isMissileActive = NO;
float x = missile.bounds.origin.x;
[missile drawAtPosition:CGPointMake(x, missile_y)];
}
At this stage, we aren’t yet checking for collisions. You’ll learn about collision detection in Chapter 7.
Before you learn how to make your enemy sprites move and “think,” it’s worth considering just what artificial intelligence (AI) is from a gaming point of view.
Game AI is less about actual machine learning than it is about creating the illusion of intelligence. The goal of good game AI is to create a non-player character (NPC) that behaves with appropriate intelligence, thereby creating a more enjoyable and less predicable game.
Many techniques and algorithms are popular in game AI, and most are beyond the scope of this book. But good game AI takes the game’s worldview into account and responds in real time based on player input or the current game state. An example of this is path finding in a real time strategy game. The player selects a unit or units, along with a destination, and the game decides the best path to take across the current game terrain, which may include many obstacles.
While it might be a bit of a stretch to call the AI in Raiders “intelligent,” its variations of sprite movement patterns and player position detection does show that thinking about how a game works can result in a better playing experience for the user.
As already mentioned, Raiders uses programmed AI to add dynamic difficulty in the game. Dynamic difficulty means that the sprites know about the placement of the player and respond accordingly. For example, an enemy might diverge from a predefined path when the player moves, so the enemy tracks the position of the player, making gameplay more difficult. Another common AI use, as in shooters like Doom, causes the enemy to hide behind in-game objects when shot at.
The first category of sprite is the DumbSprite which, frankly, isn’t even artificially intelligent. As described previously, the DumbSprite just bobs up and down, firing at the bottom of its strafing run.
DumbSprite’s strafing run code takes the following form:
- (CGPoint)calcForDumbSprite:(double)time {
float x = currentPosition.x;
float y = 0;
if (!isReturning)
y = currentPosition.y + Velocity * time;
else
y = currentPosition.y - Velocity * time;
return CGPointMake(x, y);
}
This is calculated using the standard formula:
Distance = Velocity * time
Time
is the amount of time since the run was started. This method is called on the update of the world, every one-sixtieth of a second, and because GLKit maintains this updating, the result is a constant or near constant sprite velocity.
Once the sprite reaches the bottom of the screen, it follows the inverse path and returns to where it started. This is achieved using the private variable called isReturning
. When isReturning
is true, the direction of updating is effectively made negative, so the sprite moves backward toward its point of origin.
This strafing run is limited by time (1.2 seconds) and the sprite is positioned back at its starting point to make sure that no skips or jumps are perceivable by the player. The start position is stored in startRun:
.
The Diagonal Sprite has a path that is a straight line between two points. The end point is the same as it is for all sprites, but the starting point is where the sprite starts in the grid of enemies, or the x,y point of the sprite’s bounds property before the strafing run occurs.
Because the start and end points are known, a bit of high school mathematics can calculate the equation for the line: y = mx + b where m is the slope of the line, x is the current x coordinate, and b is the y intercept.
The formula to calculate the slope of a line is:
(y2-y1) / (x2-x1)
and the formula to calculate b is:
y1 – m * x1
In code, this looks like:
- (float)calcYForLineEquation {
//calulate the slope from (y2 - y1) / (x2 - x1)
float m = (endPoint.y - startPoint.y) / (endPoint.x
- startPoint.x);
if ((m < 0 || isReturning) && startPoint.x > endPoint.x)
currentPosition.x--;
else
currentPosition.x++;
//calculate b for y = mx + b
float b = startPoint.y - m * startPoint.x;
//calculate y
float y = m * currentPosition.x + b;
return y;
}
This y value is used to determine the current position of the enemy sprite when updating, as calculated from:
- (CGPoint)calcForDiagSprite {
float y = 0;
if (!isReturning)
y = ABS([self calcYForLineEquation]);
else
y = [self calcYForLineEquation];
return CGPointMake(currentPosition.x, y);
}
The preceding code checks if the enemy sprite is returning. If it is returning, y will be a negative value; if it isn’t, y should always be positive, regardless of the formula.
Using this formula also creates an interesting side effect. The straighter the slope of the line is to the vertical, the less distance the sprite needs to travel. As a result, the sprite will travel faster along that straighter path.
Another thing to note is that the end of the strafing run for a Diagonal Sprite isn’t based on time, but is based on when it reaches (or nearly reaches) the end point’s y coordinate. This reflects the additional “if” statements in drawPlayer
.
Instead of exhibiting a fixed or random missile-firing mode, the Diagonal Sprite monitors the position of the player’s sprite with every update, and fires the missile at some point during its strafing run if the player is positioned below the Diagonal Sprite and within a short range of the player.
This is still fairly simple AI, and can be enhanced by tracking the movement of the player and predicting where the player will be when the missile lands.
This enhancement is achieved with a single “if” statement:
- (void)checkShouldFireMissile {
if (playersCurrentPosition.x < currentPosition.x + self.width + 15 &&
playersCurrentPosition.x > currentPosition.x - 15 &&
playersCurrentPosition.y < currentPosition.y + 150
) {
[self fireMissile];
}
}
The Kamikaze Sprite doesn’t really have any more “programmed” AI than a Diagonal Sprite, but it appears to be smarter because it heads toward the player. This is achieved with simple code:
- (CGPoint)calcDirectPath {
float distanceAway = currentPosition.x - playersCurrentPosition.x;
float distanceToMove = ABS(distanceAway) / 10.0f;
if (distanceAway < 0)
currentPosition.x += distanceToMove;
else
currentPosition.x -= distanceToMove;
return CGPointMake(currentPosition.x, currentPosition.y += 2);
}
The algorithm just checks where the player is in relation to the Kamikaze Sprite. It then adjusts the amount of movement across the screen based on how far the player is from the enemy—the farther away that player is located, the farther the sprite moves, while maintaining a constant velocity in the y direction. If the player isn’t moving, the kamikaze maintains a direct course toward the player.
In practice, these few lines of code make a world of difference to gameplay.
In this chapter, you created three types of enemy, and learned how a few lines of code can change the way those enemies interact with the player. You’ve gotten a brief introduction to “programmed intelligence” and discovered how even simple AI code can improve the gameplay experience.
The one major thing that is still missing from Raiders is the all-important ability to shoot and be shot. This requires collision detection, which you’ll work with in the next chapter.