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.
- 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 —
TweenHandleis 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 handledirectly (resumes on complete and kill, never throws),ToAwaitable(), pooledToUniTask(), andWithCancellation(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), andTweenPathAtSpeeddoes 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
versionDefineswhen the packages are present; nothing to configure.
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
Try it: open
Assets/Scenes/TweenShowcase.unityand 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.
Shake / punch effects — seeded, stateless, always returning exactly to the base value:
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:
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:
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:
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.
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).
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);
}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());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-safeSequence.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.
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); // deterministictransform.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.
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.
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 glyphThe 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.
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(...);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 managedManagedSlot[](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.
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).
MIT







