osu!catch level editor

osu! is a rhythm game with millions of players, but its catch mode never had an editor made for it. Mappers were working around a tool built for a different mode, so I built them a dedicated one.

Unity · C# · ShaderLab · 2018 — 2019 · solo

GitHub

A beat grid that is always exact

Everything in a rhythm game is placed on the beat, from whole notes down to sixteenths, at any tempo and any zoom level. Drawing that grid from thousands of line objects would be slow and drift out of alignment, so the whole timeline is a single quad with a fragment shader.

The shader works in pixel space rather than UV space, so a line is always exactly one pixel where it should be, and each subdivision gets its own colour, the convention mappers already know.

// Grid Shader.shader (excerpt)
float y = i.uv.y * _RectSize.y; // UV to pixels: no rounding
float rowStep = _RectSize.y / _Rows;
y += _RowOffset;                  // scrolls with the song

if (y % rowStep < _RowWidth / 2 ||
    y % rowStep > rowStep - _RowWidth / 2)
{
    // on a beat line: colour it by its divisor
    // (1/1, 1/2, 1/4, 1/8 or 1/16)
}

Undo and redo for everything

Every edit pushes a memento that knows how to revert itself. Reverting returns the opposite memento, so undo and redo are the same operation running in different directions, and adding a new kind of edit takes one small class.

static Stack<IMemento<object>> changes = new();
static Stack<IMemento<object>> revertedChanges = new();

public static void PerformUndo()
{
    if (changes.Count == 0) return;
    revertedChanges.Push(
        (IMemento<object>)changes.Pop().Revert());
}

public static void PerformRedo()
{
    if (revertedChanges.Count == 0) return;
    changes.Push(
        (IMemento<object>)revertedChanges.Pop().Revert());
}

public static void RegisterCreatedObject(GameObject obj)
{
    changes.Push(new AddGameObject(obj));
    revertedChanges.Clear(); // new edits end the redo history
}

Built with its players

A rhythm editor lives or dies by its timing. Songs are decoded with NAudio and started with PlayScheduled on Unity's DSP clock, and the timeline follows that audio clock instead of the frame clock, so playback never drifts. Hitsounds come from a pool of audio sources, so overlapping sounds never cut each other off. A built-in play mode switches between editing and playing in an instant, and maps import from and export to the osu! format, so they go straight into the game.

The feature list came from the catch community itself: I shared builds on Discord, collected feedback from mappers and turned the most requested workflows into features.


← Back to the portfolio