前回NESのエミュレータでキー入力からのPAD操作を実装することができていたが、
やはりゲームパッドでコントロールしたいという願望があったため、Nintendo Switchのプロコンを買った。
しかしながら、piston_windowは(少なくとも)Switchプロコンには(おそらく)対応していなかった。
(入力イベントが発生しない)
そこで今回piston_windowでRender Eventの60fpsを維持しつつ、プロコンの入力を得るために試行錯誤した。
使用するクレートはjoycon-rs
Gamepadから読み取りを行うのはそれなりに遅いので、
Receive Report Exampleのようにプロコンの情報を読み取る処理を別スレッドに分ける。
joycon_rs - Rust
Gamepad 読み出しスレッド
雑なエラーハンドルだが、
DeviceTypeがプロコンのものがあったら、driverをリターンし、HIDmodeで扱う。
(本当は動的なConnectionとDisconnectionにも対応したかったが後回し)
let driver = devices .iter() .flat_map(|device| SimpleJoyConDriver::new(&device)) .find(|driver| driver.joycon().deref().device_type() == JoyConDeviceType::ProCon); let pad = match driver { Some(driver) => SimpleHIDMode::new(driver).ok(), None => None, }; let pad_read = Arc::new(Mutex::new(pad::PadButton::empty())); let pad_thread = Arc::clone(&pad_read); if let Some(procon) = pad { std::thread::spawn(move || { // let tx = tx.clone(); loop { let res = procon.read_input_report(); // println!("{:?}", report); if let Ok(report) = res { let SimpleHIDReport { input_report_id, pushed_buttons, stick_direction, filler_data, } = report; if input_report_id == 63 { let btns = pushed_buttons .into_iter() .fold(pad::PadButton::empty(), |pad_btn, procon_btn| { pad_btn | pad::button_match(procon_btn) }); let pov = pad::pov_match(stick_direction); let mut pad = pad_thread.lock().unwrap(); *pad = btns | pov; } } } }); }
pad情報をArc<Mutex<T>>
を介して、共有する。
PadButtonはNESのPadに対応し、button_matchとpov_matchはプロコンの入力を変換するだけ。
bitflags! { #[derive(Default)] pub struct PadButton:u8{ const A = 1 << 0; const B = 1 << 1; const START = 1 << 2; const SELECT = 1 << 3; const UP = 1 << 4; const DOWN = 1 << 5; const LEFT = 1 << 6; const RIGHT = 1 << 7; } }
ゲームループ
共有変数のlockを何回も要求することがないように、pad_statusに代入している。
let mut pad_status = PadButton::empty(); while let Some(event) = window.next() { match event { Event::Loop(Loop::Render(_)) => { tick = std::time::Instant::now(); let duration = tick - last_tick; last_tick = tick; let fps = 1000.0 / (duration.as_millis() as f32); window.set_title(format!("fps:{:.*}", 1, fps)); pad_status = *pad_read.lock().unwrap(); let r = if pad_status.contains(PadButton::A) { 1.0 } else { 0.0 }; let g = if pad_status.contains(PadButton::B) { 1.0 } else { 0.0 }; let b = if pad_status.contains(PadButton::START) { 1.0 } else { 0.0 }; window.draw_2d(&event, |context, graphics, _device| { clear([1.0; 4], graphics); rectangle( [r, g, b, 1.0], // red [0.0, 0.0, 100.0, 100.0], context.transform, graphics, ); }); } _ => {} } }
しかしlockは要求・取得した時点でのスコープを抜けるまでlockし続けている気がする(要調査)ので、
lockしている時間を最小にするためには、それ自体を別のスコープで区切ったらいいのかもしれない(本当か?)。
こんな感じで?
... window.set_title(format!("fps:{:.*}", 1, fps)); { pad_status = *pad_read.lock().unwrap(); } let r = if pad_status.contains(PadButton::A) { 1.0 } else { 0.0 }; ...
これでも動いた。
まとめ
この拙い実装をNESエミュレータにいれたところ、しっかりと描写は60fpsを維持できて、マリオもストレスなくプレイできた。
最初はサンプルにあるような
let (tx, rx) = std::sync::mpsc::channel()
でPAD情報をsend, recvしたところ、プロコンの入力をからキャラが動くまでに遅延が生じたり、描写fpsが一瞬だけ60fps --> 10fpsになったりと不明な動きをしていた。
割と遅いIOを扱うために、マルチスレッドを使用する意義があったと思うし
そこでArc<Mutex<T>>
といった扱いやすい標準のライブラリを使えたことでRustので実装している意義もあったと思う。
そもそもとして、piston_windowがプロコンに対応していれば問題なかったのだが、ほかの純正コントローラーは値段が高い。