State Machine Example
An example of how the enemy state machine works in one of my projects.
public class EnemyStateMachine: MonoBehaviour, ISavable
{
[SerializeField] private SO_EnemyStateMachineInitializer enemyInitializer;
[SerializeField] private bool constPathfindUpdate = true;
[SerializeField] private float pathUpdateInterval = 0.5f;
[SerializeField] private DamageDeliverer damageDeliverer;
[SerializeField] private TextMeshProUGUI DEBUG_StateDisplay;
EnemyState curState;
EnemyStateMachineData data;
private Vector2 knockBack;
private bool transitionedThisFrame = false;
private UniqueID uniqueId;
private void Awake()
{
uniqueId = GetComponent<UniqueID>();
if (uniqueId != null)
{
RegisterISavable();
}
CreateFSMData();
}
private void Start()
{
if (data.group != null) data.group.AddToGroup(this);
curState = enemyInitializer.Initialize();
curState.StateInit(data);
curState.OnStateEnter();
StartCoroutine(PathFindUpdate());
}
private void CreateFSMData()
{
data = new EnemyStateMachineData
{
sprite = GetComponent<SpriteRenderer>(),
anim = GetComponent<Animator>(),
spriteFlash = GetComponent<SpriteFlashMaterialBlockForceWhite>(),
fsm = this,
transform = this.transform,
rb = GetComponent<Rigidbody2D>(),
health = GetComponent<Health>(),
group = GetComponentInParent<EnemyGroup>(),
dmgDeliverer = damageDeliverer,
pathSeeker = GetComponent<Seeker>(),
stats = GetComponent<EntityStats>(),
conditionTracker = GetComponent<EntityConditionTracker>(),
};
}
public void Transition(EnemyState newState)
{
curState.OnStateExit();
curState = newState;
curState.StateInit(data);
curState.OnStateEnter();
transitionedThisFrame = true;
if (DEBUG_StateDisplay) DEBUG_StateDisplay.text = newState.ToString();
}
private void Update()
{
curState.StateUpdate();
if (!transitionedThisFrame) curState.CheckStateTransition();
}
private void FixedUpdate()
{
curState.StateFixedUpdate();
}
private void LateUpdate()
{
curState.StateLateUpdate();
transitionedThisFrame = false;
}
private void OnTriggerEnter2D(Collider2D collision)
{
curState.StateOnTriggerEnter(collision);
}
private void OnCollisionEnter2D(Collision2D collision)
{
curState.StateOnCollisionEnter(collision);
}
private void OnTriggerStay2D(Collider2D collision)
{
curState.StateOnTriggerStay(collision);
}
private void OnCollisionStay2D(Collision2D collision)
{
curState.StateOnCollisionStay(collision);
}
private void OnTriggerExit2D(Collider2D collision)
{
curState.StateOnTriggerExit(collision);
}
private void OnCollisionExit2D(Collision2D collision)
{
curState.StateOnCollisionExit(collision);
}
public void DestroySelf()
{
if (uniqueId)
{
gameObject.SetActive(false);
}
else Destroy(gameObject);
}
public void OnDeath()
{
curState.OnDeath();
if (data.group != null) data.group.RemoveFromGroup(this);
}
public void Notify()
{
curState.Notify();
}
public void AddKnockBack(Vector2 add)
{
knockBack += add;
}
public void SetKnockBack(Vector2 set)
{
knockBack = set;
}
public Vector2 GetKnockBack() { return knockBack; }
public bool HasKnockBack()
{
return knockBack.sqrMagnitude != 0;
}
private IEnumerator PathFindUpdate()
{
while (true)
{
yield return new WaitForSeconds(pathUpdateInterval);
if (constPathfindUpdate)
{
curState.UpdatePath();
}
}
}
public void FSMAnimationEvent(FSMAnimationEventType type)
{
curState.StateAnimationEvent(type);
}
public string GetCurrentStateStringName()
{
return curState.ToString();
}
public void OnDestroy()
{
DeregisterISavable();
}
public void SetEnemyGroup(EnemyGroup group)
{
data.group = group;
}
public void RegisterISavable()
{
LevelSerializer.RegisterSavableObject(Save, Load);
}
public void DeregisterISavable()
{
LevelSerializer.DeregisterSavableObject(Save, Load);
}
public void Save(string path)
{
ES3.Save(uniqueId.ID, data.health.IsAlive(), path);
}
public void Load(string path)
{
bool isAlive = ES3.Load<bool>(uniqueId.ID, path, true);
if (!isAlive)
{
gameObject.SetActive(false);
if (data.group != null) data.group.RemoveFromGroup(this);
}
}
}
public abstract class EnemyState
{
public EnemyStateMachineData data;
public virtual void StateInit(EnemyStateMachineData data)
{
this.data = data;
}
public virtual void OnStateEnter()
{
}
public virtual void OnStateExit()
{
}
public virtual void StateUpdate()
{
}
public virtual void StateFixedUpdate()
{
}
public virtual void StateLateUpdate()
{
}
public abstract void CheckStateTransition();
public virtual void StateOnCollisionEnter(Collision2D collision)
{
}
public virtual void StateOnCollisionExit(Collision2D collision)
{
}
public virtual void StateOnCollisionStay(Collision2D collision)
{
}
public virtual void StateOnTriggerEnter(Collider2D collision)
{
}
public virtual void StateOnTriggerExit(Collider2D collision)
{
}
public virtual void StateOnTriggerStay(Collider2D collision)
{
}
public virtual void UpdatePath()
{
}
public virtual void OnDeath()
{
}
public virtual void Notify()
{
}
public virtual void StateAnimationEvent(FSMAnimationEventType type)
{
}
}
public class GoblinEnemyAttackState : EnemyState
{
public override void CheckStateTransition()
{
if (data.fsm.HasKnockBack()) data.fsm.Transition(new GoblinEnemyKnockBackState());
}
public override void StateAnimationEvent(FSMAnimationEventType type)
{
base.StateAnimationEvent(type);
if (type == FSMAnimationEventType.AnimationEnd)
{
data.fsm.Transition(new GoblinEnemyRetreatState());
}
}
public override void OnStateEnter()
{
base.OnStateEnter();
data.dmgDeliverer.SetAttackLocation((PlayerManager.instance.GetPlayer().position - data.transform.position).normalized);
data.anim.Play("GoblinAttack");
data.rb.linearVelocity = Vector2.zero;
}
public override void OnDeath()
{
base.OnDeath();
data.fsm.Transition(new GoblinEnemyDeathState());
}
public override string ToString()
{
return "Attack";
}
}
This code is responsible for coordinating multiple coordinates together to help enable the enemy state to run correctly. This involves initializing enemy states, handling state transitions, propagating events and messages down to states, and handling other aspects such as saving/loading.
This is what the base enemy state looks like. All other enemy states derive from this one. And implement their behaviour based on this classes template. These all mostly culminate into being reactions for events given by the finite state machine.
This is an example of what an implemented enemy state looks like. This state is responsible for the enemy attack sequence. It does some transition checks like, if knockback was applied, awaits for an animation event, make sure the correct animation is playing, prevents motion from happening, and watches out for if the enemy has died to make a transition to the death state. This is a more basic state. But shows the general use case of this system.