I am creating a Finite State Machine for use in Unity for an enemy in a game.
All the states are made from an EmptyState.cs blueprint that defines the state exit, update, and start functions, and all states are derived from it.
In my FSM core, I have a dictionary reference to all states, and the current state;
private Dictionary<Type, EmptyState> amalgamStates;
public EmptyState currentState;
The dictionary, looks like this;
Dictionary<Type, EmptyState> states = new Dictionary<Type, EmptyState>
{
{typeof(RoamingState), new RoamingState() },
{typeof(IdleState), new IdleState() },
{typeof(StalkingState), new StalkingState() },
{typeof(HuntingState), new HuntingState() },
{typeof(HauntingState), new HauntingState() },
{typeof(LeavingState), new LeavingState() }
};
However, when I assign a state to currentState, it always sets as null.
private void Update()
{
if (currentState == null)
{
currentState = amalgamStates.Values.First(); **<--- here**
Debug.Log(currentState);
currentState.stateStart(amalgamBrain);
}
//Run state's update and store output (output will be next state)
var temp = currentState.stateUpdate(amalgamBrain);
//If the stored state is different -> switch states to new state
if (temp != currentState.GetType())
{
switchStates(temp);
}
}
Debug.Log(currentState); always returns null. I can't even set the variable manually in the unity inspector.
I know that this should work, as I am working from a project of a similar framework that works fine, so I'm just making a dumb mistake somewhere. Does anyone have any ideas? Here is the entirety of EmptyState.cs to anyone curious.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EmptyState : MonoBehaviour
{
public AmalgamCentralAI amalgamBrain;
public virtual void stateStart(AmalgamCentralAI amalgamBrain) { }
public virtual Type stateUpdate(AmalgamCentralAI amalgamBrain) { return null; }
public virtual void stateExit(AmalgamCentralAI amalgamBrain) { }
}
EDIT: Just some additional context;
the states dictionary is part of another script that is populating the one titled amalgamStates.
I will post the relevant scripts here in its entirety.
AmalgamCentralAI.cs
using System.Collections;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class AmalgamCentralAI : MonoBehaviour
{
private float stunHealth;
public bool playerInSight;
public Transform interuptedEvent;
public GameObject playerTracker;
public Transform[] leavingAreas;
private int tensionMeter;
public AmalgamFSM FSMLogic;
private void Start()
{
interuptedEvent = null;
FSMLogic = GetComponent<AmalgamFSM>();
Dictionary<Type, EmptyState> states = new Dictionary<Type, EmptyState>
{
{typeof(RoamingState), new RoamingState() },
{typeof(IdleState), new IdleState() },
{typeof(StalkingState), new StalkingState() },
{typeof(HuntingState), new HuntingState() },
{typeof(HauntingState), new HauntingState() },
{typeof(LeavingState), new LeavingState() }
};
Debug.Log(states);
FSMLogic.GrabAllStates(states);
}
private void Update()
{
trackPlayerVisability();
}
private void trackPlayerVisability()
{
RaycastHit hit;
if (Physics.Raycast(transform.position, playerTracker.transform.position, out hit, Mathf.Infinity))
{
if(hit.transform.tag == "Player")
{
playerInSight = true;
}
else
{
playerInSight = false;
}
}
}
}
AmalgamFSM.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class AmalgamFSM : MonoBehaviour
{
public AmalgamCentralAI amalgamBrain;
public Dictionary<Type, EmptyState> amalgamStates;
public EmptyState currentState;
private void Start()
{
//Fetch all Amalgam Commands
amalgamBrain = GetComponent<AmalgamCentralAI>();
//Start in Roaming State
//Debug.Log("state start");
}
private void Update()
{
if (currentState == null)
{
currentState = amalgamStates.Values.First();
Debug.Log(currentState);
currentState.stateStart(amalgamBrain);
}
//Run state's update and store output (output will be next state)
var temp = currentState.stateUpdate(amalgamBrain);
//If the stored state is different -> switch states to new state
if (temp != currentState.GetType())
{
switchStates(temp);
}
}
public void GrabAllStates(Dictionary<Type, EmptyState> states)
{
//Fetch dictionary of all states
amalgamStates = states;
}
public void switchStates(Type state)
{
//Run current state's exit, create reference to new state, run new state's start
currentState.stateExit(amalgamBrain);
currentState = amalgamStates[state];
currentState.stateStart(amalgamBrain);
}
}
RoamingState.cs
using System.Collections;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class RoamingState : EmptyState
{
private float SearchRange;
private Transform centerPoint;
private bool pointFound;
private NavMeshAgent agent;
public override void stateStart(AmalgamCentralAI amalgamBrain)
{
//Define search range
SearchRange = 100f;
//Amalgam serves as the center of the search area
//Debug.Log("fetching center point");
centerPoint = amalgamBrain.gameObject.transform;
agent = amalgamBrain.gameObject.GetComponent<NavMeshAgent>();
pointFound = false;
}
public override Type stateUpdate(AmalgamCentralAI amalgamBrain)
{
if(!pointFound)
{
//Find suitable Roam Position
Vector3 point;
if(RandomPointOnNav(centerPoint.position, SearchRange, out point))
{
//Set roam position in navmeshagent
agent.SetDestination(point);
pointFound = true;
}
}
// if the agent is close enough to the random point
else if(agent.remainingDistance <= agent.stoppingDistance && pointFound)
{
//idle
return typeof(IdleState);
}
//if agent is still looking for a point or is travelling to a point, remain in roaming state
return typeof(RoamingState);
}
public override void stateExit(AmalgamCentralAI amalgamBrain)
{
}
public bool RandomPointOnNav(Vector3 centerPoint, float range, out Vector3 resultPoint)
{
//Find a random point in a sphere of searchrange radius around the amalgam
Vector3 RandomSearchPoint = centerPoint + UnityEngine.Random.insideUnitSphere * range;
NavMeshHit hit;
// Can the amalgam travel to this point on the navmesh?
if (NavMesh.SamplePosition(RandomSearchPoint, out hit, 1.0f, NavMesh.AllAreas))
{
//return that the amalgam has found a suitable position
resultPoint = hit.position;
return true;
}
// if not remain still and continue searching on next update
resultPoint = Vector3.zero;
return false;
}
}
Just an additional point I found weird.
currentState = amalgamStates.Values.First(); **<--- here**
Debug.Log(currentState);
currentState.stateStart(amalgamBrain);
in the currentState.stateStart(amalgamBrain);, even though debug.log is returning null, it does run the stateStart of the First amalgamState, which is the RoamingState.
amalgamStatesis assigned or filled .. please post a minimal reproducible example