Finite State Machine for Turn-Based Games

Published September 20, 2022
Advertisement

Lately I've been working on a computerized version of GasLands just for fun. Gaslands is a turn-based table-top miniatures game where you modify toy cars to look like Mad Max vehicles. The goals of a game changes based on the scenario, but in general you move around with special Maneuver Templates and shoot each other's vehicles with various weapons. Check out the Table-Top game here: https://gaslands.com/

I got the basics of vehicle movement working along the maneuver templates with enums and switch statements. Changing which team's turn it was, selecting vehicles, selecting templates, executing a maneuver, etc. It worked (technically) but extending the code to do more and debugging existing logic was a lot tougher than it should have been. I was going to put a sample of the bad code, but just showing a method or two doesn't really show the problem. And showing the whole thing I feel takes up too much room.

The code I had might be good enough for a less complex project, but the turn structure for Gaslands has quite a few steps. Each Round is split into six Gear Phases. Those are split into several Vehicle Activations. Each Vehicle Activation has three main steps: Movement, Shooting, Wipeout. And each of those steps have their own sequence of sub steps. With the code complexity being annoying just at the Movement step, I knew I'd need a more elegant solution.

Enter Finite State Machines…

A Finite State Machine (FSM) is a math/programming construct that has several defined states (A finite number of them). And the "machine" behaves differently based on which distinct state it is in. There are many programming problems that lend themselves to a FSM solution: Character Controllers, AI, and turn-based game logic. FSM's are very powerful especially considering they don't take much code to get started.

/// <summary>
/// This interface defines the methods for a distinct state of the game.
/// </summary>
public interface IGameState
{
    /// <summary>
    /// The Enter method gets called when this game state is entered. Initialization
    /// code, or code that runs once should go here.
    /// </summary>
    void Enter();

    /// <summary>
    /// The Update method gets called once per frame so long as the game is in this state.
    /// Code that makes decisions for the AI, or code that polls for user input should
    /// go here.
    /// </summary>
    void Update();

    /// <summary>
    /// The exit method gets calle when this game state is exited. Clean up/tear down code 
    /// should go here.
    /// </summary>
    void Exit();
}


/// <summary>
/// Abstract game state class that subscribes to messages when the state is entered, and
/// unsubscribes from the messages when we exit this state.
/// </summary>
public abstract class GameStateWithMessages : IGameState
{
    /// <summary>
    /// Constructor that takes the messageSystem.
    /// </summary>
    /// <param name="messageSystem">The MessageSystem for the game.</param>
    public GameStateWithMessages(MessageSystem messageSystem)
    {
        MessageSystem = messageSystem;
    }

    /// <summary>
    /// The MessageSystem we were constructed with. 
    /// </summary>
    protected MessageSystem MessageSystem { get; set; }

    /// <summary>
    /// When we enter this state, subscribe to our messages.
    /// </summary>
    public virtual void Enter()
    {
        SubscribeToMessages(shouldSubscribe: true);
    }

    /// <summary>
    /// Nothing to do for Update yet, so leave it as an abstract method.
    /// </summary>
    public abstract void Update();

    /// <summary>
    /// When we exit this state, we unsubscribe from our messages
    /// </summary>
    public virtual void Exit()
    {
        SubscribeToMessages(shouldSubscribe: false);
    }

    /// <summary>
    /// Implementers of this class need to define all the messages they subscribe to. When
    /// shouldSubscribe is true, we'll subscribe to the messages. When shouldSubscribe is 
    /// false, we'll unsubscribe from those same messages.
    /// </summary>
    /// <remarks>
    /// I prefer implementing a method like this rather than separate Subscribe/Unsubscrie
    /// methods.The reason why is if they were separate methods, sometimes you'd add a subscription
    /// into the Subscribe method but forget to add the same line to the Unsubscribe method. Implementing
    /// with one method, you're guaranteed to subscribe/unsubscribe ALL of your messages.
    /// </remarks>
    /// <param name="shouldSubscribe">Whether or not we should subscribe to the messages.</param>
    public abstract void SubscribeToMessages(bool shouldSubscribe);
}


using System;
using UnityEngine;


/// <summary>
/// This class manages several distinct states and encapsulates the logic
/// of changing between them. For this project's purposes, almost all my FSM's are
/// sequences, so I baked the "OnComplete" concept into the FSM class which
/// might not be necessary for other people's purposes.
/// </summary>
public abstract class FiniteStateMachine : GameStateWithMessages
{
    /// <summary>
    /// Constructor takes the message system for the game.
    /// </summary>
    /// <param name="messageSystem">The message system for the game.</param>
    public FiniteStateMachine(MessageSystem messageSystem) : base(messageSystem)
    {
        // TODO/CodeReview - Maybe also take the GameController? 
        // I wasn't sure if I wanted my State Machine or GameStates directly accessing the game logic. 
        // Example - the Wipeout step needs a list of vehicles that needs to wipe out. How should it get it?
    }

    /// <summary>
    /// The current state that this state machine is in. Can be null when we aren't in 
    /// a state yet, or when we've completed out state machine.
    /// </summary>
    public IGameState CurrentState { get; protected set; }

    /// <summary>
    /// This delegate is called when this FiniteStateMachine has completed its last step
    /// in a sequence.
    /// </summary>
    public Action OnComplete;

    /// <summary>
    /// This FiniteStateMachine can also be a State for a containing FSM. When this
    /// FSM is "entered", we will make sure someone is listening to the OnComplete
    /// delegate. 
    /// </summary>
    public override void Enter()
    {
        base.Enter();

        // If we nobody is listening for our OnComplete when we activate...
        if (OnComplete == null)
        {
            // Then we should log an error.
            Debug.LogError($"FiniteStateMachine[{GetType().Name}] - No one is listening to our OnComplete callback.");
        }
    }

    /// <summary>
    /// Update is intended to be called once per frame. And the CurrentState's update method
    /// is called by us. This is not a MonoBehaviour, so a containing MonoBehaviour should
    /// call this method.
    /// </summary>
    public override void Update()
    {
        CurrentState?.Update();
    }

    /// <summary>
    /// Changing State will exit the CurrentState, set the new state, and then Enter the new state.
    /// Null is a valid state to change to.
    /// </summary>
    /// <param name="newState">The new state we're changing into. Null is valid.</param>
    public virtual void ChangeState(IGameState newState)
    {
        // Exit the current state
        CurrentState?.Exit();

        // Set the new state and enter it.
        CurrentState = newState;
        CurrentState?.Enter();
    }

    /// <summary>
    /// When this FiniteStateMachine runs it's course, we'll exit our current state and 
    /// send our complete message.
    /// </summary>
    public virtual void Complete()
    {
        ChangeState(null);

        OnComplete?.Invoke();
    }
}
  • Message System - (not shown here) - lets people subscribe/unsubscribe to messages of a particular type, and lets us send messages to anyone who is listening.
  • Game States - Are very dumb. They should only know about themselves. They do not communicate directly to the containing FSM. They communicate to the outside world by sending messages and waiting for a response (either from another message, or an OnComplete that gets called)
  • Finite StateMachines - push required data down into the necessary GameStates. Won't directly call logic methods of individual states. Some logic will exist in FSM's to determing which state to go to next based on teh results of steps. They will be responsible for changing states when necessary.

Instead of rewriting my existing Turn-Based logic, I decided to implement a new bit of game state to prove out the system in the “Wipeout Step”.

State diagram of the Wipeout Step for Gaslands
  • Wipeout Step (outer purple box) - is both a State AND a FSM. The FSM contains two states - Check Vehicles for Wipeout and Wipeout Activation. External game logic will call into us to perform the Wipeout Step of our turn-based logic.
  • Check Vehicles for Wipeout - Checks to see if any vehicles need to wipe out. Yes? - perform a Wipeout Activation. No? - Tell our Wipeout Step that we're done.
  • WipeoutActivation (inner orange box) - is both a State AND a FSM.

I set to work coding this up. In general I liked my implementation of the FiniteStateMachine and IGameState classes, but for my first Wipeout implementation, I think I relied too heavily on messages. Messages triggered states, and signified state completion. This lead to two messages for each state, plus extra messages for communicating outside of the state management. It was very cluttered and time consuming to add new states. I didn't like that.

So I refactored things a second time, and now I'm happy with how the Wipeout Step is coded. The Finite State Machine decides which state to move into. Most are just sequences so it's one state after another. Instead of messages to signify work is complete, States, StateMachines, and even some Messages have an OnComplete delegate that gets called when their tasks are complete. This made the project much cleaner and I didn't have a ton of single-use message classes laying around. It also helped me identify a few different base classes for states and messages to encapsulate some common functionality:

  • MessageWithCallback - A message with an OnWorkComplete delegate
  • GameStateStep - A GameState with an OnComplete delegate
  • TimedStep - A GameStateStep that completes after a time delay
  • SendMessageStep - A GameStateStep that sends a message and completes in the next frame
  • SendMessageWithResponseStep - A SendMessageStep that sends a MessageWithCallback. The step completes when the MessageWithCallback's OnComplete delegate is invoked.

Here's what my current code looks like. I left some TODO/CodeReview comments in place to call out different decisions I'm still not sure about:

using Assets.Scripts;
using Assets.Scripts.GameState;
using System.Collections.Generic;


/// <summary>
/// This FiniteStateMachine manages all the states for the WipeoutStep. When entered, we 
/// switch states to the CheckForWipeoutState. That state checks for vehicles that need to wipeout 
/// and then perform wipeout activations for each one. Once we run out of vehicles that need to wipeout, we 
/// call our OnComplete delegate.
/// </summary>
/// <seealso cref="WipeoutStepCompleteMessage"/>
public class WipeoutStepStateMachine2 : FiniteStateMachine
{
    /// <summary>
    /// Constructor takes the game controller and message system.
    /// </summary>
    /// <param name="gameController">The game controller</param>
    /// <param name="messageSystem">The message system</param>
    public WipeoutStepStateMachine2(GameController gameController, MessageSystem messageSystem): base (messageSystem)
    {
        this.gameController = gameController;

        wipeoutVehicleList = new List<Vehicle>();
        checkForWipeoutsState = new CheckForWipeoutsState2(gameController, messageSystem, wipeoutVehicleList);
        wipeoutActivationStateMachine = new WipeoutActivationStateMachine2(gameController, messageSystem);
    }

    /// <summary>
    /// The game controller
    /// </summary>
    protected GameController gameController;

    /// <summary>
    /// List of vehicles that gets populated by the checkForWipeoutsState
    /// </summary>
    protected List<Vehicle> wipeoutVehicleList;

    /// <summary>
    /// The check for Wipeouts State
    /// </summary>
    protected CheckForWipeoutsState2 checkForWipeoutsState;

    /// <summary>
    /// The Wipeout Activation state (and state machine).
    /// </summary>
    protected WipeoutActivationStateMachine2 wipeoutActivationStateMachine;


    /// <inheritdoc/>
    public override void Enter()
    {
        base.Enter();

        // When the WipeoutStepStateMachine is entered, check to see if we have any vehicles to wipeout
        checkForWipeoutsState.OnComplete = HandleCheckForWipeoutsStateComplete;
        ChangeState(checkForWipeoutsState);
    }

    /// <inheritdoc/>
    public override void Update()
    {
        base.Update();
    }

    /// <inheritdoc/>
    public override void Exit()
    {
        base.Exit();
    }

    /// <inheritdoc/>
    public override void SubscribeToMessages(bool shouldSubscribe)
    {
        // TODO/CodeReview - We were listening to our CheckForWipeoutState2 to send one of two messages. 
        // Instead, I wait for its OnComplete delegate and look at result data inside of CheckForWipeoutState2.
        // I'm not sure which approach is better, but I'm leaning towards the OnComplete delegate. My first attempt at this
        // had messages be the sole mode of communication between States and StateMachines, but the project was getting REALLY cluttered
        // with all the various messages. It started feeling over-engineered and difficult to work in. I'm leaving this comment until
        // after I write my developer journal to show an alternate way of doing things.

        //MessageSystem.SubscribeToMessage(typeof(NoMoreWipeoutsMessage), HandleNoMoreWipeoutsMessage, shouldSubscribe);
        //MessageSystem.SubscribeToMessage(typeof(PerformWipeoutActivationMessage), HandlePerformWipeoutActivationMessage, shouldSubscribe);
    }

    /// <summary>
    /// When CheckForWipeoutsState is complete, we see if we have a vehicle to wipeout. If so, start
    /// a WipeoutActivation. If not, we're done with the WipeoutStep.
    /// </summary>
    protected void HandleCheckForWipeoutsStateComplete()
    {
        if(checkForWipeoutsState.WipeoutVehicle != null)
        {
            wipeoutActivationStateMachine.WipeoutVehicle = checkForWipeoutsState.WipeoutVehicle;
            // TODO/CodeReview - We're just setting with = instead of += and clearing it out when we're done. I'm a bit paranoid about dangling delegates holding onto memory. Probalby not worth worrying about.
            wipeoutActivationStateMachine.OnComplete = HandleWipeoutActivationComplete;
            ChangeState(wipeoutActivationStateMachine);
        }
        else
        {
            // Else there are no more vehicles to wipeout, we're done.
            Complete();
        }
    }

    /// <summary>
    /// This method gets called when the Wipeout Activation is complete. When that
    /// happens, we head back to the checkForWipeoutState.
    /// </summary>
    /// <param name="message">The WipeoutActivationcompleteMessage</param>
    protected void HandleWipeoutActivationComplete()
    {
        // TODO/CodeReview - Clear this out? We set it above, and clear it out here just to make sure GC has an easier time reclaiming us.
        wipeoutActivationStateMachine.OnComplete = null;

        // TODO/CodeReview - One drawback is we have to set it before entering the state each time instead of just once at construction so I'm not sure about the tradeoff.
        checkForWipeoutsState.OnComplete = HandleCheckForWipeoutsStateComplete;
        // Once a wipeout activation is complete, we need to check for more wipeouts.
        ChangeState(checkForWipeoutsState);
    }
}




using System.Collections.Generic;
using UnityEngine;

namespace Assets.Scripts.GameState
{
    /// <summary>
    /// A simple state to check for wipeouts.
    /// </summary>
    public class CheckForWipeoutsState2 : GameStateStep
    {
        /// <summary>
        /// Constructor
        /// </summary>
        public CheckForWipeoutsState2(GameController gameController, MessageSystem messageSystem, List<Vehicle> wipeoutVehicleList) : base(messageSystem)
        {
            this.gameController = gameController;
            this.wipeoutVehicleList = wipeoutVehicleList;
        }

        protected GameController gameController;

        protected List<Vehicle> wipeoutVehicleList;


        /// <summary>
        /// This property is set to the vehicle that needs to wipe out. If no vehicles
        /// need to wipe out, this will be set to null.
        /// </summary>
        public Vehicle WipeoutVehicle { get; protected set; }

        /// <inheritdoc/>
        public override void Enter()
        {
            CheckForWipeouts();

            base.Enter();
        }

        /// <inheritdoc/>
        public override void SubscribeToMessages(bool shouldSubscribe)
        {
        }

        protected void CheckForWipeouts()
        {
            // Get a list of vehicles that need to wipeout.
            wipeoutVehicleList.Clear();
            gameController.GetVehicles(IsVehicleWipingOut, wipeoutVehicleList);

            Debug.Log($"CheckForWipeouts - [{wipeoutVehicleList.Count }] vehicles need to wipe out");

            // If we have any vehicles that need to wipeout, pick the first one.
            if (wipeoutVehicleList.Count > 0)
            {
                WipeoutVehicle = wipeoutVehicleList[0];
            }
            else
            {
                WipeoutVehicle = null;
            }
        }

        /// <summary>
        /// Returns true when the vehicle is wiping out. We pass this in as a delegate to GameController's GetVehicles method
        /// </summary>
        /// <param name="vehicle">The vehicle we want to check for wipeout</param>
        /// <returns>True when the vehicle is wiping out.</returns>
        protected bool IsVehicleWipingOut(Vehicle vehicle)
        {
            bool isVehiclesWipingOut = vehicle.IsWipingOut;
            return isVehiclesWipingOut;
        }
    }
}




using Assets.Scripts;
using Assets.Scripts.GameState;
using Assets.Scripts.Messaging;
using UnityEngine;


/// <summary>
/// The wipeout activation state machine handles all the states for a single wipeout activation.
/// The vehicle to wipeout is set by the WipeoutStepStateMachine and then we run through the 
/// activation sequence one step at a time.
/// </summary>
public class WipeoutActivationStateMachine2 : FiniteStateMachine
{
    /// <summary>
    /// Constructor
    /// </summary>
    public WipeoutActivationStateMachine2(GameController gameController, MessageSystem messageSystem) : base (messageSystem)
    {
        this.gameController = gameController;

        startWipeoutState = new StartWipeoutState2(messageSystem);
        showFlipCheckUIState = new ShowFlipCheckUIState2(messageSystem, 1f);
        rollFlipCheckState = new RollFlipCheckState2(messageSystem);
        triggerFlipActivationState = new TriggerFlipActivationState(messageSystem);
        triggerWipeoutResetState = new TriggerWipeoutResetState(messageSystem, 2f);
        loseControlState = new LoseControlState(messageSystem);
    }

    /// <summary>
    /// The game controller
    /// </summary>
    protected GameController gameController;

    /// <summary>
    /// The vehicle that's wiping out.
    /// </summary>
    public Vehicle WipeoutVehicle { get; set; }

    // The various states of a wipe out activation
    protected StartWipeoutState2 startWipeoutState = null;
    protected ShowFlipCheckUIState2 showFlipCheckUIState = null;
    protected RollFlipCheckState2 rollFlipCheckState = null;
    protected TriggerFlipActivationState triggerFlipActivationState = null;
    protected TriggerWipeoutResetState triggerWipeoutResetState = null;
    protected LoseControlState loseControlState = null;

    Team clockwiseTeam = null;

    /// <inheritdoc/>
    public override void Enter()
    {
        base.Enter();

        // When we enter a WipeoutActivation we start with the StartWipeoutState
        clockwiseTeam = gameController.GetClockwiseTeam(WipeoutVehicle);
        startWipeoutState.wipeoutVehicle = WipeoutVehicle;
        startWipeoutState.OnComplete = OnStartWipeoutComplete;
        ChangeState(startWipeoutState);
    }

    /// <inheritdoc/>
    public override void Update()
    {
        base.Update();
    }

    /// <inheritdoc/>
    public override void Exit()
    {
        base.Exit();
    }

    /// <inheritdoc/>
    public override void SubscribeToMessages(bool shouldSubscribe)
    {
    }

    protected void OnStartWipeoutComplete()
    {
        // Wipeoutstate started, so show the flipcheck UI
        showFlipCheckUIState.wipeoutVehicle = WipeoutVehicle;
        showFlipCheckUIState.OnComplete = OnShowflipCheckUIStateComplete;
        ChangeState(showFlipCheckUIState);
    }

    protected void OnShowflipCheckUIStateComplete()
    {
        // Showed the flip check UI, so we can roll the flip check.
        rollFlipCheckState.wipeoutVehicle = WipeoutVehicle;
        rollFlipCheckState.OnComplete = OnRollFlipCheckStateComplete;
        ChangeState(rollFlipCheckState);
    }

    protected void OnRollFlipCheckStateComplete()
    {
        // If we failed our FlipCheck, we need to perform a flip activation
        if(!rollFlipCheckState.FlipCheckResult.Passed)
        {
            triggerFlipActivationState.wipeoutVehicle = WipeoutVehicle;
            triggerFlipActivationState.OnComplete = HandleFlipActivationComplete;
            ChangeState(triggerFlipActivationState);
        }
        // Otherwise, we can skip straight to the WipeoutReset step.
        else
        {
            triggerWipeoutResetState.wipeoutVehicle = WipeoutVehicle;
            triggerWipeoutResetState.OnComplete = HandleTriggerWipeoutResetComplete;
            ChangeState(triggerWipeoutResetState);
        }
    }

    protected void HandleFlipActivationComplete()
    {
        // Since flip activation is finished, we can go the the Reset step
        triggerWipeoutResetState.wipeoutVehicle = WipeoutVehicle;
        triggerWipeoutResetState.OnComplete = HandleTriggerWipeoutResetComplete;
        ChangeState(triggerWipeoutResetState);
    }

    protected void HandleTriggerWipeoutResetComplete()
    { 
        // Completed our Reset, we can move to the LoseControl step
        loseControlState.wipeoutVehicle = WipeoutVehicle;
        loseControlState.controllingTeamName = clockwiseTeam.teamName;
        loseControlState.OnComplete = HandleLoseControlComplete;
        ChangeState(loseControlState);
    }

    protected void HandleLoseControlComplete()
    {
        // Completed our LoseControl step, we can complete this WipeoutActivation
        Debug.Log("Wipeout Activation complete.");
        Complete();
    }
}



namespace Assets.Scripts.GameState
{
    /// <summary>
    /// This state starts the wipeout activation - Tells the game to focus on the wipeout vehicle
    /// and waits for that to finish beforre completing this step.
    /// </summary>
    public class StartWipeoutState2 : SendMessageWithResponseStep<FocusCameraOnVehicleMessage2>
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="gameController">The gameController</param>
        /// <param name="messageSystem">The messageSystem</param>
        /// <param name="wipeoutVehicleList">The wipeoutVehicleList</param>
        public StartWipeoutState2(MessageSystem messageSystem) : base(messageSystem)
        {
        }

        /// <summary>
        /// A reference to the wipeoutVehicle that we're wiping out
        /// </summary>
        public Vehicle wipeoutVehicle;

        public override FocusCameraOnVehicleMessage2 GetMessage()
        {
            FocusCameraOnVehicleMessage2 focusCameraOnVehicleMessage = new FocusCameraOnVehicleMessage2(wipeoutVehicle, HandleWorkCompleted);
            return focusCameraOnVehicleMessage;
        }
    }
}


namespace Assets.Scripts.Messaging
{
    /// <summary>
    /// This message gets sent to tell the camera to focus on the wipeout vehicle
    /// </summary>
    public class FocusCameraOnVehicleMessage2 : MessageWithCallback
    {
        /// <summary>
        /// Constructor takes the vehicle that we're focusing on
        /// </summary>
        /// <param name="focusVehicle">The vehicle that we're focusing on</param>
        /// <param name="workCompleteDelegate">The delegate we need to fire when we're done.</param>
        public FocusCameraOnVehicleMessage2(Vehicle focusVehicle, WorkCompleteDelegate workCompleteDelegate) : base(workCompleteDelegate)
        {
            FocusVehicle = focusVehicle;
        }

        /// <summary>
        /// The vehicle that needs to wipeout
        /// </summary>
        public Vehicle FocusVehicle { get; protected set; }
    }
}


namespace Assets.Scripts.GameState
{
    /// <summary>
    /// Shows the FlipCheck panel for a short time and then completes
    /// </summary>
    public class ShowFlipCheckUIState2 : TimedStep
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="messageSystem">The messageSystem</param>
        public ShowFlipCheckUIState2(MessageSystem messageSystem, float delayTimeLimit) : base(messageSystem, delayTimeLimit)
        {
        }

        /// <summary>
        /// A reference to the wipeoutVehicle that we're wiping out
        /// </summary>
        public Vehicle wipeoutVehicle;

        /// <inheritdoc/>
        public override void Enter()
        {
            base.Enter();

            ShowFlipCheckUIMessage2 showFlipCheckUIMessage = new ShowFlipCheckUIMessage2(wipeoutVehicle);
            MessageSystem.EnqueueMessage(showFlipCheckUIMessage);
        }

        public override void SubscribeToMessages(bool shouldSubscribe)
        {
        }
    }
}


namespace Assets.Scripts.GameState
{
    /// <summary>
    /// Rolls a flip check and shows the result for a short time before completing 
    /// </summary>
    public class RollFlipCheckState2 : SendMessageWithResponseStep<RollFlipCheckMessage2>
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="messageSystem">The messageSystem</param>
        public RollFlipCheckState2(MessageSystem messageSystem) : base(messageSystem)
        {
        }

        /// <summary>
        /// A reference to the wipeoutVehicle that we're wiping out
        /// </summary>
        public Vehicle wipeoutVehicle;


        public FlipCheckResult FlipCheckResult { get; protected set; }

        public override void Enter()
        {
            FlipCheckResult = null;

            base.Enter();
        }

        public override RollFlipCheckMessage2 GetMessage()
        {
            RollFlipCheckMessage2 rollFlipCheckMessage = new RollFlipCheckMessage2(wipeoutVehicle, HandleWorkCompleted);
            FlipCheckResult = rollFlipCheckMessage.flipCheckResult;
            return rollFlipCheckMessage;
        }

        protected override void HandleWorkCompleted(IMessage message)
        {
            // Get the result from the roll check message
            RollFlipCheckMessage2 rollFlipCheckMessage = message as RollFlipCheckMessage2;
            FlipCheckResult = rollFlipCheckMessage.flipCheckResult;

            base.HandleWorkCompleted(message);
        }
    }
}

namespace Assets.Scripts.Messaging
{
    public class RollFlipCheckMessage2 : MessageWithCallback
    {
        public RollFlipCheckMessage2(Vehicle flipCheckVehicle, WorkCompleteDelegate workCompleteDelegate) : base(workCompleteDelegate)
        {
            FlipCheckVehicle = flipCheckVehicle;
        }

        public Vehicle FlipCheckVehicle { get; protected set; }

        public bool FlipCheckPassed { get { return flipCheckResult.Passed; } }

        public FlipCheckResult flipCheckResult;
    }
}

namespace Assets.Scripts.Messaging
{
    public class ShowFlipCheckResultMessage : MessageWithCallback
    {
        public ShowFlipCheckResultMessage(FlipCheckResult flipCheckResult, WorkCompleteDelegate workCompleteDelegate) : base(workCompleteDelegate)
        {
            FlipCheckResult = flipCheckResult;
        }

        public FlipCheckResult FlipCheckResult { get; protected set; }
    }
}


namespace Assets.Scripts.GameState
{
    /// <summary>
    /// This state resets the vehicle's data. In the future, I'm probably going to make this 
    /// it's own FSM or just split it into a few states to support the experimental wipeout rules 
    /// and let the user choose what gear to drop down to. Current implementation just resets
    /// Gear to 1 and Hazards to 0
    /// </summary>
    public class TriggerWipeoutResetState : TimedStep
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="messageSystem">The messageSystem</param>
        public TriggerWipeoutResetState(MessageSystem messageSystem, float delayTimeLimit) : base(messageSystem, delayTimeLimit)
        {
        }

        /// <summary>
        /// A reference to the wipeoutVehicle that we're wiping out
        /// </summary>
        public Vehicle wipeoutVehicle;

        /// <inheritdoc/>
        public override void Enter()
        {
            base.Enter();

            wipeoutVehicle.WipeoutReset();
        }

        public override void SubscribeToMessages(bool shouldSubscribe)
        {
        }
    }
}

namespace Assets.Scripts.GameState
{
    /// <summary>
    /// This state triggers the flip activation - moving the wipeout vehicle a Medium Straight template forward.
    /// </summary>
    public class TriggerFlipActivationState : SendMessageWithResponseStep<TriggerFlipActivationMessage>
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="messageSystem">The messageSystem</param>
        public TriggerFlipActivationState(MessageSystem messageSystem) : base(messageSystem)
        {
        }

        /// <summary>
        /// A reference to the wipeoutVehicle that we're wiping out
        /// </summary>
        public Vehicle wipeoutVehicle;


        public override TriggerFlipActivationMessage GetMessage()
        {
            TriggerFlipActivationMessage triggerFlipActivationMessage = new TriggerFlipActivationMessage(wipeoutVehicle, HandleWorkCompleted);
            return triggerFlipActivationMessage;
        }
    }
}

namespace Assets.Scripts.Messaging
{
    public class TriggerFlipActivationMessage : MessageWithCallback
    {
        public TriggerFlipActivationMessage(Vehicle wipeoutVehicle, WorkCompleteDelegate workCompleteDelegate) : base(workCompleteDelegate)
        {
            WipeoutVehicle = wipeoutVehicle;
        }

        public Vehicle WipeoutVehicle { get; protected set; }
    }
}


/// <summary>
/// This state triggers when the vehicle loses control. It lets the next opponent
/// pick a facing for the wiping out vehicle.
/// </summary>
public class LoseControlState : SendMessageWithResponseStep<ShowLoseControlPanelMessage>
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="messageSystem">The messageSystem</param>
    public LoseControlState(MessageSystem messageSystem) : base(messageSystem)
    {
    }

    /// <summary>
    /// A reference to the wipeoutVehicle that we're wiping out
    /// </summary>
    public Vehicle wipeoutVehicle;

    public string controllingTeamName;

    public override void Enter()
    {
        base.Enter();
    }

    public override void Update()
    {
        base.Update();

        if(Input.GetKey(KeyCode.J))
        {
            wipeoutVehicle.Rotate(right:false);
        }

        if (Input.GetKey(KeyCode.K))
        {
            wipeoutVehicle.Rotate(right: true);
        }

        if (Input.GetKeyDown(KeyCode.Space) || Input.GetKeyDown(KeyCode.Return))
        {
            ConfirmLoseControl();
        }
    }

    public override ShowLoseControlPanelMessage GetMessage()
    {
        ShowLoseControlPanelMessage showLoseControlPanelMessage = new ShowLoseControlPanelMessage(wipeoutVehicle, controllingTeamName, HandleWorkCompleted);
        return showLoseControlPanelMessage;
    }

    protected void ConfirmLoseControl()
    {
        ConfirmLoseControlMessage confirmLoseControlMessage = new ConfirmLoseControlMessage();
        MessageSystem.EnqueueMessage(confirmLoseControlMessage);
    }

    protected override void HandleWorkCompleted(IMessage message)
    {
        base.HandleWorkCompleted(message);
    }
}

It's a decent chunk of code, but it's all pretty simple. And here's what it looks like in action.

Animated gif of a Motorcycle Wipeout in Gaslands

Next Steps

Now that I'm starting to get some more robust systems in place, I'm motivated to code up some more bits of the turn structure. When I do, I'll add some comments here on whether or not I'm still happy with my current implementation. And now that I have a MessageSystem in place, I think I might code up my sound manager.

Tips from your Uncle Eck

  1. Don't keep working in janky systems. Refactor your code to something better. - It's easy to fall into the trap of “not having time” to clean up your code. But it saves so much time in the long run. A good refactor makes implementing and debugging features much easier.
  2. One refactor may not be enough - Sometimes you'll take the time to clean up your code, but after using your new and “improved” code for a bit you find out there's still some rough edges. Take another stab at it.
2 likes 1 comments

Comments

Eck

I finally got back to this after about a year break. The code for the actual state machine and base classes for the steps I was very happy with. How I was using it though led to some difficult debugging sessions. Take a look at my next blog post here: https://www.gamedev.net/blogs/entry/2276789-finite-state-machine-refactor-take-2/​ where I go into more details.

November 04, 2023 09:17 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement