はじめに
React で「太⚪︎の達人」風のゲームを作る場合、Canvas を使用することで高速な描画が可能になり、太⚫︎の連打(高頻度の入力)にも対応できます。以下の手順で導入するとスムーズに実装できます。
↓この過去記事の続編です
https://qiita.com/Ryu-990/items/bc609c6e35270bad2bbc
Q. 最近流行りのuseOptimisticじゃダメなの?
useOptimistic は主に「サーバーとの同期を待たずに、即座に UI の状態を更新する」ためのフックですが、太⚫︎の達人のような連打を伴う高速な入力処理には向いていません。
いいね機能でハートマーク打つ位ならいいかもしれませんが(笑)。
理由
1. useOptimistic は「楽観的 UI 更新」に特化
useOptimistic は、ネットワーク通信が発生するケース (API への更新リクエストなど) で、レスポンスを待たずに UI を更新するためのもの。
しかし、太⚫︎の連打のようなリアルタイム入力には、そもそもサーバー通信の遅延がない前提のほうが望ましい。いつノルマクリアしたか分からなくなるでしょ?
API との同期を前提に useOptimistic を使うと、ネットワーク遅延が発生したときに UI のずれや整合性の問題が起こる可能性がある。
2. 高速な入力に対するパフォーマンス最適化が必要
useOptimistic は useState と似た挙動をするが、React の再レンダリングが発生するため、連打のたびにUI の更新が走るとパフォーマンスが低下する。
太⚫︎の達人のようなゲームでは、レンダリングを最小限に抑えることが重要になります。
早速やってみよう!
1. useRef で canvas を管理する
React の useRef を使って canvas 要素を取得 し、requestAnimationFrame を使って描画を行います。
実装の流れ
useRef で canvas を取得
useEffect で描画ループを開始
requestAnimationFrame を使ってアニメーションを実装
太⚫︎の入力処理を追加
2. 基本的な Canvas のセットアップ
import React, { useRef, useEffect, useState } from "react";
const TaikoGame = () => {
const canvasRef = useRef(null);
const [notes, setNotes] = useState([]);
const [score, setScore] = useState(0);
// ノーツを更新する関数
const updateNotes = () => {
setNotes((prevNotes) =>
prevNotes
.map((note) => ({ ...note, x: note.x - 5 })) // ノーツを左に移動
.filter((note) => note.x > -50) // 画面外のノーツを削除
);
};
// キーボードまたはタップで入力
const handleHit = () => {
setScore((prev) => prev + 1);
};
// 描画関数
const draw = (ctx) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// 太⚫︎を描画
ctx.fillStyle = "brown";
ctx.fillRect(50, 200, 100, 100);
// ノーツを描画
ctx.fillStyle = "red";
notes.forEach((note) => {
ctx.beginPath();
ctx.arc(note.x, 250, 20, 0, Math.PI * 2);
ctx.fill();
});
// スコア表示
ctx.fillStyle = "black";
ctx.font = "20px Arial";
ctx.fillText(`Score: ${score}`, 10, 30);
};
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
let animationFrameId;
const render = () => {
updateNotes();
draw(ctx);
animationFrameId = requestAnimationFrame(render);
};
render();
return () => cancelAnimationFrame(animationFrameId);
}, [notes, score]);
// ノーツを追加する (デモ用)
useEffect(() => {
const interval = setInterval(() => {
setNotes((prev) => [...prev, { x: 400 }]); // 右からノーツが出現
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div
tabIndex={0}
onKeyDown={handleHit} // キーボードで反応
onClick={handleHit} // クリックで反応
style={{ textAlign: "center" }}
>
<h1>React 太⚫︎の達人</h1>
<canvas ref={canvasRef} width={400} height={400} style={{ border: "1px solid black" }} />
</div>
);
};
export default TaikoGame;
3. ポイント解説
useRef で canvas の描画を管理
canvasRef を useRef で取得し、useEffect で canvas に描画する。
requestAnimationFrame でスムーズな描画
requestAnimationFrame を使ってノーツの移動を滑らかにする。
ノーツの管理とスコアの更新
useState でノーツとスコアを管理し、useEffect で定期的にノーツを生成する。
入力の処理
onKeyDown(キーボード入力)や onClick(タップ入力)で太⚪︎を叩けるようにする。
4. さらに改良するには?
onPointerDown を追加すると、スマホのタッチ操作にも対応可能
音をつける (Audio API で ド⚫︎ や ⚫︎ッ の音を再生)
複数のノーツを用意して、「ド⚫︎」「⚫︎ッ」を判定
このように Canvas を活用することで、React でも太⚫︎の連打に耐えられる高速な描画が可能になります!
番外編ミッション:PythonでMP3ファイルから譜面のコードを作成せよ!
音楽ファイル(MP3)の音の強弱(音量)に基づいて譜面を自動的に生成するPythonスクリプトを作成するためには、音楽の波形を分析して、強弱を検出する必要があります。これを実現するために、pydub や librosa といったライブラリを使うことができます。
以下に、pydub と numpy を使って、音の強弱に基づいて簡単な譜面を生成する方法を紹介します。
これには、音量のピークを検出し、指定したタイミングで「ド⚫︎」や「⚫︎ッ」の譜面を作成するロジックが含まれています。
必要なライブラリ
まず、必要なライブラリをインストールします。
pip install pydub numpy
Pythonコード例
from pydub import AudioSegment
import numpy as np
import os
# MP3ファイルをロード
def load_audio(file_path):
audio = AudioSegment.from_mp3(file_path)
return audio
# 音量(dB)を計算
def calculate_volume(audio):
# オーディオを短いチャンクに分割して音量を計算
chunk_size = 100 # ミリ秒単位
samples = []
for i in range(0, len(audio), chunk_size):
chunk = audio[i:i+chunk_size]
rms = chunk.rms # Root Mean Square 音量
samples.append(rms)
return samples
# 音の強弱に基づいて譜面を作成
def generate_notes(file_path):
audio = load_audio(file_path)
volumes = calculate_volume(audio)
notes = []
time_unit = 100 # ミリ秒単位(音量を測るための時間間隔)
threshold_d*n = 10000 # ド⚫︎の音量閾値
threshold_*a = 5000 # ⚫︎ッの音量閾値
for i, volume in enumerate(volumes):
time = i * time_unit
if volume > threshold_d*n:
notes.append({"time": time, "type": "d*n"})
elif volume > threshold_*a:
notes.append({"time": time, "type": "*a"})
return notes
# 譜面を出力する
def export_notes(notes):
notes_js = "export const notes = [\n"
for note in notes:
notes_js += f" {{ time: {note['time']}, type: '{note['type']}' }},\n"
notes_js += "];"
return notes_js
# メイン処理
if __name__ == "__main__":
mp3_file = "your_song.mp3" # 解析したいMP3ファイルのパス
if not os.path.exists(mp3_file):
print(f"ファイル {mp3_file} が見つかりません")
exit(1)
notes = generate_notes(mp3_file)
notes_js = export_notes(notes)
print(notes_js)
処理の流れ
音楽の読み込み: pydub を使ってMP3ファイルを読み込みます。
音量の計算: 音楽の各チャンク(100ミリ秒ごと)で音量を計算します。ここでは、音量のRMS(Root Mean Square)を使用しています。
譜面の生成: 音量に基づいて、音量が閾値(ここでは d*n と *a の閾値)を超えたタイミングに譜面を生成します。
JS形式で出力: 譜面データを export const notes の形式で出力します。
出力例
このスクリプトを実行すると、次のようなJS形式の譜面データがコンソールに出力されます。
export const notes = [
{ time: 100, type: 'd*n' },
{ time: 300, type: '*a' },
{ time: 500, type: 'd*n' },
{ time: 700, type: '*a' },
// 他のノーツも続きます...
];
調整
音量の閾値 (threshold_d*n と threshold_*a): これらの値を調整することで、どの音量で「⚫︎ン」や「⚫︎ッ」を出すかを変更できます。大きい音量で「⚫︎ン」、小さい音量で「⚫︎ッ」といった感じです。
チャンクサイズ (chunk_size): 音量を計算する時間間隔を変更できます。
注意点
音楽の分析には限界があり、複雑なメロディやバックグラウンドノイズが多い場合は、完全に正確な譜面を作成するのは難しいです。そのため、音量に基づく簡易的なアプローチとして考えてください。