Execute Sequential components(actions)

Hi. Assume we have sequential actions that we want to execute them consecutively.
A->B->C->D…
Do we have a better way like state machines or unityEvents? or implement command class with nextCommand field?

If the components depend on previous component or components data, how can we handle that?
Should I use the object sender and cast it to special type then I can get the data of the component?
Is the approach appropriate only for independent components?

Example:
Execute a video-> Execute an animation-> show rewards like chests-> Select one of them-> return to first scene

public class SequentialCommandExecuter : MonoBehaviour, IExecutableCommandList
    {
        [SerializeField] private GameObject[] _commandObjs;
        [SerializeField] private bool _executeOnEnable;
        [SerializeField] private int _currentCommandIndex;

        private ICommand[] _commands;

        private void OnEnable()
        {
            _commands=new ICommand[_commandObjs.Length];
            for (var i = 0; i < _commandObjs.Length; i++)
            {
                _commands[i] = _commandObjs[i].GetComponent<ICommand>();
            }

            if (_executeOnEnable)
            {
                ExecuteCommand();
            }
        }

        public void SetCurrentCommandIndex(int index = 0)
        {
            _currentCommandIndex = index;
        }
        public void ExecuteCommand()
        {
            if (_currentCommandIndex >= _commands.Length) return;
            _commands[_currentCommandIndex].CommandExecutionFinished += CommandExecutionFinished;
            _commands[_currentCommandIndex].Execute();
        }

        private void CommandExecutionFinished(object sender, EventArgs e)
        {
            ++_currentCommandIndex;
            ExecuteCommand();
        }
    }

    public interface ICommand
    {
        void Execute();
        event EventHandler CommandExecutionFinished;
    }

    public interface IExecutableCommandList
    {
        void SetCurrentCommandIndex(int index = 0);
        void ExecuteCommand();
    }

Why not coroutines? This is their best use case.

1 Like

Coroutine for execute commands? or new method?

I mean for a sequence of events. That’s what they’re made for.

video = ShowVideo();

yield return new WaitForSeconds(video.length);

animation = ShowAnimation();

yield return new WaitForSeconds(animation.length);

rewards = ShowRewards();

while(rewards.playerSelectedOption == -1)
    yield return null;

// take them back to wherever

I’m not sure what you mean by decouple in this case. At some point, if you want a sequence of events to transpire, something is going to have to build that logic. Having one object use / consume multiple other objects isn’t inherently bad.

It’s hard to say without knowing exactly what you’re using it for. Waiting for discrete events to finish (watch the video->play the animation->show the player choices->wait for their choice) is a no-brainer to use coroutines to me. Queuing a sequence of generic commands or callbacks, at the end of the day, is simply re-making the coroutine system with none of the elegance.

Now if you’re looking for callbacks as routines finish so other objects can listen and respond, that’s something else entirely.

\

animation = ShowAnimation();
yield return new WaitForSeconds(animation.length);

video = ShowVideo();
yield return new WaitForSeconds(video.length);

rewards = ShowRewards();

while(rewards.playerSelectedOption == -1)
    yield return null;

// take them back to wherever

Thank you yes it is the first approach that we can write.
If I want to implement first watch video then open a chest and finally play an animation I need to write other script because the orders are different! and if I intend other orders I have to write codes again. It is not generic I think.
Also if I want more complex scenarios, the class above will be complex and long

And how does your command/callback chaining fix the issue if the order of events changes? You still need some sort of object that defines the order.

I only change the order in the inspector (GameObjects that have components implementing ICommand interface) not create a new class with new order

You are building a pipeline. Instead of thinking of the components as Commands, think of them as pipes which can be connected together. Each pipe should specify its input type and output type. It should also have a method to “connect” it to the next pipe.

The controller class would then iterate over the pipes wiring them together. If a pipe on the list didn’t match the output of a previous pipe, I would just throw an Exception at start up. Better would be to write a custom editor window that could enforce the types at edit time. Alternatively, you could write code to coerce the types, or just pass in null (previous pipe results would be lost)

Nesting method calls, or using temp locals to store output from one method call to pass to another is basically the same thing. This is what Grozzler is suggesting. The pipeline technique is simply a way of modeling method calls as objects so we can “programmatically program”.

1 Like

This. By definition, a coroutine is a finite state machine. Which makes them ideal for sequential actions. You can even plug in an inspector with a collection of Unity Events to make the set up easy.

1 Like

Thank you eisenpony.
My problem is how I can send data from the previous pipe to the next pipe. Because data may have different types.
Should I use object or general eventArgs in FinishedEventHandler and casting it to special known type inside the next pipe?

To further abuse the metaphor, I would add in adapters between each ‘pipe’ that can take the data from one ‘pipe’ and convert it to what the other ‘pipe’ requires.

Before we go any further, I want to reframe what you’re attempting so it’s easier to discuss long term costs and payoffs. What your CommandExecuter is capable of right now is basically this:

{
  ShowVideo();
  ShowAnimation();
  ShowRewards();
}

Now, there’s something important to notice about these calls. They all accept no parameters and have no return types. Now you are proposing to add parameters and return types so you can do something like this:

{
  AddRewardToInventory(
    ChooseReward(
      GenerateRandomReward()));
}

Of course, in a real language, like C# you would probably just assign into a variable.

{
  var options = GenerateRandomReward();
  var chosen = ChooseReward(options);
  AddRewardToInventory(chosen);
}

But the meaning is really not any different. In the first example, see how the chained calls work a little like a pipeline. The return of one method feeds into the input of the next.

At this point, a pipeline is an okay analogy and can accomplish your goals. However, see what happens when we get ambitious … Let’s say we want more control over the components.

{
  var options = GenerateRandomReward();
  if (OnlyContainsConsumables(options))
  {
    var chosen1 = ChooseReward(options);
    var chosen2 = ChooseReward(options);
    AddRewardsToInventory(chosen1, choosen2);
  }
  else
  {
    var chosen = ChooseReward(options);
    AddRewardToInventory(chosen);
  }
}

Suddenly, a pipeline isn’t good enough. You can see that we need to pass options around to a few more components. Plus, there is a fork in the road. How can this be modeled as a pipeline? Technically, it’s still possible by creating a graph of pipes instead of just a straight line but … now your task to reduce the amount of simple code you need to write has just become a very complex problem. Unless you are writing tools for a team of hundreds, it’s very unlikely you will save time doing this.

If you continue down this road and want to add constants, loops, delegates, etc… you are essentially creating a new language. There’s nothing particularly complex about that as it’s a problem well studied by computer scientists: it’s a parser/interpreter! You can pretty easily create one if you study the interpreter design pattern. But at this point you really need to step back and ask your self why are you doing this?? You already have a perfectly good interpreter right in front of you. It’s called the Unity compiler.

One case where you really need an interpreter is when you are inventing a language that makes sense for the domain you are working in. For instance, you might want a designer to be able to write code that looks like this:

 Generate 2 Random Consumables And 1 Random Equipment Then Offer Choice Then Add Choosen To Inventory.

Alternatively, you might be building a kind of application which needs the user to write code. If that’s the case, you have no idea what their requirements are, so you need to parse arbitrary code into meaningful commands.

So there’s my rant about long term implications of your endeavor. If you’re certain you’ll be happy with the passing of values from component to component and won’t try to take this further by adding more flow control, then I think the pipeline idea handles your issue of types. I’ll attempt to show you how …

“Pipes” are basically objects which take an input and give an output. the pipes know their input and output type and can be written to make sure they pass output to a pipe which acepts that output as input. The job of the Executer would be then to wire the commands up. So given the items:

var pipes = new List<IPipe>
{
  new GenerateRandomReward(),
  new ChooseReward(),
  new AddRewardToInventory()
};

The Executer would do something like

{
  for (var i = 0; i < pipes.Length - 1; i ++)
    pipes[i].Subscribe(pipes[i + 1]);
}

The trick is in how you implement the pipes.

public interface IPipe
{
  void Subscribe(IPipe nextPipe);
}

public interface IInPipe<TIn>
{
  void OnNext(TIn item);
}

public abstract class OutPipe<TOut>
{
  protected IInPipe<TOut> NextPipe { get; set; }
  public void Subscribe(IPipe nextPipe)
  {
    if (nextPipe is IInPipe<TOut>)
      Subscribe(nextPipe as IInPipe<TOut>);
    else
      throw new InvalidOperationException("Types don't match");
  }

  public void Subscribe(IInPipe<TOut> nextPipe)
  {
    NextPipe = nextPipe;
  }

  protected void SendNext(TOut item)
  {
    NextPipe?.OnNext(item);
  }
}

public abstract class Pipe<TIn, TOut> : OutPipe<TOut>, IPipe, IInPipe<TIn>
{
  public abstract void OnNext(TIn item);
}

public class GenerateRandomReward : OutPipe<IEnumerable<Reward>>
{ // ... }
public class ChooseReward : Pipe<IEnumerable<Reward>, Reward>
{ // ... }
public class AddRewardToInventory : IInPipe<Reward>
{ // ... }

Notice how the Executer can work on the objects as IPipes, simply calling Subscribe. It is up to the OutPipe to make sure the pipe that is subscribing to it is of the correct type. If it is, then NextPipe is populated, and calling SendNext from a derived type will pass the output on to the next pipe.

Unfortunately, all of this is an orthogonal problem to what Grozzler and Kiwasi have pointed out already. Even with your current code, you are going to find out that all your commands execute sequentially without interruption. That is to say, everything will happen before Unity is permitted to advance the frame count. In order to have the commands appear to execute in real time, while the frames continue, you need to return control to unity after each small step. This is actually quite a difficult job and is why Unity provides coroutines.

My guess is that you could adapt the pipeline to use coroutines internally either by executing a coroutine in each pipe or by wrapping the whole thing into a coroutine. Either way, it won’t be trivial to work out so it’s probably worth it to reconsider if your generic solution is justified. Why not just write all the combination of coroutines you will need? Depending on the size of your project, it might be a lot faster.

2 Likes

You are an angel thank you for spending your time.
Yes my procedure does not have enough flexibility in new scenarios with parameters and return values.
You really pointed the fact correctly if we continue we arrive at a new language.
It is better to keep it simple.
Watching videos, playing animations and giving chests are completely different and it is not appropriate to implement the same interface and iterate over them.
Maybe I think that I need to change orders in the future or in other places I require different orders and therefore concluded to use sequential commands or pipelines to avoid to create new class with new orders. Thank you again.