Action Over Time

v1.0

These actions are defined by HasDuration() = true.

Here is a typical pattern you can follow:

Only Actions inheriting from ActionOverTime / ActionOverTimeWithBaseValues should call yield return base.ExecuteInternal(); actions inheriting directly from Action, InstantAction, or ActionWithForceTimeUntilNextAction should not.

protected override IEnumerator ExecuteInternal()
{
    // CRITICAL: Call base first
    yield return base.ExecuteInternal();
    
    // Get valid targets
    var validTargets = GetValidLights(); // or GetValidTransforms(), etc.
    
    // Handle restart vs fresh execution
    List<Light> selectedTargets;
    if (IsExecutorRestart)
        selectedTargets = GetLastSelectedTargets(validTargets, l => l.GetInstanceID());
    
    if (selectedTargets == null || selectedTargets.Count == 0)
        selectedTargets = SelectTargets(validTargets, selectionMode, TargetCount);
    
    SetLastSelectedTargets(selectedTargets, l => l.GetInstanceID());
    
    // Animate each target over duration
    float elapsed = 0f;
    while (elapsed < duration)
    {
        float t = elapsed / duration;
        float curveValue = valueCurve.Evaluate(t);
        
        foreach (var target in selectedTargets.Where(t => t != null))
        {
            // Apply animation using curveValue
        }
        
        elapsed += Executor.Clock.DeltaTime;
        yield return null;
    }
}

What to override

ExecuteInternal()

✅ Yes (required)

Implement the over-time behavior

Must begin with yield return base.ExecuteInternal();

Tick(float t) (or equivalent per-frame hook)

⭕ Optional (recommended)

Apply the effect each frame

Use t as normalized 0→1 progress

ComputeEndValue() / BuildGoal()(if you have one)

⭕ Optional

Precompute target goal values

Prefer doing expensive work once at start

OnActionStart()

⭕ Optional

Initialize per-run state

Run-level setup that should happen on fresh start

OnRestart()

⭕ Optional

Reset per-run state on restart

Do not recache “base/original” values here

OnCleanupExecution()

⭕ Optional

Unsubscribe / cleanup transient state

Always remove event hooks, temp allocations, etc.

CanExecute()

⭕ Optional

Validate before execution begins

Return false to skip

Target interface methods (GetTargets etc.)

⭕ Optional

If action operates on targets

Use standard selection helpers

circle-info

If your action needs stable “base/original value” caching across restarts (like “end = base + offset”), use ActionOverTimeWithBaseValues instead.

Execution lifecycle (typical)

  1. Executor starts the action

  2. ExecuteInternal() begins

    ✅ yield return base.ExecuteInternal(); runs framework setup/bookkeeping

  3. Targets are resolved (if applicable)

  4. Action runs for Duration

    • each frame, compute normalized progress t

    • apply effect to targets

  5. Action completes

  6. OnCleanupExecution() runs

  7. Executor proceeds to next action

Critical Rules

  • ✅ Always call base first (ActionOverTime only):

    • This ensures duration bookkeeping, restart flags, cancellation, and executor coordination are correct.

  • ❌ Do not recache “base/original” values on restart.

    If you need base caching, use ActionOverTimeWithBaseValues.

  • ✅ Make restarts idempotent.

    If you subscribe to events in start logic, ensure you don’t double-subscribe on restart.

  • ✅ Prefer “compute once, apply many.”

    Precompute goal/end values at the start of execution and only apply interpolation per frame.

Minimal Example Template

Common mistakes (and how to avoid them)

  • ❌ Forgetting yield return base.ExecuteInternal();

    → Causes restart/loop bookkeeping issues and inconsistent behavior.

  • ❌ Computing expensive values every frame

    → Compute once at start, then only interpolate.

  • ❌ Using ActionOverTime for “base + offset” style actions

    → Use ActionOverTimeWithBaseValues to avoid offset drift after restarts.

Last updated