環境 前提
Windows 10
Unity 2018 3.11f.1
Visual studio 2017
仕様コントローラー
LogiCool F310
背面スイッチ XInput
はじめに
初めまして、こんにちは。
ゲーム作ってる学生です。
Unityでローカル対戦ゲームを作っていたのですが、複数個繋いだコントローラーの入力に四苦八苦していました。
Input.GetKey(KeyCode.JoyStick1Button0)
とか書いても、二個目以降の入力が取れなかったので。
これはいかん!
と思ったので、XInputを使って四個のコントローラーの入力を取得するScriptを作成しました。
本記事では実装の手順とか面倒なことは書きません。作成にあたって参照したサイトのURLをその都度書いておきますので、そちらから実装手順等を確認してください。
XInputの準備
まずは、入力を判定するためのXInputのプログラムを書いていきます。
とはいえ、完成が知りたい。過程は後でも良いという人はUnity_XInputこちらを参照ください。
今回作成した、Unityデータがあります。
さて、判定用のプログラムですが、なんてことないXInputのプログラムになります。
まずはC++でDLLファイルの作成を行います。
参照サイト
C++でDLLを作成してC#から呼ぶ
プロジェクト名は"DllXInput"とでもしておきましょう。
作成したプロジェクトにクラスを追加していきます。
クラス名は"InputJudg"にしました。
余談ですが、ソリューションエクスプローラーの追加項目のクラスって便利ですね、今回の件まで知らなかったよ……
InputJudgクラスの解説
InputJudgではコントローラーの入力の全てをやってもらいます。
参照サイト
XInputAPIの使い方
XInputのリファレンス
まずは、呼び出した瞬間のコントローラーの状態を取得します。
int GetGamePadState(const int padNum = 0)
{
GetGamePadState関数では引数で指定したコントローラーの状態を取得します(デフォルトは0)。
ここで注意なのは、引数の値が0~3の通し番号であることです。
Unityで慣れてると、1~4でやりそうになりますが0~3です。
(プログラマにとっては、こっちの方が楽ですが)
ZeroMemory(&gamePadState, sizeof(XINPUT_STATE));
ここでは、プロパティのgamePadStateを初期化しています。
やらなくても良さそうだけど、一応やっとく。
DWORD dwResult = XInputGetState(padNum, &gamePadState);
if (dwResult == ERROR_SUCCESS)
{
padStateNormal = true;
lastTimeButton = &lastTimeButtons[padNum];
return 1;
}
else
{
padStateNormal = false;
return 2;
}
}
この関数最大の仕事はここ。
最初に一時変数"dwResult"に成功か失敗の二択を格納してあげます。
XInputGetStateはその為のものだと思ってもらって結構です。
"dwResult"に入ってきた値が成功なら、コントローラーの状態は正常だとフラグで指定して、intの値"1"を返します。
失敗なら、コントローラーの状態は異常だとフラグで指定して、intの値"2"を返します(今にして思えば何故"0"にしなかったのやら)。
えっ?
lastTimeButton = &lastTimeButtons[padNum];
これはなんだって?
そんなのほっとけ! 嘘です、後で使うので今は割愛させてください。
何の成功か失敗なの?
※知らんで良い人は読み飛ばしてください。
XInputGetStateは第一引数(コントローラーの順番)に対応したコントローラーの状態を、第二引数(取得したコントローラーの状態を格納する変数)に押し込みます。
このとき、PCに接続しているコントローラーが1個なのに1~3の数値が渡されたら?
はたまた、4以上の数値が渡されてしまったら?
まずいよ、なにもない! どうしよう!
となるのが、簡単なプログラムです。
もちろん、これらの状態はエラー回避等でプログラマが責任をもって回避するのが鉄則です。
ですが、エラーは回避しきれないものだと私は思っています。
そんなわけで、不正な値が渡ってきたら戻り値で"失敗"を返してくれるのが"XInputGetState"なわけです。
他にも、接触の問題で状態を所得出来ない時にも"失敗"を返してくれます。
便利ですね。
ボタンの判定をしてみる
さてさて、状態が取得出来たらいよいよ入力の判定です。
bool InputJudg::GetButton(const int judgButton = 0x00)
{
bool isButton = false;
if (padStateNormal)
{
if (gamePadState.Gamepad.wButtons & judgButton)
{
isButton = true;
}
}
return isButton;
}
基本的にはこれだけです。
引数で受け取ったコントローラーのボタンに対応するコードでマスクするだけ。
簡単だね。
……コードがどこにあるの? って話ですよね、はい。
コントローラーのボタンにはプログラムで判断するためのコードが一つ一つに割り当てられてます。
(冒頭のJoyStick1Button0とか)
こいつが書いてある場所は"Xinput.h"の中です。
ナニソレ? って人ごめん自分で調べて……。
コントローラーのコード一覧
ボタン名 | #defne | 実数値 |
---|---|---|
A | XINPUT_GAMEPAD_A | 0x1000 |
B | XINPUT_GAMEPAD_B | 0x2000 |
X | XINPUT_GAMEPAD_X | 0x4000 |
Y | XINPUT_GAMEPAD_Y | 0x8000 |
LB | XINPUT_GAMEPAD_LEFT_SHOULDER | 0x0100 |
RB | XINPUT_GAMEPAD_RIGHT_SHOULDER | 0x0200 |
Back | XINPUT_GAMEPAD_BACK | 0x0020 |
Start | XINPUT_GAMEPAD_START | 0x0010 |
Up | XINPUT_GAMEPAD_DPAD_UP | 0x0001 |
Down | XINPUT_GAMEPAD_DPAD_DOWN | 0x0002 |
Left | XINPUT_GAMEPAD_DPAD_Left | 0x0004 |
Right | XINPUT_GAMEPAD_DPAD_Right | 0x0008 |
LeftStick | XINPUT_GAMEPAD_LEFT_THUMB | 0x0040 |
RightStick | XINPUT_GAMEPAD_RIGHT_THUMB | 0x0080 |
コントローラーのコードはこんな感じになっています。
2列目の#defineは"Xinput.h"のなかでマクロしてされているものです。
if (gamePadState.Gamepad.wButtons & XINPUT_GAMEPAD_A)
{
C++開発でXInputを使うならこんな感じで書くとわかりやすいですね。
ですが、今回はUnityで使うのでマクロは使いません。
面倒ですね……なのでUnity側のScriptに列挙してしまいました。
static class Code
{
public const int pad_Dpad_Up = 0x0001;
public const int pad_Dpad_Down = 0x0002;
public const int pad_Dpad_Left = 0x0004;
public const int pad_Dpad_Right = 0x0008;
public const int pad_Start = 0x0010;
public const int pad_Back = 0x0020;
public const int pad_A = 0x1000;
public const int pad_B = 0x2000;
public const int pad_X = 0x4000;
public const int pad_Y = 0x8000;
public const int pad_LB = 0x0100;
public const int pad_RB = 0x0200;
public const int pad_LStick = 0x0040;
public const int pad_RStick = 0x0080;
};
これで判定はCodeクラスのプロパティを書くだけです。
ボタン入力の発展
先ほどのGetButton関数では、ボタンを押している間ずっと"true"を返してしまいます。
テトリスで例えるなら、落ちてきたテトリミノがずっと回転している状態です。
実に使いにくい……
そこで、UnityにあるようなGetButtonDownやGetButtonUpを作っていきます。
ここで登場するのがコントローラーの状態を取得するときに解説してもらえなかった可哀そうな
lastTimeButton = &lastTimeButtons[padNum];
です。
bool InputJudg::GetButtonDown(const int judgButton = 0x1000)
{
bool isDownButton = false;
if (padStateNormal)
{
if (gamePadState.Gamepad.wButtons & judgButton)
{
if (*lastTimeButton != judgButton)
{
isDownButton = true;
*lastTimeButton = judgButton;
}
}
else
{
*lastTimeButton = 0x00;
}
}
return isDownButton;
}
bool InputJudg::GetButtonUp(const int judgButton = 0x00)
{
bool isUpButton = false;
if (padStateNormal)
{
if (gamePadState.Gamepad.wButtons & judgButton)
{
if (*lastTimeButton != judgButton)
{
*lastTimeButton = judgButton;
}
}
else if(*lastTimeButton == judgButton)
{
isUpButton = true;
*lastTimeButton = 0x00;
}
}
return isUpButton;
}
まぁ、ざっくり説明すると、前回の入力を配列の"lastTimeButtons[]"に格納してあり状態取得の時に引数の番号に応じた領域のアドレスを"lastTimeButton"に格納して参照しているだけです。
複雑なことは一切してません。
(それゆえに改善点は多そうですが……)
スティックの移動を作る
ボタンが出来たら、スティックも組んでしまいましょう。
Vector2 InputJudg::GetLeftAxis()
{
Vector2 input = { 0.0f, 0.0f };
if (padStateNormal)
{
if (gamePadState.Gamepad.sThumbLY >= XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
{
input.y += 1.0f;
}
else if (gamePadState.Gamepad.sThumbLY <= -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
{
input.y -= 1.0f;
}
if (gamePadState.Gamepad.sThumbLX >= XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
{
input.x += 1.0f;
}
else if (gamePadState.Gamepad.sThumbLX <= -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
{
input.x -= 1.0f;
}
}
return input;
}
こちらが、左スティックの判定用関数になります。
今までの参照サイトにも書いてありますが、スティックは遊び(デッドゾーン)があるので、以上以下の判定で入力の値を取っています。
あれ? cppなのに戻り値がVecotr2? ナニコレ?
と思った人。ちゃんと読んでくれてありがとう。
今回、Axisの判定を返すにあたって"Horizontal"と"Vertical"の二回に分けて返すことに面倒臭さを感じた私は両方の値を格納した"Vector2"で返そうと思いました。
しかし、C++には"Vector2"なんて当然無い。どうしよう……
作ればえぇんやで
struct Vector2
{
float x;
float y;
};
要は、"float"型の値を二つ返せば良いんだもん、構造体で代用できるよね。
と、hpp内に宣言してしまいました。ゴリ押しです。
Unityで呼び出そう
ここまでやったら後は簡単(で量が多くて面倒)な作業よ。
Unityからアクセスするための外部関数作って、バッチビルドして、Unityのプロジェクトに入れて、呼び出す為のC#Script用意して……
参照サイト
C++でDLLを作成してC#から呼ぶ
UnityでC++を使う方法
なにやかんや、やったら完成です。
細かく、解説してたら私の頭が沸騰してしまいます……許して。
改めて完成形のリンクを置いておきます。
Unity_XInput
まとめ
今回は仕方なくこんな感じにやったけど、面倒なので楽できることは楽した方が先決だと思いました。
そう考えると、色々言いたい所はあるもののUnityって凄いんだなぁ……
更新
2020/01/06 ジョイスティックボタンの実装、トリガーの判定不具合改善