常に正確なビートグリッド
リズムゲームでは、全音符から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でビルドを共有して譜面制作者からフィードバックを集め、要望の多いワークフローを機能にしていきました。