6. Shoot to Kill

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 Sprite Class

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.


You face a tricky choice when deciding whether to have specific classes for each enemy, or have a single enemy class with different properties for each sprite. There is no right or wrong answer to this dilemma. Usually it comes down to developer preference.

Raiders has one enemy class that can be configured as any of the three enemy types, because for the most part, each enemy shares common traits with only minor differences.


Strafing Run

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


Note

image

For more on protocols and delegates, see Appendix A.


Missiles

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.

Enemy Movement and Intelligence

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.

Creating Dynamic Difficulty Using AI

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.

DumbSprite

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

Diagonal Sprite

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.

Diagonal Sprite AI

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];
    }
}

Kamikaze Sprite AI

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.

Wrapping Up

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.107.94