That would have to be determined by the profiler.
I mean, as I mentioned, it’s simple to set up your own system to start off with something simple, that can then easily be built up.
For example, lets outline some basic data objects first:
public interface ISequenceEntry
{
void OnEnterEntry();
float EntryTime { get; }
}
[System.Serializable]
public class SequenceEntry : ISequenceEntry
{
[SerializeField]
private float _entryTime = 2f;
public float EntryTime => _entryTime;
public virtual void OnEnterEntry()
{
Debug.Log($"Waiting {_entryTime} seconds...");
}
}
[System.Serializable]
public class DialogueSequenceEntry : SequenceEntry
{
[SerializeField, TextArea(2, 5)]
private string _entryDialogue;
public string EntryDialogue => _entryDialogue;
public override void OnEnterEntry()
{
Debug.Log(_entryDialogue);
}
}
Then we create an object to hold a sequence with an internal enumerator class:
[System.Serializable]
public class DialogueSequence : IEnumerable<ISequenceEntry>
{
#region Inspector Fields
[SerializeReference]
private List<ISequenceEntry> _dialogueEntries = new();
#endregion
#region Sequence Methods
// more enumerator methods than you can poke a stick at
public DialogueSequenceEnumerator GetEnumerator() => new DialogueSequenceEnumerator(this);
IEnumerator<ISequenceEntry> IEnumerable<ISequenceEntry>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
public class DialogueSequenceEnumerator : IEnumerator<ISequenceEntry>
{
private int _index = -1;
private float _currentTime = 0f;
private readonly DialogueSequence _dialogueSequence;
public ISequenceEntry Current => _dialogueSequence._dialogueEntries[this._index];
object IEnumerator.Current => this.Current;
#region Constructor
public DialogueSequenceEnumerator(DialogueSequence sequence)
{
_dialogueSequence = sequence;
MoveToNextEntry();
}
#endregion
/// <summary>
/// Returns false when either at the end of the dialogue, or while
/// the timer has not yet ticked over until the next entry.
/// </summary>
public bool MoveNext()
{
if (ReachedEnd() == true)
{
return false;
}
ISequenceEntry entry = this.Current;
_currentTime += Time.deltaTime;
if (_currentTime >= entry.EntryTime)
{
bool notAtEnd = MoveToNextEntry();
return notAtEnd;
}
return false;
}
public bool ReachedEnd()
{
return _index >= _dialogueSequence._dialogueEntries.Count;
}
public void Reset()
{
_currentTime = 0f;
_index = 0;
}
void IDisposable.Dispose() { }
private bool MoveToNextEntry()
{
_index++;
_currentTime = 0f;
bool notAtEnd = ReachedEnd() == false;
if (notAtEnd)
{
var entry = this.Current;
entry.OnEnterEntry();
}
return notAtEnd;
}
}
}
Then you just need something to run the sequence:
public sealed class DialogueSequenceRunner : MonoBehaviour
{
#region Inspector Fields
[SerializeField]
private DialogueSequence _dialogueSequence;
#endregion
#region Internal Members
private DialogueSequence.DialogueSequenceEnumerator _dialogueEnumerator;
#endregion
#region Unity Callbacks
private void Awake()
{
_dialogueEnumerator = _dialogueSequence.GetEnumerator();
}
private void Update()
{
if (_dialogueEnumerator.ReachedEnd() == false)
{
_dialogueEnumerator.MoveNext();
}
}
#endregion
}
Then we just set some stuff up in the inspector and run it:

Hey presto! You have a base that can easily expanded upon as needed.
I used an enumerator object here as that’s basically how co-routines work. But were I actually to spend more than 10 minutes on it, I would take a different foundational approach (so as to support rewinding, pausing, etc).
I feel like you’re too laser focused on coroutines for some reason. Again, they are just enumerator state machine objects. There’s nothing fancy about them. Except you can’t extend how they work at all.
If you build your own, you have full control and can design it precisely to your needs.