Better way to make sequence of actions without using coroutines?

Guys, I’m doing a shmup and I’m trying to make AI of the bosses.
I’m a beginner with game development and I’m using state machine for the first time, but I don’t know if I implemented it right.
There are some sequences that I didn’t know how to do. So I ended up using Coroutines.

For example: after n seconds, the boss enter in weapon2 state, and should:
1 - go to the middle of the screen and idle
2 - wait a second
3 - shoot in different directions
4 - stop firing and wait another second
5 - move again

I’ve managed the flow with Coroutines but it doesn’t seem right. Can you give me ideas on how I could do better?

using System.Collections;
using UnityEngine;

public class Boss1: Enemy
{
    [Header("Gun")]
    public float g_MinCooldown;
    public float g_MaxCooldown;
    public int g_ShootsPerBurst;
    public float g_TimeBetweenShoots;
    float _g_Cooldown;

    Transform _gun;

    [Header("TeD")]
    public float td_MinCooldown;
    public float td_MaxCooldown;
    public float td_Initial_Delay;
    public int td_ShootsPerBurst;
    public float td_TimeBetweenShoots;
    float _td_Cooldown;

    [Header("Bullet Wall")]
    public float bw_MinCooldown;
    public float bw_MaxCooldown;
    public float bw_Initial_Delay;
    public int bw_ShootsPerBurst;
    public float bw_TimeBetweenShoots;
    float _bw_Cooldown;

    new void Start()
    {
        _gun = transform.Find("_gun");
        ResetGunCooldown();
        ResetTeDCooldown();
        ResetBulletWallCooldown();

        base.Start();
        _sm.SetState(Move);
    }

    void Move()
    {
        _g_Cooldown -= Time.deltaTime;
        _td_Cooldown -= Time.deltaTime;
        _bw_Cooldown -= Time.deltaTime;

        if (_g_Cooldown <= 0)
        {
            ResetGunCooldown();
            _sm.SetState(Gun);
        }
        else if (_td_Cooldown <= 0)
        {
            ResetTeDCooldown();
            _sm.SetState(TeD);
        }
        else if (_bw_Cooldown <= 0)
        {
            ResetBulletWallCooldown();
            _sm.SetState(BulletWall);
        }
    }

    void Gun()
    {
        StartCoroutine(Gun_Fire());
        _sm.SetState(Move);
    }
    IEnumerator Gun_Fire()
    {
        for (int i = 0; i < g_ShootsPerBurst; i++)
        {
            LookAtPlayer();

            var l_direction = _gun.up.normalized;
            var laser = Instantiate(Laser, _gun.position, _gun.rotation);
            laser.GetComponent<Rigidbody2D>().velocity = l_direction * ProjectileSpeed;

            AudioSource.PlayClipAtPoint(projectileSound, Camera.main.transform.position, projectileSoundVolume);
            yield return new WaitForSeconds(g_TimeBetweenShoots);
        }
    }

    void TeD()
    {
        if (!_pathing.IsStopped())
            StartCoroutine(TeD_Fire());
    }
    IEnumerator TeD_Fire()
    {
        _pathing.Stop(true);

        var destination = Camera.main.ViewportToWorldPoint(new Vector3(0.5f, 0.75f));
        destination.z = 0f;

        while (transform.position != destination)
        {
            transform.position = Vector3.MoveTowards(transform.position, destination, 2f * Time.deltaTime);
            yield return new WaitForEndOfFrame();
        }

        yield return new WaitForSeconds(td_Initial_Delay);

        for (int i = 0; i < td_ShootsPerBurst; i++)
        {
            LookAtPlayer();

            var l_direction = _gun.up.normalized;
            var laser = Instantiate(Laser, _gun.position, _gun.rotation);
            laser.GetComponent<Rigidbody2D>().velocity = l_direction * ProjectileSpeed;

            AudioSource.PlayClipAtPoint(projectileSound, Camera.main.transform.position, projectileSoundVolume);
            yield return new WaitForSeconds(td_TimeBetweenShoots);
        }

        yield return new WaitForSeconds(td_Initial_Delay);

        _sm.SetState(Move);
        _pathing.Stop(false);
    }

    void BulletWall()
    {
        if (!_pathing.IsStopped())
            StartCoroutine(BulletWall_Fire());
    }
    IEnumerator BulletWall_Fire()
    {
        _pathing.Stop(true);

        //prepare Position
        var destination = Camera.main.ViewportToWorldPoint(new Vector3(0.5f, 0.75f));
        destination.z = 0f;

        while (transform.position != destination)
        {
            transform.position = Vector3.MoveTowards(transform.position, destination, 5f * Time.deltaTime);
            yield return new WaitForEndOfFrame();
        }

        //attack
        var turn = false;
        var angle = 0;

        for (int i = 0; i < bw_ShootsPerBurst; i++)
        {
            angle = !turn ? Random.Range(-55, -45) : Random.Range(-45, -35);


            for (int r = angle; r <= -angle; r += 20)
            {
                LookAtBottom();
                _gun.rotation = _gun.rotation * Quaternion.Euler(0f, 0f, r);
                var l_direction = _gun.up.normalized;
                var laser = Instantiate(Laser, _gun.position, _gun.rotation);
                laser.GetComponent<Rigidbody2D>().velocity = l_direction * ProjectileSpeed;
            }

            AudioSource.PlayClipAtPoint(projectileSound, Camera.main.transform.position, projectileSoundVolume);
            yield return new WaitForSeconds(bw_TimeBetweenShoots);

            turn = !turn;
        }

        _pathing.Stop(false);
        _sm.SetState(Move);
    }

    void LookAtPlayer()
    {
        if(_player == null) return;

        Vector3 diff = _player.transform.position - transform.position;
        diff.Normalize();
        float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;
        _gun.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);
    }
    void LookAtBottom()
    {
        float rot_z = Mathf.Atan2(-1, 0) * Mathf.Rad2Deg;
        _gun.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);
    }
    void ResetGunCooldown()
    {
        _g_Cooldown = ResetCooldown(g_MinCooldown, g_MaxCooldown);
    }
    void ResetTeDCooldown()
    {
        _td_Cooldown = ResetCooldown(td_MinCooldown, td_MaxCooldown);
    }
    void ResetBulletWallCooldown()
    {
        _bw_Cooldown = ResetCooldown(bw_MinCooldown, bw_MaxCooldown);
    }
    float ResetCooldown(float min, float max)
    {
        return Random.Range(min, max);
    }
}

State machines are a bit of an old fashioned way of working with AI now. I would recommend looking into Behavior Trees for AI. They are one of my favorite systems to work on personally. They can easily manage to do what you are describing and can be built through visual editing with minor coding.
Check out this website to get an idea of what they do Game Platforms recent news | Game Developer
Unity has quite a few behavior tree assets. Some are free, some cost quite a bit, but I can’t really recommend any one of them.

There’s a half-decent behaviour tree system build into one of Unity’s 2D samples

THB, I looked into FSM and Behaviour Trees - Playmaker, Eliot, Invector, Node Canvas and Behaviour Designer, and while I agree that most of them have a certain elegance (especially BD), I found that for 99% of the work on games, you simply can’t beat straight code. It’s one of the cases where old-fashioned doesn’t mean outdated or inferior; quite the opposite, actually.

Hand-crafted state-machines are more flexible, can perform much better, and do exactly what you code (to a fault, actually). Unless you are looking into a game that requires highly sophicticated dialogues, and storylines that evelove based on your past actions, hand-crafted AI is the way to go. It does require some experience writing them, You’ll soon notice that they tend to look very similar:

  • You’ll have an enum for the current state. States are usually a superset of (idle, patrolling, attacking, flee, dead).
  • You’ll have a doState (e.g. doIdle, doPatrol, doAttack, …) method that is branched to during Update()
  • You’ll have a method to determine the next state based on the current one.
  • You’ll have a counter counting down, and when it reaches zero, you’ll Trigger a check (Change state of agitation, boredom, fear, etc - just General purpose cooldown).
  • And you’ll have an environment-based trigger that forces state change (being hit, seeing the enemy).

Pretty soon you’ll write your own basic AI ‘shell’ that works well for you, and base the particular AI you are looking for on that shell. Another big advantages of hand-crafted AI has to do with one of Unity’s shortcomings: integrating model Animation. That’s where most FSM/BT fall flat on their nose, and grafting on your Animation Controller is usually as much work as writing the basic AI yourself. So nine out of ten times I found that rolling my own AI was perhaps more pedestrian, but also much more efficient than using an FSM or BT.

All of that is IMHO, of course.

-ch

There’s nothing about an FSM or a Behaviour Tree that excludes “straight code”. If you get some sort of framework that exposes behaviour trough a visual graph or a custom scripting language, then sure. But that’s got nothing to do with the underlying pattern.

And that’s just what FSMs and Behaviour Trees are; patterns. You can create those by just writing code. What you’re describing as “rolling your own AI” is a bog standard FSM.

So I’d recommend OP to look into Behaviour Trees or FSMs, as they’re a very good way to structure AI code. I’d also recommend to not buy or download a package that provides BT or FSM through else than a code interface.

1 Like