DOTween has been the default answer for Unity tweening for years, and it is still a very strong library. But for a new code-driven project, I would no longer choose it automatically.
After using LitMotion recently, I found its API especially comfortable: Transform values, UI values, and custom values all follow the same Create -> With -> Bind pattern. LitMotion v2 also adds Sequence support, Inspector editing through LitMotion.Animation, debugging tools, and improved cancellation/control APIs.
This article compares LitMotion, PrimeTween, and DOTween, then shows the parts of LitMotion I consider important in production: setup, MotionHandle, object lifetime, Time.timeScale, Sequence, UniTask, Punch/Shake, TextMeshPro, and migration design.
The version information here was checked on July 28, 2026. Recheck release pages if you read this much later.
My recommendation
| Situation | Recommendation |
|---|---|
| New project, animation logic mainly in C# | LitMotion |
| Very sensitive to tween CPU/GC cost | Benchmark LitMotion and PrimeTween in your real workload |
Prefer Tween.Position()-style target APIs |
PrimeTween |
| Want short Inspector-edited animations | Evaluate LitMotion.Animation, PrimeTween PRO, and DOTween Pro |
| Existing project already uses DOTween heavily | Usually keep DOTween |
| Third-party assets depend on DOTween | Keep DOTween, or strictly separate ownership |
For a new code-first project, LitMotion is currently my first candidate.
It emphasizes low-allocation tween creation, uses the C# Job System and Burst, supports arbitrary values, and keeps a consistent API. It is MIT licensed and supports Unity 2021.3 or later.
That does not mean your whole animation path becomes allocation-free. Capturing lambdas, strings, LINQ, async code, Canvas rebuilds, and whatever you do in the binding still matter.
At the research date, LitMotion's latest release was v2.0.2. PrimeTween was also actively maintained, so I consider it a serious alternative rather than a secondary option.
LitMotion, PrimeTween, and DOTween
LitMotion
LitMotion is a data-oriented tween library built around LMotion.
LMotion.Create(0f, 1f, 0.3f)
.WithEase(Ease.OutCubic)
.Bind(x => alpha = x);
Create a value transition, add options, then bind it to a target. The same structure works for Transform, UI, TextMeshPro, and your own values.
LitMotion v2 adds LSequence, LitMotion.Animation, TryCancel() / TryComplete() style handle control, and LitMotion Debugger.
PrimeTween
PrimeTween is also designed around modern, low-allocation usage, but its API feels more target-oriented.
Tween.PositionY(
transform,
endValue: 10f,
duration: 1f,
ease: Ease.InOutSine);
Starting from Tween. is very discoverable in an IDE. If you prefer choosing a target operation first, PrimeTween may feel more natural than LitMotion.
DOTween
DOTween remains excellent when you already have production code and tools around it.
transform.DOMoveX(10f, 0.5f)
.SetEase(DG.Tweening.Ease.OutCubic);
It has a mature ecosystem, many shortcuts, strong Sequence support, and DOTween Pro. If your project already depends on those strengths and profiling shows no problem, migration can easily cost more than it saves.
What about MagicTween?
MagicTween's repository was archived in 2025 and points users toward LitMotion. I would not choose MagicTween for a new project.
Do not choose from a benchmark alone
The author's public TweenPerformance project shows strong LitMotion results in its tested environment, including very low allocation during tween creation.
But benchmark versions differ from the current libraries, the environment is specific, and real games also pay for bindings, Canvas work, closures, strings, async state machines, materials, and game-specific logic.
Use benchmarks as a hint. Make the final decision from a Player build on your target hardware.
Installing LitMotion
For LitMotion v2.0.2, the package requirements are:
- Unity 2021.3+
- Burst 1.6.0+
- Collections 1.5.1+
- Mathematics 1.0.1+
Add the package through Unity Package Manager. In a team project, pin a tag or commit.
https://github.com/annulusgames/LitMotion.git?path=src/LitMotion/Assets/LitMotion#v2.0.2
The ?path=... part comes before #v2.0.2.
Typical namespaces are:
using LitMotion;
using LitMotion.Extensions;
UniTask integration requires com.cysharp.unitask. TextMeshPro integration requires TMP. If you use custom asmdefs, verify references to LitMotion, LitMotion.Extensions, UniTask, and Unity.TextMeshPro as needed.
The snippets in this article are based on LitMotion v2.0.2's public API and source. I would still compile representative examples in a minimal Unity project before adopting them as team standards.
The core pattern: Create, configure, Bind
LMotion.Create() returns a builder. The motion becomes useful when you configure it and finally bind it.
LMotion.Create(0f, 1f, 0.3f)
.WithEase(Ease.OutCubic)
.Bind(x => alpha = x);
For a Transform:
LMotion.Create(target.position, destination, 0.4f)
.WithEase(Ease.OutCubic)
.BindToPosition(target)
.AddTo(target.gameObject);
For an arbitrary value:
LMotion.Create(volume, 1f, 0.5f)
.WithEase(Ease.Linear)
.Bind(x => volume = x)
.AddTo(gameObject);
A capturing lambda can allocate. If this is a hot path, use a state-passing overload instead:
LMotion.Create(volume, 1f, 0.5f)
.Bind(this, (x, self) => self.volume = x)
.AddTo(gameObject);
For built-in properties, dedicated BindTo...() helpers are usually clearer.
Ease, delay, and loops
Builder options compose naturally:
LMotion.Create(0f, 1f, 0.25f)
.WithDelay(0.1f)
.WithEase(Ease.OutBack)
.Bind(x => value = x);
List entrance effects are easy to stagger:
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
LMotion.Create(0f, 1f, 0.25f)
.WithDelay(i * 0.04f)
.WithEase(Ease.OutCubic)
.BindToLocalScaleXYZ(item)
.AddTo(item.gameObject);
}
For infinite loops:
LMotion.Create(0.95f, 1.05f, 0.5f)
.WithLoops(-1, LoopType.Yoyo)
.BindToLocalScaleXYZ(transform)
.AddTo(gameObject);
-1 means the motion does not end by itself, so always provide a lifetime through object destruction, a stored handle, or cancellation.
MotionHandle: stop old animations before starting new ones
Bindings return a MotionHandle.
public void Replay()
{
handle.TryCancel();
handle = LMotion.Create(0f, 1f, 0.4f)
.WithEase(Ease.OutCubic)
.Bind(x => value = x);
}
Use TryComplete() when you want to apply the final value instead of stopping at the current one.
The bigger design issue is property ownership. If open/close, hover, selection, and click feedback all write the same Scale or Alpha, they can fight each other even if every individual tween is valid.
private MotionHandle fadeHandle;
public void SetVisible(bool visible)
{
fadeHandle.TryCancel();
fadeHandle = LMotion.Create(
canvasGroup.alpha,
visible ? 1f : 0f,
0.2f)
.WithEase(Ease.OutCubic)
.BindToAlpha(canvasGroup)
.AddTo(gameObject);
}
Starting from the current value also avoids a jump when the animation reverses halfway through.
AddTo(gameObject) and CancellationToken are different tools
For UI and temporary objects, I normally tie a motion to its owner:
LMotion.Create(0f, 1f, 1f)
.Bind(x => value = x)
.AddTo(gameObject);
AddTo(gameObject) cancels the motion when the GameObject is destroyed. It is lifetime ownership, not async-flow control.
LitMotion v2.0.2's ToUniTask(token) uses CancelBehavior.Cancel with cancelAwaitOnMotionCanceled: true. If you want the intent visible in code, write it explicitly:
await handle.ToUniTask(
CancelBehavior.Cancel,
cancelAwaitOnMotionCanceled: true,
cancellationToken: token);
If token cancellation should move the motion to its end value, use CancelBehavior.Complete.
Decide whether a screen token, GameObject lifetime, explicit handle, or caller token owns the animation. Do not add all of them blindly and assume they mean the same thing.
UI animation while Time.timeScale == 0
Pause menus often still need animation after gameplay time is stopped.
LMotion.Create(0f, 1f, 0.2f)
.WithScheduler(MotionScheduler.UpdateIgnoreTimeScale)
.WithEase(Ease.OutCubic)
.BindToAlpha(canvasGroup)
.AddTo(gameObject);
Useful schedulers include normal Update, UpdateIgnoreTimeScale, FixedUpdate, and the PreLateUpdate / PostLateUpdate families.
The scheduler also changes ordering relative to other scripts, so verify camera, layout, and physics interactions instead of treating this as only a time-scale switch.
Sequence for fixed timelines
LitMotion v2 provides LSequence:
var sequenceHandle = LSequence.Create()
.Append(
LMotion.Create(0f, 1f, 0.25f)
.WithEase(Ease.OutCubic)
.BindToAlpha(canvasGroup))
.Join(
LMotion.Create(0.9f, 1f, 0.25f)
.WithEase(Ease.OutBack)
.BindToLocalScaleXYZ(panel))
.AppendInterval(0.15f)
.Append(
LMotion.Create(
panel.localPosition.y,
panel.localPosition.y + 20f,
0.2f)
.WithLoops(2, LoopType.Yoyo)
.BindToLocalPositionY(panel))
.Run()
.AddTo(gameObject);
Append() is sequential, Join() is parallel, Insert() places a motion at a specific time, and AppendInterval() adds a wait. Run() starts the sequence.
If initial values are being applied too early while assembling a sequence, consider WithImmediateBind(false).
Sequence is best for a fixed timeline. Input waits, network waits, branching, and complex cancellation are usually clearer in async code or a state machine.
UniTask: await handle and ToUniTask() do not cancel the same way
LitMotion supports directly awaiting a MotionHandle, but in v2.0.2 await handle; resumes on either completion or cancellation, and MotionAwaiter.GetResult() does not throw.
If you need CancellationToken integration or want motion cancellation to propagate as async cancellation, use ToUniTask().
A UI animation that can be triggered repeatedly can share one linked CTS so the next play request cancels the previous one:
private CancellationTokenSource playCts;
public async UniTask PlayAsync(CancellationToken cancellationToken)
{
playCts?.Cancel();
var currentCts = CancellationTokenSource
.CreateLinkedTokenSource(cancellationToken);
playCts = currentCts;
var token = currentCts.Token;
try
{
var fade = LMotion.Create(canvasGroup.alpha, 1f, 0.25f)
.BindToAlpha(canvasGroup)
.AddTo(gameObject);
var scale = LMotion.Create(panel.localScale.x, 1f, 0.3f)
.WithEase(Ease.OutBack)
.BindToLocalScaleXYZ(panel)
.AddTo(gameObject);
await UniTask.WhenAll(
ToCancelableUniTask(fade, token),
ToCancelableUniTask(scale, token));
}
finally
{
if (ReferenceEquals(playCts, currentCts))
playCts = null;
currentCts.Dispose();
}
}
private static UniTask ToCancelableUniTask(
MotionHandle handle,
CancellationToken token)
{
return handle.ToUniTask(
CancelBehavior.Cancel,
cancelAwaitOnMotionCanceled: true,
cancellationToken: token);
}
When a new call cancels the previous PlayAsync(), the previous caller observes cancellation. If that is normal UI control flow, handle it at a clear boundary:
try
{
await resultPanelAnimation.PlayAsync(token);
}
catch (System.OperationCanceledException)
{
// Re-entry, screen transition, or destruction is normal here.
}
The important point is not to hide cancellation semantics. Decide where cancellation becomes a normal result.
Punch and Shake
Punch is useful for button feedback:
var baseScale = buttonTransform.localScale;
LMotion.Punch.Create(baseScale, Vector3.one * 0.15f, 0.3f)
.WithFrequency(8)
.WithDampingRatio(1f)
.BindToLocalScale(buttonTransform)
.AddTo(buttonTransform.gameObject);
The second argument is strength, not an end value. Do not pass Vector3.zero as the start value unless you intentionally want to oscillate around zero scale.
Shake works similarly:
var basePosition = cameraRoot.localPosition;
LMotion.Shake.Create(basePosition, new Vector3(8f, 8f, 0f), 0.35f)
.WithFrequency(20)
.WithDampingRatio(1f)
.WithRandomSeed(123)
.BindToLocalPosition(cameraRoot)
.AddTo(cameraRoot.gameObject);
For cameras, a dedicated shake-offset transform is often safer than letting Cinemachine, follow code, recoil, and shake all write the same transform.
TextMeshPro integration
LitMotion can animate fixed-size strings:
LMotion.String.Create128Bytes(
"",
"<color=#FFD54F>MISSION COMPLETE</color>",
1.2f)
.WithRichText()
.WithScrambleChars(ScrambleMode.Uppercase)
.BindToText(messageText)
.AddTo(messageText.gameObject);
128 is a byte capacity, not a character count. Localized Japanese text, emoji, and RichText tags can consume more space than a short English sample. Choose capacity from the longest production string.
Numeric binding is also convenient:
LMotion.Create(0, 9999, 0.8f)
.WithEase(Ease.OutCubic)
.BindToText(scoreText)
.AddTo(scoreText.gameObject);
Formatted binding is supported too:
LMotion.Create(0f, 100000f, 1f)
.BindToText(scoreText, "{0:N2}")
.AddTo(scoreText.gameObject);
The standard formatted path uses string.Format(), so it can allocate. If this is a hot path, consider the documented ZString integration and profile the full UI update cost.
LitMotion.Animation for Inspector workflows
LitMotion.Animation is a separate package:
https://github.com/annulusgames/LitMotion.git?path=src/LitMotion/Assets/LitMotion.Animation#v2.0.2
When using Git URL dependencies, I prefer pinning both core and animation packages to the same tag explicitly:
{
"dependencies": {
"com.annulusgames.lit-motion": "https://github.com/annulusgames/LitMotion.git?path=src/LitMotion/Assets/LitMotion#v2.0.2",
"com.annulusgames.lit-motion.animation": "https://github.com/annulusgames/LitMotion.git?path=src/LitMotion/Assets/LitMotion.Animation#v2.0.2"
}
}
It supports Edit Mode and Play Mode previews and is useful for short Prefab-specific animation tuning.
I would still define a team boundary: keep shared duration, ease, and cancellation conventions in code or configuration, and leave only local presentation tuning to the Inspector.
Common production mistakes
A few problems matter more than the exact tween API:
-
Forgetting to bind:
LMotion.Create(...).WithEase(...)is only a builder until you callBind()/BindTo...(). - Multiple motions write one property: separate ownership or cancel the previous handle.
- Infinite loops have no shutdown: combine them with object lifetime, a handle, or cancellation.
- Pause UI stops: use an ignore-time-scale scheduler only where appropriate.
- "Zero allocation" is interpreted too broadly: closures, strings, async code, and Unity systems still allocate or consume CPU.
- Everything goes into Sequence: use async/state machines for branching and external waits.
LitMotion Debugger (Window > LitMotion Debugger) is useful for finding duplicate motions and missing cancellation. Use it in Editor Play Mode; profile final CPU/GC with the debugger disabled in a Player build.
Migrating from DOTween
Simple cases are easy to rewrite:
// DOTween
transform.DOMove(targetPosition, 0.5f)
.SetEase(DG.Tweening.Ease.OutCubic);
// LitMotion
LMotion.Create(transform.position, targetPosition, 0.5f)
.WithEase(LitMotion.Ease.OutCubic)
.BindToPosition(transform)
.AddTo(gameObject);
The real migration risk is behavior around the one-liner. Review:
- Kill vs cancel behavior;
- whether cancellation keeps the current value or applies the final value;
- Sequence behavior;
- update timing and
Time.timeScale; - relative tweens;
- DOTween Pro data;
- third-party integrations and internal wrappers.
Migrate feature by feature, not library-wide, and never let two tween systems own the same property at the same time.
Adoption checklist
Before standardizing LitMotion in a team, I would verify:
- Unity/Burst/Collections/Mathematics requirements;
- custom asmdef references for LitMotion, extensions, UniTask, and TMP;
- Git dependencies pinned to a tag or commit;
- current LitMotion Releases and PrimeTween Changelog;
- representative snippets compiled in a minimal Unity project;
- third-party assets and internal tooling dependencies;
- cancellation rules for destruction, reopening, and repeated input;
- which animations ignore
Time.timeScale; - Sequence vs async/await conventions;
- Player-build CPU and GC on target hardware.
Also test cancellation halfway through, object destruction, rapid reversal, and repeated clicks—not only normal completion.
Conclusion
For a new Unity project in 2026 where tween logic is mainly written in C#, LitMotion is the first library I would evaluate.
Its Create -> With -> Bind model is consistent across Transform values, arbitrary data, TextMeshPro, Punch/Shake, Sequence, and UniTask integration. LitMotion v2 also addresses many of the practical features that once made mature alternatives easier to justify by default.
PrimeTween remains a strong alternative, especially if target-oriented APIs fit your team better. DOTween is still the pragmatic choice for projects that already have a working DOTween ecosystem.
Whichever library you choose, the production problems are usually property ownership, re-entry, cancellation, object lifetime, time scale, and the code around the tween—not the number of characters in the tween call.
References
Official docs and source
- LitMotion documentation
- LitMotion GitHub
- LitMotion v2.0.2 TextMeshPro extensions
- LitMotion v2.0.2 UniTask extensions
- PrimeTween GitHub
- DOTween documentation
Top comments (0)