
ゲームコントローラ入力感度の調整
ぼっちgamedev Advent Calendar 2018の5日目。
・ゲームにおけるコントローラの役割
・アナログとデジタル
・入力感度
・コントローラ入力感度の調整
について書く。
ゲームにおけるコントローラの役割
ゲームのコントローラは、ゲームへ向けてこちらの意図を伝える、いわば通信機の役割を果たす。
プレイヤー自身はゲームのキャラクターでは無い。プレイヤーが左足を前に出せばキャラクターの左足も前に出たり、右手を挙げればキャラクターの右手が挙がる訳では無い。こちらからゲーム世界を観測出来ても、ゲームの中からこちらが見えたりはしない。そのためコントローラが必要となる。
現代ではプレイヤーの動きをキャプチャしてゲーム操作を行う機構も存在するが、使われているゲームジャンルは限定的で、ダンスゲームなど、操作が人間のリアルに寄ったゲームが該当する。
多くのゲームに登場するキャラクターの大半は超人であったり(人間は10mも高くジャンプ出来ない)、超能力を使ったり(手から火の玉は出せない)、人間が同じ動きをするのは無理がある。
逆に、コントローラという制限の中、上手い操作を行うことがゲームの面白さに繋がっている側面もあるかもしれない。
ゲームと現実は乖離しているが、コントローラによって直感的に操作出来るような入力設計を行うことが重要である。
アナログとデジタル
コントローラは電子機器であるため、基本的にその入力はボタンの番号と、押されている(1)・押されていない(0)の二値で表される。
また、アナログスティックと呼ばれる、回転角と傾きを入力することが可能なものを備えたコントローラも多い。
PCゲームの場合は、マウスとキーボードがコントローラの代わりとなる。マウスはポインティングデバイスと呼ばれ、画面上での座標を入力として扱える。Wiiリモコンもポインティングデバイスの一種である。
入力感度
ゲームの処理速度は、CPUのクロック数に依存するが、少なくとも画面の描画速度よりも高速である。人間が見る画面のリフレッシュレートは一般的に60Hz以上であり、1秒間に60回以上画面が更新される。ゲームの描画もそれに追従するよう60FPS(Frame Per Second)以上が基本だ。
そしてupdate関数がそれ以上の回数呼ばれることになるため、適切に入力感度を設定していない場合、ボタンを1回だけ押したつもりでも、キャラクターがずっと先へ移動してしまうことがある。
人間の時間分解能は50ms程度と言われているが、update関数が1秒間に60回呼ばれたとしても約16msと、ボタンを押している間に少なくとも4回はupdate関数が呼ばれる可能性がある。
そのため、人間の感覚に合わせて、ゲームが入力に反応する間隔と感度を調整する必要があり、ここではまとめて入力感度と呼んでいる。
コントローラ入力感度の調整
FPSやTPSでは、マウスをアナログスティックに見立て、そのXY移動量を回転角と傾きに変換し、プレイヤーの首振りに利用している。しかしマウスによってセンサ解像度が違ったり、マウスの可動域が異なる場合もあるため、首振りのマウス移動量の調整が可能なものが多い。
ここではボタンの入力感度を調整するための実装を見ていく。まず、入力状態と入力受け取り関数を以下のように定義した。
struct InputState {
left: bool,
up: bool,
right: bool,
down: bool,
a: bool,
b: bool,
}
fn input() -> InputState {
InputState {
left: check_button(0),
up: check_button(1),
right: check_button(2),
down: check_button(3),
a: check_button(4),
b: check_button(5),
}
}
コントローラの対応するボタンの状態をそれぞれ真偽値で管理している。
ここではプレイヤーの移動を実装することにする。矢印ボタンを押すとプレイヤーのX軸加速度が増加し、加速度分移動していくようにした。
impl Update for Player {
fn update(&mut self, env: &Environment) {
let input_state = input();
if input_state.left {
self.ax -= 0.05;
if self.ax < -2.0 {
self.ax = -2.0;
}
}
if input_state.right {
self.ax += 0.05;
if self.ax > 2.0 {
self.ax = 2.0;
}
}
let next_pos = Point {
x: self.renderable.position.x + self.ax,
y: self.renderable.position.y
};
if env.stage.collision(&next_pos) {
self.renderable.position = next_pos;
}
}
}
実はこの実装は良くない。なぜなら移動量がupdate関数の呼ばれる回数に比例することになるからである。動作環境が固定ならば大きな問題にはならないかもしれないが、ハードウェアが変わると操作性が変わってしまう可能性がある。
JavaScriptで似たような実装を行った場合の例を用意した。
update関数の呼ばれる回数では無く、最後に入力を反映した時間からの経過時間を移動量に反映しよう。
impl Update for Player {
fn update(&mut self, env: &Environment) {
let input_state = input();
let interval = get_current_time() - self.last_input_time;
if input_state.left {
self.ax -= 0.05 * interval;
if self.ax < -2.0 {
self.ax = -2.0;
}
}
if input_state.right {
self.ax += 0.05 * interval;
if self.ax > 2.0 {
self.ax = 2.0;
}
}
let next_pos = Point {
x: self.renderable.position.x + self.ax as i32,
y: self.renderable.position.y
};
if env.stage.collision(&next_pos) {
self.renderable.position = next_pos;
}
self.last_input_time = get_current_time();
}
}
同様に、JavaScriptでの例を用意した。
JavaScriptの場合、メインループがrequestAnimationFrameのタイミングによって実行されるため、updateとrender共に60FPS程度で動作する。ネイティブ実装の場合は同じようには行かないだろう。
まとめ
・コントローラはゲーム世界への通信機
・コントローラの入力は基本的にデジタル(二値)
・人間の時間分解能はゲームの処理速度より遅い
・人間の感覚に合わせて入力感度を設定するのが重要
・最後に入力を受け付けた時間からのタイマーを活用して感度調整
明日は画面解像度とキャラクターの大きさについて書きたい。