Search code examples
c#unity-game-engineorganization

Implement functions based on different game modes


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?


Solution

  • 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();
            }
        }
    }