Search code examples
c#unity-game-engineinheritancesubclassgame-development

Creating abstract enemy state machines in Unity3D


I'm trying to implement a reusable finite state machine for enemy behaviors in my game and am struggling to figure out how to pass derived classes as parameters. Below are the two base template classes I have for the different States that enemies can have as well as the State Managers that act as the context in the finite state machine implementation. The State Manager scripts are attached to the enemy game objects and should contain common behaviors and data used by all enemies.

public class EnemyState : MonoBehaviour
{
    protected float stateStartTime;

    public virtual void EnterState(EnemyStateManager enemy)
    {
        stateStartTime = Time.time;
    }

    public virtual void ExitState(EnemyStateManager enemy) {}

    public virtual void UpdateState(EnemyStateManager enemy) {}
}
public class EnemyStateManager : MonoBehaviour
{
    protected EnemyState currentState;
    public float health;

    public virtual void Start() {}

    void Update()
    {
        currentState.UpdateState(this);
    }

    public void SwitchState(EnemyState newState)
    {
        currentState.ExitState(this);
        currentState = newState;
        currentState.EnterState(this);
    }

    public virtual void Attack() {}

    public virtual void Die() {}

    public virtual bool CheckWall() {}

    public virtual bool CheckLedge() {}

    public void SetVelocity(float speed) {}
}

My intention is to then create concrete classes that derive from the above classes to speed up designing different enemy AI behavior so that I don't have to rewrite common enemy logic that is already contained in the base template and can be reused between different types of enemies.

public class SoldierStateManager : EnemyStateManager
{
    public SoldierIdleState IdleState = new SoldierIdleState();
    public SoldierDeadState UnconsciousState = new SoldierUnconsciousState();

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

        currentState = IdleState;
        currentState.EnterState(this);
    }
}
public class SoldierIdleState : EnemyState
{
    float moveSpeed = 2f;

    public override void EnterState(SoldierStateManager soldier) 
    {
        base.EnterState(soldier);
        soldier.SetVelocity(moveSpeed);
    }

    public override void ExitState(SoldierStateManager soldier) 
    {
        base.ExitState(soldier);
    }

    public override void UpdateState(SoldierStateManager soldier) 
    {
        base.UpdateState(soldier);
        if (soldier.health <= 0)
        {
            soldier.SwitchState(soldier.UnconsciousState);
        }
    }
}
public class SoldierUnconsciousState : EnemyState
{
    float moveSpeed = 0f;
    float unconsciousDuration = 5f;

    public override void EnterState(SoldierStateManager soldier) 
    {
        base.EnterState(soldier);
        soldier.SetVelocity(moveSpeed);
    }

    public override void ExitState(SoldierStateManager soldier) 
    {
        base.ExitState(soldier);
    }

    public override void UpdateState(SoldierStateManager soldier) 
    {
        base.UpdateState(soldier);
        if (Time.time >= stateStartTime + unconsciousDuration)
        {
            soldier.SwitchState(soldier.IdleState);
        }
    }
}

I'm stuck because when I try to implement the idea above, I get errors in the concrete State classes such as: 'SoldierIdleState.EnterState(SoldierStateManager)': no suitable method found to override in all the methods I attempt to override from the base class, I'm guessing because the method signature of EnterState() is expecting type EnemyStateManager and does not accept SoldierStateManager even though it is derived from EnemyStateManager.

Is there any way I can pass EnemyStateManager subclasses into the derived methods as I'm pretty new to C# and inheritance so any guidance on how I can make this work is appreciated.


Solution

  • I eventually got what I was looking for by removing the need to pass the state managers as parameters and instead passing them through the constructor. For some reason the constructor will accept the derived state manager class as a valid parameter and now any of the child state classes I create can access fields from SoldierStateManager.

    public abstract class EnemyState
    {
        protected EnemyStateManager entity;
        protected float stateStartTime;
    
        public EnemyState(EnemyStateManager enemy)
        {
            this.entity = enemy;
        }
    
        public virtual void EnterState()
        {
            stateStartTime = Time.time;
        }
    
        public virtual void ExitState() { }
    
        public virtual void UpdateState() { }
    }
    
    
    public class SoldierIdleState : EnemyState
    {
        protected SoldierStateManager soldier;
        float moveSpeed = 2f;
    
        public SoldierIdleState(SoldierStateManager enemy) : base(enemy)
        {
            soldier = enemy;
        }
        public override void EnterState() 
        {
            base.EnterState(soldier);
            soldier.SetVelocity(moveSpeed);
        }
    
        public override void ExitState() 
        {
            base.ExitState();
        }
    
        public override void UpdateState() 
        {
            base.UpdateState(soldier);
            if (soldier.health <= 0)
            {
                soldier.SwitchState(soldier.UnconsciousState);
            }
        }
    }
    
    public class SoldierStateManager : EnemyStateManager
    {
        public SoldierIdleState IdleState { get; private set; }
        public SoldierUnconsciousState UnconsciousState { get; private set; }
    
        public override void Start()
        {
            base.Start();
    
            IdleState = new SoldierIdleState(this);
            UnconsciousState = new SoldierUnconsciousStateState(this);
    
            currentState = IdleState;
            currentState.EnterState();
        }
    }