These things are always super-fiddly to set up, especially if you want a bunch of different people to work on the assets involved, like one person to fiddle with the text, one person to change the animations, ordering, etc.
The way I have always done tutorials is to just not do them. I strongly suggest this approach. 
In all seriousness, when I have done tutorials, what I like is a single master coroutine that flows vertically through doing all the parts, usually by enabling, disabling, or sending some kind of message to other parts.
That coroutine would also have calls out for “Wait until A is pressed” and “wait until animation completes,” the usual inputs required.
This approach does require a bit more code maintenance in the form of the actual coroutine flowing it all together, but personally I find it is the simplest way to staple long strings of interactive stuff together, plus it retains a high degree of editability for you to change stuff around in the future.
I wish I knew a better more flexible way, but every time I try to use someone else’s system I seem to always bump the limitations and assumptions and then I’m back to doing it my way to quickly get what I want.
Here’s a “how to maneuver” tutorial I’m working on for my upcoming Mecha game:
namespace Mecha.Tutorials
{
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// This is the primary sequencer to drive Tutorial1, starting
// from this first maneuvering section.
//
// See 20240813_tutorial_design.txt for details. Thanks ChatGPT!
//
public class Tutorial1_Maneuvering : MonoBehaviour
{
// this must appear in the scene before this script moves forward
const string s_Activator = "Tutorial1_Maneuvering:Present";
[Header( "Markers for maneuvering.")]
public GameObject HangarGoto1Left;
public GameObject HangarGoto1Right;
[Header("Marker in front of the hangar door.")]
public GameObject HangarGoto1Center;
[Header("Marker just outside of the hangar door.")]
public GameObject HangarGoto1Outside;
[Header( "Main Hangar Door (MHDoor):")]
public Transform MHDoor_MovableTransform;
public Transform MHDoor_Closed;
public Transform MHDoor_Open;
public float MHDoor_Opening_Interval = 1.0f;
[Header( "The hangar door noises.")]
public AudioSource MHDoor_Opening;
public AudioSource MHDoor_Closing;
IEnumerator Start()
{
// close the hangar door
MHDoor_MovableTransform.position = MHDoor_Closed.position;
// shut off all the markers
HangarGoto1Left.SetActive(false);
HangarGoto1Right.SetActive(false);
HangarGoto1Center.SetActive(false);
HangarGoto1Outside.SetActive(false);
// linger here inactive if we don't find this object!
while (true)
{
yield return new WaitForSeconds(0.25f);
var go = GameObject.Find(s_Activator);
if (go)
{
break;
}
}
yield return new WaitForSeconds(2.0f);
SlidingMessage.Create(UT.CS("Let's start with basic maneuvering!"));
yield return new WaitForSeconds(2.0f);
HangarGoto1Left.SetActive(true);
HangarGoto1Right.SetActive(true);
SlidingMessage.Create(UT.CS("Move to each marker, one at a time."));
// TODO: blink and remind legend perhaps???
// possible IDEA: squish the markers with your feet??? meh...
int markersHit = 0;
while (markersHit < 2)
{
if (UT.TestPlayerAtTarget(ref HangarGoto1Left) ||
UT.TestPlayerAtTarget(ref HangarGoto1Right))
{
markersHit++;
if (markersHit == 1)
{
SlidingMessage.Create(UT.CS("Nice! Maneuver to the other marker now."));
}
}
yield return null;
}
SlidingMessage.Create(UT.CS("Well done! Let's get out of here... go to the door."));
HangarGoto1Center.SetActive(true);
while( !UT.TestPlayerAtTarget( ref HangarGoto1Center))
{
yield return null;
}
SlidingMessage.Create(UT.CS("Door opening..."));
// open the hangar door
MHDoor_Opening.Play();
StartCoroutine( UT.TweenPosition(MHDoor_MovableTransform,
MHDoor_Closed.position, MHDoor_Open.position, MHDoor_Opening_Interval));
yield return new WaitForSeconds(2.0f);
SlidingMessage.Create(UT.CS("Door is jammed: go ahead and jump over it."));
HangarGoto1Outside.SetActive(true);
while (!UT.TestPlayerAtTarget(ref HangarGoto1Outside))
{
yield return null;
}
SlidingMessage.Create(UT.CS("Maneuver tutorial complete."));
// close the hangar door
MHDoor_Closing.Play();
StartCoroutine(UT.TweenPosition(MHDoor_MovableTransform,
MHDoor_Open.position, MHDoor_Closed.position, MHDoor_Opening_Interval));
yield return new WaitForSeconds(2.0f);
Tutorial1_Weapons1.Activate();
}
}
}
and here’s the snippets for some of those UT calls above, nothing magical:
namespace Mecha
{
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class UT
{
public static string CS(string s)
{
return s.ToUpperInvariant();
}
public static float FlatDistance(Vector3 a, Vector3 b)
{
a.y = 0;
b.y = 0;
return Vector3.Distance(a, b);
}
public static IEnumerator TweenPosition( Transform obj, Vector3 p1, Vector3 p2, float interval)
{
float time = 0;
float fraction = 0;
while (fraction < 1)
{
fraction = time / interval;
time += Time.deltaTime;
Vector3 position = Vector3.Lerp(p1, p2, fraction);
obj.position = position;
yield return null;
}
}
public static bool PlayerIsCloseTo(GameObject target)
{
if (MechaPlayer.Instance)
{
Vector3 playerPosition = MechaPlayer.Instance.transform.position;
Vector3 targetPosition = target.transform.position;
if (FlatDistance(playerPosition, targetPosition) < 3)
{
return true;
}
}
return false;
}
public static bool TestPlayerAtTarget( ref GameObject target)
{
if (target)
{
if (PlayerIsCloseTo(target))
{
GameObject.Destroy(target);
target = null;
return true;
}
}
return false;
}
}
}