Common Logic & Patterns

v1.0

Any Action which targets a specific type of object or component can utilize common structures and patterns. We believe this helps provide end users with a logical understanding of what Actions can and do, and provide flexible options that increase the value of individual Actions.

Target Interface Pattern

These interfaces include, but aren't limited to: ILightTargets, ITransformTargets, IRendererTargets, IGameObjectTargets, etc.

The target interface patterns provide a standardized multi-target selection system that ensures consistent behavior and UX across all Actions. They enable users to configure a list of targets in the Inspector, choose how targets are selected (Sequential, Random, or Random No Repeat), and optionally fall back to Executor.Target when no targets are specified.

Under the hood, the interfaces leverage shared base class helpers that handle selection logic, restart coordination (remembering which specific targets were chosen), per-target state management via Blackboard keys, and parallel execution of animations across multiple targets simultaneously.

This architecture eliminates code duplication, ensures predictable restart behavior when actions are interrupted, and provides type-safe access to component-specific properties—all while maintaining a familiar, unified Inspector experience regardless of whether you're animating Lights, Transforms, Renderers, or any other Unity component types or objects.

When writing custom Actions, we strongly suggest you implement one of these interfaces, when applicable.

Blackboard Value Caching

Many actions need to remember values between frames, between restarts, and (sometimes) between different targets. Juicy Actions uses a Blackboard to store this state in a structured way.

The most common pitfall: caching a “base/original value” into the wrong scope, causing offsets to be applied inconsistently (especially after restarts or looped execution).

Key scopes (what they mean)

Think of Blackboard keys as belonging to one of these scopes:

  1. Action-scoped keys

    • Shared by the action asset while it is executing.

    • Useful for state that should be shared across all targets within a single action instance.

    • Avoid for anything that must be unique per target.

  2. Target-scoped keys

    • Unique per target object (or per target-id) for this action.

    • Use this for base/original values and for any per-target state that must survive restarts.

  3. Execution-scoped keys

    • Namespaced to a particular execution/run (often via an execution id).

    • Great for temporary values you don’t want to collide across runs.

    • Important: execution-scoped keys are not guaranteed to be automatically purged. They are “disposable by design,” but you should not store long-lived or persistent values here.

Rule of thumb:

Base/original values → Target scope.

Temporary per-run scratch values → Execution scope.

Shared per-action run values → Action scope.

Base/original value (position/rotation/scale/etc.)

Target

Must be stable across restarts and loops; must not be overwritten mid-motion

“ShouldStop” / cancellation flag

Target (or Execution if truly per-run)

Needs to be readable by the running coroutine and any restart logic

Start time / elapsed time / per-run timers

Execution

Per-run only; should not bleed into another execution

“HasInitialized” / “HasBase” style flags

Target

Usually tied to a target’s cached base values

Debug counters, scratch intermediates, temporary lerp values

Execution

Prevent collisions across runs; OK if discarded

Critical rules for base/original values

If your action uses a “base/original value” concept (ex: “endRotation = baseRotation + offset”):

  1. Cache base values exactly once per target per ‘fresh run’.

    • Fresh run = first time this target is acted on for this action’s current setup.

    • Restarts / loops must not overwrite base values.

  2. Never store base values using execution-scoped keys.

    Execution-scoped keys are tied to a specific run and may be re-created each restart. Even if they persist in memory, their purpose is not long-term stability.

  3. Never store base values using action-scoped keys unless the action is guaranteed single-target.

    Action-scoped keys will be shared across targets and can cross-contaminate.

  4. Use a “HasBase” or equivalent flag per target.

    Your action should be able to detect whether the base value is already cached for this target and skip recaching on restart.

Logging System & Best Practices

The Action Executor Inspector has a checkbox to enable logging.

All Actions inherit context-aware logging methods (Log(), LogWarning(), LogError(), LogException()) that automatically include the action's display name and executor context in console output.

Use Log() sparingly for actionable debugging insights—such as when selection logic picks a specific target, when a cached base value is being restored, or when a non-obvious code path executes. These will only display when the enableLogging is on.

Reserve LogWarning() for recoverable issues that users should know about, like "No valid Lights found" when target lists are empty or when fallback logic is activated.

Use LogError() for critical failures that prevent the action from functioning correctly, such as missing required components or invalid configuration.

Never log during normal successful execution paths (e.g., don't log every frame of animation), and avoid logging in tight loops or per-target iterations—instead, summarize results at the action level.

This approach keeps the console clean during normal gameplay while providing powerful diagnostic information when needed.

Simple Mode

Actions can often do many things, making them extremely versatie. However, some may benefit from a "Simple Mode" which only exposes the fields that are most commonly used by devs. To enable the option for a "Simple Mode", the Action class must override simpleModeDescription, and mark some fields as SimpleOverride.

Not all Actions will need a Simple Mode. If that is the case, simply skip this step entirely, and all fields with the CanOverride or MustOverride attributes will be displayed by default. SimpleOverride attribute will also show, but generally those are only used when Simple Mode is intended to be used.

If you are using the Action Validator, you can add an IGNORE string to your code to skip warnings about Simple Mode.

circle-check

Last updated