はじめまして。eiryplusと申します。 この記事は、 Beat Saber Advent Calendar 2020 23日目の記事となります。
譜面を解析してみる
Beat Saberの譜面の難易度については、 NPS (norts per Second、ノーツ数を曲の長さで割った値)と、レベル(AIが自動的に算出した難易度)の2つが標準で利用されていると思います。 しかし、実際は脳トレ譜面が得意だったり、ストリームが得意だったりと、人によって得手不得手があります。(なお、私はストリームが大の苦手です)
解析の目的は、難易度とは別に、事前に譜面の傾向を把握したり、ブラインドノーツのような暗記しなければ対応できない配置が無いかを素早く把握する事となります。
なお、この記事で説明している内容は、複数の譜面データを比較して推測した情報で、仕様として明記されている情報ではありません。 間違いなどありましたら、Twitter: @eiryplus までご連絡をお願いします。
TL;DR
忙しい人のためのまとめです。
- 解析に必要なデータのとり方と、各データの説明
- 簡単な解析例。見せ方重要
- 今後は作成中の譜面に変な配置がないかチェックするサービスつくったり、三妖精のような味のある譜面の難易度を解析したい
譜面の構成
譜面は、凡そ以下のファイルで構成されています。
役割 | ファイル名 | データ形式 |
---|---|---|
作成者や曲、難易度など譜面全体の情報 | info.dat | JSON |
曲データ | song.egg | Ogg Vorbis |
カバー画像 | Cover.jpg | jpeg |
難易度別のマップデータ。 {難易度}の部分はEasy、Normal、Hard、Export、ExpertPlus |
{難易度}.dat | JSON |
曲データやカバー画像などのファイル名は、一般的に利用されている名前です。実際は info.dat に定義されているので、別名でも問題ないと思います。
解析で利用するデータについて
各ファイルから、利用するデータの取得方法を記載していきます。
info.dat
オリジナルのデータから一部書き換えていますが、以下のようなデータ構造になります。
最重要のデータは _beatsPerMinute というキーで格納されています。後ほど出てくるノーツの解析で利用します。
{
"_version": "2.0.0",
"_songName": "曲名",
"_songSubName": "",
"_songAuthorName": "アーティスト名",
"_levelAuthorName": "マッパーさん",
"_beatsPerMinute": 154,
"_songTimeOffset": 0,
"_shuffle": 0,
"_shufflePeriod": 0.5,
"_previewStartTime": 12,
"_previewDuration": 10,
"_songFilename": "song.egg",
"_coverImageFilename": "cover.jpg",
"_environmentName": "BigMirrorEnvironment",
"_customData": {
"_contributors": [],
"_customEnvironment": "",
"_customEnvironmentHash": ""
},
"_difficultyBeatmapSets": [
{
"_beatmapCharacteristicName": "Standard",
"_difficultyBeatmaps": [
{
"_difficulty": "Hard",
"_difficultyRank": 5,
"_beatmapFilename": "Hard.dat",
"_noteJumpMovementSpeed": 10,
"_noteJumpStartBeatOffset": 0,
"_customData": {
"_difficultyLabel": "",
"_editorOffset": -33,
"_editorOldOffset": -33,
"_warnings": [],
"_information": [],
"_suggestions": [],
"_requirements": []
}
},
{
"_difficulty": "Expert",
"_difficultyRank": 7,
"_beatmapFilename": "Expert.dat",
"_noteJumpMovementSpeed": 10,
"_noteJumpStartBeatOffset": 0,
"_customData": {
"_difficultyLabel": "",
"_editorOffset": -33,
"_editorOldOffset": -33,
"_warnings": [],
"_information": [],
"_suggestions": [],
"_requirements": []
}
}
]
}
]
}
Pythonでこのデータを取得する場合のサンプルコードです。シンプルなJSONなので、標準のライブラリだけで取得できます。
import json
with open("info.dat", "r", encoding="utf-8") as fp:
info_dat = json.load(fp)
bpm = info_dat["_beatsPerMinute"]
曲データ
info.dat や 譜面データの中に、曲の長さに関する情報が格納されていません。 NPSや解析データグラフに表示する場合など、 曲の長さが必要なときは、曲データから直接取得します。
Pythonでこのデータを取得する場合、(多分)標準のライブラリでは不可能なので、 PyGame というゲーム開発用のライブラリを利用しています。
import pygame
pygame.mixer.init() # PyGameの初期化
song = pygame.mixer.Sound("song.egg")
song_length = song.get_length()
難易度別のマップデータ
データが多かったため大幅に間引いています。また、ノーツの情報である、 _notes 以外は説明を省略いたします。
{
"_version": "2.0.0",
"_BPMChanges": [],
"_events": [],
"_notes": [
{
"_time": 11.915300369262695,
"_lineIndex": 0,
"_lineLayer": 0,
"_type": 0,
"_cutDirection": 6
},
{
"_time": 12.915300369262695,
"_lineIndex": 1,
"_lineLayer": 0,
"_type": 1,
"_cutDirection": 6
}
],
"_obstacles": [
{
"_time": 195.91529846191406,
"_lineIndex": 0,
"_type": 0,
"_duration": 39,
"_width": 1
},
{
"_time": 195.91529846191406,
"_lineIndex": 3,
"_type": 0,
"_duration": 39,
"_width": 1
}
],
"_bookmarks": []
}
_notes
ノーツの位置や向き、タイミングなどの設定値が格納されています。 { } で囲まれたブロック1つが1ノーツの情報になります。
Pythonでこのデータを取得する場合のサンプルコードです。info.datと同じく、シンプルなJSONなので、標準のライブラリだけで取得できます。
import json
with open("Hard.dat", "r", encoding="utf-8") as fp:
difficulty_map = json.load(fp)
notes = difficulty_map["_notes"] # ノーツ全てを取得
note = notes[0] # 先頭のノーツを1つだけ取得
ノーツ1つは以下のようなデータで構成されています。
key | 説明 |
---|---|
_time | ノーツを切るタイミング。BPMの値になっているので、以下の計算で秒に変換します。 _timeの値 × 60 ÷ bpm(info.datから取得したbpm) |
_lineIndex | 左右の位置。0~3で設定されており、0が一番左、3が一番右になります |
_lineLayer | 上下の位置。0~2で設定されており、0が一番下、2が一番上になります |
_type | どちらの手で切るか。0,1 で設定されており、0が左手、1が右手になります |
_cutDirection | ノーツ種類です。設定値とノーツの組み合わせは以下の表をご確認ください |
_cutDirectionの設定値とノーツ
値 | 向き |
---|---|
0 | |
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 |
これらの値を使うことで、壁やボムを除いた譜面の情報が取得できます。 なお、MODで特殊な動きをするノーツは、おそらくこの方法では解析出来ないと思います。引き続き試して行きますので、気長にお待ち下さい。
解析の例
おなじみのNPS
NPSはノーツの数を曲の秒数で割って算出される値になります。
Pythonでこのデータを取得する場合のサンプルコードです。曲データから曲の長さを、難易度別のマップデータからノーツの数を取得して計算します。
import json
import pygame
pygame.mixer.init()
song = pygame.mixer.Sound("song.egg")
song_length = song.get_length() #
with open("Hard.dat", "r", encoding="utf-8") as fp:
difficulty_map = json.load(fp)
notes = difficulty_map["_notes"] # ノーツ全てを取得
nps = len(notes) / song_length
# 今回サンプルで利用しているMAPだと、2.41 となってます。
この値の欠点ですが、全ての平均なので、ノーツが密集している部分があっても気がつけません。なので、1秒毎のノーツ数を数えて、ノーツが密集している場所を探してみようと思います。
1秒ごとのノーツ数を数えてグラフにする
難易度の一つとして、やはりノーツの密度は見逃せません。 毎秒のノーツ数をカウントして、突出して密度が高い時間帯がないか確認します。
※ ここからはコードが長くなるので、記事では省略させていただきます。別途サンプルコードを上げていますので、そちらをご確認ください。
直に全部出力した結果、わけが分からなくなってますね。ただ、200秒過ぎのところに秒間6ノーツのポイントがあるようですが、まんべんなくバラけている印象です。
少しデータを間引いて、5秒ごとに一番密度が高かった場所を参考値として利用するようにしてみます。
ちょっとマシになったとおもいます。今回サンプルとして使っているMAPは、NPS:2.41 の譜面ですが、最も密度が高い所だとNPS:6あることがわかります。
今後の展望について
手元にある譜面のチェック
今後は、Webサービスとして作ろうと思っているものが1つあります。MAPデータをアップロードすると、ノーツの分布をグラフで表示させたり、ウインドノーツやブラインドノーツなど、一般的に好まれない配置があるかチェックするようなサービスを考えてます。
DLしてきたMAPの内容のチェックだけでなく、マッパーさんが作成中の譜面のチェックでも使えるかなーと思っています。
特殊譜面の解析について
三妖精を代表とする?特殊譜面については、密度や使われているノーツの種類の分布を見ても、それほど変な値になりませんでした。
別アプローチとして、ノーツをまっすぐ切るために剣先を移動させる行為を「予備動作」とし、予備動作でどれだけ大きく剣先が動くか?といったアプローチで解析を頑張ってます。。。が、間に合いませんでした。ごめんなさい。お詫びの975をやっておきます。
長々とありがとうございました。それでは、引き続きビートセイバーを楽しんでいきましょう。