Game object states and Semiautonomous Behaviors
After completing this chapter, you will be able to:
So far, the behaviors you have added to game objects have been fairly straightforward, consisting mostly of a single state. Therefore, the game objects’ behaviors have not been malleable to conditions that would modify their initial behaviors. In this chapter, we will show you how to create more complex and dynamic behaviors for your game objects. These include common game behaviors that react autonomously to outside conditions or triggers, such as player input or timers. To implement these malleable behaviors, each game object will need to implement its own finite state machine.
Review of finite state machines
A finite state machine can be thought of as a model or functionality added to an object—or in your case, a game object. By definition, a finite state machine has a limited or finite number of states and can exist in only one state at a time. The transition between states is dependent upon the behavior desired for the state machine and is triggered by some condition.
For example, take a look at the generalized state machine in Figure 6-1. This finite state machine can be reduced to three primary states and their transitions. If you look closely at the figure, you can see that each state is outlined and exists as a separate entity. A condition generally must be met to cause a transition between states. This transition condition is often (but does not necessarily need to be) unique for each state within the finite state machine. It is also important to notice that this state machine contains several illegal transitions. For example, there’s no way to transition from state 2 back to state 1 in this example. If a state is not connected to the state machine via at least one transition condition, then it does not exist within the state machine and should be removed.
Figure 6-1. A basic finite state machine diagram with three states
Creating a spinning arrow
The first finite state machine you will implement centers around a simple spinning arrow. The arrow in this project demonstrates three distinct states, as well as the conditions required to transition from one state to the next. For the purposes of this project, each state can transition to only one other state. This means that a condition or trigger is associated with each of the three states. The transition between states can occur if and only if the condition has been met.
The Spinning Arrow project
This project lets you control a hero by moving it around the screen. The project contains an arrow that spins until the hero enters the arrow’s detection range; when that occurs, the arrow points toward the hero. The project also displays the arrow’s current state in the top-left corner of the window. You can see an example of this project running in Figure 6-2.
Figure 6-2. Running the Spinning Arrow project
The arrow’s finite state machine
The only new concept introduced in this project from the previous chapter’s projects is the arrow’s finite state machine; because of this, let’s take a closer look at the specifics involved. The arrow itself starts by spinning up to the maximum speed. It continues to spin at its maximum speed until it detects that the hero character is within range. When the hero character is detected, the arrow will stop spinning and point in the direction of the hero. After the hero leaves the arrow’s detection range, the arrow begins spinning again, thereby returning to its starting state. You can see this reflected in Figure 6-3. Notice that once the arrow’s state machine has started, it cannot break away from its state loop and must be within one of its three states.
Figure 6-3. The Spinning Arrow project’s finite state machine
As shown in Figure 6-3, the arrow’s three states are as follows:
This project has one control, as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Add the following resources, which can be found in the Chapter06SourceCodeResources folder, into your content project before you begin:
The SpinningArrow class will contain all the behavior and functionality of the arrow. This includes the three possible states and the current state of the arrow, as well as the spinning behavior.
Creating the SpinningArrow class
public class SpinningArrow : GameObject
{
...
}
public enum SpinningArrowState
{
ArrowTransition,
ArrowSpinning,
ArrowPointsToHero
};
private SpinningArrowState mArrowState = SpinningArrowState.ArrowTransition;
private float mSpinRate = 0f;
#region Constants
private const float kHeroTriggerDistance = 15f;
private const float kMaxSpinRate = (float) Math.PI / 10f;
private const float kDeltaSpin = kMaxSpinRate / 200f;
#endregion
public SpinningArrow(Vector2 position) :
base("RightArrow", position, new Vector2(10, 4))
{
InitialFrontDirection = Vector2.UnitX;
}
public SpinningArrowState ArrowState
{
get { return mArrowState; }
}
Note Vector2.UnitX is a default provided for a unit vector of (1, 0).
private void SpinTheArrow()
{
RotateAngleInRadian += mSpinRate;
if (RotateAngleInRadian > (2 * Math.PI))
RotateAngleInRadian -= (float)(2 * Math.PI);
}
public void UpdateSpinningArrow(TexturedPrimitive hero)
{
switch (mArrowState)
{
case SpinningArrowState.ArrowTransition:
UpdateTransitionState();
break;
case SpinningArrowState.ArrowSpinning:
UpdateSpinningState(hero);
break;
case SpinningArrowState.ArrowPointsToHero:
UpdatePointToHero(hero.Position - Position);
break;
}
}
private void UpdateTransitionState()
{
SpinTheArrow();
if (mSpinRate < kMaxSpinRate)
mSpinRate += kDeltaSpin;
else
// Transition to Spin state
mArrowState = SpinningArrowState.ArrowSpinning;
}
private void UpdateSpinningState(TexturedPrimitive hero)
{
SpinTheArrow();
Vector2 toHero = hero.Position - Position;
if (toHero.Length() < kHeroTriggerDistance)
{
mSpinRate = 0f;
mArrowState = SpinningArrowState.ArrowPointsToHero;
UpdatePointToHero(toHero);
}
}
private void UpdatePointToHero(Vector2 toHero)
{
float dist = toHero.Length();
if (dist < kHeroTriggerDistance)
{
FrontDirection = toHero;
}
else
mArrowState = SpinningArrowState.ArrowTransition;
}
Now you can create the class used to control the hero. The PlayerControlHero class will provide the simple behavior of moving the hero when the left thumbstick is used. Additionally, the hero’s image is swapped depending on which direction it moves.
Creating the PlayerControlHero class
public class PlayerControlHero : GameObject
{
...
}
public PlayerControlHero(Vector2 position) :
base("KidLeft", position, new Vector2(7f, 8f))
{
...
}
public void UpdateHero()
{
Vector2 delta = InputWrapper.ThumbSticks.Left;
Position += delta;
if (delta.X > 0)
mImage = Game1.sContent.Load<Texture2D>("KidRight");
else
mImage = Game1.sContent.Load<Texture2D>("KidLeft");
}
All that remains is to use the new classes in the GameState class.
PlayerControlHero mHero;
SpinningArrow mArrow;
public GameState()
{
mArrow = new SpinningArrow(new Vector2(50, 40));
mHero = new PlayerControlHero(new Vector2(5, 5));
}
public void UpdateGame()
{
mHero.UpdateHero();
mArrow.UpdateSpinningArrow(mHero);
}
public void DrawGame()
{
mHero.Draw();
mArrow.Draw();
FontSupport.PrintStatus("Arrow State: " + mArrow.ArrowState.ToString(), null);
}
Adding many spinning arrows
With your finite state machine for the SpinningArrow project complete, you will now increase the number of arrows within the project from 1 to 20. The state functionality for each arrow remains the same; however, you will add visual feedback for each of the arrow’s states. By increasing the number of arrows in the project, you increase the project’s complexity rather drastically. Adding many arrows should also serve to reinforce the benefits of taking an object-oriented approach when creating your game objects.
The Many Spinning Arrows project
As before, this project lets a user control a hero by moving it around the screen. The project contains a set of arrows that spin until the hero has entered an arrow’s detection range, at which time the arrows stop and point toward the hero. The project also reflects the arrow states by changing their color when the state changes. The colors for each state are as follows: gray for state 1 (arrow transition), green for state 2 (arrow spinning), and red for state 3 (arrow points to hero). In addition, each arrow has smaller black arrows equal to its current state number. You can see an example of this project running in Figure 6-4.
Figure 6-4. Running the Many Spinning Arrows project
The project has one control, as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Add the following resources, which can be found in the Chapter06SourceCodeResources folder, into your content project before you begin:
You first need to add the visual indicators for state changes by swapping to the appropriate image when the changes occur. You can achieve this by modifying the SpinningArrow class.
Modifying the SpinningArrow class
public SpinningArrow(Vector2 position) :
base("TransientArrow", position, new Vector2(10, 4))
{
// Arrow initially facing postive x direction
InitialFrontDirection = Vector2.UnitX;
}
private void UpdateTransitionState()
{
...
// Transition to Spin state
mArrowState = SpinningArrowState.ArrowSpinning;
mImage = Game1.sContent.Load<Texture2D>("RightArrow");
...
}
private void UpdateSpinningState(TexturedPrimitive hero)
{
...
// Transition to ArrowPointsToHero state
mSpinRate = 0f;
mArrowState = SpinningArrowState.ArrowPointsToHero;
mImage = Game1.sContent.Load<Texture2D>("PointingArrow");
...
}
private void UpdatePointToHero(Vector2 toHero)
{
...
// Go back to TransitionState for spinning up the arrow
mArrowState = SpinningArrowState.ArrowTransition;
mImage = Game1.sContent.Load<Texture2D>("TransientArrow");
...
}
Now it is time to create the arrows. Rather than creating each arrow separately or creating an array, a better approach is to create a separate class for the array of arrows and instantiate it once within the GameState class. You can think of the SpinningArrowSet class as you would a deck of cards. To create a deck of cards, you would create a card class and a deck class that holds an array or set of 52 cards.
Creating the SpinningArrowSet class
public class SpinningArrowSet
{
private const int kNumRows = 4;
private const int kNumColumns = 5;
private List<SpinningArrow> mTheSet = new List<SpinningArrow>();
...
}
public SpinningArrowSet()
{
Vector2 min = Camera.CameraWindowLowerLeftPosition;
Vector2 max = Camera.CameraWindowUpperRightPosition;
Vector2 size = max - min;
float deltaX = size.X / (float)(kNumColumns + 1);
float deltaY = size.Y / (float)(kNumRows + 1);
for (int r = 0; r < kNumRows; r++)
{
min.Y += deltaY;
float useDeltaX = deltaX;
for (int c = 0; c < kNumColumns; c++)
{
Vector2 pos = new Vector2(min.X + useDeltaX, min.Y);
SpinningArrow arrow = new SpinningArrow(pos);
mTheSet.Add(arrow);
useDeltaX += deltaX;
}
}
}
public void UpdateSpinningSet(TexturedPrimitive hero)
{
foreach (var arrow in mTheSet)
arrow.UpdateSpinningArrow(hero);
}
public void DrawSet()
{
foreach (var arrow in mTheSet)
arrow.Draw();
}
Now you can use the SpinningArrowSet class you just created within the GameState class instead of the SpinningArrow class.
PlayerControlHero mHero;
SpinningArrowSet mArrows;
public GameState()
{
mArrows = new SpinningArrowSet();
mHero = new PlayerControlHero(new Vector2(5, 5));
}
public void UpdateGame()
{
mHero.UpdateHero();
mArrows.UpdateSpinningSet(mHero);
}
public void DrawGame()
{
mHero.Draw();
mArrows.DrawSet();
}
Creating a patrol enemy
The Patrol Enemy project serves as another example of a finite state machine. As in the previous project, the patrol enemy loops through its states until a specific condition is met; however, the states themselves become more sophisticated in this project.
The Patrol Enemy project
In this project, the user again controls a hero by moving it around the screen. The game window contains a rocket that patrols different designated areas or regions. The hero does not affect the rocket’s state. You can see an example of this project running in Figure 6-5.
Figure 6-5. Running the Patrol Enemy project
The project has one control, as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Add the following resource, which can be found in the Chapter06SourceCodeResources folder, into your content project before you begin:
The patrol enemy’s state machine
The patrol enemy starts by randomly selecting a location within a random region in which to spawn; after spawning, the enemy transitions to its next state. When it spawns at a random point of interest within a region, its state-change condition of being close enough to its target destination is immediately met, and it must therefore target a new random point of interest. You can see an example of this in Figure 6-6. The rocket spawns in the bottom-left region and then selects its new target within the top-left region.
Figure 6-6. The regions defined in the Patrol Enemy project and a random target position
After completing its spawning behavior, the rocket continues to move toward its new target until it is close enough that it must find a new target. In Figure 6-7, you can see an example of this behavior in the finite state machine that the patrol enemy uses. As you can see, the states themselves are quite similar: move toward your target until you reach it, and then find a new target within the next region to move toward.
Figure 6-7. The Patrol Enemy project’s finite state machine
Now that you have an understanding of the state machine, you can begin creating your patrolling enemy. The PatrolEnemy class is self-contained since includes its finite state machine as well as its random region and random target functionality.
Creating the PatrolEnemy class
public class PatrolEnemy : GameObject
{
protected enum PatrolState {
TopLeftRegion,
TopRightRegion,
BottomLeftRegion,
BottomRightRegion
}
// Target position we are moving toward
private Vector2 mTargetPosition;
// Current state
private PatrolState mCurrentState;
private const float kPatrolSpeed = 0.3f;
private const float kCloseEnough = 1f;
...
}
public PatrolEnemy() :
base("PatrolEnemy", Vector2.Zero, new Vector2(5f, 10f))
{
InitialFrontDirection = Vector2.UnitY;
// Causes update state to always change to a new state
mTargetPosition = Position = Vector2.Zero;
#region Generate a random state to begin
double initState = Game1.sRan.NextDouble();
if (initState < 0.25)
{
UpdateBottomLeftState();
}
else if (initState < 0.5)
{
UpdateBottomRightState();
}
else if (initState < 0.75)
{
UpdateTopLeftState();
}
else
{
UpdateTopRightState();
}
Position = mTargetPosition;
#endregion
}
public void UpdatePatrol()
{
// Operation common to all states ...
base.Update(); // Moves game object by velocity
switch (mCurrentState)
{
case PatrolState.BottomLeftRegion:
UpdateBottomLeftState();
break;
case PatrolState.BottomRightRegion:
UpdateBottomRightState();
break;
case PatrolState.TopRightRegion:
UpdateTopRightState();
break;
case PatrolState.TopLeftRegion:
UpdateTopLeftState();
break;
}
}
private void UpdateBottomLeftState()
{
if ((Position - mTargetPosition).LengthSquared() < kCloseEnough)
{
mCurrentState = PatrolState.TopLeftRegion;
mTargetPosition = RandomBottomRightPosition();
ComputePositionAndVelocity();
}
}
private void UpdateBottomRightState()
{
if ((Position - mTargetPosition).LengthSquared() < kCloseEnough)
{
mCurrentState = PatrolState.BottomLeftRegion;
mTargetPosition = RandomTopRightPosition();
ComputePositionAndVelocity();
}
}
private void UpdateTopRightState()
{
if ((Position - mTargetPosition).LengthSquared() < kCloseEnough)
{
mCurrentState = PatrolState.BottomRightRegion;
mTargetPosition = RandomTopLeftPosition();
ComputePositionAndVelocity();
}
}
private void UpdateTopLeftState()
{
if ((Position - mTargetPosition).LengthSquared() < kCloseEnough)
{
mCurrentState = PatrolState.TopRightRegion;
mTargetPosition = RandomBottomLeftPosition();
ComputePositionAndVelocity();
}
}
private void ComputePositionAndVelocity()
{
Speed = kPatrolSpeed * (0.8f + (float)(0.4 * Game1.sRan.NextDouble()));
Vector2 toNextPosition = mTargetPosition - Position;
VelocityDirection = toNextPosition;
FrontDirection = VelocityDirection;
}
private const float sBorderRange = 0.45f;
private Vector2 ComputePoint(double xOffset, double yOffset)
{
Vector2 min = Camera.CameraWindowLowerLeftPosition;
Vector2 max = Camera.CameraWindowUpperRightPosition;
Vector2 size = max - min;
float x = min.X + size.X * (float)(xOffset + (sBorderRange * Game1.sRan.NextDouble()));
float y = min.Y + size.Y * (float)(yOffset + (sBorderRange * Game1.sRan.NextDouble()));
return new Vector2(x, y);
}
private Vector2 RandomBottomRightPosition()
{
return ComputePoint(0.5, 0.0);
}
private Vector2 RandomBottomLeftPosition()
{
return ComputePoint(0.0, 0.0);
}
private Vector2 RandomTopRightPosition()
{
return ComputePoint(0.5, 0.5);
}
private Vector2 RandomTopLeftPosition()
{
return ComputePoint(0.0, 0.5);
}
All that you need now is to modify the GameState class. Add a variable for the hero and the enemy, and instantiate them within the constructor. Call the hero and patrol’s update and draw functions correspondingly, as shown following:
public class GameState
{
PlayerControlHero mHero;
PatrolEnemy mEnemy;
public GameState()
{
mEnemy = new PatrolEnemy();
mHero = new PlayerControlHero(new Vector2(5, 5));
}
public void UpdateGame()
{
mHero.UpdateHero();
mEnemy.UpdatePatrol();
}
public void DrawGame()
{
mHero.Draw();
mEnemy.Draw();
}
}
Implementing smooth turning
After running the previous project, you may have noticed that the rocket or patrolling enemy abruptly changes directions when transitioning between states. This can be a jarring effect—especially when it occurs in quick succession. This project is devoted to smoothing out the sudden change in front direction so that the patrolling enemy’s movement becomes more fluid.
The Smooth Turning Patrol project
As before, you control a hero by moving it around the screen. The game window contains a rocket that patrols different designated areas or regions. The hero character does not affect the rocket’s state. You can see an example of this project running in Figure 6-8.
Figure 6-8. Running the Smooth Turning Patrol project
The project has one control, as follows:
The goal of this project is as follows:
The project will be completed in one step, as follows:
When considering object-oriented programming, it is generally good practice to make the behaviors a game object has as self-contained as possible. This reduces the amount potential bugs and the complexity of your project, as well as increases its scalability. Due to the object-oriented structure you have implemented thus far, you need to modify only the PatrolEnemy class to add smooth turning behavior to your patrolling enemy.
Modifying the PatrolEnemy class
public class PatrolEnemy : GameObject
{
...
private const int kStateTimer = 60 * 5;
private int mStateTimer;
public PatrolEnemy() :
base("PatrolEnemy", Vector2.Zero, new Vector2(5f, 10f))
{
mStateTimer = kStateTimer;
...
}
...
}
public void UpdatePatrol()
{
base.Update();
mStateTimer--;
Vector2 toTarget = mTargetPosition - Position;
float distToTarget = toTarget.Length();
toTarget /= distToTarget;
ComputeNewDirection(toTarget);
switch (mCurrentState)
{
...
}
}
Note Because these calculations work with floating-point numbers, it is often difficult or impossible to hit zero exactly. To account for this, you use float.Epsilon, because it is the smallest single value greater than zero.
private void ComputeNewDirection(Vector2 toTarget)
{
double cosTheta = Vector2.Dot(toTarget, FrontDirection);
float theta = (float)Math.Acos(cosTheta);
if (theta > float.Epsilon)
{
Vector3 frontDir3 = new Vector3(FrontDirection, 0f);
Vector3 toTarget3 = new Vector3(toTarget, 0f);
Vector3 zDir = Vector3.Cross(frontDir3, toTarget3);
RotateAngleInRadian -= Math.Sign(zDir.Z) * 0.03f * theta;
VelocityDirection = FrontDirection;
}
}
private void UpdateBottomLeftState(float distToTarget)
{
if ((mStateTimer < 0) || (distToTarget < kCloseEnough))
{
mCurrentState = PatrolState.BottomRightRegion;
mTargetPosition = RandomBottomRightPosition();
ComputeNewSpeedAndResetTimer();
}
}
private void UpdateBottomRightState(float distToTarget)
{
if ((mStateTimer < 0) || (distToTarget < kCloseEnough))
{
mCurrentState = PatrolState.TopRightRegion;
mTargetPosition = RandomTopRightPosition();
ComputeNewSpeedAndResetTimer();
}
}
private void UpdateTopRightState(float distToTarget)
{
if ((mStateTimer < 0) || (distToTarget < kCloseEnough))
{
mCurrentState = PatrolState.TopLeftRegion;
mTargetPosition = RandomTopLeftPosition();
ComputeNewSpeedAndResetTimer();
}
}
private void UpdateTopLeftState(float distToTarget)
{
if ((mStateTimer < 0) || (distToTarget < kCloseEnough))
{
mCurrentState = PatrolState.BottomLeftRegion;
mTargetPosition = RandomBottomLeftPosition();
ComputeNewSpeedAndResetTimer();
}
}
private void ComputeNewSpeedAndResetTimer()
{
Speed = kPatrolSpeed * (0.8f + (float)(0.4 * Game1.sRan.NextDouble()));
mStateTimer = (int) (kStateTimer * (0.8f + (float)(0.6 * Game1.sRan.NextDouble())));
}
Patrol that chases
The Patrol That Chases project is similar to the previous projects in that it contains a rocket that patrols around the game window. However, additional functionality has been added to the patrolling game object in the form of a new state. This additional state determines whether the rocket game object is patrolling or chasing.
The Patrol That Chases project
This project lets you control a hero character by moving it around the window. The game window also contains a patrolling rocket game object. The rocket will begin to chase the hero when the hero has entered its aggro range (the radius in which a monster will attack the player). After the chasing state has been activated, the patrolling object will continue to chase until it has touched the hero or until the chase timer expires. The game records the number of times the hero has been caught by the rocket in the upper-left corner. You can see an example of this project running in Figure 6-9.
Figure 6-9. Running the Patrol That Chases project
The project has one control, as follows:
The goal of the project is as follows:
The steps for creating the project are as follows:
Adding the chase state
In the previous projects, the patrol state contained several states that defined the behavior of the rocket. These states corresponded to different regions that the rocket could move to. In this project, in addition to the previous behavior, you will add a chase state. The chase state exists alongside the patrol states. This means that the rocket must be in one of these states. If it is in the patrol state, it behaves as in the previous projects, and if it is in the chase state, it chases the hero object until its conditions are met. For further clarification, refer to Figure 6-10.
Figure 6-10. The Patrol That Chases project’s finite state machine
It is now time to add the chasing functionality to the enemy patrol object.
Modifying the PatrolEnemy class
protected enum PatrolState
{
PatrolState,
ChaseHero
}
private const float kDistToBeginChase = 15f;
private void RandomNextTarget()
{
mStateTimer = kStateTimer;
mCurrentState = PatrolState.PatrolState;
// Generate a random begin state
double initState = Game1.sRan.NextDouble();
if (initState < 0.25)
mTargetPosition = RandomBottomRightPosition();
else if (initState < 0.5)
mTargetPosition = RandomTopRightPosition();
else if (initState < 0.75)
mTargetPosition = RandomTopLeftPosition();
else
mTargetPosition = RandomBottomLeftPosition();
ComputeNewSpeedAndResetTimer();
}
public PatrolEnemy() :
base("PatrolEnemy", Vector2.Zero, new Vector2(5f, 10f))
{
InitialFrontDirection = Vector2.UnitY;
// Causes update state to always change into a new state
mTargetPosition = Position = Vector2.Zero;
RandomNextTarget();
Position = mTargetPosition;
}
public bool UpdatePatrol(GameObject hero)
{
bool caught = false;
base.Update();
mStateTimer--;
Vector2 toTarget = mTargetPosition - Position;
float distToTarget = toTarget.Length();
toTarget /= distToTarget;
ComputeNewDirection(toTarget);
switch (mCurrentState)
{
case PatrolState.PatrolState:
UpdatePatrolState(hero, distToTarget);
break;
case PatrolState.ChaseHero:
caught = UpdateChaseHeroState(hero, distToTarget);
break;
}
return caught;
}
private void DetectHero(GameObject hero)
{
Vector2 toHero = hero.Position - Position;
if (toHero.Length() < kDistToBeginChase)
{
mStateTimer = (int)(kStateTimer * 1.2f); // 1.2 times as much time for chasing
Speed *= 2.5f; // 2.5 times the current speed!
mCurrentState = PatrolState.ChaseHero;
mTargetPosition = hero.Position;
mImage = Game1.sContent.Load<Texture2D>("AlertEnemy");
}
}
private void UpdatePatrolState(GameObject hero, float distToTarget)
{
if ((mStateTimer < 0) || (distToTarget < kCloseEnough))
{
RandomNextTarget();
ComputeNewSpeedAndResetTimer();
}
DetectHero(hero);
}
private bool UpdateChaseHeroState(GameObject hero, float distToHero)
{
bool caught = false;
Vector2 pos;
caught = PixelTouches(hero, out pos);
mTargetPosition = hero.Position;
if (caught || (mStateTimer < 0))
{
RandomNextTarget();
mImage = Game1.sContent.Load<Texture2D>("PatrolEnemy");
}
return caught;
}
The only modification you need to make to the GameState class is to track the number of times the hero has been caught. In the code that follows, the variable mNumCaught has been added, incremented in the update function, and printed in the draw function:
public class GameState
{
PlayerControlHero mHero;
PatrolEnemy mEnemy;
int mNumCaught = 0;
public GameState()
{
mEnemy = new PatrolEnemy();
mHero = new PlayerControlHero(new Vector2(5, 5));
}
public void UpdateGame()
{
mHero.UpdateHero();
if (mEnemy.UpdatePatrol(mHero))
mNumCaught++;
}
public void DrawGame()
{
mHero.Draw();
mEnemy.Draw();
FontSupport.PrintStatus("Caught=" + mNumCaught.ToString(), null);
}
}
Creating many enemies
The project in this section is similar to the Many Spinning Arrows project in that it increases the number of objects that can chase the hero. As with the Many Spinning Arrows project, this increases the overall complexity; however, the functionality of the enemies remains the same as in the previous project.
The Many Enemies project
As before, in this project you control the hero character by moving it around the window. The game window contains many patrolling rocket game objects. The rockets will begin to chase the hero when the hero enters any rocket’s aggro range. Once the chasing state for a rocket has been activated, that rocket will continue to chase until it touches the hero or the chase timer expires. The game records the number of times the hero has been caught in the upper-left corner. You can see an example of this project running in Figure 6-11.
Figure 6-11. Running the Many Enemies project
The project has one control, as follows:
The goal of the project is as follows:
The steps for creating the project are as follows:
Since this project is functionally identical to the previous project, all that you need to do is create a PatrolEnemySet class, which handles generating many enemies at once.
Creating the PatrolEnemySet class
public class PatrolEnemySet
{
private List<PatrolEnemy> mTheSet = new List<PatrolEnemy>();
const int kDefaultNumEnemy = 15;
...
}
public PatrolEnemySet()
{
CreateEnemySet(kDefaultNumEnemy);
}
public PatrolEnemySet(int numEnemy)
{
CreateEnemySet(numEnemy);
}
private void CreateEnemySet(int numEnemy)
{
for (int i = 0; i < numEnemy; i++)
{
PatrolEnemy enemy = new PatrolEnemy();
mTheSet.Add(enemy);
}
}
public int UpdateSet(GameObject hero)
{
int count = 0;
foreach (var enemy in mTheSet)
{
if (enemy.UpdatePatrol(hero))
count++;
}
return count;
}
public void DrawSet()
{
foreach (var enemy in mTheSet)
enemy.Draw();
}
The only change you need to implement in the GameState class is to use the new PatrolEnemySet class instead of the PatrolEnemy class. In the following code, mEnemies uses the PatrolEnemySet constructor, update, and draw functions in place of the PatrolEnemy functions.
public class GameState
{
PlayerControlHero mHero;
PatrolEnemySet mEnemies;
int mNumCaught = 0;
public GameState()
{
mEnemies = new PatrolEnemySet();
mHero = new PlayerControlHero(new Vector2(5, 5));
}
public void UpdateGame()
{
mHero.UpdateHero();
mNumCaught += mEnemies.UpdateSet(mHero);
}
public void DrawGame()
{
mHero.Draw();
mEnemies.DrawSet();
FontSupport.PrintStatus("Caught=" + mNumCaught.ToString(), null);
}
}
Summary
In this chapter, you learned how to use a finite state machine to create autonomous behavior for your game objects. By breaking down the game objects’ behavior into a set of rules formulated as state and transition conditions, you can apply the techniques described here to implement any number of behaviors.
You also learned how to increase the complexity of the project by increasing the number of game objects within the GameState class. You achieved this by creating a set of objects that increase the project’s complexity while minimizing the additional code needed, letting you manage a group of objects in the GameState class as if they were a single entity.
Finally, you learned how to implement several gamelike behaviors, including distance triggers, patrolling, and chasing. By combining these concepts, you can create sophisticated and interesting games.
Quick reference
To |
Do this |
---|---|
Support semiautonomous behavior of game objects | Define a finite state machine, as follows: 1. Determine the behaviors of the machine. 2. Group distinct behaviors into separate states. 3. Define what triggers the transitions between states, as well as how and when they’re triggered. |
Implement a finite state machine | 1. Define a new class to encapsulate the behavior. 2. Define an enum for each of the states. 3. Define an update function with a switch or case statement for each of the states. 4. Define a separate state service function for each state. |
Introduce unpredictability to a finite state machine | Include randomness in, for example, positional computation or duration of states. |
Support many instances of objects with finite state machine behavior | 1. Define a new class with a collection of the finite state machine object. 2. Implement Update() and Draw() functions in the new class to iterate through and call the corresponding Update() and Draw() functions of the finite state machine objects in the collection. For example, the SpinningArrowSet class has a collection of SpinningArrow objects, and the Update() and Draw() functions of the SpinningArrowSet class iterate through and call the corresponding Update() and Draw() functions of the SpinningArrow objects in the collection. |
Patrol across regions or areas | 1. Identify regions of patrol as states. 2. Randomly generate a position in each region. 3. Move the patrol object toward a new region until sufficiently close to the border. 4. Transition to the new state or region. |
Include unpredictability in patrolling | Include randomness when generating target positions for each region, and when computing the next state. |
Smooth transitioning between patrol regions | Integrate home-in functionality to gradually turn patrol objects toward a destination position: 1. Compute the vector from the patrol object to the destination position. 2. Compute the dot product between the computed vector and the patrol’s front direction. 3. Turn the patrol clockwise or counterclockwise depending on the sign of the cross product between the two vectors. |
Ensure a patrolling enemy does not orbit a target position | Include a countdown timer to force a transition to a new state. |
Make a patrolling enemy chase a hero | Support a state that uses the hero’s location as the target position. In this case, it is important to remember to transition out of the hero-chasing state the when hero is sufficiently far away. |
Create many patrolling enemies | Define an enemy patrol set to include a collection of patrolling enemies. |