I'm writing a game in Unity where one has different game modes but eventually uses the same level, it is only that things behave differently.
The difference is only in how the player has to aim for example, so s/he has to quickly find the same places for the targets (the targets appear by the same order) or they appear randomly.
It is the same stage but with different rules so I thought an interface
would be beneficial - the GameManager class implements it based on the game mode. Of course I could write different Game Managers or use a switch
in one but I wish to keep things organized for a change.
Question is, how to do it?
This is how you setup a simple state machine:
//state
public enum GameMode { Normal, Endless, Campaign, Idle, Count }
private GameMode gameMode;
//state machine array
private delegate void UpdateDelegate();
private UpdateDelegate[] UpdateDelegates;
void Awake()
{
//setup all UpdateDelegates here to avoid runtime memory allocation
UpdateDelegates = new UpdateDelegate[(int)GameMode.Count];
//and then each UpdateDelegate
UpdateDelegates[(int)GameMode.Normal] = UpdateNormalState;
UpdateDelegates[(int)GameMode.Endless] = UpdateEndlessState;
UpdateDelegates[(int)GameMode.Campaign] = UpdateCampaignState;
UpdateDelegates[(int)GameMode.Idle] = UpdateIdleState
gameMode = GameMode.Idle;
}
void Update()
{
//call the update method of current state
if(UpdateDelegates[(int)gameMode]!=null)
UpdateDelegates[(int)gameMode]();
}
Now you can separate the logic of each state:
void UpdateNormalState() {
//...
//write logic for normal state
}
//...
//same for other states
This way when you change gameMode, the update method of new state will be iteratively called after end of current frame.
for more info you can watch this video
The good thing about state machines is that they are easy to handle (compared with switch-case or many ifs). you have an array of methods and can do anything you want with them and still be sure that only one of them can run at a time. the maximum delay for changing states is always as short as Time.deltaTime (if Update method is used to call state machine methods)
You can even make the state machine 2D. but make sure you assign all of UpdateDelegates
public enum GameMode { Normal, Endless, Campaign, Idle, Count }
public enum GameState { Playing, Paused, GameOver, Idle, Count }
private UpdateDelegate[,] UpdateDelegates;
UpdateDelegates = new UpdateDelegate[(int)GameMode.Count, (int)GameState.Count];
If it's not enough for your game you can use an advanced state machine. here's a sample code I copied from somewhere and haven't tested yet:
This approach uses transitions between states. e.g. call MoveNext
with a given Command
and it changes the state to next ProcessState
considering the current ProcessState
of state machine and the given command.
using System;
using System.Collections.Generic;
namespace Juliet
{
public enum ProcessState
{
Inactive,
Active,
Paused,
Terminated
}
public enum Command
{
Begin,
End,
Pause,
Resume,
Exit
}
public class Process
{
class StateTransition
{
readonly ProcessState CurrentState;
readonly Command Command;
public StateTransition(ProcessState currentState, Command command)
{
CurrentState = currentState;
Command = command;
}
public override int GetHashCode()
{
return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
public override bool Equals(object obj)
{
StateTransition other = obj as StateTransition;
return other != null && this.CurrentState == other.CurrentState && this.Command == other.Command;
}
}
Dictionary<StateTransition, ProcessState> transitions;
public ProcessState CurrentState { get; private set; }
public Process()
{
CurrentState = ProcessState.Inactive;
transitions = new Dictionary<StateTransition, ProcessState>
{
{ new StateTransition(ProcessState.Inactive, Command.Exit), ProcessState.Terminated },
{ new StateTransition(ProcessState.Inactive, Command.Begin), ProcessState.Active },
{ new StateTransition(ProcessState.Active, Command.End), ProcessState.Inactive },
{ new StateTransition(ProcessState.Active, Command.Pause), ProcessState.Paused },
{ new StateTransition(ProcessState.Paused, Command.End), ProcessState.Inactive },
{ new StateTransition(ProcessState.Paused, Command.Resume), ProcessState.Active }
};
}
public ProcessState GetNext(Command command)
{
StateTransition transition = new StateTransition(CurrentState, command);
ProcessState nextState;
if (!transitions.TryGetValue(transition, out nextState))
throw new Exception("Invalid transition: " + CurrentState + " -> " + command);
return nextState;
}
public ProcessState MoveNext(Command command)
{
CurrentState = GetNext(command);
return CurrentState;
}
}
public class Program
{
static void Main(string[] args)
{
Process p = new Process();
Console.WriteLine("Current State = " + p.CurrentState);
Console.WriteLine("Command.Begin: Current State = " + p.MoveNext(Command.Begin));
Console.WriteLine("Command.Pause: Current State = " + p.MoveNext(Command.Pause));
Console.WriteLine("Command.End: Current State = " + p.MoveNext(Command.End));
Console.WriteLine("Command.Exit: Current State = " + p.MoveNext(Command.Exit));
Console.ReadLine();
}
}
}