デレステのノーツを再現してみた。

[デレステのノーツを再現してみた #2](https://qiita.com/Magiqych/items/f35790add3ebaf234b0c)

:::note info
著作権情報　THE IDOLM@STER™& ©Bandai Namco Entertainment Inc.
:::

![画面録画 2025-01-10 113821.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/a227c04f-5268-1be0-49b8-07694cbd8408.gif)


## デレステには創作譜面というジャンルがある 

https://www.google.com/search?q=%E5%89%B5%E4%BD%9C%E8%AD%9C%E9%9D%A2%E3%80%80%E3%83%87%E3%83%AC%E3%82%B9%E3%83%86

要はユーザーが独自に作成した譜面であり、独自に作成されたアプリとレベル作成ツールを使用し、自由に楽曲を選んで自由に譜面を作る、楽しそうな遊びである。
個人的に楽曲に飽きてきたし、
* [EDM](https://soundcloud.com/happymachine/colate-feat-good-night-happy-machine-remix)
* [ジプシージャズ](https://www.youtube.com/watch?v=4J3VIrziL9k&list=PLNv_UjqCVBXzEjlG5cwYObZNw5_ygzzGK)
* [room335](https://www.youtube.com/watch?v=aRTD12fl6Mo)
* [クラシック](https://www.youtube.com/watch?v=R2-43p3GVTQ)
* [フォーク](https://www.youtube.com/watch?v=Ld6fAO4idaI&list=RDQMEny8coFL3vM&start_radio=1)
とかのとにかくいろいろな創作譜面作りたいし、できればプレイもしてみたい。

:::note info
しかしながら最初に作るのは[「ゆめりあベンチマーク」の曲](https://www.youtube.com/watch?v=KW2EDqTVT4c)に決めている。
往年のPであればティンと来るはずである。
:::


## 俺もやってみたい！
ということで、色々検索してみたのだがプレーヤのようなものはダウンロードできたがレベル作成ツールが手に入れることができなかった。

## 諦めたくない！
ということで自分で作ることにした。
ゲーム作成は学生時代に遊びでXNAかMonoを使って以来である。Unityってのを使えば簡単にできんだろと思っていたが私にとっては学習曲線が急だったようで今のところ詰まっている。
ただ以下のサイトを参考にして流れは掴んだ。流れは…

https://feynman.co.jp/unityforest/game-create-lesson/music-game/

## デレステのノーツ表現空間は本当に2Dなの
テンプレートやアセットを見ていて思ったこと。少なくともUnityのアセットストアにデレステのような音ゲーのものはない。ノーツの流れは2Dっぽいし2Dのテンプレートとかばかり見ていた。

<iframe width="560" height="315" src="https://www.youtube.com/embed/Izb8OA8rs0c?si=V17ICWJtr-YMIPFP" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

しかし自分のプレイ動画見ているとデレステのノーツの形状を見てみると緩やかな曲線を描いている事がわかった。もっと詳細に見てみると生成地点から若干上ずっているようにも見える。
おそらく擬似3D変換関数みたいなものを実装して最適化しているのだろうが、こっちはゲーム開発においては素人同然だ。まずはノーツの流れる３次元空間を理解しようと試みた。

## デレステのノーツ３次元空間ってこんな感じなんじゃないかな？

![rough1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/604fa8ca-386d-fbd2-4d04-6ecbc6a9f6f9.jpeg)

ということで、模型を紙で作って遊んでみた。こんな感じなんじゃないかなと思って作ってみた。
SpawnAreaというノーツ生成エリアからノーツが発射されてTapAreaに到着する感じ。
ノーツ速度はY成分で定義し、それによってノーツを発射するタイミングとZ成分の加速度を計算する。
イメージとしてはノーツ生成エリアからノーツ速度でY方向に大砲を発射さるイメージ。重力加速度とかはよしなに計算してタップエリアに収まるようにするイメージ。

![スクリーンショット 2025-01-09 204902.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/1c66d1f9-c5e0-df0e-869a-53bd67a884df.png)

なんかごちゃごちゃ書いているけど要はノーツ速度はVyで定義するから到達時間は小学生で学んだ距離/速度になるから、それできれいにタップエリアに落ちてくれるようにZ方向の加速度を計算しますよーってこと。

ちなみに私は高校のときは化学、生物選択なので加速度の計算とかリアル中学生以来であると言おうと思ったが、社会人になってコロナ禍でクソ暇な時に学び直しとして放送大学に入って物理系の講義を取った記憶があるので嘘になるのでやめた。

## 検証してみよう
Unityは俺には難しいことがわかった。だがとりあえずモック的なものを作ってみたいと思った。
もう高校生自体に卒業した[HSP](https://hsp.tv/)という言語を久々に使ってみることにした。
未だにメンテナンスされていて、しかもcodeでコーディングができるとは時代の流れとコントリビューターに感謝である。

https://qiita.com/umuy/items/8e8e6f1e4ccc0387be2c#%E8%BF%BD%E8%A8%98-20231106

https://hsp.tv/contest2023/list_n2.html#2317

実際にノーツ生成エリアとタップエリアを写してみたのが以下のスクリーンショットになる

* 紙の模型を撮ったもの
![rough2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/4e5002a0-1743-f1e5-164f-2d180601e85c.jpeg)

* スクリーンショット
![スクリーンショット 2025-01-09 210604.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/589e268b-6628-e6e7-908f-f39f2609deff.png)

実際の座標などはノーツの見え方などから色々調節はしているが大分、再現できているのではないかと思う。

## 実際にノーツをランダム作成して流してみた。
現状としてはシングルノーツのみの実装になるが、試しに動かしてみた。

![画面録画 2025-01-10 113821.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/a227c04f-5268-1be0-49b8-07694cbd8408.gif)

ノーツ生成初期の上ずりも、曲線の形も、そこそこの再現できたのではないかと思う。
~~原因は不明だがノーツ数を増やすと何故か速度が上がるのと、シングルノーツなのにロングノーツのような形状になっている。前者はおそらくコードで解決できるが後者は解決できない気がする。~~
＝＞ 単純なバグだった

まぁ少なくともモックでそれっぽい挙動は再現できた。フリックの描写アルゴリズムはシングルノーツと同一である(タップ時の処理とテクスチャが変わるだけ)。ロングノーツ、連結フリックと紫色のやつのモックを実装すれば、後はUnityを頑張って学習してなんとかするだけである。

レベル作成ツールは何を使って作成するかdemucsあたりと連携させてドラム、ベース抽出しレベル作成のヒントに使いたいなとか思っていたりアイデアだけは油田のように湧いてくるのである。

https://github.com/facebookresearch/demucs

## Gnuderellaプロジェクト
モックである程度再現できたので、これはいけると踏んで新しいプロジェクトを立ち上げた。
その名も「Gnuderella」である。

やはり美少女キャラは必要であると言うことで、水牛の擬人化した美少女キャラをCopilotくんとChatGptくんに書いてもらって、今のところ一番気に入っているのがこの子である。最高かよ。

![gnu3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/158440ce-3ef8-c5d7-7d6b-6c7dcd095d8a.png)

https://github.com/Magiqych/Gnuderella/tree/feature/%231make-mock

以下が今回のサンプルコードである。HSPにはクラスという概念がない。配列で頑張るのだ。

```cpp a 
;============================================================
;                                     	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


```

[デレステのノーツを再現してみた #2](https://qiita.com/Magiqych/items/f35790add3ebaf234b0c)