🦜
GoとPythonでホイールコントローラーをNSWに繋いだ話2
その後
ブラウザのgamepad-APIの不安定さに悩み、
こちらを使わせてもらってブラウザレスで繋がるように再調整しました!
成果物はこちら。
シンプルに実装しなおした
- Python側実装はそのまま
- GoのラッパーバイナリでJoystickデバイスの読み取りとPythonプロセスの制御を完結
- Linux限定のjoystickライブラリの場合HATスイッチも読めた!
- なのでボタン類はほぼ足りて別途UIで補完する必要がなくなりました!
固定割り当て
微妙に足りないボタンはコンビネーションボタンを割り当てました。
- L2: fullbrake
- R2: fullaccel
- LeftPaddle: L-button
- RightPaddle: R-button
- ○: A-button
- ×: B-button
- △: X-button
- □: Y-button
- L3: ZL-button
- R3: ZR-button
- SHARE: Minus-button
- OPTIONS: Plus-button
- PS+SHARE: Capture-button
- PS+OPTIONS: Home-button
- PS+L3: LStick-button
- PS+R3: RStick-button
- Hat: Left/Right/Up/Down(DPad)
- PS+Hat: RStick
- Wheel: LStick-X(range:-100..100)
- LeftPeddale: LStick-Y(range:0..100)
- RightPeddale: RStick-Y(range:0..-100)
デッドゾーンのオートキャンセルを実装
以下の実装により生の操作値からデッドゾーンを自動で発見しながら
デッドゾーンを切り取った値にノーマライズします。
var (
deadzonePlus = float32(0.0)
deadzoneMinus = float32(0.0)
)
// normalize returns a value in the range [-1, 1].
// auto deadzone canceler
func normalize(v float32) float32 {
if v == 0.0 {
return 0.0
}
if v > 0.0 {
if deadzonePlus == 0 {
deadzonePlus = v
} else {
if v < deadzonePlus {
deadzonePlus = v // minimum
}
}
if v < deadzonePlus {
return 0.0
}
return (v - deadzonePlus) / (1 - deadzonePlus)
} else {
if deadzoneMinus == 0 {
deadzoneMinus = v
} else {
if v > deadzoneMinus {
deadzoneMinus = v // maximum
}
}
if v > deadzoneMinus {
return 0.0
}
return (v - deadzoneMinus) / (1 + deadzoneMinus)
}
}
ガンマ補正
- 入手したハンコンの動作範囲は180°
- ラリー車は270°、一般車は900°あたりが一般的
- つまり、微妙な操作が180°モデルでは難しい
- そこでガンマ補正をいれるとよりコントローラブルになる
func gamma(v float64) float64 {
const g = 1.5
if v > 0 {
return math.Pow(v, g)
}
return -1 * math.Pow(-v, g)
}
左右が操作量に対し上下の出力にすることでニュートラル付近の操作を細かくします。
デッドゾーン最小値で必ず最小値を出力する
ガンマ補正を入れてから、ハンドルの最小反応値と近傍の2〜3の操作量値に対応するNSW側の認識がゼロ相当の出力になるようになりました。これでは疑似的にデッドゾーンの幅が広がっていることになります。
nxbt上では-100...+100の値がNSWに渡されるのですが、デッドゾーン最近傍の値がNSWにわたる値を-1、+1(つまり最小有効値)にシフトするという調整を入れました。
これにより任意のガンマ補正指定に対してもホイールの物理的なデッドゾーン範囲がNSWの認識最小値にマッピングされることになります。
最小操作値1%分の値の算出は0.01(=1%)のガンマ補正の逆数である「0.01の(1/g)乗」を最小操作値としました。
とにかくデッドゾーンはこの手のゲームの敵
- WRC9をやりこんでみてわかったのは高速走行時には-10%..+10%くらいの範囲で微妙に操作するという方法が有効でした
- それも操作量を微調整できることが狭い橋などの通り抜けには重要であるということもわかりました
- これだとデッドゾーンが10%もあったりしたら操作範囲は以下のようになります
- -20%...-10%...0%...+10%...+20%
- -10%...+10%の範囲が操作量ゼロとなり、-20%時に−10%相当、+20%時に+10%相当の操作になる
- では操作量を−5%から+5%に切り返し、その後+10%にしたい時、
- 実際の操作は-15%から+15%に切り返した後5%だけ増やすという操作をしなければなりません
- これは操作が難しくなる原因の一つです
- だからできる限りデッドゾーンは削る必要があるのです
デジタル操作のアナログ化
- 「V-Rally4」というタイトルのNSW版は困ったことにアクセルブレーキにデジタルボタンしか割り当てることができないという制約を持っています
- そこで、PWMという変調方式でボタン操作の連射を行いつつONとOFFの時間比を変化させることで擬似アナログ操作を実現しました。
- 1/12秒サイクルのノコギリ波を生成しておき、アナログ操作量をノコギリ波が下回る時ONで上回る時にOFFというデジタル出力をします。
- これでコントローラーは1/60秒ごとにボタンのON/OFF情報を送っているので、「オフとオン5段階」の計6段階の擬似アナログ操作ができました。
- 若干実際のエンジンの音はうおんうおん波打つ感じにはなりますが、ハーフアクセルができるだけでもだいぶコントローラブルになりました。
まとめ
- Thrustmaster-T80専用にはなりましたが、他のデバイスの合わせ込みも簡単にできると思います
- デッドゾーンの自動認識&キャンセルを実装してみたのは面白い試み
- ガンマ補正でナイーブなハンドリング操作も可能に
- デジタルアクセル・ブレーキしかできないタイトルもアナログ風に操作可能に
- これでWRC9/V-Rally4が快適に楽しく遊べる
Discussion
nxbt側に工場出荷時のデッドゾーン設定があったのでそれを1%にした。
ホイールコントローラーがすでにデッドゾーンを持っているのでこれでもちゃんとニュートラルに戻る。
すると、さらにナイーブな操作ができるようになった。
デッドゾーンを設けるレイヤ
アプリケーション側までデッドゾーンを最小化して持ってくるのが理想。
そしてアプリケーションのデッドゾーン設定で調整するのが最適解。
とはいえ、ハードウェア機構上の遊びをゼロにすることはできないし、
その操作量認識はどうしても経年劣化による変動が含まれるので、
耐用年数以内にはちゃんとニュートラルに戻ることを保証するために
どうしてもデバイスファームウェア側でデッドゾーンを設けざるを得ない。
OS側ドライバはできるだけデッドゾーンに触れてほしく無いのだけど、
レガシーなものと共用なドライバだとデッドゾーンの固定値が入ってしまっているものもある。
さすがにNSWを含む今時のコンソールのOSはデバイスの申告値を使うようになってるみたい。
X軸とY軸を同時操作するとお互い最大値が出せなくなるという問題があるなぁ。
やはりハンドルとアクセルブレーキは別のスティックに割り当てる方がいいのかもしれない。
ブレーキとアクセルが分離しているメリットはヒールアンドトゥができること(エンジンをふかすのとブレーキを同時にできる)
フルブレーキ+ハンドルを100%切ることがあるのかというとほぼなさそう。