10.6 - Common Game-Related Algorithmic Patterns
Game development involves solving many recurring problems. Over time, the industry has developed standard algorithmic patterns to address these challenges efficiently. Understanding these patterns will help you implement common game features without reinventing the wheel.
Pathfinding Algorithms
Pathfinding is the process of finding the shortest or most efficient route between two points. It's essential for enemy AI, navigation systems, and procedural level generation.
A* (A-Star) Algorithm
A* is the most widely used pathfinding algorithm in games. It combines the advantages of Dijkstra's algorithm (accuracy) and greedy best-first search (speed).
How A* Works
- Maintain two lists: open (nodes to be evaluated) and closed (already evaluated nodes)
- Start with the starting node in the open list
- Repeatedly process the node with the lowest f-score from the open list
- For each processed node, evaluate its neighbors and update their scores
- Continue until you reach the goal or exhaust all possibilities
The key to A* is its scoring function:
- f(n) = g(n) + h(n)
- g(n) = cost from start to node n
- h(n) = heuristic estimate of cost from n to goal
Implementation
public class Node
{
public int X { get; }
public int Y { get; }
public bool IsWalkable { get; set; }
public int GCost { get; set; } // Cost from start
public int HCost { get; set; } // Estimated cost to goal
public int FCost => GCost + HCost; // Total cost
public Node Parent { get; set; } // For reconstructing the path
public Node(int x, int y, bool isWalkable)
{
X = x;
Y = y;
IsWalkable = isWalkable;
}
}
public class Grid
{
private Node[,] nodes;
private int width, height;
public Grid(int width, int height, bool[,] walkablePositions)
{
this.width = width;
this.height = height;
nodes = new Node[width, height];
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
nodes[x, y] = new Node(x, y, walkablePositions[x, y]);
}
}
}
public Node GetNode(int x, int y)
{
if (x >= 0 && x < width && y >= 0 && y < height)
{
return nodes[x, y];
}
return null;
}
public List<Node> GetNeighbors(Node node)
{
List<Node> neighbors = new List<Node>();
// Check all 8 surrounding nodes
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
if (x == 0 && y == 0)
continue;
int checkX = node.X + x;
int checkY = node.Y + y;
Node neighbor = GetNode(checkX, checkY);
if (neighbor != null && neighbor.IsWalkable)
{
neighbors.Add(neighbor);
}
}
}
return neighbors;
}
}
public class Pathfinding
{
private Grid grid;
public Pathfinding(Grid grid)
{
this.grid = grid;
}
public List<Node> FindPath(Node startNode, Node targetNode)
{
List<Node> openSet = new List<Node>();
HashSet<Node> closedSet = new HashSet<Node>();
openSet.Add(startNode);
while (openSet.Count > 0)
{
// Get node with lowest FCost
Node currentNode = openSet[0];
for (int i = 1; i < openSet.Count; i++)
{
if (openSet[i].FCost < currentNode.FCost ||
(openSet[i].FCost == currentNode.FCost && openSet[i].HCost < currentNode.HCost))
{
currentNode = openSet[i];
}
}
openSet.Remove(currentNode);
closedSet.Add(currentNode);
// Found the path
if (currentNode == targetNode)
{
return RetracePath(startNode, targetNode);
}
// Check all neighbors
foreach (Node neighbor in grid.GetNeighbors(currentNode))
{
if (closedSet.Contains(neighbor))
continue;
// Calculate new path cost to neighbor
int newMovementCostToNeighbor = currentNode.GCost + GetDistance(currentNode, neighbor);
if (newMovementCostToNeighbor < neighbor.GCost || !openSet.Contains(neighbor))
{
neighbor.GCost = newMovementCostToNeighbor;
neighbor.HCost = GetDistance(neighbor, targetNode);
neighbor.Parent = currentNode;
if (!openSet.Contains(neighbor))
{
openSet.Add(neighbor);
}
}
}
}
// No path found
return null;
}
private List<Node> RetracePath(Node startNode, Node endNode)
{
List<Node> path = new List<Node>();
Node currentNode = endNode;
while (currentNode != startNode)
{
path.Add(currentNode);
currentNode = currentNode.Parent;
}
path.Reverse();
return path;
}
private int GetDistance(Node nodeA, Node nodeB)
{
int dstX = Math.Abs(nodeA.X - nodeB.X);
int dstY = Math.Abs(nodeA.Y - nodeB.Y);
// Diagonal movement costs more (approximation)
if (dstX > dstY)
return 14 * dstY + 10 * (dstX - dstY);
return 14 * dstX + 10 * (dstY - dstX);
}
}
Navigation Mesh (NavMesh)
For 3D games or complex environments, navigation meshes provide a more efficient pathfinding solution. A NavMesh simplifies the walkable areas into a connected mesh of polygons.
Unity provides a built-in NavMesh system that handles pathfinding efficiently:
using UnityEngine;
using UnityEngine.AI;
public class EnemyController : MonoBehaviour
{
private NavMeshAgent agent;
private Transform player;
void Start()
{
agent = GetComponent<NavMeshAgent>();
player = GameObject.FindGameObjectWithTag("Player").transform;
}
void Update()
{
// Set destination to player position
agent.SetDestination(player.position);
}
}
The NavMesh system handles all the complex pathfinding calculations behind the scenes, allowing you to focus on game logic rather than algorithm implementation.
Finite State Machines (FSM)
Finite State Machines are a pattern for organizing behavior into discrete states with defined transitions. They're commonly used for:
- Character behavior and AI
- Game state management
- Animation control
- UI flow
Basic FSM Implementation
// Define states as an enum
public enum EnemyState
{
Idle,
Patrol,
Chase,
Attack,
Flee
}
public class Enemy
{
private EnemyState currentState;
private Dictionary<EnemyState, Action> stateActions;
private Dictionary<EnemyState, Dictionary<EnemyState, Func<bool>>> stateTransitions;
public Enemy()
{
// Initialize with Idle state
currentState = EnemyState.Idle;
// Set up state actions
stateActions = new Dictionary<EnemyState, Action>
{
{ EnemyState.Idle, IdleAction },
{ EnemyState.Patrol, PatrolAction },
{ EnemyState.Chase, ChaseAction },
{ EnemyState.Attack, AttackAction },
{ EnemyState.Flee, FleeAction }
};
// Set up state transitions
stateTransitions = new Dictionary<EnemyState, Dictionary<EnemyState, Func<bool>>>();
// From Idle state
stateTransitions[EnemyState.Idle] = new Dictionary<EnemyState, Func<bool>>
{
{ EnemyState.Patrol, ShouldStartPatrolling },
{ EnemyState.Chase, CanSeePlayer }
};
// From Patrol state
stateTransitions[EnemyState.Patrol] = new Dictionary<EnemyState, Func<bool>>
{
{ EnemyState.Idle, ShouldReturnToIdle },
{ EnemyState.Chase, CanSeePlayer }
};
// From Chase state
stateTransitions[EnemyState.Chase] = new Dictionary<EnemyState, Func<bool>>
{
{ EnemyState.Attack, IsInAttackRange },
{ EnemyState.Patrol, HasLostPlayer },
{ EnemyState.Flee, HealthTooLow }
};
// From Attack state
stateTransitions[EnemyState.Attack] = new Dictionary<EnemyState, Func<bool>>
{
{ EnemyState.Chase, IsOutOfAttackRange },
{ EnemyState.Flee, HealthTooLow }
};
// From Flee state
stateTransitions[EnemyState.Flee] = new Dictionary<EnemyState, Func<bool>>
{
{ EnemyState.Idle, IsHealthRecovered }
};
}
public void Update()
{
// Check for state transitions
CheckTransitions();
// Execute current state action
stateActions[currentState]?.Invoke();
}
private void CheckTransitions()
{
if (stateTransitions.TryGetValue(currentState, out var transitions))
{
foreach (var transition in transitions)
{
if (transition.Value())
{
// Transition to new state
currentState = transition.Key;
Console.WriteLine($"Transitioning to {currentState} state");
break;
}
}
}
}
// State actions
private void IdleAction() { /* Implementation */ }
private void PatrolAction() { /* Implementation */ }
private void ChaseAction() { /* Implementation */ }
private void AttackAction() { /* Implementation */ }
private void FleeAction() { /* Implementation */ }
// Transition conditions
private bool ShouldStartPatrolling() { /* Implementation */ return false; }
private bool CanSeePlayer() { /* Implementation */ return false; }
private bool ShouldReturnToIdle() { /* Implementation */ return false; }
private bool IsInAttackRange() { /* Implementation */ return false; }
private bool HasLostPlayer() { /* Implementation */ return false; }
private bool HealthTooLow() { /* Implementation */ return false; }
private bool IsOutOfAttackRange() { /* Implementation */ return false; }
private bool IsHealthRecovered() { /* Implementation */ return false; }
}
Hierarchical State Machines
For more complex behaviors, you can implement hierarchical state machines where states can contain sub-states:
public abstract class State
{
protected Enemy enemy;
public State(Enemy enemy)
{
this.enemy = enemy;
}
public virtual void Enter() { }
public virtual void Execute() { }
public virtual void Exit() { }
}
public class CombatState : State
{
private Dictionary<string, State> subStates;
private State currentSubState;
public CombatState(Enemy enemy) : base(enemy)
{
subStates = new Dictionary<string, State>
{
{ "Melee", new MeleeCombatState(enemy) },
{ "Ranged", new RangedCombatState(enemy) },
{ "Defensive", new DefensiveCombatState(enemy) }
};
currentSubState = subStates["Melee"];
}
public override void Enter()
{
currentSubState.Enter();
}
public override void Execute()
{
// Handle transitions between sub-states
CheckSubStateTransitions();
// Execute current sub-state
currentSubState.Execute();
}
public override void Exit()
{
currentSubState.Exit();
}
private void CheckSubStateTransitions()
{
// Implement sub-state transition logic
}
public void ChangeSubState(string stateName)
{
if (subStates.TryGetValue(stateName, out State newState))
{
currentSubState.Exit();
currentSubState = newState;
currentSubState.Enter();
}
}
}
Object Pooling
Object pooling is a pattern that pre-instantiates objects and reuses them instead of creating and destroying objects repeatedly. This is particularly useful for frequently spawned objects like bullets, particles, or enemies.
Basic Object Pool Implementation
public class ObjectPool<T> where T : class, new()
{
private readonly List<T> availableObjects;
private readonly List<T> usedObjects;
private readonly Func<T> createFunc;
private readonly Action<T> resetAction;
public ObjectPool(int initialSize, Func<T> createFunc = null, Action<T> resetAction = null)
{
this.createFunc = createFunc ?? (() => new T());
this.resetAction = resetAction;
availableObjects = new List<T>(initialSize);
usedObjects = new List<T>(initialSize);
// Pre-instantiate objects
for (int i = 0; i < initialSize; i++)
{
availableObjects.Add(this.createFunc());
}
}
public T Get()
{
T obj;
if (availableObjects.Count > 0)
{
// Get object from pool
int lastIndex = availableObjects.Count - 1;
obj = availableObjects[lastIndex];
availableObjects.RemoveAt(lastIndex);
}
else
{
// Create new object if pool is empty
obj = createFunc();
}
usedObjects.Add(obj);
return obj;
}
public void Return(T obj)
{
if (obj == null) return;
// Reset object if needed
resetAction?.Invoke(obj);
// Return to pool
usedObjects.Remove(obj);
availableObjects.Add(obj);
}
public void ReturnAll()
{
// Return all used objects to the pool
foreach (T obj in usedObjects.ToArray())
{
Return(obj);
}
}
}
Game Development Example: Bullet Pool
public class Bullet
{
public bool IsActive { get; private set; }
public Vector2 Position { get; private set; }
public Vector2 Velocity { get; private set; }
public void Initialize(Vector2 position, Vector2 velocity)
{
Position = position;
Velocity = velocity;
IsActive = true;
}
public void Update(float deltaTime)
{
if (!IsActive) return;
Position += Velocity * deltaTime;
// Check if bullet is out of bounds or hit something
// ...
}
public void Deactivate()
{
IsActive = false;
}
}
public class WeaponSystem
{
private ObjectPool<Bullet> bulletPool;
private List<Bullet> activeBullets;
public WeaponSystem(int maxBullets)
{
bulletPool = new ObjectPool<Bullet>(maxBullets,
createFunc: () => new Bullet(),
resetAction: (bullet) => bullet.Deactivate());
activeBullets = new List<Bullet>(maxBullets);
}
public void FireBullet(Vector2 position, Vector2 direction, float speed)
{
Bullet bullet = bulletPool.Get();
bullet.Initialize(position, direction * speed);
activeBullets.Add(bullet);
}
public void Update(float deltaTime)
{
for (int i = activeBullets.Count - 1; i >= 0; i--)
{
Bullet bullet = activeBullets[i];
bullet.Update(deltaTime);
if (!bullet.IsActive)
{
activeBullets.RemoveAt(i);
bulletPool.Return(bullet);
}
}
}
}
Unity provides an ObjectPool
class in the UnityEngine.Pool
namespace (Unity 2021.1+):
using UnityEngine;
using UnityEngine.Pool;
public class BulletManager : MonoBehaviour
{
[SerializeField] private GameObject bulletPrefab;
[SerializeField] private int maxPoolSize = 100;
private IObjectPool<GameObject> bulletPool;
void Awake()
{
bulletPool = new ObjectPool<GameObject>(
createFunc: CreateBullet,
actionOnGet: OnGetBullet,
actionOnRelease: OnReleaseBullet,
actionOnDestroy: OnDestroyBullet,
defaultCapacity: 20,
maxSize: maxPoolSize
);
}
private GameObject CreateBullet()
{
GameObject bullet = Instantiate(bulletPrefab);
bullet.GetComponent<Bullet>().SetPool(bulletPool);
return bullet;
}
private void OnGetBullet(GameObject bullet)
{
bullet.SetActive(true);
}
private void OnReleaseBullet(GameObject bullet)
{
bullet.SetActive(false);
}
private void OnDestroyBullet(GameObject bullet)
{
Destroy(bullet);
}
public void FireBullet(Vector3 position, Vector3 direction)
{
GameObject bullet = bulletPool.Get();
bullet.transform.position = position;
bullet.GetComponent<Bullet>().Initialize(direction);
}
}
Procedural Generation
Procedural generation creates content algorithmically rather than manually. It's used for generating levels, terrain, textures, and more.
Perlin Noise
Perlin noise is a gradient noise function that produces natural-looking random patterns. It's commonly used for terrain generation, cloud textures, and other natural phenomena.
public class PerlinNoiseGenerator
{
private readonly Random random;
public PerlinNoiseGenerator(int seed)
{
random = new Random(seed);
}
// Generate a 2D heightmap using Perlin noise
public float[,] GenerateHeightmap(int width, int height, float scale, int octaves, float persistence, float lacunarity)
{
float[,] heightmap = new float[width, height];
// Random offsets for each octave
Vector2[] octaveOffsets = new Vector2[octaves];
for (int i = 0; i < octaves; i++)
{
float offsetX = random.Next(-100000, 100000);
float offsetY = random.Next(-100000, 100000);
octaveOffsets[i] = new Vector2(offsetX, offsetY);
}
if (scale <= 0)
{
scale = 0.0001f;
}
float maxNoiseHeight = float.MinValue;
float minNoiseHeight = float.MaxValue;
// Calculate noise values
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float amplitude = 1;
float frequency = 1;
float noiseHeight = 0;
// Sum multiple octaves of noise
for (int i = 0; i < octaves; i++)
{
float sampleX = (x / scale) * frequency + octaveOffsets[i].X;
float sampleY = (y / scale) * frequency + octaveOffsets[i].Y;
// Use Perlin noise function (simplified here)
float perlinValue = Mathf.PerlinNoise(sampleX, sampleY) * 2 - 1;
noiseHeight += perlinValue * amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
// Track min and max values for normalization
if (noiseHeight > maxNoiseHeight)
{
maxNoiseHeight = noiseHeight;
}
if (noiseHeight < minNoiseHeight)
{
minNoiseHeight = noiseHeight;
}
heightmap[x, y] = noiseHeight;
}
}
// Normalize values to range [0, 1]
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
heightmap[x, y] = Mathf.InverseLerp(minNoiseHeight, maxNoiseHeight, heightmap[x, y]);
}
}
return heightmap;
}
}
// Unity's Mathf class for this example
public static class Mathf
{
public static float PerlinNoise(float x, float y)
{
// This is a simplified placeholder
// In a real implementation, you would use Unity's Mathf.PerlinNoise
// or implement the full Perlin noise algorithm
return (float)new Random((int)(x * 1000 + y * 10000)).NextDouble();
}
public static float InverseLerp(float a, float b, float value)
{
if (a != b)
{
return (value - a) / (b - a);
}
return 0;
}
}
Cellular Automata
Cellular automata use simple rules applied to cells in a grid to create complex patterns. They're useful for cave generation, map creation, and simulating natural processes.
public class CellularAutomata
{
private int width;
private int height;
private bool[,] map; // true = wall, false = floor
private Random random;
public CellularAutomata(int width, int height, int seed, float fillPercent)
{
this.width = width;
this.height = height;
random = new Random(seed);
// Initialize map randomly
map = new bool[width, height];
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// Edges are always walls
if (x == 0 || x == width - 1 || y == 0 || y == height - 1)
{
map[x, y] = true;
}
else
{
map[x, y] = random.NextDouble() < fillPercent;
}
}
}
}
public bool[,] GenerateMap(int iterations)
{
for (int i = 0; i < iterations; i++)
{
SimulationStep();
}
return map;
}
private void SimulationStep()
{
bool[,] newMap = new bool[width, height];
// Apply cellular automaton rules
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// Count neighboring walls
int neighborWalls = CountNeighborWalls(x, y);
// Apply Conway's Game of Life rules (modified for cave generation)
if (map[x, y])
{
// Cell is currently a wall
newMap[x, y] = neighborWalls >= 4; // Stay a wall if 4+ neighbors are walls
}
else
{
// Cell is currently a floor
newMap[x, y] = neighborWalls >= 5; // Become a wall if 5+ neighbors are walls
}
// Ensure edges remain walls
if (x == 0 || x == width - 1 || y == 0 || y == height - 1)
{
newMap[x, y] = true;
}
}
}
map = newMap;
}
private int CountNeighborWalls(int x, int y)
{
int count = 0;
for (int neighborX = x - 1; neighborX <= x + 1; neighborX++)
{
for (int neighborY = y - 1; neighborY <= y + 1; neighborY++)
{
// Skip the cell itself
if (neighborX == x && neighborY == y)
continue;
// Count walls, including out-of-bounds cells as walls
if (neighborX < 0 || neighborX >= width || neighborY < 0 || neighborY >= height)
{
count++;
}
else if (map[neighborX, neighborY])
{
count++;
}
}
}
return count;
}
}
Component-Based Architecture
Component-based architecture is a design pattern where game objects are composed of reusable components rather than using deep inheritance hierarchies. This pattern is the foundation of Unity's design.
Basic Component System
public interface IComponent
{
void Update(float deltaTime);
}
public class GameObject
{
private List<IComponent> components = new List<IComponent>();
public string Name { get; }
public GameObject(string name)
{
Name = name;
}
public void AddComponent(IComponent component)
{
components.Add(component);
}
public T GetComponent<T>() where T : IComponent
{
foreach (var component in components)
{
if (component is T typedComponent)
{
return typedComponent;
}
}
return default;
}
public void Update(float deltaTime)
{
foreach (var component in components)
{
component.Update(deltaTime);
}
}
}
// Example components
public class TransformComponent : IComponent
{
public Vector2 Position { get; set; }
public float Rotation { get; set; }
public Vector2 Scale { get; set; } = new Vector2(1, 1);
public void Update(float deltaTime)
{
// Transform logic (if needed)
}
}
public class MovementComponent : IComponent
{
private TransformComponent transform;
private GameObject gameObject;
public Vector2 Velocity { get; set; }
public MovementComponent(GameObject gameObject)
{
this.gameObject = gameObject;
transform = gameObject.GetComponent<TransformComponent>();
if (transform == null)
{
throw new InvalidOperationException("MovementComponent requires a TransformComponent");
}
}
public void Update(float deltaTime)
{
if (transform != null)
{
transform.Position += Velocity * deltaTime;
}
}
}
Event-Driven Systems
Event-driven systems decouple components by using events and listeners instead of direct method calls. This pattern is useful for handling user input, game state changes, and communication between systems.
Basic Event System
public class EventSystem
{
private Dictionary<string, List<Action<object>>> eventListeners =
new Dictionary<string, List<Action<object>>>();
// Subscribe to an event
public void Subscribe(string eventName, Action<object> listener)
{
if (!eventListeners.ContainsKey(eventName))
{
eventListeners[eventName] = new List<Action<object>>();
}
eventListeners[eventName].Add(listener);
}
// Unsubscribe from an event
public void Unsubscribe(string eventName, Action<object> listener)
{
if (eventListeners.ContainsKey(eventName))
{
eventListeners[eventName].Remove(listener);
}
}
// Trigger an event
public void TriggerEvent(string eventName, object data = null)
{
if (eventListeners.ContainsKey(eventName))
{
// Create a copy of the list to avoid issues if listeners are added/removed during iteration
List<Action<object>> listeners = new List<Action<object>>(eventListeners[eventName]);
foreach (var listener in listeners)
{
listener.Invoke(data);
}
}
}
}
// Example usage
public class GameManager
{
private EventSystem eventSystem = new EventSystem();
public void Initialize()
{
// Subscribe to events
eventSystem.Subscribe("PlayerDeath", OnPlayerDeath);
eventSystem.Subscribe("LevelComplete", OnLevelComplete);
}
private void OnPlayerDeath(object data)
{
Console.WriteLine("Player died! Restarting level...");
// Restart level logic
}
private void OnLevelComplete(object data)
{
int levelNumber = (int)data;
Console.WriteLine($"Level {levelNumber} completed! Loading next level...");
// Load next level logic
}
public void PlayerDied()
{
// Trigger player death event
eventSystem.TriggerEvent("PlayerDeath");
}
public void CompleteLevel(int levelNumber)
{
// Trigger level complete event with level number
eventSystem.TriggerEvent("LevelComplete", levelNumber);
}
}
Conclusion
These algorithmic patterns form the foundation of many game systems. By understanding and implementing these patterns, you can solve common game development challenges efficiently and create more robust, maintainable code.
Remember that these patterns are tools in your toolbox—choose the right one for each specific problem. Often, the best solution combines multiple patterns to create a complete game system.
In the next section, we'll apply these concepts to build a complete turn-based combat system, integrating multiple algorithmic patterns into a cohesive gameplay feature.