DXライブラリでゲームパッドを使って操作できる方法について説明してます。XInputで3Dゲーム(=左右のスティックを使う)を作る前提です。
ゲームパッドで操作できるようにする
PCゲームの場合キーボードで操作できるようにするのが基本だがやはりゲームパッドで操作できるようにしたいところ(タイピングゲームとかチャットなら別)。
ゲームパッドにはDirectInputとXInputという2つの形式があるが今回はXInputの方を採用。理由はDirectInputだと各社によってゲームパッドの仕様が異なりやりにくいため(特に右スティックが曲者)。
XInputは主にXBOXで使われるゲームパッドの形式だったが近年ではPCゲーム(特にSteam)で推奨されることが多い。ちなみにNintendo Switch1/2で使われているのはDirectInput形式。
XInput対応のゲームパッドなんてないよ、という人はLogicoolのF310rあたりを用意しましょう。
以降3Dゲーム(=左右のスティックを両方使う)前提で進めます。
サンプルコード
早速サンプルコード。ゲームパッドの入力の取得だけでなくキーボードの入力と統合するコード。
Input.cpp/hに以下のコードを追加。
Input.h
//以下の定義・関数を追加
//XINPUTゲームパッドの各ボタン・スティックの定義
typedef enum {
XINPUT_POV_UP = 0,
XINPUT_POV_DOWN,
XINPUT_POV_LEFT,
XINPUT_POV_RIGHT,
XINPUT_START,
XINPUT_BACK,
XINPUT_LEFT_THUMB,
XINPUT_RIGHT_THUMB,
XINPUT_LEFT_SHOULDER,
XINPUT_RIGHT_SHOULDER,
XINPUT_A = 12,
XINPUT_B,
XINPUT_X,
XINPUT_Y,
XINPUT_LEFT_TRIGGER,
XINPUT_RIGHT_TRIGGER,
XINPUT_THUMBL_UP,
XINPUT_THUMBL_DOWN,
XINPUT_THUMBL_LEFT,
XINPUT_THUMBL_RIGHT,
XINPUT_THUMBR_UP,
XINPUT_THUMBR_DOWN,
XINPUT_THUMBR_LEFT,
XINPUT_THUMBR_RIGHT,
XINPUT_ALL
} eXInput;
//ゲーム内の操作一覧
typedef struct {
int up;
int down;
int left;
int right;
int confirm; //決定ボタン
int attack;
int jump; //ここのサンプルコードではキャンセルボタンも兼ねる
int dash;
int menu;
}config_t;
extern config_t config;
void Input_Initialize();
void Input_Merge(int* p, int k);
void Input_UpdateGamepad();
bool Input_GetGamepadDown(int KeyCode);
int Input_GetGamepad(int KeyCode);
bool Input_GetGamepadUp(int KeyCode);
void Input_AllUpdate();
列挙体enumを使ってXInputのゲームパッドの各ボタン・スティックに対する番号を用意。DXライブラリ側でXInputの各ボタンの定義は用意されているがアナログ入力の部分がないのとそこだけこちらで用意するというのもなんか気持ち悪いので全部列挙している。
またゲーム内の操作をまとめた構造体 config_t も用意する。この構造体の各変数に対応するゲームパッドのボタン・スティックの番号を格納する。
Input.h 内で config_t を宣言するときは extern をつけること。これがないと多重定義エラーが出るので注意。
Input.cpp
//以下の定義・変数・関数を追加
const static int TRIGGER_DEADZONE = 127; //トリガーの無効範囲(この値までを無効)
const static int STICK_DEADZONE = 12000; //スティックの無効範囲(この値までを無効)
static int Input_Gamepad[XINPUT_ALL]; //ゲームパッドが押されているフレーム数を格納する
config_t config;
//入力関連の初期化
void Input_Initialize() {
config.up = XINPUT_POV_UP;
config.down = XINPUT_POV_DOWN;
config.left = XINPUT_POV_LEFT;
config.right = XINPUT_POV_RIGHT;
config.confirm = XINPUT_B;
config.attack = XINPUT_X;
config.jump = XINPUT_A;
config.dash = XINPUT_RIGHT_THUMB;
config.menu = XINPUT_START;
}
//2つの引数の比較。大きい方を1つ目の引数に代入
void Input_Merge(int* p, int k) {
//片方が離された瞬間(-1)でもう片方が入力なし(0)の場合
if (*p == -1 && k == 0) {
return;
}
else if (*p == 0 && k == -1) {
*p = k;
return;
}
//それ以外
*p = *p > k ? *p : k;
}
//略
//ゲームパッドの入力状態を更新する
void Input_UpdateGamepad() {
int i;
XINPUT_STATE input; //現在のゲームパッドの入力状態を格納する
// 入力状態を取得
GetJoypadXInputState(DX_INPUT_PAD1, &input);
for (i = 0; i < XINPUT_ALL; i++) {
if (i < XINPUT_LEFT_TRIGGER) { //ボタン
if (input.Buttons[i] != 0) { //i番に対応するボタンが押されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_LEFT_TRIGGER) { //Lトリガー
if (input.LeftTrigger > TRIGGER_DEADZONE) { //Lトリガーが一定以上押し込まれていれば
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_RIGHT_TRIGGER) { //Rトリガー
if (input.RightTrigger > TRIGGER_DEADZONE) { //Rトリガーが一定以上押し込まれていれば
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_THUMBL_UP) { //左スティック
if (input.ThumbLY > STICK_DEADZONE) { //左スティックが上に一定以上倒されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_THUMBL_DOWN) {
if (input.ThumbLY < -STICK_DEADZONE) { //左スティックが下に一定以上倒されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_THUMBL_LEFT) {
if (input.ThumbLX < -STICK_DEADZONE) { //左スティックが左に一定以上倒されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_THUMBL_RIGHT) {
if (input.ThumbLX > STICK_DEADZONE) { //左スティックが右に一定以上倒されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_THUMBR_UP) { //右スティック
if (input.ThumbRY > STICK_DEADZONE) { //右スティックが上に一定以上倒されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_THUMBR_DOWN) {
if (input.ThumbRY < -STICK_DEADZONE) { //右スティックが下に一定以上倒されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_THUMBR_LEFT) {
if (input.ThumbRX < -STICK_DEADZONE) { //右スティックが左に一定以上倒されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
else if (i == XINPUT_THUMBR_RIGHT) {
if (input.ThumbRX > STICK_DEADZONE) { //右スティックが右に一定以上倒されていたら
Input_Gamepad[i]++; //加算
}
else {
if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば
Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値
}
else {
Input_Gamepad[i] = 0; //0にする
}
}
}
}
//キーボードとゲームパッドの入力を統合
Input_Merge(&Input_Gamepad[config.up], Input_Keyboard[KEY_INPUT_UP]);
Input_Merge(&Input_Gamepad[config.down], Input_Keyboard[KEY_INPUT_DOWN]);
Input_Merge(&Input_Gamepad[config.left], Input_Keyboard[KEY_INPUT_LEFT]);
Input_Merge(&Input_Gamepad[config.right], Input_Keyboard[KEY_INPUT_RIGHT]);
Input_Merge(&Input_Gamepad[config.confirm], Input_Keyboard[KEY_INPUT_RETURN]);
Input_Merge(&Input_Gamepad[config.attack], Input_Keyboard[KEY_INPUT_A]);
Input_Merge(&Input_Gamepad[config.jump], Input_Keyboard[KEY_INPUT_S]);
Input_Merge(&Input_Gamepad[config.dash], Input_Keyboard[KEY_INPUT_LSHIFT]);
Input_Merge(&Input_Gamepad[config.menu], Input_Keyboard[KEY_INPUT_SPACE]);
}
//指定のゲームパッドのボタンが押された瞬間か調べる
bool Input_GetGamepadDown(int KeyCode) {
if (KeyCode >= XINPUT_ALL) return false;
if (Input_Gamepad[KeyCode] == 1) return true;
return false;
}
//指定のゲームパッドのボタンが押されているか調べる。返り値は押されているフレーム数
int Input_GetGamepad(int KeyCode) {
if (KeyCode >= XINPUT_ALL) return 0;
return Input_Gamepad[KeyCode];
}
//指定のゲームパッドのボタンが離された瞬間か調べる
bool Input_GetGamepadUp(int KeyCode) {
if (KeyCode >= XINPUT_ALL) return false;
if (Input_Gamepad[KeyCode] == -1) return true;
return false;
}
//全ての入力状態を更新する
void Input_AllUpdate() {
//キーボード
Input_UpdateKeyboard();
//ゲームパッド
Input_UpdateGamepad();
}
ごり押し感が強いがそれはアナログ入力があるせい。
まず static int Input_Gamepad[XINPUT_ALL] を宣言。これはキーボードおよびゲームパッドの入力状態を格納する配列。
XInputのゲームパッドの入力を得る場合は GetJoypadXInputState() を使う。
| 宣言 | int GetJoypadXInputState(int InputType, XINPUT_STATE *XInputState) | |
| 概要 | ジョイパッド(XInput)から取得できる情報を得る | |
| 引数 | int InputType | 入力状態を取得するパッドの識別子 |
| XINPUT_STATE *XInputState | XInputから得られる情報を格納する構造体のアドレス | |
| 戻り値 | 0 | 成功 |
| -1 | エラー発生 | |
XINPUT_STATEについて詳しくはこちら。
XInputの入力を得たらfor文で回して各ボタン・スティックの入力があるか更新する。スティックおよびアナログトリガーの入力は一定以上倒された・押し込まれたら入力があったとして処理する。スティックの場合は STICK_DEADZONE で、アナログトリガーは TRIGGER_DEADZONE を使って判定している。
最後に Input_Merge() を使ってキーボードとゲームパッドの入力を統合する。最終的な結果はここでは Input_Gamepad[XINPUT_ALL] に代入することにした。
最後に System.cpp および Menu.cpp を以下のように追加・変更する。
System.cpp
//以下のように追加・変更
//DXライブラリなどの初期化
bool System_Intialize() {
//略
//キーボードおよびゲームパッド入力の初期化(追加部分)
Input_Initialize();
return true;
}
//メインループ
bool System_MainLoop() {
while (1) {
//略
//キーボードとゲームパッドの入力状態を更新(Input_KeyboardUpdate()から変更)
Input_AllUpdate();
//略
}
return true;
}
Menu.cpp
//以下の関数を次のように変更
//更新
void Menu_Update() {
if (Input_GetGamepadDown(config.down) || Input_GetGamepadDown(XINPUT_THUMBL_DOWN)) { //下が押されていたら
NowSelect = (NowSelect + 1) % eMenu_Num;//選択状態を一つ下げる
}
if (Input_GetGamepadDown(config.up) || Input_GetGamepadDown(XINPUT_THUMBL_UP)) { //上が押されていたら
NowSelect = (NowSelect + (eMenu_Num - 1)) % eMenu_Num;//選択状態を一つ上げる
}
if (Input_GetGamepadDown(config.confirm)) { //エンターキーか決定ボタンが押されたら
switch (NowSelect) {//現在選択中の状態によって処理を分岐
case eMenu_Game://ゲーム選択中なら
SceneMgr_ChangeScene(eScene_Game);//シーンをゲーム画面に変更
break;
case eMenu_Config://設定選択中なら
SceneMgr_ChangeScene(eScene_Config);//シーンを設定画面に変更
break;
default:
break;
}
}
}
Game.cpp/Config.cpp も同じようにキー入力の部分を変更する。
補足
サンプルコードではキーボードとゲームパッドの入力を統合しているが、上の実装例を見て「ゲームパッドの十字キーと左スティックの入力も1つにまとめないのか」と思った人がいると思う。
これは実際の3Dゲーム上で操作する場合、十字キーと左スティックには違う操作を割り当てることが多い(例:十字キーはホットキー移動・切り替え、左スティックはプレイヤーの移動)のでサンプルコードでは統一していない。
弾幕STGゲームなど操作数が少ない場合はもちろん統合してもよし。

コメント