JuiceBox
01 Setting Up
JuiceBox animates GameObjects through a component and a graph editor. The component holds the animation data. The graph editor is where you build it visually.
Example: First-Time Setup
- Select any GameObject in your scene (or create an empty one called AnimTarget).
- In the Inspector, click Add Component and search for JuiceBoxAnimation.
- With the GameObject still selected, open the graph editor via the menu bar: Window > JuiceBox > Sequence Editor.
The graph editor appears as a window with an empty canvas. The Inspector shows the JuiceBoxAnimation component with an empty Sequences list. Both views show the same data: the Inspector shows raw fields, the graph editor shows the visual layout. You will do most of your work in the graph editor.
The graph editor follows your selection. When you select a different GameObject that has a JuiceBoxAnimation, the editor switches to show that animation's sequences.
02 Sequences
A sequence is one lane of animation. It has a name and a chain of effects that run in order. The sequence's property type is determined automatically when you add the first effect, so you don't need to choose it up front.
The property type controls what kind of value the sequence animates:
| Type | What it is | Use it for |
|---|---|---|
| Float | A single number | Opacity, volume, health bar fill, any scalar |
| Vector2 | Two numbers | UI position, 2D movement |
| Vector3 | Three numbers | 3D position, scale, euler angles |
| Vector4 | Four numbers | Color channels, shader parameters |
| Quaternion | A rotation | 3D orientation with correct interpolation |
Example: Creating a Sequence
- Right-click the graph editor canvas and choose Add Sequence.
- A new filmstrip lane appears. Click the name field in the left cap and type FadeIn.
The filmstrip is empty: no effects yet. When you add your first effect (section 4), the sequence type locks to match it.
Give every sequence a descriptive but unique name. No two sequences can have the same name. You use the sequence name to start it from scripts and to save and load it to other GameObjects.
03 OnUpdate
OnUpdate is the delegate that writes the animated value to something in your scene every frame. Without it the sequence computes values internally but nothing visible happens. Always set OnUpdate.
The DelegatePicker lets you point OnUpdate at any compatible target on the GameObject, which includes public methods, public fields, and properties. For example, you can point a Vector3 sequence's OnUpdate directly at Transform.localPosition without writing any code.
Example: Animating a Transform's Position
- In the graph editor, look at the left cap of your sequence.
- Click the OnUpdate slot. The DelegatePicker opens.
- It lists components on the GameObject. Expand Transform.
- Select localPosition. The picker closes and the slot shows the binding.
Every frame the sequence runs, JuiceBox writes the current animated value into transform.localPosition.
Example: OnUpdate with a Custom Script
If you need to do something more than set a single field (say, fading a sprite's alpha), you can write a method on your own script and point OnUpdate at it:
using UnityEngine;
public class FadeController : MonoBehaviour
{
private SpriteRenderer _sr;
void Awake()
{
_sr = GetComponent<SpriteRenderer>();
}
public void SetAlpha(float value)
{
Color c = _sr.color;
c.a = value;
_sr.color = c;
}
}
Attach this script to the same GameObject, open the DelegatePicker for OnUpdate, find FadeController > SetAlpha, and select it.
The DelegatePicker shows every compatible public member on every component attached to the GameObject. Fields, properties, and methods all appear, so pick whichever suits the situation.
04 Tween
A Tween interpolates from a starting value to a target value over a fixed duration. It is the simplest effect type: give it a target, a duration, and optionally an easing curve.
The target value is set through a delegate: it reads from a field, property, or method on one of your scripts. This lets you change the target at runtime or share the same value across multiple effects.
A Tween is one of two closely related ways to move a value to a target; the other is Follow, covered in the next section. The defining trait of a Tween is that it always lands on its target in exactly the Duration you set, speeding up or slowing down along the way to stay on schedule. When the target does not move, that makes a Tween the easier of the two to define: you pick the value and the time, and JuiceBox handles the rest.
Example: Fading from 0 to 1 Over Two Seconds
First, add a field to hold the target value on a script attached to your GameObject:
public class FadeSettings : MonoBehaviour
{
public float targetAlpha = 1f;
}
Then set up the Tween:
- Right-click inside the FadeIn filmstrip and choose Add Effect > Tween.
- An effect node appears in the filmstrip. On the node, click the GetTargetValue slot.
- In the DelegatePicker, find FadeSettings > targetAlpha and select it.
- Set Duration to
2(seconds).
When this sequence plays, the float value goes from 0 to 1 over two seconds. The Tween reads targetAlpha from your script to know what value to interpolate toward.
Easing
By default a Tween interpolates linearly. You can change the shape of the interpolation by setting an easing function on the node. JuiceBox provides a built-in library:
| Family | Variants |
|---|---|
| Sine | SineIn, SineOut, SineInOut |
| Power | Pow2In/Out/InOut through Pow5In/Out/InOut |
| Elastic | ElasticIn, ElasticOut, ElasticInOut |
| Bounce | BounceIn, BounceOut, BounceInOut |
| Back | BackIn, BackOut, BackInOut |
Select an easing function from the Easing slot on the Tween node. The "In" variants start slow and accelerate, "Out" variants start fast and decelerate, and "InOut" variants do both.
For UI animations, Pow2InOut is a good default: it feels smooth and polished without being dramatic. For bouncy, playful motion, try BackOut or ElasticOut.
05 Follow
If a Tween commits to the clock, a Follow commits to a speed. Both do the same job, moving a value to a target, so it helps to think of them as two sides of one coin. The difference only matters when the target moves while the effect is running: a Tween keeps adjusting its rate to arrive on time anyway, while a Follow keeps chasing at a steady, more natural-looking speed and arrives when it arrives. So the choice comes down to one question: if the target shifts mid-animation, do you want the value to stay on schedule, or to move believably? When the target cannot move at all, either works, and a Tween is usually the simpler one to set up.
Which computes faster, Tweens or Follows? Tweens are slightly faster, but in practice the difference is so small that you would hardly be able to tell even with 10,000 of them running at once.
A Follow chases a target value at a given speed. Unlike a Tween, there is no fixed interpolation curve: the value moves toward the target each frame at the speed you set. This makes Follow useful for targets that move or values that need to arrive at varying speeds.
Like Tween, the target value comes from a delegate: a field, property, or method on one of your scripts.
Example: An Object Chasing a Moving Target
Write a script that exposes the target position:
public class ChaseSettings : MonoBehaviour
{
public Transform targetObject;
public Vector3 GetTargetPosition()
{
return targetObject.position;
}
}
Create a Vector3 sequence called ChaseTarget with OnUpdate bound to Transform.localPosition. Then:
- Right-click the filmstrip and choose Add Effect > Follow.
- Click the GetTargetValue slot. In the DelegatePicker, select ChaseSettings > GetTargetPosition.
- Set Speed to
5(units per second).
Each frame, the Follow reads the target's current position and moves toward it at speed 5.
End Conditions
A Follow needs to know when it is "done" so the next effect in the chain can start. There are three options:
- Time: stops after a fixed number of seconds regardless of whether it reached the target. This gives the Follow a fixed duration, similar to a Tween.
- Value in Range: stops when the current value is within a threshold of the target. Good for "close enough" arrival.
- Condition: stops when a custom delegate returns true.
Smoothing
By default a Follow moves in a straight line at constant speed and stops abruptly. To get smooth acceleration and deceleration, connect a SmoothingNode:
- Right-click the canvas near the Follow node and choose Add Smoothing Node.
- Drag an edge from the SmoothingNode's output port to the Follow node's Smoothing input.
- Adjust Frequency (how fast the spring responds) and Damping Ratio (how quickly oscillations die out).
A damping ratio below 1.0 will overshoot and oscillate. At 1.0 it arrives smoothly without overshoot. Above 1.0 it arrives sluggishly.
Frequency 3-5 and Damping Ratio 0.7-1.0 is a good starting range for UI elements. Lower frequency with damping ratio around 0.3 gives a springy, playful feel.
06 Shake
A Shake oscillates around the starting value using a periodic waveform. It's ideal for camera shake, UI jitter, hit reactions, or any vibration effect. Shake does not chase a target: it always returns to where it started. An optional easing curve controls how the intensity decays over the duration.
Example: Camera Shake on Impact
Suppose you want the camera to shake when the player takes damage. Create a Vector3 sequence called CameraShake with OnUpdate bound to a script that offsets the camera's local position. Write a settings script:
public class ShakeSettings : MonoBehaviour
{
public Vector3 shakeOffset;
public void ApplyShake(Vector3 value)
{
shakeOffset = value;
}
}
- Right-click the filmstrip and choose Add Effect > Shake.
- The Shake node shows a Waveform dropdown (Sine, Triangle, Square, Sawtooth) and a Hz field for oscillation frequency. Set the waveform to
Sineand Hz to12. - Set the Amp (amplitude) fields. For a subtle horizontal shake, set X to
0.1, Y to0.05, Z to0. - Set Duration to
0.5seconds. This is how long the shake lasts before the effect ends. - Set Easing to
Pow2In. The easing curve controls how the shake intensity decays: Pow2In starts strong and fades out smoothly.
Duration and Easing
The Duration field controls how long the shake runs. Toggle Inf for a continuous shake that never ends on its own (useful for idle vibrations or engine hum). When Duration is set to Inf, the Easing row is disabled: there's no finite duration to decay over.
When a duration is set, the Easing curve shapes the intensity envelope. Without easing, the shake runs at full amplitude for the entire duration and stops abruptly. With easing, the amplitude is modulated by the derivative of the easing curve, producing a natural fade.
07 Effect Chaining
Effects in a sequence run one after another, left to right in the filmstrip. When one effect's end condition is met, it finishes and the next effect starts. The next effect picks up from whatever value the previous one left off at.
Example: Fade In, Hold, Fade Out
Using the FadeIn float sequence, set up a script with two fields:
public class FadeSettings : MonoBehaviour
{
public float fullyVisible = 1f;
public float fullyHidden = 0f;
}
- You already have a Tween with GetTargetValue bound to
fullyVisibleand Duration set to 2 seconds (fade in). - Right-click the filmstrip to the right of the first Tween and add a second Tween. Bind GetTargetValue to
fullyVisibleand set Duration to1. Because the value is already at 1 and the target is 1, this Tween holds the value steady for one second. - Add a third Tween. Bind GetTargetValue to
fullyHiddenand set Duration to2. This fades back out.
The sequence now runs three effects in order: fade in over 2s, hold for 1s, fade out over 2s. Each effect inherits the value the previous one ended at.
You can mix effect types in a chain. A Tween followed by a Follow followed by a Shake is perfectly valid: the value flows continuously from one effect to the next.
08 Tweening UI Elements
Animating Canvas UI uses the same Tween and effect machinery as the rest of this guide. The only thing that changes is which member OnUpdate writes to. UI lives on a RectTransform, so you drive anchoredPosition and sizeDelta rather than transform.position, and you fade through a material or text color rather than a SpriteRenderer.
You rarely need to write any code for this. JuiceBox ships a set of ready-made UI setters and getters in the built-in StandardFunctions class, registered by default, so they appear in the DelegatePicker's Static section as soon as you open a slot.
Common UI Targets
| What you want | Point OnUpdate at | Value type |
|---|---|---|
| Move a panel (both axes) | RectTransform.anchoredPosition | Vector2 |
| Move one edge | SetRectTransformLeft / Right / Top / Bottom | float |
| Resize | SetRectTransformWidth / SetRectTransformHeight | float |
| Tint | SetMaterialColor / SetTextColor | Vector4 (Vector3 for RGB) |
| Fade | SetMaterialAlpha / SetTextAlpha | float |
Position is the one row that needs no helper: anchoredPosition is an ordinary Vector2 property on RectTransform, so the DelegatePicker lists it directly under the component. The edge, size, color, and fade rows are JuiceBox.StandardFunctions entries under Static, and each setter has a matching getter (GetRectTransformWidth, GetMaterialAlpha, and so on) for reading the current value. To fade a whole panel at once instead of a single graphic, point OnUpdate at CanvasGroup.alpha, which is also a plain float property.
Example: Slide a Panel In While Fading It Up
This drives one panel with two sequences: one slides it on screen, the other fades it from clear to solid. In the editor, park the panel at its off-screen start position with an alpha of 0; the tweens carry it the rest of the way.
- Select the panel, add a JuiceBoxAnimation, and add a sequence for the slide.
- Add a Tween effect. Click the sequence's OnUpdate slot, expand RectTransform in the DelegatePicker, and select anchoredPosition. The sequence type locks to Vector2.
- Set the Tween's target to the on-screen position, the same way you set a target in section 4, and choose an easing such as
Pow2InOut. - Right-click the canvas, choose Add Sequence for the fade, and add a Tween to it. Click OnUpdate, switch to the Static section, and select JuiceBox.StandardFunctions > SetMaterialAlpha. The sequence type locks to float.
- Set that Tween's target to
1.
Start both sequences together from script and the panel slides and fades in at once. Running more than one sequence on a single object is covered in section 12.
Three UI habits worth keeping: drive anchoredPosition and sizeDelta, never transform.position; fade through SetMaterialAlpha or SetTextAlpha for one graphic, or CanvasGroup.alpha for a whole panel; and reach for Pow2InOut as a good default easing for UI motion.
09 HookNodes: OnStart and OnDone
Every effect has OnStart and OnDone signal slots. These fire once when an effect begins and once when it finishes. They call parameterless methods: they don't pass the animated value, they just notify your code that something happened.
To use them, you connect a HookNode to the effect's output port.
Example: Playing a Sound When a Tween Finishes
Suppose you have a script with a parameterless method:
public class AudioTrigger : MonoBehaviour
{
public AudioSource clip;
public void PlaySound()
{
clip.Play();
}
}
- On the effect node in the filmstrip, find the OnDone output port (small circle on the bottom of the node).
- Drag an edge from the OnDone port into empty space below the filmstrip. A HookNode appears automatically.
- Click the HookNode. The DelegatePicker opens.
- Find AudioTrigger > PlaySound and select it.
When the Tween reaches its duration and finishes, JuiceBox calls PlaySound() once.
OnStart works the same way: drag from the OnStart port, wire a HookNode, and pick a method. It fires once at the moment the effect begins running.
10 Looping
A LoopNode makes a sequence restart from the beginning when it reaches the end of its effect chain. Without a loop, the sequence plays once and stops.
Example: An Endlessly Pulsing Glow
Create a Float sequence called Pulse with two Tweens: one targeting your fullyVisible field over 1 second and one targeting fullyHidden over 1 second (a fade up / fade down cycle). Then:
- Right-click the filmstrip and choose Add Loop Node.
- The LoopNode appears at the end of the filmstrip. The sequence will now restart from the first effect after the last effect finishes.
The pulse now runs forever. Each cycle plays the full effect chain (fade up, fade down), then loops back to the start.
11 Relative Delegates
The DelegatePicker doesn't just show components on the current GameObject; it also lets you pick targets on other GameObjects in the scene hierarchy. When you do this, JuiceBox stores the reference as a relative path (like "the child named Model" or "the parent's sibling named Light") rather than a direct object reference.
This is what makes JuiceBox animations work with prefabs. Because the path is relative, every instance of the prefab resolves to its own children automatically. As long as the GameObjects maintain the same names and relative positions in the hierarchy, the animation just works.
Example: Animating a Child Object
Say your hierarchy looks like this:
Enemy ← JuiceBoxAnimation is here
└─ Model ← MeshRenderer with your script is here
You want the sequence on Enemy to call a method on a script attached to Model.
- Open the DelegatePicker for any slot (OnUpdate, GetTargetValue, etc.).
- Instead of picking a target on the current GameObject, select the Model child object from the picker.
- The picker updates to show components on Model. Select the method or field you want.
JuiceBox stores this as a relative reference: "the child named Model." If you instantiate the Enemy prefab ten times, each instance's animation finds its own Model child.
Relative vs. Absolute References
The DelegatePicker defaults to storing references as relative whenever there is a parent/child relationship between the JuiceBoxAnimation's GameObject and the target. If there is no hierarchical relationship (for example, two unrelated objects in the scene), the reference is stored as absolute.
You can override this with the Absolute checkbox in the picker. An absolute reference won't break if you rename the target GameObject or move it around in the hierarchy, but it works poorly with shared sequences and prefabs: the reference points to one specific object in the scene rather than resolving by path.
Stick with relative references whenever possible. They are what make animations portable across prefab instances and loadable onto new GameObjects via snapshots.
12 Multiple Sequences
A single JuiceBoxAnimation can hold multiple sequences. Each sequence runs independently: it has its own property type, its own effect chain, and its own OnUpdate. This lets you coordinate complex animations on one object.
Example: Position and Rotation Together
- On a GameObject that already has a JuiceBoxAnimation with a Vector3 movement sequence, right-click the graph editor canvas and choose Add Sequence.
- Name it SpinUp.
- Add a Quaternion Tween effect to it. The sequence type locks to Quaternion.
- Wire OnUpdate to
Transform.localRotation.
When you start both sequences from script, they run in parallel: the object moves and rotates at the same time. Each sequence manages its own value independently.
Use multiple sequences whenever different aspects of an animation have different timing or different property types. Position (Vector3) and rotation (Quaternion) are a natural split. Scale (Vector3) might be a third. Each can have its own easing, duration, and loop behavior.
13 Combining Sequences
Sometimes you want two sequences to control different parts of the same value. For example, one sequence controls horizontal movement (X and Z) while another controls vertical movement (Y). JuiceBox provides StandardFunctions, a built-in library of helper methods that map sequence outputs to specific axes of a transform.
Example: Separate Horizontal and Vertical Movement
You want to animate an object's X/Z position with one sequence (for ground movement) and its Y position with another (for jumping). The two sequences have different timing and different effect types, but they both write to the same transform.position.
- Create a Vector2 sequence called GroundMove.
- For its OnUpdate, open the DelegatePicker and find StandardFunctions > SetPositionXZ. This writes the Vector2's X to position.x and Y to position.z, leaving position.y untouched.
- Create a Float sequence called Jump.
- For its OnUpdate, wire it to a script method that sets only
transform.position.y.
Now the two sequences can run simultaneously without fighting over the transform. GroundMove handles the horizontal plane and Jump handles the vertical axis. Each StandardFunctions method writes only the axes it names, leaving the others at their current value: this is what makes it safe to have multiple sequences writing to the same transform.
14 Playing and Stopping from Script
Auto-Start vs. Manual Trigger
By default, sequences are set to start automatically when the GameObject becomes active (OnEnable). This means the animation plays as soon as the object is enabled in the scene, no script needed.
If you want full control over when a sequence starts, change the trigger dropdown on the sequence to Manual. A manual sequence does nothing until you start it from code.
Runtime API
JuiceBoxAnimation exposes a simple API for controlling playback. Every method has two overloads: one takes a sequence index (int), the other takes the sequence name (string).
| Method | What it does |
|---|---|
| StartSequence(int) | Start a sequence by index |
| StartSequence(string) | Start a sequence by name |
| Stop() | Stop all sequences |
| Stop(int) | Stop a sequence by index |
| Stop(string) | Stop a sequence by name |
| Pause() / Pause(int) / Pause(string) | Pause all, by index, or by name |
| Resume() / Resume(int) / Resume(string) | Resume after pausing |
| IsPlaying(int) / IsPlaying(string) | Returns true if the sequence is running |
Example: Starting an Animation from an Input Action
using UnityEngine;
using UnityEngine.InputSystem;
using JuiceBox;
public class AnimationLauncher : MonoBehaviour
{
[SerializeField] private InputAction startAction;
[SerializeField] private InputAction stopAction;
private JuiceBoxAnimation _anim;
void Awake()
{
_anim = GetComponent<JuiceBoxAnimation>();
startAction.Enable();
stopAction.Enable();
}
void Update()
{
if (startAction.WasPressedThisFrame())
_anim.StartSequence("FadeIn");
if (stopAction.WasPressedThisFrame())
_anim.Stop();
}
}
Triggering the start action begins the "FadeIn" sequence. Triggering the stop action stops everything.
Use the string overloads for readability. Use the int overloads in tight loops where you want to avoid the name lookup.
15 Saving and Loading Snapshots
JuiceBox automatically saves snapshots of your sequences as you work. Snapshots are stored outside the scene file, so you can restore a previous version of a sequence if you make a mistake or want to try a different approach.
Example: Restoring a Previous Version
- In the graph editor, look at the left cap of the sequence you want to restore.
- Click Restore from Snapshot. A window opens listing all saved snapshots for that sequence, ordered by time.
- Click a snapshot to restore it. The sequence reverts to that saved state.
Snapshots are keyed by the sequence name. When you rename a sequence, the snapshots transfer to the new name. The old name becomes available for a new sequence.
If you don't want to produce any snapshots for a sequence, just don't give the sequence a name. Unnamed sequences are skipped by the snapshot system.
Loading a Snapshot as a New Sequence
You can also pull a snapshot into the current animation as a brand new sequence:
- Right-click the graph editor canvas and choose Load Sequence.
- A window lists all snapshot sequence names across your entire project.
- Click a name to load its most recent snapshot as a new strip in the current animation.
16 Copying Sequences to Other Animations
Snapshots double as a transport mechanism. Because they are stored globally by sequence name, any JuiceBoxAnimation in your project can load any snapshot.
Example: Reusing a Fade Sequence on Multiple Objects
- Build your "FadeIn" sequence on one object and make sure it has been saved (snapshots happen automatically as you edit).
- Select a different GameObject and add a JuiceBoxAnimation component.
- Open the graph editor and right-click the canvas.
- Choose Load Sequence and select FadeIn.
- The sequence appears as a new strip with all its effects, easing, and hook configurations intact.
- Wire up OnUpdate to the new object's components.
The loaded sequence is a full independent copy: editing it does not affect the original. OnUpdate needs to be rewired because it pointed to the original object's components.
If you use relative delegates throughout (section 11), a loaded sequence may already resolve correctly on the new object, provided the hierarchy structure and child names match. This makes relative delegates especially valuable for reusable animation templates.
17 Custom Static Functions
Every slot you have wired so far (OnUpdate, OnStart, OnDone) has pointed at a member of a component sitting on the target GameObject. Static functions lift that restriction. A public static method belongs to no instance, so it can route an effect's signal straight into a project-wide manager, an event bus, an object pool, or a third-party plugin from the Asset Store: anything that does not happen to be a component on the animated object.
That makes static functions the natural way to build reusable custom hooks. Write them once in a static class, register that class with JuiceBox, and they appear in the DelegatePicker's Static section for every animation in the project.
Writing the Class
There are only three rules: methods must be public static, they must be declared directly on the class (inherited statics are ignored), and the class must be registered (next step). Nested classes are allowed: refer to one later with a +, for example MyGame.JuiceHooks+Audio.
using UnityEngine;
namespace MyGame
{
public static class JuiceHooks
{
// Parameterless hook: valid for OnStart and OnDone.
public static void NotifyComplete()
{
GameEvents.AnimationFinished();
}
// GameObject-first hook: JuiceBox supplies the animated object.
public static void PlayHitEffect(GameObject target)
{
ImpactSystem.Spawn(target.transform.position);
}
}
}
The second method shows the most useful convention: if the first parameter is a GameObject, JuiceBox fills it with the animation's target object when the hook fires. That one parameter is what lets a static function act on the right object without being attached to it, which is ideal for dispatching into your own systems or an external plugin.
Registering the Class
- Let Unity finish compiling. A class only becomes selectable once it exists and has at least one
public staticmethod. - Open the graph editor and click the gear icon in the toolbar to open JuiceBox Settings.
- Under Additional static method classes, pick the assembly and then the type from the two dropdowns, and press +.
- Press Apply. The class is saved with the project (shared through version control) and the picker refreshes.
The built-in StandardFunctions entries always sit at the top of that list and cannot be removed; your own classes appear below them, each with a remove button.
Example: A Custom OnDone Hook
- With MyGame.JuiceHooks registered, drag an edge from an effect's OnDone port into empty space to spawn a HookNode (see section 9).
- Click the HookNode to open the DelegatePicker, then switch to the Static section.
- Find MyGame.JuiceHooks > PlayHitEffect and select it.
When the effect finishes, JuiceBox calls PlayHitEffect(go) with the animation's GameObject. NotifyComplete appears in the same list and would be called with no arguments.
The same class can serve more than hooks. A static method matches OnUpdate when it returns void and takes the sequence's value type (void Method(float) or void Method(GameObject, float)) and matches a getter such as GetTargetValue when it returns that value type. One static class can hold a whole library of reusable setters, getters, and hooks for your project.
If it is ever unclear how to shape one of these methods, the built-in StandardFunctions class is full of working getters and setters to model your own on. Read it for reference, but keep your own functions in your own class rather than editing StandardFunctions directly: edits there work, but they are overwritten every time you update JuiceBox to a newer version.
