Part1 | Part2 | Part3 | Part4【完結】
はじめに
Part3でSDL2を使ったGUI描画ができるようになりました。
でもまだ足りないものがある...
- キー入力 → ゲームを操作できない
- サウンド → ピコピコ音が出ない
今回はこの2つを実装します。意外とムズいんですよ、これが。
目次
CHIP-8のキー入力
16キーのレイアウト
CHIP-8は16個のキー(0-9, A-F)を使います:
CHIP-8のキー:
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ C │
├───┼───┼───┼───┤
│ 4 │ 5 │ 6 │ D │
├───┼───┼───┼───┤
│ 7 │ 8 │ 9 │ E │
├───┼───┼───┼───┤
│ A │ 0 │ B │ F │
└───┴───┴───┴───┘
これをキーボードにマッピングします:
キーボード:
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │
├───┼───┼───┼───┤
│ Q │ W │ E │ R │
├───┼───┼───┼───┤
│ A │ S │ D │ F │
├───┼───┼───┼───┤
│ Z │ X │ C │ V │
└───┴───┴───┴───┘
キー関連命令
| オペコード | 説明 |
|---|---|
| EX9E | VXのキーが押されていたらスキップ |
| EXA1 | VXのキーが押されていなかったらスキップ |
| FX0A | キーが押されるまで待機 |
FX0A命令(キー入力待ち)
この命令が一番厄介でした...
仕様
FX0A - LD Vx, K
キーが押されるまでエミュレータを停止し、
押されたキーの値をVxに格納する
問題: 「エミュレータを停止」ってどうやるの?
実装方法
フラグを使って状態管理します:
struct Chip8 {
// ...
waiting_for_key: bool, // キー入力待ち状態
key_register: usize, // キーを格納するレジスタ
}
FX0A命令が来たらフラグを立てる:
(0xF, _, 0x0, 0xA) => {
self.waiting_for_key = true;
self.key_register = x;
println!("Waiting for key input...");
}
cycle関数で状態チェック:
fn cycle(&mut self) {
// キー入力待ち中は何もしない
if self.waiting_for_key {
return;
}
let opcode = self.fetch();
self.execute(opcode);
}
キーが押されたら解除:
fn key_press(&mut self, key: u8) {
if key < KEY_COUNT as u8 {
self.keypad[key as usize] = true;
// キー入力待ち中だったら解除
if self.waiting_for_key {
self.v[self.key_register] = key;
self.waiting_for_key = false;
println!("Key {:X} pressed!", key);
}
}
}
図解:キー入力待ちの流れ
┌─────────────┐
│ FX0A実行 │
└─────┬───────┘
↓
┌─────────────┐
│ waiting = true │
│ register = X │
└─────┬───────┘
↓
┌─────────────┐ ┌─────────────┐
│ cycle()は │────────→│ キー押下? │
│ 何もしない │←────────│ (NO) │
└─────────────┘ └─────────────┘
│
↓ (YES)
┌─────────────┐
│ V[X] = key │
│ waiting=false│
└─────────────┘
SDL2でのキー処理
キーマッピング関数
fn keycode_to_chip8(keycode: Keycode) -> Option<u8> {
match keycode {
Keycode::Num1 => Some(0x1),
Keycode::Num2 => Some(0x2),
Keycode::Num3 => Some(0x3),
Keycode::Num4 => Some(0xC),
Keycode::Q => Some(0x4),
Keycode::W => Some(0x5),
Keycode::E => Some(0x6),
Keycode::R => Some(0xD),
Keycode::A => Some(0x7),
Keycode::S => Some(0x8),
Keycode::D => Some(0x9),
Keycode::F => Some(0xE),
Keycode::Z => Some(0xA),
Keycode::X => Some(0x0),
Keycode::C => Some(0xB),
Keycode::V => Some(0xF),
_ => None,
}
}
イベント処理
for event in event_pump.poll_iter() {
match event {
Event::KeyDown { keycode: Some(keycode), .. } => {
if let Some(key) = keycode_to_chip8(keycode) {
chip8.key_press(key);
}
}
Event::KeyUp { keycode: Some(keycode), .. } => {
if let Some(key) = keycode_to_chip8(keycode) {
chip8.key_release(key);
}
}
// ...
}
}
ポイント:KeyDownとKeyUpの両方を処理
CHIP-8のEX9E/EXA1命令は「今この瞬間キーが押されているか」をチェックします。
なので:
-
KeyDown→keypad[key] = true -
KeyUp→keypad[key] = false
両方処理しないとキーが押しっぱなしになります。
サウンドタイマー
CHIP-8のサウンド仕様
CHIP-8のサウンドはめちゃくちゃシンプル:
-
sound_timer > 0のときビープ音が鳴る -
sound_timerは60Hzでデクリメント - 音の周波数は規定されていない(一般的には440Hz)
fn tick_timers(&mut self) -> bool {
let should_beep = self.sound_timer > 0;
if self.delay_timer > 0 {
self.delay_timer -= 1;
}
if self.sound_timer > 0 {
self.sound_timer -= 1;
}
should_beep
}
矩形波を生成する
SDL2のオーディオ
SDL2でオーディオを使うには AudioCallback トレイトを実装します:
use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired};
struct SquareWave {
phase_inc: f32,
phase: f32,
volume: f32,
}
impl AudioCallback for SquareWave {
type Channel = f32;
fn callback(&mut self, out: &mut [f32]) {
for sample in out.iter_mut() {
// 矩形波を生成
*sample = if self.phase <= 0.5 {
self.volume
} else {
-self.volume
};
self.phase = (self.phase + self.phase_inc) % 1.0;
}
}
}
矩形波とは
┌──┐ ┌──┐ ┌──┐
│ │ │ │ │ │
──┘ └──┘ └──┘ └──
High (+volume) → Low (-volume) を繰り返す
レトロゲームのピコピコ音は、この矩形波で作られています。
オーディオデバイスの作成
fn create_audio_device(sdl_context: &sdl2::Sdl) -> Result<AudioDevice<SquareWave>, String> {
let audio_subsystem = sdl_context.audio()?;
let desired_spec = AudioSpecDesired {
freq: Some(44100), // サンプリングレート
channels: Some(1), // モノラル
samples: Some(256), // バッファサイズ
};
audio_subsystem.open_playback(None, &desired_spec, |spec| {
SquareWave {
phase_inc: 440.0 / spec.freq as f32, // 440Hz(ラの音)
phase: 0.0,
volume: 0.25,
}
})
}
再生制御
// メインループ内
if should_beep && !sound_playing {
audio_device.resume(); // 再生開始
sound_playing = true;
} else if !should_beep && sound_playing {
audio_device.pause(); // 一時停止
sound_playing = false;
}
実装と動作確認
キー入力デモプログラム
キーを押すと対応する文字が表示されるデモ:
fn create_key_demo_program() -> Vec<u8> {
let mut program = Vec::new();
// CLS
program.extend_from_slice(&[0x00, 0xE0]);
// LD V0, K (キー入力待ち)
program.extend_from_slice(&[0xF0, 0x0A]);
// CLS
program.extend_from_slice(&[0x00, 0xE0]);
// LD F, V0 (フォントアドレス)
program.extend_from_slice(&[0xF0, 0x29]);
// LD V1, 28 (X座標)
program.extend_from_slice(&[0x61, 0x1C]);
// LD V2, 13 (Y座標)
program.extend_from_slice(&[0x62, 0x0D]);
// DRW V1, V2, 5
program.extend_from_slice(&[0xD1, 0x25]);
// LD V3, 30 (サウンドタイマー = 0.5秒)
program.extend_from_slice(&[0x63, 0x1E]);
// LD ST, V3 (サウンド開始)
program.extend_from_slice(&[0xF3, 0x18]);
// JP 0x202 (ループ)
program.extend_from_slice(&[0x12, 0x02]);
program
}
実行結果
cargo run --bin chip8-sound
出力:
🎮 CHIP-8 Emulator with Sound
================================
Controls:
1234 → 123C
QWER → 456D
ASDF → 789E
ZXCV → A0BF
ESC → Quit
SPACE → Reset
P → Pause/Resume
================================
No ROM specified, running key input demo...
ROM loaded: 22 bytes
ウィンドウが開き、キーを押すと:
- 画面中央に対応する文字(0-F)が表示される
- 「ピー」とビープ音が0.5秒鳴る
- 次のキー入力待ちになる
意外とちゃんとゲームっぽくなってきた!
つまづいたポイント
1. キーが離されたときの処理
最初KeyUpを処理してなくて、キーが押しっぱなしになってました。
// ❌ NG: KeyDownだけ処理
Event::KeyDown { .. } => {
chip8.key_press(key);
}
// ⭕ OK: KeyUpも処理
Event::KeyDown { .. } => { chip8.key_press(key); }
Event::KeyUp { .. } => { chip8.key_release(key); }
2. サウンドが止まらない
sound_timer > 0 のチェックをタイマー更新前にやってしまい、永遠にビープ音が...
// ❌ NG: デクリメント後にチェック
self.sound_timer -= 1;
let should_beep = self.sound_timer > 0;
// ⭕ OK: デクリメント前にチェック
let should_beep = self.sound_timer > 0;
self.sound_timer -= 1;
3. ウィンドウタイトルで状態表示
キー入力待ち中は何も反応しないので、ユーザーが困惑する...
解決策:ウィンドウタイトルに状態を表示!
if chip8.is_waiting_for_key() {
canvas.window_mut().set_title("CHIP-8 - Waiting for key input...").ok();
} else if paused {
canvas.window_mut().set_title("CHIP-8 - PAUSED").ok();
} else {
canvas.window_mut().set_title("CHIP-8 Emulator").ok();
}
まとめ
今回実装したもの ✅
- 16キー入力 のサポート
- FX0A(キー入力待ち) の完全実装
- SDL2オーディオ による矩形波生成
- サウンドタイマー との連携
- ポーズ機能 (Pキー)
- ウィンドウタイトル での状態表示
学んだこと
-
状態管理 が重要
- キー入力待ちはフラグで管理
- cycle()を止めるだけでOK
-
AudioCallback の仕組み
- バッファにサンプルを書き込む
- resume/pauseで再生制御
-
ユーザーフィードバック の大切さ
- 何も反応しないと不安になる
- ウィンドウタイトルで状態を伝える
参考資料
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!
ここまで読んでくれてありがとうございました!