Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tween System

Zero-allocation, struct-based tween engine for Unity. One engine, three authoring styles — a fluent code API with first-class async/await, serialized settings triggered from code, and fully no-code Inspector components with edit-mode preview. Pick per situation, mix freely.

Unity Tests License

What's inside

  • 0 B/frame in steady state — verified by allocation-asserting tests: creating, configuring, binding, updating, completing and killing tweens allocates nothing (after a one-time prewarm).
  • Struct-based, data-oriented core — tween state lives in contiguous dense arrays (blittable hot data / managed cold data split), pumped by a single loop injected into the PlayerLoop. No MonoBehaviour singleton.
  • Safe handles — TweenHandle is an id (storage + index + version). A stale handle is a silent no-op: double-Kill, controlling a finished tween, or holding a handle across scene loads can never touch the wrong tween or throw.
  • Async-first — await handle directly (resumes on complete and kill, never throws), ToAwaitable(), pooled ToUniTask(), and WithCancellation(ct) with thread-safe kill routing back to the main thread.
  • Re-entrancy safe — callbacks may create, kill or complete any tween (including the one currently updating); tombstone-then-compact removal makes this a supported, tested pattern.
  • Sequences are tweens — same storage base, so sequences get pause/kill/loops/yoyo/await for free; children are driven by an absolute-time playhead, scrub-safe even backwards through yoyo loops.
  • Deterministic stepping — UpdateGroup.Manual + Tween.ManualUpdate(dt) gives frame-exact control over time. The entire test suite runs in Edit Mode on this, and the inspector's edit-mode preview reuses the same mechanism.
  • Deterministic effects — shake/punch use seeded stateless hash noise: pure functions of time that end exactly at the base value, replay identically per seed, and scrub correctly inside sequences.
  • Constant-speed paths — Catmull-Rom splines edited in the Scene view, sampled by arc length: a linear ease is constant speed, any other ease is a speed profile.
  • Spring physics — damped-harmonic-oscillator tweens evaluated in closed form: no integration state, deterministic, scrub-safe, exact landing on the target, with optional initial velocity for momentum-preserving retargets.
  • Relative & speed-based — WithRelative() targets current + delta (rotations compose correctly, resolved after any start-from-current recapture); WithSpeed(unitsPerSecond) derives the duration from the actual travel distance (degrees/second for rotations), and TweenPathAtSpeed does the same from a path's arc length.
  • Group control — tag tweens with WithId(id) and kill/complete/pause/resume them in bulk (Tween.KillAll(id), PauseAll(id)…); Tween.PauseAll()/ResumeAll() freeze the world while unscaled UI tweens keep running.
  • TMP text effects — typewriter reveal (by duration or characters/second), a score/money counter that formats into a pooled char buffer (this library allocates no strings per frame), a per-character wave and a staggered pop-in reveal (per-glyph vertex scaling with back-ease overshoot) — all vertex effects are pure functions of time that land exactly on the base mesh.
  • Optional dependencies — UniTask and TextMeshPro integrations activate automatically via asmdef versionDefines when the packages are present; nothing to configure.

Install

Embedded in this project at Packages/com.tweensystem. For other projects, add via git URL:

https://github.com/berkencami/unity-tween-system.git?path=Packages/com.tweensystem

In action

Try it: open Assets/Scenes/TweenShowcase.unity and press Play — one cube per feature (move, scale, rotate, shake, punch, spring, sequence, path following), all driven by Tween Player / Tween Sequence Player components with no scene scripts.

Tween Player — no-code tweens: pick a property (transform, RectTransform, UI, shake/punch, path), configure, use Play On Enable + UnityEvents. The ▶ Preview button plays the tween in Edit Mode and restores the object afterwards.

Tween Player inspector

Edit-mode preview

Shake / punch effects — seeded, stateless, always returning exactly to the base value:

Shake and punch effects

Springs — motion from physics, not curves. The damping ratio decides the personality: below 1 it overshoots and oscillates, at 1 it settles as fast as possible without overshooting:

Spring tweens at three damping ratios

TMP text effects — a staggered per-character pop-in reveal (the title), a typewriter, a rolling score counter (formatted into a pooled char buffer, no per-frame strings on this side), and a per-character vertex wave — every vertex effect lands exactly on the base mesh:

TMP typewriter, counter and wave effects

Paths — lay out a Catmull-Rom spline by dragging waypoint handles in the Scene view (shift-click the curve to insert one), then follow it at constant speed:

Editing a Tween Path in the Scene view

A Tween Player following a path

For multi-step animations, the Tween Sequence Player builds an Append / Join / Insert / Interval timeline of clips entirely in the Inspector — with a color-coded timeline visualization (live playhead included) and the same edit-mode preview across every step's target. The result is a regular Sequence, so loops, delay, time scale and await all apply.

Tween Sequence Player inspector with timeline

Code

Basics

using TweenSystem;

transform.TweenPosition(target, 0.5f)
    .WithEase(Ease.OutBack)
    .OnComplete(this, static self => self.OnArrived());

transform.TweenLocalScale(Vector3.one * 2f, 0.3f);
canvasGroup.TweenAlpha(0f, 0.25f);
image.TweenFillAmount(1f, 0.5f);

// raw value tween — bind to anything
Tween.Value(0f, 1f, 2f)
    .WithEase(Ease.InOutSine)
    .WithLoops(-1, LoopType.Yoyo)                 // -1 = infinite
    .Bind(material, static (v, m) => m.SetFloat(GlowId, v));

// relative & speed-based
transform.TweenLocalPosition(new Vector3(0f, 2f, 0f), 1f).WithRelative(); // target = current + delta
transform.TweenPosition(target, 1f).WithSpeed(5f);   // duration = distance / 5 (deg/s for rotations)

// timing & control
handle.WithDelay(0.5f).WithUnscaledTime().WithTimeScale(2f).WithUpdateGroup(UpdateGroup.LateUpdate);
handle.Pause(); handle.Resume(); handle.Kill(); handle.Complete();
Tween.KillAll(target); Tween.CompleteAll();

// group control — tag with an id, control in bulk
const int UiTweens = 1;
canvasGroup.TweenAlpha(1f, 0.3f).WithId(UiTweens);
Tween.KillAll(UiTweens);            // also CompleteAll(id) / PauseAll(id) / ResumeAll(id)
Tween.PauseAll(); Tween.ResumeAll(); // global pause (sequence children follow their sequence)

Extensions cover Transform, RectTransform, CanvasGroup, Graphic/Image, SpriteRenderer, AudioSource, Camera and TMP — all with cached static appliers (no per-call delegate allocation).

Settings in the Inspector, trigger in code

public TweenSettings<Vector3> moveIn;   // duration, ease, loops, delay… all serialized
public TweenSettings<float> fadeOut;

void Show()
{
    transform.TweenLocalPosition(moveIn);   // code only supplies the target
    canvasGroup.TweenAlpha(fadeOut);
}

Zero-alloc callbacks

State is passed explicitly and the lambda is static — no closure, no allocation:

transform.TweenPosition(target, 1f)
    .OnUpdate(this, static self => self.OnMoved())
    .OnComplete(this, static self => self.OnArrived())
    .OnCancel(this, static self => self.OnInterrupted());

Async / await

await transform.TweenPosition(target, 1f);      // resumes on complete AND kill, never throws
await handle.ToAwaitable();                     // Unity Awaitable — kill cancels the await
await handle.ToUniTask(ct);                     // pooled IUniTaskSource, zero steady-state alloc
handle.WithCancellation(destroyCancellationToken); // token → Kill, thread-safe

Sequences

Sequence.Create()
    .Append(transform.TweenPosition(top, 0.5f).WithEase(Ease.OutCubic))
    .Join(sprite.TweenAlpha(1f, 0.5f))            // parallel with previous
    .AppendInterval(0.2f)
    .AppendCallback(static () => Debug.Log("!"))
    .Insert(0.1f, transform.TweenLocalScale(1.2f, 0.3f))
    .WithLoops(2, LoopType.Yoyo)
    .OnComplete(this, static self => self.Done());

A Sequence converts implicitly to TweenHandle, so every control/config/await operation applies.

Shake / Punch

transform.TweenShakePosition(duration: 0.6f, strength: 0.5f, frequency: 12f);
transform.TweenPunchScale(Vector3.one * 0.3f, 0.5f);
camera.transform.TweenShakeRotation(0.4f, new Vector3(2f, 2f, 0f), seed: 42); // deterministic

Springs

transform.TweenSpringPosition(target, 1f, frequency: 3f, damping: 0.5f); // bouncy
transform.TweenSpringScale(Vector3.one * 1.2f, 0.8f, damping: 1f);       // critically damped

// retarget mid-flight with the current velocity — natural, momentum-preserving handoff
transform.TweenSpringPosition(newTarget, 1f, velocity: currentVelocity);

// spring anything
SpringExtensions.SpringValue(0f, 1f, 0.5f).Bind(group, static (float v, CanvasGroup g) => g.alpha = v);

Springs are the closed-form solution of a damped harmonic oscillator — a pure function of time, like shake/punch. No integration state means zero-alloc, deterministic replay, correct scrubbing inside sequences, and an exact landing on the target. Ease is not applied: the physics is the easing.

Paths

transform.TweenPath(path, 3f);                       // constant speed, start to end
transform.TweenPathAtSpeed(path, 2.5f);              // 2.5 units/s — duration from arc length
transform.TweenPath(path, 3f, PathOrient.ToPath)     // face the direction of travel
    .WithEase(Ease.InOutSine)
    .WithLoops(-1, LoopType.Yoyo);

Sampling is arc-length normalized, so progress maps to distance along the curve. Path follows are zero-alloc in steady state (pooled follow state), and FollowPath is a property kind in the Tween Player and sequence steps like everything else. Waypoints live in the path object's local space — a path parented to a moving platform rides along with it; a path on the very object it moves freezes its world frame at play so it can't chase its own follower.

TMP text effects

label.TweenTypewriter(2f);                                  // reveal over 2 seconds
label.TweenTypewriterAtSpeed(charactersPerSecond: 30f);     // rate-based — duration from length
score.TweenCounter(0, 125000, 1.5f, thousandsSeparator: true); // rolls "0" → "125,000"
title.TweenCharWave(amplitude: 3f, duration: 2f)            // wave through the glyphs
    .WithLoops(-1);                                         // seamless — zero offset at both ends
title.TweenCharReveal(charDuration: 0.5f, charInterval: 0.05f); // staggered pop-in, per glyph

The counter formats into a pooled char buffer via SetCharArray (no per-frame string allocation on this side); the wave and the pop-in reveal rewrite glyph vertices from a cached snapshot as a pure function of time — like every effect here, they replay deterministically and land exactly on the base mesh. The reveal scales each character in from zero around its own center with a back-ease overshoot, staggered by character index — one tween animates the whole text (no per-character sequences or closures), so it scrubs and loops like anything else.

Custom adapters

Interpolate anything by implementing a struct adapter (devirtualized via generics — still zero-alloc):

public struct MyRadiusAdapter : ITweenAdapter<float, NoOptions>
{
    public float Evaluate(in TweenValues<float> v, in NoOptions o, float t)
        => Mathf.Sqrt(Mathf.Lerp(v.Start * v.Start, v.End * v.End, t)); // area-linear radius
}

Tween.Custom<float, MyRadiusAdapter>(1f, 4f, 1f).Bind(...);

Architecture

TweenHandle (id: storage, sparse index, version)
     │  O(1) validation — stale handles no-op
     ▼
TweenStorageBase                 ── sparse set, control ops, callbacks, async plumbing
 ├─ TweenStorage<TValue,TOptions,TAdapter>   ── dense hot loop, devirtualized adapters
 │    float/Vector2/3/4/Quaternion/Color × NoOptions
 │    Vector3/float × ShakeOptions / PunchOptions
 └─ SequenceStorage              ── absolute-time playhead driving owned children via ScrubTo
  • Hot/cold split: blittable TweenState[] + TweenValues<T>[] vs managed ManagedSlot[] (targets, delegates, curves, promises).
  • Update: PlayerLoop injection after script Update/LateUpdate/FixedUpdate.
  • Domain-reload-disabled safe: all statics reset on SubsystemRegistration.
  • Destroyed targets: Unity-object binds are fake-null-checked each apply and auto-killed.

Tests & benchmark

174 EditMode tests: handle safety, lifecycle, easing reference values, a dedicated re-entrancy suite, allocation asserts (Is.Not.AllocatingGCMemory), async/UniTask/cancellation, sequences (ordering, nesting, yoyo rewind, cascades), effect determinism, path sampling/following (constant speed, orient, pooling), spring physics (overshoot/critical damping, initial velocity, exact landing), and the authoring layer (settings application, TweenPlayer/sequence dispatch, zero-alloc play paths).

Samples~/Benchmark spawns 10k+ looping tweens with an on-screen GC/frame-time HUD (expected steady state: 0 B/frame).

License

MIT

About

Zero-allocation, struct-based tween engine for Unity — fluent API, async/await, inspector authoring

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages