著作権情報 THE IDOLM@STER™& ©Bandai Namco Entertainment Inc.
デレステには創作譜面というジャンルがある
要はユーザーが独自に作成した譜面であり、独自に作成されたアプリとレベル作成ツールを使用し、自由に楽曲を選んで自由に譜面を作る、楽しそうな遊びである。
個人的に楽曲に飽きてきたし、
しかしながら最初に作るのは「ゆめりあベンチマーク」の曲に決めている。
往年のPであればティンと来るはずである。
俺もやってみたい!
ということで、色々検索してみたのだがプレーヤのようなものはダウンロードできたがレベル作成ツールが手に入れることができなかった。
諦めたくない!
ということで自分で作ることにした。
ゲーム作成は学生時代に遊びでXNAかMonoを使って以来である。Unityってのを使えば簡単にできんだろと思っていたが私にとっては学習曲線が急だったようで今のところ詰まっている。
ただ以下のサイトを参考にして流れは掴んだ。流れは…
デレステのノーツ表現空間は本当に2Dなの
テンプレートやアセットを見ていて思ったこと。少なくともUnityのアセットストアにデレステのような音ゲーのものはない。ノーツの流れは2Dっぽいし2Dのテンプレートとかばかり見ていた。
しかし自分のプレイ動画見ているとデレステのノーツの形状を見てみると緩やかな曲線を描いている事がわかった。もっと詳細に見てみると生成地点から若干上ずっているようにも見える。
おそらく擬似3D変換関数みたいなものを実装して最適化しているのだろうが、こっちはゲーム開発においては素人同然だ。まずはノーツの流れる3次元空間を理解しようと試みた。
デレステのノーツ3次元空間ってこんな感じなんじゃないかな?
ということで、模型を紙で作って遊んでみた。こんな感じなんじゃないかなと思って作ってみた。
SpawnAreaというノーツ生成エリアからノーツが発射されてTapAreaに到着する感じ。
ノーツ速度はY成分で定義し、それによってノーツを発射するタイミングとZ成分の加速度を計算する。
イメージとしてはノーツ生成エリアからノーツ速度でY方向に大砲を発射さるイメージ。重力加速度とかはよしなに計算してタップエリアに収まるようにするイメージ。
なんかごちゃごちゃ書いているけど要はノーツ速度はVyで定義するから到達時間は小学生で学んだ距離/速度になるから、それできれいにタップエリアに落ちてくれるようにZ方向の加速度を計算しますよーってこと。
ちなみに私は高校のときは化学、生物選択なので加速度の計算とかリアル中学生以来であると言おうと思ったが、社会人になってコロナ禍でクソ暇な時に学び直しとして放送大学に入って物理系の講義を取った記憶があるので嘘になるのでやめた。
検証してみよう
Unityは俺には難しいことがわかった。だがとりあえずモック的なものを作ってみたいと思った。
もう高校生自体に卒業したHSPという言語を久々に使ってみることにした。
未だにメンテナンスされていて、しかもcodeでコーディングができるとは時代の流れとコントリビューターに感謝である。
実際にノーツ生成エリアとタップエリアを写してみたのが以下のスクリーンショットになる
実際の座標などはノーツの見え方などから色々調節はしているが大分、再現できているのではないかと思う。
実際にノーツをランダム作成して流してみた。
現状としてはシングルノーツのみの実装になるが、試しに動かしてみた。
ノーツ生成初期の上ずりも、曲線の形も、そこそこの再現できたのではないかと思う。
原因は不明だがノーツ数を増やすと何故か速度が上がるのと、シングルノーツなのにロングノーツのような形状になっている。前者はおそらくコードで解決できるが後者は解決できない気がする。
=> 単純なバグだった
まぁ少なくともモックでそれっぽい挙動は再現できた。フリックの描写アルゴリズムはシングルノーツと同一である(タップ時の処理とテクスチャが変わるだけ)。ロングノーツ、連結フリックと紫色のやつのモックを実装すれば、後はUnityを頑張って学習してなんとかするだけである。
レベル作成ツールは何を使って作成するかdemucsあたりと連携させてドラム、ベース抽出しレベル作成のヒントに使いたいなとか思っていたりアイデアだけは油田のように湧いてくるのである。
Gnuderellaプロジェクト
モックである程度再現できたので、これはいけると踏んで新しいプロジェクトを立ち上げた。
その名も「Gnuderella」である。
やはり美少女キャラは必要であると言うことで、水牛の擬人化した美少女キャラをCopilotくんとChatGptくんに書いてもらって、今のところ一番気に入っているのがこの子である。最高かよ。
以下が今回のサンプルコードである。HSPにはクラスという概念がない。配列で頑張るのだ。
;============================================================
; magiqych/2025-01-09
; GnuDerella mock
;
;============================================================
#include "d3m.hsp"
;カメラの初期設定
dim cam,6
;camPosition
cam(0) = 0 : cam(1) = 0: cam(2) = 100
;camDestination
cam(3) = 0 : cam(4) = 100 : cam(5) = 100
d3setcam cam(0),cam(1),cam(2),cam(3),cam(4),cam(5)
;レーン数設定
lane_num = 5
;ノーツ生成エリアの初期設定
notesy = 1500.0
notesz = 700.0
ddim noteSpawnCircle,lane_num,3
;tapCircleの初期座標設定
for index,0,lane_num,1
noteSpawnCircle(index,0) = double(-200 + (500/lane_num) * index)
noteSpawnCircle(index,1) = notesy
noteSpawnCircle(index,2) = notesz
next
;タップエリアの初期設定
tapy = 350.0
tapz = 0.0
ddim tapCircle,lane_num,3
;tapCircleの初期座標設定
for index,0,lane_num,1
tapCircle(index,0) = noteSpawnCircle(index,0)
tapCircle(index,1) = tapy
tapCircle(index,2) = tapz
next
;ノーツの初期設定
single_notes_num = 1000;シングルノーツ数
notes_num = single_notes_num ;ノーツ数
;ノーツの速度成分を計算する
;計算上必要なプロパティ
notes_speed_settings = -9.0
delta_x = 0.0
delta_y = notesy - tapy
delta_z = notesz - tapz
delta_t = double(delta_y / abs(notes_speed_settings))
;シングルノーツ速度の設定
ddim single_note_speed,3
single_note_speed(0) = 0.0
single_note_speed(1) = notes_speed_settings
single_note_speed(2) = 0.0
dim single_note_acceleration,3
single_note_acceleration(0) = 0.0
single_note_acceleration(1) = 0.0
single_note_acceleration(2) = 0.0 - double((delta_z*2)/(delta_t*delta_t))
;シングルノーツプロパティ
dim is_single_notes_spawn,single_notes_num ;ノーツ生成フラグ
ddim single_notes_spawn_timing,single_notes_num ;ノーツ生成タイミング
ddim single_notes_pos,single_notes_num,3 ;ノーツ座標
ddim single_notes_speed,single_notes_num,3 ;ノーツスピード
ddim single_notes_acceleration,single_notes_num,3 ;ノーツ加速度
ddim single_notes_destination,single_notes_num,3 ;ノーツ目的地
;notesプロパティ初期設定
for index,0,single_notes_num,1
;ノート生成フラグと生成タイミングの初期設定
is_single_notes_spawn(index) = 0
single_notes_spawn_timing(index) = double(10 * (index+1))
;ノーツの目的地の初期設定
dstIndex = rnd(lane_num)
single_notes_destination(index,0) = tapCircle(dstIndex,0)
single_notes_destination(index,1) = tapCircle(dstIndex,1)
single_notes_destination(index,2) = tapCircle(dstIndex,2)
;ノーツの座標の初期設定
single_notes_pos(index,0) = noteSpawnCircle(dstIndex,0)
single_notes_pos(index,1) = noteSpawnCircle(dstIndex,1)
single_notes_pos(index,2) = noteSpawnCircle(dstIndex,2)
;ノーツのスピードの初期設定
single_notes_speed(index,0) = 0.0
single_notes_speed(index,1) = notes_speed_settings
single_notes_speed(index,2) = 0.0
;ノーツの加速度の初期設定
single_notes_acceleration(index,0) = single_note_acceleration(0)
single_notes_acceleration(index,1) = single_note_acceleration(1)
single_notes_acceleration(index,2) = single_note_acceleration(2)
next
;タイミング変数
timing = 0
repeat
;ログ変数
logging = ""
;描写先を仮想画面にスイッチ
redraw 0
; 画面クリア
color 0, 0, 0 : boxf
; 座標平面描画
color 0,255,0
repeat 31
a = cnt * 2000 / 30 - 1000
d3line a, 1000, 0, a, -1000, 0
d3line 1000, a, 0, -1000, a, 0
loop
; タップエリア描画
color 255,0,0
for index,0,lane_num,1
d3circle tapCircle(index,0),tapCircle(index,1),tapCircle(index,2), 20
next
; ノーツ生成エリア描画
color 255,0,0
for index,0,lane_num,1
d3circle noteSpawnCircle(index,0),noteSpawnCircle(index,1),noteSpawnCircle(index,2), 20
next
; ノーツ描画
;シングルノーツ
;シングルノーツ生成フラグ調整
for single_notes_index,0,single_notes_num,1
;タイミングが来たノーツを生成
if single_notes_spawn_timing(single_notes_index) <= timing{
is_single_notes_spawn(single_notes_index) = 1
}
;タップエリアより手前にノーツが来た場合はノーツを消去
if is_single_notes_spawn(single_notes_index) = 1 && single_notes_pos(single_notes_index,1) <= tapy{
is_single_notes_spawn(single_notes_index) = 0
}
next
;シングルノーツ描画
notes_cn = 0
for single_notes_index,0,single_notes_num,1
color 0,0,255
if is_single_notes_spawn(single_notes_index) = 1 {
notes_cn = notes_cn + 1
//速度座標計算
for i,0,3,1
single_notes_speed(single_notes_index,i) = single_notes_speed(single_notes_index,i) + single_notes_acceleration(single_notes_index,i)
single_notes_pos(single_notes_index,i) = single_notes_pos(single_notes_index,i) + single_notes_speed(single_notes_index,i)
next
logging = str(single_notes_speed(single_notes_index,0)) + "," + str(single_notes_speed(single_notes_index,1)) + "," + str(single_notes_speed(single_notes_index,2))
d3circle single_notes_pos(single_notes_index,0),single_notes_pos(single_notes_index,1),single_notes_pos(single_notes_index,2), 20,1
}
next
logging = logging + "\n notes:" + str(notes_cn)
logging = logging + "\n timing:" + str(timing)
;タイミング描写
color 255,255,255
d3mes logging, -200,tapy,265
;タイミング変数インクリメント
timing = timing + 1
;画面更新
redraw 1
await 30
loop