Finite State Gameplay

0
372

Finite State Gameplay in Theory

Finite state refers to a condition that will have a limited number of possible values. The age of a person is a finite state value range from 0 to around 80, with no exceptions. To ensure that an age does not exceed the bounds, the actual limits must be used, not just the most common limits. So, age really should be 0 to 120, because even today there are a few people still alive who were born in the nineteenth century. Let’s study finite state theory and then write some sample code to see how well it works in a game.

Finite State Theory

The speed of a car is a finite state range from 0 to around 100. But what if we’re dealing with a sports car? In that case, the speed might go up to 200 or more (the Bugatti Veyron has a top speed of 260, but it costs a million dollars). Then, what about top fuel dragsters that can reach speeds in excess of 300 miles per hour? As you can see, the range is not easily determined because there are always exceptions to the rule, challenges for our assumptions. There are even land-speed-record cars that can go faster than these numbers. What we have to do, then, is reduce the scope of the object we’re describing with finite state variables. For instance, we might limit the scope to consumer passenger vehicles that cost less than $40,000, to come up with a safe range of 0 to 120 (as a reasonable upper limit).

Finite states need not be numeric or even purely composed of whole numbers, either. We can have a finite state variable dealing with very small numbers indeed, such as the trace widths of computer chips, measured in billionths of a meter (nanometer), or even non-numeric states. Consider a light switch. It is either on or off. There is no in-between value because to hold the switch in between still causes the light to be either on or off. There is no “somewhat on” or “somewhat off” in a two-state switch.

Enumerations can also be used to describe the possible states of an object. For instance, the condition of a food item at a grocery store might have these possible values: Fresh, Good, Aged, or Spoiled. It is the job of the store employees to keep track of the state of their food items. Usually, when the state of, say, a banana goes from Fresh to Good, the price will be discounted. When the state degrades from Good to Aged, the price will be reduced further for perhaps one final day, and then if it hasn’t sold, it is discarded.

We humans categorize items all day long. That’s a very significant way that our brains work. We are surprised upon encountering a new thing, which can bring on emotional excitement or intrigue! Have you ever noticed that when you find something new, you often feel like a child again? For children, everything in the world is a new experience almost every day, which is what made childhood so much fun for most people (all things being equal). When something new or weird is discovered, the first thing we do is try to categorize it. “Hey, look, it’s a bird! No, it’s a plane! No, it’s Superman!” Hunting in the deep forest is an exciting sport for many people because they never know what they’ll run into in the woods. Biologists are often attracted to the field because of the excitement of finding new species and trying to categorize them. The same might be said of anthropology, the study of ancient human remains, and the related field, archaeology, the study of their civilizations. I was attracted to computer science for the same reason: Often, code would produce completely unexpected results, which I found exciting. Indeed, all the sciences involve discovery at the root of the field, so naturally curious people are those who enjoy the unexpected challenge of categorizing new things.

State-Driven Game Entity Behavior

Teaching computers to recognize new things and categorizing them is one area of work at the height of artificial intelligence research today. We will be looking at just large-scale game state this hour, but the potential is here for giving behaviors to game entities (represented with Sprite objects in the sample game). The behaviors have been explored somewhat already in the Animation class, but that was intended primarily to accommodate drawing with special effects, like rotation and alpha fading. The OrbitalMovement class was actually a behavior packaged as an Animation subclass, so we might use that class again for our game. The states of a game entity might be classified in terms of simple navigation—choosing a direction and velocity and moving, or heading toward a target location at a certain velocity. Some decision-making logic might be added so that an entity will follow or run away from another entity. These are all behaviors that can be programmed into a reusable class that will give a game much more of a scripted quality rather than a “hard-programmed” quality, which tends to be less flexible.

“State” can be set and used in logic using something as simple as a number variable, where the state is a discrete value from 0 to 100, with each number representing a different behavior for the entity. The state value might be used as a lookup index into an enumeration of behaviors that are available to all entities in a game. Consider these, for example:

  • IDLE = 0
  • RANDOM = 1
  • CHASING = 2
  • FLEEING = 3
  • SHADOWING = 4
  • HIDING = 5

These are all properties that can be encoded into an enumeration so that an entity’s behavior is determined with a simple integer variable used as the index.

Adaptive States: Rudimentary A.I.

The use of a state variable and enumerated state values may be considered a simple form of intelligence, but complex systems are made of simple items and rules that determine how they interact. Consider how an ant colony with only reactionary behavior can accomplish the stripping of food from nearby shrubs without a guiding intelligence directing them? One might consider their simple behaviors collectively as a hive intellect. Agitate one of the ants so that it dies, and a pheromone is given off, causing all nearby ants to charge toward the threat and exude the pheromone themselves, so that soon half the colony is on the attack. A similar process directs bee behavior, and indeed all hive species.

A static state variable with an enumeration of behaviors works well for most games. But if more advanced behavior is needed, a change is needed to the way behaviors are chosen for the entity. The behaviors themselves don’t change. In other words, the action items in the enumeration do not change. What changes is the way in which the index arrives at a value that is meaningful within the enumeration.

Consider sprite movement and animation for a minute. A sprite has a position based on Vector2, with floating-point X and Y properties. These floats do not have to equal a specific whole number representing a pixel in order for the sprite to continue to draw. If the sprite is drawn only when the X and Y position values equal a whole number, the sprite will most often be invisible! What’s happening in the case of sprite rendering is a natural form of adaptive state-based behavior. In short, the value is rounded to arrive at the nearest whole number. How that value changes from 10.00378 to 10.00377 is the issue, not whether the whole-number part, 10, is affected. The decimal value might seem irrelevant since the sprite does not move from the 10 position unless the decimal crosses a rounding border (0.0 or 5.0), causing the whole number to change when evaluated. For instance, 9.9 is equivalent to 10.4 when the whole number is considered, even though these numbers could be a million miles apart in decimal terms. It’s the way in which the values change that concerns adaptive state programming.

Although the indexed enumeration structure does not change, the values must change for the adaptive algorithms to make any sense. Using the list given previously, updating the index from 0.0 toward 1.0 will cause the entity to go from IDLE to RANDOM, which might not make any sense in the game. But bumping the entity several times while it is IDLE might push it into a FLEEING state. This is just an example, because the enumeration would need to be relevant to the game at hand. Instead of the items being just casually ordered, they are ordered in terms of “fear” or “aggression,” and the RANDOM item has been removed because it does not make sense in this context (it is more of an ambivalent or apathetic behavior):

  • FLEEING = -2
  • HIDING = -1
  • IDLE = 0
  • SHADOWING = 1
  • CHASING = 2

In this new ordering, note that the values reflect the fear factor of the game entity. As long as nothing happens to it, the state will remain at IDLE. If another sprite bumps into it, the state might go down a bit. The amount of change is dependent on the gameplay, but if a value of 0.2 represents a rock being thrown by an enemy, then it will take three rock hits before the entity goes from IDLE to HIDING:

  1. state = 0 (IDLE)
  2. state -= 0.2 (-0.2)
  3. state -= 0.2 (-0.4)
  4. state -= 0.2 (-0.6)
  5. state = -0.6 (HIDING)

The HIDING state will persist until it reaches -1.5, which is rounded to the FLEEING state of -2. If our game entity starts throwing rocks back at the other guy, perhaps scoring a hit will increase the state by 0.3 or some other fine-tuned value. Also, a natural tendency to return to the IDLE or neutral state must be included in the game logic. This might be a small amount added to the state variable every frame, such as 0.001 (when negative) or -0.001 (when positive) so that inactivity will cause the entity to go back to IDLE. The natural balance factor should not be so strong that gameplay events are overridden. Getting hit or scoring a hit should always be far greater an impact (pun not intended) than the balancing value applied at every update.

Testing Game State

I have prepared an example for this hour that will demonstrate finite state programming in an effective way, while also launching the start of our sample game that will be built over the remainder

IGameModule

To get started, we will use an interface class to describe the format of all state classes so that they can be invoked from the main game with a generic call. In other words, we don’t want to write a bunch of conditions to look for a specific state and launch that screen; we want all the screens to have the same functionality so that they can be called from an indexed array. The methods listed here must be implemented in all classes that share this interface. Another thing about an interface class is that it can’t have any variables. There is an argument to be made in favor of just using an abstract class rather than an interface class. If you really need to have class variables and some private items, go with an abstract, because those cannot be defined in an interface.

[code]
interface IGameModule
{
void LoadContent(ContentManager content);
void Update(TouchLocation touch, GameTime gameTime);
void Draw(GameTime gameTime);
}
[/code]

Interface classes cannot contain variables (properties) or scope modifiers.

TitleScreenModule

The first module we’ll cover is the TitleScreenModule class. This class inherits from IGameModule. Since that is an interface class, TitleScreenModule must incorporate all the methods defined in IGameModule. Each of these screens is like a miniprogram on its own, and that’s the whole point—we don’t want the main game to get too complicated with variables for each game state. Having a class for every state might seem like overkill, but it helps keep the game more organized, which leads to better results. Figure 21.1 shows the title screen module, and the source code is found in Listing 21.1. There are three buttons that trigger a different game state (Start Game, Options, Game Over), and an Exit button that ends the program.

The title screen module.
FIGURE 21.1 The title screen module.

LISTING 21.1 Source Code for the TitleScreenModule Class

[code]
class TitleScreenModule : IGameModule
{
Game1 game;
SpriteFont font;
SpriteFont guifont;
Label lblTitle;
Button[] btnMenu;
public TitleScreenModule(Game1 game)
{
this.game = game;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
guifont = content.Load<SpriteFont>(“GUIFont”);
lblTitle = new Label(content, game.spriteBatch, font);
lblTitle.text = “Title Screen”;
Vector2 size = font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400-size.X/2, 10);
btnMenu = new Button[4];
btnMenu[0] = new Button(content, game.spriteBatch, guifont);
btnMenu[0].text = “Start Game”;
btnMenu[0].position = new Vector2(400, 160);
btnMenu[0].scaleV = new Vector2(3.0f, 1.2f);
btnMenu[1] = new Button(content, game.spriteBatch, guifont);
btnMenu[1].text = “Options”;
btnMenu[1].position = new Vector2(400, 250);
btnMenu[1].scaleV = new Vector2(3.0f, 1.2f);
btnMenu[2] = new Button(content, game.spriteBatch, guifont);
btnMenu[2].text = “Game Over”;
btnMenu[2].position = new Vector2(400, 340);
btnMenu[2].scaleV = new Vector2(3.0f, 1.2f);
btnMenu[3] = new Button(content, game.spriteBatch, guifont);
btnMenu[3].text = “Exit”;
btnMenu[3].position = new Vector2(400, 430);
btnMenu[3].scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
int tapped = -1;
int n = 0;
foreach (Button btn in btnMenu)
{
btn.Update(touch);
if (btn.Tapped)
tapped = n;
n++;
}
switch (tapped)
{
case 0:
game.gameState = Game1.GameStates.PLAYING;
break;
case 1:
game.gameState = Game1.GameStates.OPTIONS;
break;
case 2:
game.gameState = Game1.GameStates.GAMEOVER;
break;
case 3:
game.Exit();
break;
}
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
foreach (Button btn in btnMenu)
{
btn.Draw();
}
}
}
[/code]

PlayingModule

The PlayingModule class represents the normal playing state of the game when the player is engaged in the main gameplay. If the player chooses to manually exit, or wins the game, or loses the game, then the state will return to either the title screen or the game over screen. The source code is found in Listing 21.2, and Figure 21.2 shows the output. There isn’t much to see here, but it’s important to simulate the flow of the game with a Return button that jumps back to the title screen.

The game playing module.
FIGURE 21.2 The game playing module.

LISTING 21.2 Source Code for the PlayingModule Class

[code]
class PlayingModule : IGameModule
{
Game1 game;
SpriteFont font;
SpriteFont guifont;
Label lblTitle;
Button btnReturn;
public PlayingModule(Game1 game)
{
this.game = game;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
guifont = content.Load<SpriteFont>(“GUIFont”);
lblTitle = new Label(content, game.spriteBatch, font);
lblTitle.text = “Game Play Screen”;
Vector2 size = font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400 – size.X / 2, 10);
btnReturn = new Button(content, game.spriteBatch, guifont);
btnReturn.text = “Return”;
btnReturn.position = new Vector2(400, 430);
btnReturn.scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
btnReturn.Update(touch);
if (btnReturn.Tapped)
game.gameState = Game1.GameStates.TITLE;
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
btnReturn.Draw();
}
}
[/code]

OptionsModule

The game options screen would allow the player to change the audio levels or toggle the mute option, among other settings. Figure 21.3 shows the output, which looks similar to the preceding screen—and for good reason, because these should behave in a similar way but contain unique content. Listing 21.3 contains the source code for the class.

The game options module.
FIGURE 21.3 The game options module.

LISTING 21.3 Source Code for the OptionsModule Class

[code]
class OptionsModule : IGameModule
{
Game1 game;
SpriteFont font;
SpriteFont guifont;
Label lblTitle;
Button btnReturn;
public OptionsModule(Game1 game)
{
this.game = game;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
guifont = content.Load<SpriteFont>(“GUIFont”);
lblTitle = new Label(content, game.spriteBatch, font);
lblTitle.text = “Options Screen”;
Vector2 size = font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400 – size.X / 2, 10);
btnReturn = new Button(content, game.spriteBatch, guifont);
btnReturn.text = “Return”;
btnReturn.position = new Vector2(400, 430);
btnReturn.scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
btnReturn.Update(touch);
if (btnReturn.Tapped)
game.gameState = Game1.GameStates.TITLE;
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
btnReturn.Draw();
}
}
[/code]

GameOverModule

The game over screen is shown in Figure 21.4. Like the previous two modules, this just displays the module name (using a Label control), and a Return button at the bottom. It would be filled in with actual content to reflect that the player either won or lost the round or the game. Does it seem as though there is a lot of duplicated code here, with the label and button and font and so forth? Don’t be concerned with optimization if that occurs to you while you’re looking at these source code listings. There is no real content here yet, but each module will have its own unique logic and functionality, and all we see so far is structure. Listing 21.4 contains the source code for the class.

The game over module.
FIGURE 21.4 The game over module.

LISTING 21.4 Source Code for the GameOverModule Class

[code]
class GameOverModule : IGameModule
{
Game1 game;
SpriteFont font;
SpriteFont guifont;
Label lblTitle;
Button btnReturn;
public GameOverModule(Game1 game)
{
this.game = game;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
guifont = content.Load<SpriteFont>(“GUIFont”);
lblTitle = new Label(content, game.spriteBatch, font);
lblTitle.text = “Game Over Screen”;
Vector2 size = font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400 – size.X / 2, 10);
btnReturn = new Button(content, game.spriteBatch, guifont);
btnReturn.text = “Return”;
btnReturn.position = new Vector2(400, 430);
btnReturn.scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
btnReturn.Update(touch);
if (btnReturn.Tapped)
game.gameState = Game1.GameStates.TITLE;
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
btnReturn.Draw();
}
}
[/code]

Game1

Listing 21.5 contains the main source code for the example. This file contains the GameStates enumeration and shows how to instantiate (remember, that’s a fancy word that means “to create an object from the blueprint of a class”) each of the modules, and call their mutual Update() and Draw() methods. Despite having four different classes for the four modules, they are all defined as an array of IGameModule! That is the key to using these behavioral/state classes—being able to swap them at any time without rewriting much code. That array is then indexed with the state variable.

LISTING 21.5 Main Source Code for the Example

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
public enum GameStates
{
TITLE = 0,
PLAYING = 1,
OPTIONS = 2,
GAMEOVER = 3
}
public GraphicsDeviceManager graphics;
public SpriteBatch spriteBatch;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
public GameStates gameState;
IGameModule[] modules;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
modules = new IGameModule[4];
modules[0] = new TitleScreenModule(this);
modules[1] = new PlayingModule(this);
modules[2] = new OptionsModule(this);
modules[3] = new GameOverModule(this);
foreach (IGameModule mod in modules)
{
mod.LoadContent(Content);
}
gameState = GameStates.TITLE;
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
TouchCollection touchInput = TouchPanel.GetState();
TouchLocation touch = new TouchLocation();
if (touchInput.Count > 0)
{
touch = touchInput[0];
oldTouch = touch;
}
//update current module
modules[(int)gameState].Update(touch ,gameTime);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
//draw current module
modules[(int)gameState].Draw(gameTime);
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]