A generic, UI Toolkit timeline drawer for authoring time-based events on a normalized 0–1 track, directly in the Unity inspector.
Lots of gameplay systems need to fire things at specific moments along a normalized duration — a skill that spawns a projectile 40% into its animation, a VFX that plays on the last frame, an audio cue halfway through. Storing those as a bare list of structs works, but editing them by hand in the default inspector is slow and error-prone.
Timeline Editor UI turns any Timeline<T> field into an interactive track. You scrub a marker across the timeline, drop events with a button, and edit the selected event's properties inline. Event types opt into the create menu with a single attribute, and you can mix multiple event types on the same track thanks to [SerializeReference].
The runtime side is intentionally tiny and dependency-free — it is just a serializable container. All of the editing experience lives in the editor assembly, so it adds nothing to your build.
Unity package (recommended) — download and import the latest .unitypackage.
Package Manager (git URL) — see how to install from a git URL:
https://github.com/platinio/TimelineEditorUI.git
It is fully self-contained — the runtime Timeline<T> container and the editor drawer have no external dependencies.
A complete, runnable version of everything below ships in the Sample folder. See Sample Project.
You build on two runtime pieces: Timeline<T>, a serializable list of events, and the [TimelineEventCreateMenu] attribute that registers an event type in the track's create dropdown.
1. Define your event base type
Your events need a serialized normalizeTime in the 0–1 range and a serialized name — the drawer reads both by name to place and label markers. Add whatever behavior hook your game needs (here, an Execute method):
using System;
using UnityEngine;
[Serializable]
public abstract class AbilityEvent
{
[SerializeField] private string name;
[SerializeField, Range(0f, 1f)] private float normalizeTime;
public float NormalizeTime => normalizeTime;
public abstract void Execute(GameObject context);
}2. Add concrete event types
Derive from your base type and tag each one with [TimelineEventCreateMenu]. The string is the label shown in the create dropdown (/ nests it into a submenu). Because Timeline<T> stores events with [SerializeReference], one track can freely mix these types.
using System;
using ArcaneOnyx.TimelineEditorUI;
using UnityEngine;
[Serializable]
[TimelineEventCreateMenu("Audio/Play Sound")]
public class PlaySoundEvent : AbilityEvent
{
[SerializeField] private AudioClip clip;
public override void Execute(GameObject context)
=> AudioSource.PlayClipAtPoint(clip, context.transform.position);
}
[Serializable]
[TimelineEventCreateMenu("VFX/Spawn Particles")]
public class SpawnParticlesEvent : AbilityEvent
{
[SerializeField] private GameObject prefab;
public override void Execute(GameObject context)
=> Object.Instantiate(prefab, context.transform.position, Quaternion.identity);
}Note
Only types decorated with [TimelineEventCreateMenu] appear in the create dropdown. A base type without the attribute (like AbilityEvent above) holds shared fields and logic but is not directly creatable.
3. Add a timeline field
Create a serializable subclass of Timeline<T>, where T is your event base type, and expose it wherever you need it — a ScriptableObject, a MonoBehaviour, or a nested serializable class.
using System;
using ArcaneOnyx.TimelineEditorUI;
using UnityEngine;
[Serializable]
public class AbilityTimeline : Timeline<AbilityEvent> { }
public class Ability : MonoBehaviour
{
[SerializeField] private AbilityTimeline timeline;
}4. Draw it with the timeline UI
In an editor script, register a property drawer by subclassing BaseTimelinePropertyDrawer<T> — there is nothing to write beyond the class declaration.
using ArcaneOnyx.TimelineEditorUI;
using UnityEditor;
[CustomPropertyDrawer(typeof(AbilityTimeline))]
public class AbilityTimelineDrawer : BaseTimelinePropertyDrawer<AbilityEvent> { }Select the object that owns the field, and the timeline renders in the inspector.
The drawer gives you a scrub bar and a small toolbar above the track:
| Control | Action |
|---|---|
| Drag on the track | Move the scrub marker; its position is the normalized time new events are placed at |
| + | Create an event at the marker's current position |
| – | Remove the currently selected event |
| ◀ / ▶ | Select the previous / next event along the track |
| Event markers | Click a marker to select it and load its properties below |
Selecting an event shows its inspector underneath the track. The first field is a type dropdown listing every creatable type that derives from your base type — switching it converts the event to the new type in place while preserving its normalizeTime. Edit the Normalize Time field to reposition an event precisely; the marker follows the value.
The selected event is remembered per field (via EditorPrefs), so reopening the object restores your place on the track.
BaseTimelinePropertyDrawer<T> exposes two extension points for building richer timelines like animation or skill editors:
public class SkillTimelineDrawer : BaseTimelinePropertyDrawer<SkillTimelineEvent>
{
// Point at a custom UXML layout in a Resources folder.
protected override string UXMLPath => "SkillTimeline";
// Called every time the scrub marker moves — drive a preview here,
// e.g. sample an Animator at the current normalized time.
protected override void OnTimelineMarkerMove(float normalizeTime)
{
// preview logic
}
}Override CreatePropertyGUI (calling base.CreatePropertyGUI first) to append extra controls to the returned container — for example a dropdown that binds additional fields on your timeline subclass.
Timeline<T> exposes its events as a read-only list. The container only stores the events and their normalized times — ordering, triggering, and interpolation are left to your own code. A typical pattern is to track playback progress and fire events whose NormalizeTime falls within the range advanced this frame:
private void FireEventsInRange(float from, float to)
{
foreach (var abilityEvent in timeline.TimeLineEvents)
{
if (abilityEvent.NormalizeTime > from && abilityEvent.NormalizeTime <= to)
abilityEvent.Execute(gameObject);
}
}The Sample folder contains a small, self-contained example that mirrors this guide:
| File | What it shows |
|---|---|
SampleTimelineEvent.cs |
A base event type (SampleTimelineEvent) and the SampleSequence : Timeline<T> container |
SampleTimelineEvents.cs |
Three creatable events — log a message, spawn a prefab, play a sound |
SampleSequencePlayer.cs |
A MonoBehaviour that plays the sequence over a duration and fires each event as the playhead crosses it |
Editor/SampleSequenceDrawer.cs |
The one-line drawer registration that renders the track in the inspector |
Add SampleSequencePlayer to a GameObject, author a sequence on it with the timeline, and press Play (or use the component's Play context-menu item) to watch the events execute in order.
None. Both the runtime assembly (ArcaneOnyx.TimelineEditorUI.Runtime) and the editor assembly (ArcaneOnyx.TimelineEditorUI.Editor) are fully self-contained.
