osu!catch レベルエディター

osu!は数百万人のプレイヤーがいるリズムゲームですが、catchモード専用のエディターはありませんでした。譜面制作者は別のモード向けのツールで工夫しながら作業していたため、専用のエディターを作りました。

Unity · C# · ShaderLab · 2018年 — 2019年 · 個人開発

GitHub

常に正確なビートグリッド

リズムゲームでは、全音符から16分音符まで、どんなテンポや拡大率でもすべてが拍の上に置かれます。何千本もの線オブジェクトでグリッドを描くと重く、ずれも生じるため、タイムライン全体を一枚のクアッドとフラグメントシェーダーで描画しています。

シェーダーはUV空間ではなくピクセル空間で計算するので、線は常に正しい位置にちょうど1ピクセルで描かれます。分割ごとに色を変え、譜面制作者が見慣れた配色にしています。

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

すべての操作にアンドゥとリドゥ

編集のたびに、自分を元に戻す方法を知っているメメントを積みます。元に戻すと逆向きのメメントが返るので、アンドゥとリドゥは同じ操作を逆方向に実行するだけ。新しい種類の編集を追加するのも小さなクラス一つで済みます。

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
}

プレイヤーとともに作る

リズムエディターはタイミングがすべてです。曲はNAudioでデコードし、PlayScheduledで再生を開始。タイムラインはフレームの時計ではなくオーディオの時計(AudioSettings.dspTime)に従うので、再生がずれることはありません。ヒットサウンドはオーディオソースのプールから鳴らすので、重なっても途切れません。内蔵のプレイモードで編集とプレイを瞬時に切り替えられ、譜面はosu!形式でインポート・エクスポートできるのでそのままゲームで遊べます。

機能はcatchコミュニティから生まれました。Discordでビルドを共有して譜面制作者からフィードバックを集め、要望の多いワークフローを機能にしていきました。


← ポートフォリオに戻る