Rust歴7日ですが音ゲーを作りました

2021/12/09

top

ターミナル上で動く某太鼓的音ゲーを,ずっと勉強したかったRustで作ってみました.

音ゲーとは

音楽に合わせて音符が流れてくるので,タイミングよくボタンを押すゲームです.

流れてくる音符が

ワクに重なったら

心をこめてボタンを押す

そして,タイミングの正確さによってGREAT,GOOD,BADのように判定が行われ,それに応じた得点が入ります.

作ってみた

構成図

楽曲の再生:rodio

音ゲーなので楽曲を再生しないことには始まりません.rodioを使用することで簡単にMP3を鳴らせます.このライブラリは,MP3やFlac,MP4といったメディアファイルのデコードからLinuxのサウンドAPIを叩くところまで,まるっとすべてやってくれるのでとても便利です.

ゲームエンジン:ruscii

rusciiはターミナルベースのゲームを作るためのライブラリです.メインループ,画面描画,キーイベントの捕捉などを行ってくれます.例えば以下のようにして音ゲーを作成できるはずです.

// ゲームループ
app.run(|app_state: &mut State, window: &mut Window| {
    for key_event in app_state.keyboard().last_key_events() {
        // key_eventを見て,押されたキーに対応する判定処理を行う
        // ...
    }

    // 音符の描画
    // ...
});

rusciiを使う上で困ったこと

これは事前に確認しなかった私が悪いのですが,key_eventキーの押された時刻が含まれない という仕様に悩みました.イベント自体に情報がないので,判定処理時の時刻を使うしか無いわけですが…

// ゲームループ
app.run(|app_state: &mut State, window: &mut Window| {
    for key_event in app_state.keyboard().last_key_events() {
        // key_eventにイベントの発生時刻が含まれない!
        // つまり,キーの押された正確な時刻がわからない…

        // 仕方なく現在の時刻で近似するが…?
        let pressed_time = SystemTime::now();
    }
});

これではプレイヤーがキーを押した時刻を,最大で 約1フレーム分遅れて認識することになります.

判定のズレ

判定にうるさいのが音ゲーマーなので,ここはしっかりと検証することにしました.まず,一般的な音ゲーはどのくらいの精度で判定しているのでしょうか.ゲームによって判定幅は違いますが,最も良い(GREAT)判定幅を ±33msとするものが多いようです.次に,rusciiの判定精度を検証しました.私のPC,Terminator on Ubuntu 20.04 on Thinkpad E14でベンチマークした結果,60fpsを安定して出せることがわかりました.つまり,

1000ms÷60fps=16.666ms1000ms \div 60fps = 16.666 \cdots ms

ですから,約16ミリ秒の精度で判定できることになります.カンの良い方は気づいていたと思われますが,±33msという判定幅は60fpsの4フレーム分に相当します.実は,この「フレームごとに判定する」という処理方法は音ゲー界隈では結構一般的なのだそうです.一般の音ゲーで問題になっていないわけですから,今回の実装上も問題ないでしょう.

TTYでは動かなかった

±33msを16msで判定…と考えると,本音を言えばもう少し精度がほしいところですね.というわけで,試しに120fps設定でアプリを起動してみましたが,117〜119fpsと安定しませんでした.リリース用の最適化ビルドも試してみましたが効果なし.ここで以前 TTYの動作がサクサクだったことを思い出しました.これなら120fpsの安定動作が狙えるのでは?と思いましたが,結論から言うと,rusciiはキーイベントの取得にX11のAPIを使用しているのでTTYでは動かないとわかりました.ざーんねん.

そういえば,先行研究:ターミナルで遊べる音ゲー作ってみたでもキーイベント関連で苦労されていたな〜ということを思い出しました.先行研究ではtelnetでのキー送信のためにKarabinerをいじっていましたが,やはりターミナルゲームにとってキー周りは難しい課題なのでしょうか.

譜面の表現

譜面の表現フォーマットには,デファクトスタンダードであるTJAを採用しました.TJAでは,例えば次のようにして譜面を表します.

BPM:160
WAVE:Rahatt - Fericire.mp3
OFFSET:0.17

#START
1111,
1020112010201120,

冒頭のヘッダでは曲のテンポ(BPM)や音源ファイルの名前(WAVE)を指定します.

#START以降が譜面の本体で,「,」は小節の終わりを示します.1がドン,2がカッ,0は休符を表します.上の例では1小節目は4分間隔で「ドン  ドン  ドン  ドン」,2小節目は「ドッカッドドカッドッカッドドカッ」となります.

デモ

楽曲:Rahatt - Fericire(OthermanRecordsより,CC BY-NC 2.1,ただしデモのために短く編集しています)

ソースコードは非公開です.

私のRust学習日記

ここまでのRust学習を日記的に振り返ります

Rust 3部作と称して好きなアプリを3つ作り,その都度必要な知識を深堀するという方針で勉強してきました.

1日目:基本の勉強

はじめに,Rustの基礎知識を勉強しました.

平日だったので業務終わりに上記を読むだけで終わりました.各種文法や予約語の意味などを学習しました.

2〜3日目:レイトレーサーの作成

レンダリング結果

3DCGが趣味のエンジニアとして,一度はレイトレーサーを書いてみたいと思っていました.

えっ!初手からレイトレーサー作るの!?

と驚かれるかもしれませんが,実際は下記の資料のコードを写経していただけなので何ということはありません.

Rustという難しい言語の初心者にとって,やはり0バイトからコードを書くのは大変なので,写経によって「正しい書き方」をまず学べたのは良かったと思います.

Rust以外では,物理の公式や数学の知識に苦しめられました.基本的な知識が少ないので,ベクトルの計算をミスって見知らぬ土地に飛ばさることもしばしばでした.特に数学のクォータニオンは鬼門でした.難しすぎるんじゃ.ひたすら「 i2=j2=k2=ijk=1i^2 = j^2 = k^2 = ijk = -1 …」とつぶやいていました.

4〜6日目:sha1sumの作成

以前から「ハッシュ関数って中身はどうなっているんだろう」と気になっていたのでSHA-1を実装してみました.

写経ではタイプミスさえしなければエラーは発生しませんが,自分なりにコードを書くことで「ダメな書き方」というのを学べました.「様々なエラーに遭遇 → 討伐」をループすることでRustレベルを上げられたと思います.

SHA-1の実装自体は,IPAが公開している日本語の資料がとても参考になりました.

上記の資料通りコードを書き,testという文字列を試しに入力した瞬間を覚えています.正解はA94A8FE5CCB19BA61C4C0873D391E987982FBBD3なのですが,私のプログラムが17という結果を返した時には衝撃を受けました.ポンコツすぎるだろ.

結局IPAが提供しているCのコードと自分のコードをステップ実行し,ハッシュ関数のどの部分で計算が食い違っているのかを確認しました.ちなみに原因は次のようなものでした.

let ans = (u64::from(self.0) + u64::from(y.0)) % 2^32;

本人は2の32乗で割ったあまりを計算している気になっています.しかし,実際は2 XOR 32,つまり34であまりを計算しています.そのためハッシュ空間がとんでもなく小さくなっていたのでした.アホですね.

7〜8日目:音ゲーの作成

Rust 3部作のラストの作品として音ゲーを作りました.自分でもつまらないことを言ったなと思います.

休日である7日目の朝7時から書き始めたのですが,13時くらいには音符が流れるようになり,18時くらいには音符を叩いて遊べるようになりました.

8日目は音源を選定し譜面を制作して,実際に自分のツールに読み込ませて遊んでみました.プレイしていると,普段音ゲーで遊んでいる時と同じような演奏感が得られて,「自分の書いたプログラムで遊べてる!」という感動がありました.

まとめ

自給自足ごりらという音ゲー用語があります.「ごりら」というのは,人間とは思えないほど音ゲーが上手いプレイヤーを表す用語で,語源は諸説あるのですが,大量の音符を処理し続けられる腕力がゴリラっぽいからですたぶん.そして,そんな上級者が音ゲーをきっかけにアーティストまたは制作スタッフとなった状態を 自給自足ごりら といいます.自分で作った難しい楽曲を自分でクリアするから自給自足です.

自分で作った音ゲーシステムを自分でクリアするエンジニアもまた,ある意味での自給自足ごりらと言えるかもしれません.私は音ゲーの腕もプログラミングのスキルも,まだまだゴリラと呼べる領域に至っていません.これからも努力を重ねて,「つよつよ フロントエンド バックエンド インフラ 低レイヤー 完全理解 ゴリラ音ゲーマーエンジニア 」を目指そうと思います.

ところで,本記事はRecruit Advent Calendar 2021の9日目でした.仕事と全く関係ない内容ですみません.昨日はtakepepeさんの「next/linkのPrefetchingと型安全」でした.「Routing / Prefetching / Caching / TypeSafe」が不可分というのは,なるほどなぁ〜という感じです.

明日はyutachaosさんが何かを書いてくれるそうです.楽しみですね!

続けて読む…

【ラーメンズ】特殊な50音表を探索してみた【50 on 5】

2020/05/17

コーディングで流す曲5選

2020/12/26

Blenderで花火大会

2022/08/18

Credits(Frums)をマジのターミナルで

2021/04/29

不安だらけのリクルートインターン体験記【Recruit Internship for Specialist 2019】

2019/10/02

Blenderと魚

2017/06/13

書いた人

sititou70のアイコン画像
sititou70

都内の社会人エンジニア3年生。Web技術、3DCG、映像制作が好き。