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¶
ITargetedCommand¶
Combines IProcess and ITargetable so lists of targeted commands can be typed without runtime checks:
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 --
AttachmentPointContainerCommandwithTransferAttachablesOperationorchestrates 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<TTarget, TOperation>"]
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