Skip to content

platinio/Unity-TimelineEditorUI

Repository files navigation

Timeline Editor UI

A generic, UI Toolkit timeline drawer for authoring time-based events on a normalized 0–1 track, directly in the Unity inspector.

Overview

TimelineEditorUI

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.

How to Install

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.

Getting Started

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.

Using the Timeline

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.

Extending the Drawer

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.

Reading Events at Runtime

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);
    }
}

Sample Project

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.

Dependencies

None. Both the runtime assembly (ArcaneOnyx.TimelineEditorUI.Runtime) and the editor assembly (ArcaneOnyx.TimelineEditorUI.Editor) are fully self-contained.

About

A generic, UI Toolkit timeline drawer for authoring time-based events on a normalized 0–1 track, directly in the Unity inspector.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages