Game programming patterns in Unity with C#

Source code: GitHub

Share me

twitter logo facebook logo

...and follow me on Twitter: @eriknordeus

6. State pattern

This tutorial is outdated, you can find the latest version here: GitHub

What's the state pattern?

We begin with Wikipedia's defintion of the state pattern:

The state pattern, which closely resembles Strategy Pattern, is a behavioral software design pattern, also known as the objects for states pattern. This pattern is used in computer programming to encapsulate varying behavior for the same object based on its internal state.

The basic idea behind the pattern is that what you are trying to control has different states. If you have a car, it can either drive forward, brake, reverse, or you could have parked it. To write this behavior of a car in code is not that simple as it first might seem. If we are moving forward and braking, then we don't want to reverse even though the reverse button and the brake button is often the same key. And what if we are reversing and pressing the forward key, should we brake or accelerate? And what if we have more states than just four? Then you will need the state pattern to avoid nested if-else-statements with several booleans.

So the first thing you have to ask yourself is: What are all the states or possible situations the object is going to find itself in? This idea to divide the behavior into a fixed set of states that the object can be in is called a Finite State Machine, or FSM. The object can only be in one state at a time: you shouldn't accelerate and press the brake pedal at the same time!

It's common to use enums when coding a state machine in C#, and it looks like this with our car example:

enum CarFSM
{
	Forward,
	Brake,
	Reverse,
	Park
}
//Default state
CarFSM carMode = CarFSM.Park; 
//And then you change the state with something like
if (carMode == CarFSM.Park && IsPressingGasPedal())
{
	carMode = CarFSM.Forward;
}
else if (carMode == CarFSM.Forward && IsPressingBrakePedal())
{
	carMode == CarFSM.Brake;
}
//...and so on

The state pattern in Unity

To illustrate the state pattern we will here make a Mincecraft clone. In Minecraft you have nasty creatures called Skeletons and Creepers. A Skeleton will attack you from distance with a bow and arrow, and the Creeper will attack you by close combat. If the player is far away from the Skeleton and Creeper, they will simply walk around in random directions, waiting for you to come closer.

So begin by creating:

  • A plane which will act as our ground
  • A sphere which will be the player being killed by enemies
  • A green box which will symbolize a creeper
  • A white box which will symbolize a skeleton
  • An empty gameobject called _GameController

Everything should now look like this:

State pattern start scene

The GameController script looks like this:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace StatePattern
{
 public class GameController : MonoBehaviour
 {
 public GameObject playerObj;
 public GameObject creeperObj;
 public GameObject skeletonObj;
 //A list that will hold all enemies
 List<Enemy> enemies = new List<Enemy>();
 
 void Start()
 {
 //Add the enemies we have
 enemies.Add(new Creeper(creeperObj.transform));
 enemies.Add(new Skeleton(skeletonObj.transform));
 }
 void Update()
 {
 //Update all enemies to see if they should change state and move/attack player
 for (int i = 0; i < enemies.Count; i++) { enemies[i].UpdateEnemy(playerObj.transform); } } } } 

The state pattern scripts

Our state pattern scripts consists of an Enemy parent class, and two child classes for each enemy. The basic idea is that we first update the state of the enemy, such as Attack, Flee, Stroll, or Move-towards-the-player-to-kill, by checking if we should change to another state from the current state the enemy is in. So if the enemy is strolling around and the player is within a certain distance, then the enemy should begin hunting the player.

The main difference between the Creeper and the Skeleton is that the Skeleton can attack from distance, while the Creeper has to be very close to the player to attack. I've not implemented everything, such as the health system, so neither of the enemies will run away when they are damaged.

The base enemy class looks like this:

using UnityEngine;
using System.Collections;
namespace StatePattern
{
 //The enemy base class
 public class Enemy 
 {
 protected Transform enemyObj;
 //The different states the enemy can be in
 protected enum EnemyFSM
 {
 Attack,
 Flee,
 Stroll,
 MoveTowardsPlayer
 }
 //Update the enemy by giving it a new state
 public virtual void UpdateEnemy(Transform playerObj)
 {
 }
 //Do something based on a state
 protected void DoAction(Transform playerObj, EnemyFSM enemyMode)
 {
 float fleeSpeed = 10f;
 float strollSpeed = 1f;
 float attackSpeed = 5f;
 switch (enemyMode)
 {
 case EnemyFSM.Attack:
 //Attack player
 break;
 case EnemyFSM.Flee:
 //Move away from player
 //Look in the opposite direction
 enemyObj.rotation = Quaternion.LookRotation(enemyObj.position - playerObj.position);
 //Move
 enemyObj.Translate(enemyObj.forward * fleeSpeed * Time.deltaTime);
 break;
 case EnemyFSM.Stroll:
 //Look at a random position
 Vector3 randomPos = new Vector3(Random.Range(0f, 100f), 0f, Random.Range(0f, 100f));
 enemyObj.rotation = Quaternion.LookRotation(enemyObj.position - randomPos);
 //Move
 enemyObj.Translate(enemyObj.forward * strollSpeed * Time.deltaTime);
 break;
 case EnemyFSM.MoveTowardsPlayer:
 //Look at the player
 enemyObj.rotation = Quaternion.LookRotation(playerObj.position - enemyObj.position);
 //Move
 enemyObj.Translate(enemyObj.forward * attackSpeed * Time.deltaTime);
 break;
 }
 }
 }
}

The Skeleton class looks like this:

using UnityEngine;
using System.Collections;
namespace StatePattern
{
 //The skeleton class
 public class Skeleton : Enemy
 {
 EnemyFSM skeletonMode = EnemyFSM.Stroll;
 float health = 100f;
 public Skeleton(Transform skeletonObj)
 {
 base.enemyObj = skeletonObj;
 }
 //Update the creeper's state
 public override void UpdateEnemy(Transform playerObj)
 {
 //The distance between the Creeper and the player
 float distance = (base.enemyObj.position - playerObj.position).magnitude;
 switch (skeletonMode)
 {
 case EnemyFSM.Attack:
 if (health < 20f) { skeletonMode = EnemyFSM.Flee; } else if (distance> 6f)
 {
 skeletonMode = EnemyFSM.MoveTowardsPlayer;
 }
 break;
 case EnemyFSM.Flee:
 if (health> 60f)
 {
 skeletonMode = EnemyFSM.Stroll;
 }
 break;
 case EnemyFSM.Stroll:
 if (distance < 10f) { skeletonMode = EnemyFSM.MoveTowardsPlayer; } break; case EnemyFSM.MoveTowardsPlayer: //The skeleton has bow and arrow so can attack from distance if (distance < 5f) { skeletonMode = EnemyFSM.Attack; } else if (distance> 15f)
 {
 skeletonMode = EnemyFSM.Stroll;
 }
 break;
 }
 //Move the enemy based on a state
 DoAction(playerObj, skeletonMode);
 }
 }
}

The Creeper class looks like this:

using UnityEngine;
using System.Collections;
namespace StatePattern
{
 //The creeper class
 public class Creeper : Enemy
 {
 EnemyFSM creeperMode = EnemyFSM.Stroll;
 float health = 100f;
 public Creeper(Transform creeperObj)
 {
 base.enemyObj = creeperObj;
 }
 //Update the creeper's state
 public override void UpdateEnemy(Transform playerObj)
 {
 //The distance between the Creeper and the player
 float distance = (base.enemyObj.position - playerObj.position).magnitude;
 switch (creeperMode)
 {
 case EnemyFSM.Attack:
 if (health < 20f) { creeperMode = EnemyFSM.Flee; } else if (distance> 2f)
 {
 creeperMode = EnemyFSM.MoveTowardsPlayer;
 }
 break;
 case EnemyFSM.Flee:
 if (health> 60f)
 {
 creeperMode = EnemyFSM.Stroll;
 }
 break;
 case EnemyFSM.Stroll:
 if (distance < 10f) { creeperMode = EnemyFSM.MoveTowardsPlayer; } break; case EnemyFSM.MoveTowardsPlayer: if (distance < 1f) { creeperMode = EnemyFSM.Attack; } else if (distance> 15f)
 {
 creeperMode = EnemyFSM.Stroll;
 }
 break;
 }
 //Move the enemy based on a state
 DoAction(playerObj, creeperMode);
 }
 }
}

If you now press play, move the camera to a from-above-position, and move the player by changing its coordinates in Unity's window, you should see something like this:

State pattern in action scene

Both boxes will move towards you if you are close enough, but only the green box will move towards your exact position, because the white box can attack you from a certain distance. But if the blue sphere is far away, both boxes will vibrate at their position. You should maybe implement a time parameter so they are actually strolling and not just changing direction each update, but changing random direction after a certain time!


Back

AltStyle によって変換されたページ (->オリジナル) /