Skip to content

Processes & Commands

Any operation - a lerp, a transfer, an animation - follows the same lifecycle: start, run, complete. This means you can swap any operation for any other without changing the component that runs it.

Want an object to slide into place with a smooth lerp? Later decide it should snap instantly? Or animate along a curve? All three are interchangeable. The component just says "run this operation" and does not care what the operation actually does.

Instant actions work the same way. Setting a parent, firing an event, toggling a flag - these are one-shot operations that share the same lifecycle, so they can be used anywhere a process is expected.


The Lifecycle

Every process follows the same state flow:

State Diagram

stateDiagram-v2
    [*] --> Idle
    Idle --> Running : Begin()
    Running --> Paused : Pause()
    Paused --> Running : Unpause()
    Running --> Complete : natural completion
    Running --> Complete : Complete()
    Paused --> Running : Begin() [resets]
    Complete --> Running : Begin()
    Complete --> [*]
Action What Happens
Begin Starts execution. An optional callback fires when it finishes naturally.
Pause Freezes execution without losing progress.
Unpause Resumes from where it was paused.
Complete Snaps to the final state instantly. Safe to call in any state.
State Meaning
Running Actively executing
Complete Reached final state, either naturally or via Complete
Paused Frozen mid-execution via Pause

Two Layers: Processes and Operations

The system separates what runs (processes) from what does the work (operations).

Processes

A process manages lifecycle and orchestration. It knows when to start, pause, resume, and complete. Processes can hold other processes (via ProcessContainer) or hold operations (via Command).

Operations

An operation is a unit of work held by a command. Operations can be instant (complete synchronously) or timed (self-tick until done). They expose a minimal interface: IsComplete, Complete(), Pause(), Unpause(). How an operation begins depends on its domain - a targeted operation takes a target and a callback, a non-targeted operation takes just a callback.

When a command runs, it begins all its operations in parallel and completes when every operation has finished.


Technical API

IProcess

The core interface for all processes:

public interface IProcess
{
    void Begin(Action onComplete = null);
    void Pause();
    void Unpause();
    void Complete();

    bool IsRunning { get; }
    bool IsComplete { get; }
    bool IsPaused { get; }
    bool HasDefinedDuration => Duration >= 0f;
    float Duration { get; }
}

IOperation

The interface for units of work held by commands:

public interface IOperation
{
    bool IsComplete { get; }
    void Complete();
    void Pause();
    void Unpause();
}

Operations do not define a Begin method here. The specific begin signature lives on the domain operation interface (e.g., an attachment operation takes a target + callback, a resource flow operation takes a tank + callback). This keeps the base interface minimal while allowing each domain to pass exactly the data it needs.


Base Classes

Command

Abstract base class for processes that hold and execute operations in parallel. Handles all the IProcess plumbing (Begin/Pause/Unpause/Complete, callback tracking, sentinel logic) so subclasses only provide the operations and how to start them:

public abstract class Command : IProcess
{
    protected abstract int GetOperationCount();
    protected abstract IOperation GetOperationAt(int index);
    protected abstract void BeginOperationAt(int index, Action onComplete);
    protected virtual bool OnBegin() => true;
}
Method Purpose
GetOperationCount() How many operations this command holds
GetOperationAt(index) Return the operation at a given index (for pause/complete forwarding)
BeginOperationAt(index, onComplete) Start the operation with whatever domain-specific data it needs
OnBegin() Optional setup hook. Return false to abort (command completes immediately).

All operations begin simultaneously. The command completes when every operation has finished.

TargetedCommand\<TTarget, TOperation>

Generic base for targeted commands. Adds target resolution and typed operation access on top of Command:

public abstract class TargetedCommand<TTarget, TOperation> : Command, ITargetedCommand, IGraphable
    where TTarget : UnityEngine.Object
    where TOperation : class, IOperation
{
    public GameObject OverrideTarget { get; set; }
    protected abstract TTarget ResolveTarget();
    protected abstract IList<TOperation> GetOperations();
    protected abstract void BeginOperation(TOperation op, TTarget target, Action onComplete);
}

This is the class you extend when building a command that operates on a specific Unity Object. The generic handles target resolution, override injection, and bridges the typed operation list to the base Command plumbing.

// Example: a command that runs resource flow operations on a Tank
[Serializable]
public class ResourceFlowCommand : TargetedCommand<Tank, IResourceFlowOperation>
{
    [SerializeField] private TankValue tankValue;
    [SerializeReference] [JungleClassSelection]
    private List<IResourceFlowOperation> operations = new();

    protected override Tank ResolveTarget() =>
        OverrideTarget ? OverrideTarget.GetComponent<Tank>() : tankValue?.Object;

    protected override IList<IResourceFlowOperation> GetOperations() => operations;

    protected override void BeginOperation(
        IResourceFlowOperation op, Tank target, Action onComplete)
    {
        op.Begin(target, onComplete);
    }
}

ProcessContainer

Abstract base class for processes that hold and run child IProcess instances in parallel. Same parallel-completion pattern as Command, but for child processes instead of operations:

public abstract class ProcessContainer : IProcess
{
    protected abstract int GetProcessCount();
    protected abstract IProcess GetProcessAt(int index);
    protected virtual bool OnBegin() => true;
    protected virtual void PrepareProcess(IProcess process) { }
}

PrepareProcess is called on each child before it begins, giving the container a chance to inject context (e.g., setting OverrideTarget on targeted commands).

SharedTargetCommands

A ProcessContainer that executes a list of targeted commands on a shared GameObject. Each child command receives the same target before execution. When nested inside another targeted context, the target is inherited from the parent via OverrideTarget.

public class SharedTargetCommands : ProcessContainer, ITargetedCommand, IGraphable
{
    public GameObject OverrideTarget { get; set; }
    // Resolves target, injects it into each child command, runs them in parallel
}

Targeting

Some processes operate on a specific GameObject rather than implicit state. The targeting interfaces allow orchestrators to inject context into child processes:

ITargetable

public interface ITargetable
{
    GameObject OverrideTarget { get; set; }
}

ITargetedCommand

Combines IProcess and ITargetable so lists of targeted commands can be typed without runtime checks:

public interface ITargetedCommand : IProcess, ITargetable { }

Targeted commands are used extensively in Octoputs, where drag/drop operations need to act on the object being dragged. The orchestrator sets OverrideTarget before calling Begin(), and the command's ResolveTarget() picks it up automatically.


How Octoputs Uses Processes

In Octoputs, processes drive the entire drag-and-drop lifecycle:

  • Attach phase -- A process animates the object to its attachment point
  • Detach phase -- A process animates the object away from its attachment point
  • Transfer -- AttachmentPointContainerCommand with TransferAttachablesOperation orchestrates moving objects between containers

Each phase is an IProcess. When a phase completes, the next one begins. This makes the animation and behavior sequence fully configurable in the Inspector without code changes.


Summary

flowchart TD
    IP["IProcess"] --> CMD["Command (base class)"]
    IP --> PC["ProcessContainer"]
    CMD --> CMDT["TargetedCommand&lt;TTarget, TOperation&gt;"]
    PC --> STC["SharedTargetCommands"]
    CMD -.->|holds| IOP["IOperation"]
    CMDT -->|implements| ITC["ITargetedCommand"]
    STC -->|implements| ITC
    ITC --> ITA["ITargetable"]
    ITC --> IP
  • Use Command to run operations in parallel with full lifecycle management
  • Use TargetedCommand\<TTarget, TOperation> when operations need a resolved target (the most common pattern)
  • Use ProcessContainer to orchestrate child processes in parallel
  • Use SharedTargetCommands to run multiple targeted commands on the same GameObject
  • Use IOperation to define units of work -- instant or timed -- that commands execute
  • All processes share the same Begin/Pause/Complete lifecycle, making them interchangeable