『ボタン同時押し』実装を考える

たまにはゲーム制作っぽいのも書く。

横スクロールの格ゲーっぽいゲームを作る時を考える。 このとき、攻撃ボタンを押すとパンチが出て、下キーを押してしゃがみながら攻撃するとキックが出るようにしたいとする。 面倒なのでコマンド攻撃とか諸々は考えない。

この状態遷移図的なのを描くと以下のようになる。 今のフレームでの状態と押しているボタンによって、次フレームの遷移先状態が決まる。

f:id:mizuooon:20171019215019p:plain:w250

だが、実際作ってみるとわかるがこのまま愚直に実装すると恐ろしく手触りが悪くなる。 というのも、立っている状態からキックを出そうとすると結構な割合で出ないのである。 これはボタンの同時押しに対応した実装になっていないからである。 今回はこれを"いい感じ"にすることについて考える。

同時押しの処理

とりあえずどうしてそうなるのか考えてみる。 例えば、立ち状態から 下+攻撃 を入力したとする。 このとき、攻撃の優先度が高ければパンチが出てしまう。 ではしゃがみの優先度を上げればいいかというとそうではなく、 その場合も次フレームでしゃがみ状態に移行したときには 『攻撃ボタンが押下された瞬間のフレーム』ではなくなっているので結局そのまま攻撃は不発になる。 これらは明らかに想定している挙動ではない。

f:id:mizuooon:20171019220203p:plain

これについて対処法をいくつか考える。

まず、単純に思い付くのは『攻撃ボタンが押下された瞬間以降のフレーム』も遷移させる方法だが、 これはゲーム性に関わるので簡単には採用できない。 また、1フレームで複数回の状態遷移を許すという方法も考えられるが、 これは同じボタンを押すたびに互いにスイッチする状態の組があった際などには永久ループになるので好ましくない。 ボタンの状態を次フレームも『押下された瞬間のフレーム』に書き換えるという方法も考えてしまいそうだが明らかに行儀が悪い。

とりあえずまともそうな方法は、立ち状態に 下+攻撃 の遷移を追加する方法である。 このときの状態は以下のようになる。 遷移が増えるとちょっとややこしい感じになりそうだがまあ上記の方法たちよりも許される気がする。

f:id:mizuooon:20171019220842p:plain:w260

だが、上記のパターンに加えて例えばジャンプ攻撃を考えるとこの方法は困ったことになる。 状態遷移は以下のようになるが、この場合空中攻撃がジャンプ状態を経由しない。 ジャンプの加速処理をジャンプ状態に入った瞬間に行うなどしているとすると、 この空中攻撃はジャンプをしないまま出る。これは困る。

f:id:mizuooon:20171019221437p:plain:w320

結局自分が落ち着いた方法は、 同時押しした際に使わなかったボタンを退避させておくバッファを用意する方法である。 仮にこれをコマンドバッファと呼ぶと、処理は以下の図のような感じになる。 f:id:mizuooon:20171019222020p:plain

コマンドバッファを使うと先述したような諸々の問題は回避可能である。 実装時にはコンボ用の先行入力と絡めてコマンドバッファを用意すると綺麗になる気がする。

同時押ししたつもり押しの処理

で、これを実装するとさらにわかるのだが、実はまだ手触りが悪い。

以上の議論は1フレームでのボタンの同時押しがちゃんとできている場合を想定したものであるが、 実際は人間はそんなに正確にボタンを押すことはできない。 ボタンを押すタイミングが多少前後するのである。

『下』を『攻撃』よりも先に押しているのであればそのままキックは出るが、 『攻撃』を先に押している場合はパンチが出る。 これが手触りの悪さに繋がっている。

これについても対処法を考える。

まず考えられるのは、 パンチ状態に移行する前にバッファの状態を用意する方法である。 バッファの状態で何フレームか待ち、『下』を押した場合は『キック』、 『下』を押さなかった場合は『パンチ』に移行する。

f:id:mizuooon:20171020141336p:plain:w350

しかしこの方法は状態が増えると遷移の作成が大変なことになるのが想像できる。 また、余計な状態が増えることによって敵キャラクター用のAIの作成も煩雑にしてしまいかねない。

そこで、次に考えられる対処法が コントローラからの入力をバッファする方法であり、こちらが本命である。 これは、下図のように 人間が入力している物理コントローラの状態に対して 『同時押ししたことにさせてやる』処理としてバッファリングを行い、 その結果を仮想コントローラの状態としてキャラクタに入力してやろうというものである。

f:id:mizuooon:20171020141344p:plain

このとき、AIは常に完全な同時押しが可能なのでバッファリングを噛ませずに 仮想コントローラの状態を直接出力する。 逆にバッファリングをAIの処理に対して行うと 『同時押ししてないのに同時押しになる』場合を回避する処理が必要となるため煩雑になってしまう。

バッファリングの処理は下図のようになっている。

f:id:mizuooon:20171019232347p:plain

物理コントローラの行ったボタンを『押し』たり『放し』たりといったイベントは、 一度イベントキューに蓄積される。 そして、 イベントキュー内の最古のイベントの発生時間より一定フレーム経ったとき、 一気にそのイベントを仮想コントローラの状態へと反映させ、イベントキューをクリアする。 ちなみに、このバッファリングを行うフレーム数は大体1~3フレーム程度が触った時の感じとしてよいと思われる。

ボタンを押すだけでなく放すのもイベントなので、 『下を放す+攻撃』で『座り』状態から『パンチ』を出すのにも対応できるのにも注目して欲しい。

実装時に注意しなければならないのは、 イベントキュー内に同じボタンについて『押した』と『放した』が同時に存在すると フラッシュした際にそのボタンが押された事実が消滅することである。 つまりボタンを連打すると反映されない。 この対策としては、同じボタンについてのイベントが既にキューの中にあった場合には 強制的にキューをフラッシュしてやるといい。

このコントローラバッファリングの欠点として考えられるのは、 バッファリングの存在によって入力遅延が発生し AIに可能な動きが理論上人間には不可能になっていることであるが、 そもそもAIの動きは人間には事実上不可能なので そこは妥協して許されるだろうと思う。

おわり

真面目に考えるとこういう処理ひとつ取っても結構大変ですね。