Why am I using animators?

Enemy AIs can be implemented in a lot of different ways, for example each AI could have its own MonoBehaviour script.

This is completely fine and likely a much better option in smaller games, however it means more scripts therefore longer compilation and build times as you add more. It also doesnt fit in with my design view for this game. I want mechanics to all be in place before adding any content, and I want adding content to not require programming.

For these reasons, I will be using the Unity animator controller system to create my AIs. I’ve been thinking of something node-based and while I could create my own system, that would be a lot of work and I don’t see any fatal flaws in doing it this way.

Concept

Nodes

State machines are a nice way to represent the AI, as each node is an attack + movement pattern, and each transition is, well a transition.

Phase

These transitions can be timed, or based on a condition/event (or multiple).

Transition

I can even sort of use variables inside the AI, by having a StateMachineBehaviour that sets an animator parameter in OnStateEnter().

Programming

We will make use of the OnStateEnter() method to start coroutines rather than using OnStateUpdate(). I haven’t tested it but am 99.9% sure it will lead to much better performance.

There is one problem though, we can’t start coroutines in State Machine Behaviours. However we can use the animator parameter passed in to GetComponent() on the GameObject, and start our coroutines on that instead.

For example, in the ShootAtPlayer behaviour we have this:

public class ShootAtPlayer : StateMachineBehaviour
{
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        Shooting shooting = animator.GetComponent<Shooting>();
        shooting.StartCoroutine(Shoot(shooting));
    }

    IEnumerator Shoot(Shooting shooting)
    {
        while(true)
        {
            shooting.Shoot(bullet, shooting.transform.position.DirectionTo(playerPos));
            yield return wait;
        }
    }
}

I’ve removed most code here for simplicity to show how to start the coroutines.

We also need to make sure to stop the coroutine when exiting the state, as to not keep shooting while in a different phase.

public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    animator.GetComponent<Shooting>().StopAllCoroutines();
}

Next, we need to manage updating animator parameters so that we can use them. This will be done with a new script, which I will call AIParameterUpdater.

[RequireComponent(typeof(Animator))]
public class AIParameterUpdater : MonoBehaviour
{
    Animator animator;

    void Start()
    {
        animator = GetComponent<Animator>();

        // Disable warnings when assigning to parameter that doesnt exist
        // TODO: disable updating those variables completely
        animator.logWarnings = false;

        // Update PlayerDistance
        StartCoroutine(UpdatePlayerDistance(1f));

        // Update Health
        if (TryGetComponent<Health>(out Health health))
        {
            animator.SetFloat("Health", health.healthFraction);
            health.OnHealthChanged += (sender, e) => animator.SetFloat("Health", health.healthFraction);
        }
    }

    IEnumerator UpdatePlayerDistance(float interval)
    {
        WaitForSeconds wait = new(interval);

        yield return new WaitForSeconds(UnityEngine.Random.value * interval);
        Transform playerTransform = FindObjectOfType<PlayerShooting>().transform;
        while (playerTransform != null)
        {
            animator.SetFloat("PlayerDistance", Vector3.Distance(transform.position, playerTransform.position));
            yield return wait;
        }
    }
}

Here I have a coroutine that updates the PlayerDistance every second, starting with a random delay to avoid all enemies doing this at once.

We also subscribe to the OnHealthChanged event and update the Health parameter everytime it’s invoked. The parameter doesnt represent actual health, but the fraction of health out of max since this will be easier to work with. Health.healthFraction is a public field with a getter:

public float healthFraction { get { return (float) currentHealth / maxHealth; } }

(notice the float cast, as both currentHealth and maxHealth are ints, without it this would return 0 most of the time.)

ShootAtPlayer is the only behaviour I’ve implemented for now just to get the infrastructure set up for adding more. To add movement behaviours I first want to set up a Movement script shared between all entities including the player, so that status effects can work the same without code duplication (similar to what I did with Shooting).