目次
はじめに
みなさん、音ゲー大好きですよね?僕も大好きです。
いざ音ゲーを作ろうにも、情報が少なかったり、「簡単だから記事が少ない」なんて文言を見かけることもあります。
個人開発であれば、ある程度動けば満足出来ますが、事業規模であればしっかりと作り込まなければなりません。
音ゲーで重要なもののうちに、『譜面』たるものがあります。
よく『ノーツ』なんて言われることがある、上(時には四方八方)から降ってくるあいつが、
楽曲単位でひとまとまりになっているものです。
(イメージはピアノやバンド(スコア)の譜面と一緒)
Unityで開発すると、譜面を作成する人に向けてエディタを拡張することが第一案として上がってきそうですが、
より直感的に、より簡単に、よりスムーズに!!!(そして編集したらその場ですぐプレビューさせろ!!!)
なんて声が聞こえてきそうです。
そこでとあるAssetと出会い、拡張すればいい感じに作れるんじゃないか?と思い、実装したので、
一部抜粋して紹介したいと思います。
結論
UnityのTimelineを使う
(プレイしながらリアルタイム編集が可能で優秀)
参考アセット
Rhythm Timeline
https://assetstore.unity.com/packages/tools/audio/rhythm-timeline-190908
エディタ画面
(公式に載っている情報のみ抜粋)
■Timelineエディタ
お馴染みとなったUnityのアニメーション作成(用途は様々)エディタツール。(画像左下)
※Timelineに関しては割愛します。
【公式マニュアル】
https://docs.unity3d.com/ja/2018.4/Manual/TimelineSection.html
【構成部分】(Assetはデモとして既に構成されている)
・楽曲を再生するために、トラック部分にAudioSourceを生成
・トラックごとの情報を取得するために、レーンのインデックスや、配置されているノーツの時間を保存する[RhythmTrack]というScriptableObjectを作成
・レーン数に応じてRhythmTrackを生成する
・ノーツもScriptableObjectで作成されており、ノーツのタイプや配置時間を保持している
■Sceneエディタ
音ゲーの根幹となるインゲーム部分です。
どんな音ゲーにするかによって、この辺りは変わってくるので割愛しますが、
こちらのアセットでは3Dでレーンを配置して、ボタンの位置をローカル単位で0に設定しています。
ノーツをタップするタイミングと着弾するタイミングの計算で、最終値を0で計算するためです。
(ボタンの位置に最終値がくれば問題なし)
■Playable Directorコンポーネント
譜面を楽曲単位で保存、表示するために、GameObjectにこちらのコンポーネントをアタッチします。
【Playable】とInspectorに表示されている部分に、TimelineAssetを継承したクラスを配置します。
この継承したクラスがTimelineで編集した値と位置情報を保存してくれるので、譜面を楽曲単位で扱えます。
※この辺りもTimelineの基本なので割愛します
■RhythmTimelineアセットの実装まとめ
RhythmTimelineアセットの基本部分は、UnityのTimelineで設計されており、
以下のような手順で実装されています。
1.TimelineAssetを継承したクラスを一つの譜面として保存(エディタ拡張でベースを生成、楽曲ごとに扱えるようになっている)
2.1で作成した譜面をPlayableDirectorコンポーネントにアタッチし、Timelineエディタに表示
3.必要に応じて楽曲専用のレーントラックをScriptableOblectとして作成し、Timelineトラックに生成
4.同じくScriptableObjectとして作成されたノーツをトラックに配置
5.トラックごとに保存されているノーツのデータをインゲーム内に情報を渡し、ゲームに反映している
と、ここまで書きましたが全て公式Youtubeのチュートリアルで説明されてます!!!
(全部英語)
足りない部分を探す(メイン)
上記アセットはあくまでも譜面を作る最低限必要な導入編といっても過言ではありません。
当然と言えば当然ですが、公開されているアセットは、拡張してなんぼです。
今回、要求された品質は天下の『プロジェクトセカイ』!!!!!!
….
ということで、これは導入しておきたいと思った足りない部分をリストアップすることにしました。
・スライドノーツ(レーンをまたいで水平移動するやつ)
・ロングノーツの最後がフリックVer
・スライドノーツの最後がフリックVer
・ノーツの速度計算(よくあるスピード1〜12とかまで設定するやつに合わせて計算する必要がある)
・インゲームに渡す情報を別途保存
・キーボード入力で、再生しながらノーツをトラックに配置する(難易度:高)
・マルチタップ対応(今回は割愛)
・CRI対応(今回は割愛。Androidでプレイする時に遅延問題があるので、必須…?。一応Timeline用のトラックが用意されている)
他にも大量にあったのですが、メインとなる追加実装はこの辺りかなと思います。
(仕様によるのであくまでも参考程度に)
ノーツの追加
ノーツの追加自体は簡単です。
例えば、【全方向】に対応したフリックノーツを実装したい場合、
ScriptableObjectとして作成したノーツのタイプを追加し、画像や色を設定して配置すればいいだけです。
RhytymTimelineのアセットの場合、タイプはクリップ自体の長さでネーミングされています。
なので、わかりやすくTap/LongNotes/Flickなど変更するといいかもです。
(その他のパラメーターはトラック上に配置した時にわかりやすくするための画像など)
ただ、スライドノーツの場合はレーンをまたぐので、Timelineだと設定しづらかったりします。
そこで、今回は以下のような配置で実装しました。
スライドのタイプを3段階に分け、かつフリックがある場合は最後のスライドノーツにフリックタイプを当て込む形にしました。
トラックに配置したノーツ(公式=クリップ)は長さが変更でき、両端の開始位置と終了位置の時間を取得することが出来ます。
両端が取れれば、間の時間を取るのは簡単です。
・開始のスライドノーツ(開始時間) Clip Timing(Start)
・途中のスライドノーツ(間の時間)
・終了のスライドノーツ(終了の時間)Clip Timing(End)
各それぞれでレーンのインデックスと時間、IDを設定してあげれば、ひとまとまりのノーツとして管理することが出来ます。
ノーツの速度計算
そもそもノーツがボタンに着弾するまでの計算ですが、BPMを計算したりする記事をよく見かけますが、
楽曲の進行している時間が取れれば、着弾する時間から逆算してノーツを生成すればいいので、難しい計算は不要となります。
あとは更新方法の取捨選択です。
ノーツの速度計算に関しては、
プレイヤーが設定した速度に対して、どれくらいの時間で着弾するかの時間に定数を掛けるだけで実装できました。
(今回はプロセカを参考に、画面に映る時間から着弾するまでの時間に寄せて計算)
インゲームに渡す情報を別途保存
楽曲の譜面が完成したら、最終的にはその情報を保存しておかなければなりません。
ノーツタイプ/ノーツの着弾時間/レーンのインデックス/etc…
結果としてはScriptableObjectに保存するようにしました。
基底のScriptableObjectを作成し、保存するときにこれをコピーし、楽曲単位で保存するようにしました。
※一部抜粋
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
[System.Serializable] public class SongScoreData : ScriptableObject { public List<ScoreData> ScoreDataList = new List<ScoreData>(); public IEnumerable<ScoreData> GetNotesBetweenTime(float start, float end) { return ScoreDataList.Where(x => start < x.Time && x.Time <= end); } } [System.Serializable] public struct ScoreData { [SerializeField] int id; [SerializeField] float time; [SerializeField] int lineId; [SerializeField] int notesType; [SerializeField] float longEndTime; [SerializeField] int slideId; public int ID => id; public float Time => time; public int LineId => lineId; public int NotesType => notesType; public float LongEndTime => longEndTime; public int SlideId => slideId; public ScoreData(int id, float time, int lineID, int notesType, float longEndTime, int slideId) { this.id = id; this.time = time; this.lineId = lineID; this.notesType = notesType; this.longEndTime = longEndTime; this.slideId = slideId; } } |
選択理由は公式をご覧いただければと思います。
https://docs.unity3d.com/ja/2018.4/Manual/class-ScriptableObject.html
キーボード入力で再生しながらノーツを配置する
ノーツをちまちま配置するのが面倒…
といった意見をいただいたので、実装しました。確かに、あると楽です。笑
トラックの数=レーン数に合わせて、キーボードの1〜^を左詰めでノーツを生成するように拡張しました。
【難易度:高】と書かせていただいた理由ですが、
UnityのTimelineの癖がこれに当たります。
というのも、Timelineで取得できるメソッドのうち、常に更新しているMixer関連(ProcessFrame)があるのですが、
基本的にトラックにある情報を常に更新することに特化しているので、
新しくクリップを作成するまでは簡単ですが、それをTimeline専用の更新処理にのっけないとデータが追従されないという、
悲しい結果になってしまいます。
そこで、以下の手順で実装することで、問題を解決しました。
・クリップ生成後は、データの再設定を行う(基本は構造体)
・生成後、更新するときはProcessFrameから派生したクラスを更新する(直接指定は引数を指定できない….と思われる。(継承前提のため))
→共通の更新処理を作ると楽
→これをしないと、クリップ(ノーツ)の位置やタイプを変更してもProcessFrameから更新される
【実装したときのサンプル動画】
(キーボードクリックでノーツを生成しています)
最後に
今回は音ゲーの譜面エディタをTimelineで実装するべく、参考アセットの紹介と、
必要そうな拡張に関して紹介させていただきました。
音ゲーは意外と奥が深く、実装するとなかなかハードでありつつも楽しい経験となるので、
機会があれば、その他の機能や実装に関しても記事にしていこうかと思います。