ゲームパッドをコマコンにしてみる

2023年7月31日

はじめに

コマコン作成のイメージ図

こんな感じです。

コマコンを作るきっかけ

きっかけは、steam版のブレイブルー(格闘ゲーム)を購入したことでした。
昔PS版で長いことプレイしていたのですが、当時理論上出せそうと思い、何かの間違いでもいいから一度だけでいいから出したい入力がありました。

ただ、技術的に難しい、というよりは人間的に入力できないコマンドでした。
コマコンなら試せるのになぁと思いつつ、それだけのためにコマコンを用意するのもめんどくさいと思って放置していたのです。

PCでできるとなれば仮想コマコンを作って夢のコマンド技が出せる気がしたので試してみました。

試したいコマンド

試すコマンドはブレイブルーのテイガーが使用する超必殺技ジェネシックエメラルドテイガーバスター(通称:GETB)です。
コマンドとしては2回転(左右どちらでも可)+Cです。
テンキーでの入力では4123698741236987+Cですが、ブレイブルーは簡易入力ができ、斜め入力が無くてもコマンドが成立する技があります。
GETBに関していうと最小ケースなら42684268+CでGETBが出せるはずなんです。

ただ、途中に上入力があるので生で出そうとするとどうしてもジャンプしてしまいます。
そのため、別の技の硬直中に上部分の入力を済ましてしまう方法や、ジャンプキャンセルキャンセル(通称:jcc)を利用して別の技を出し、その技の出始めをさらにキャンセルする入力方法がありました。
現実的なところでいうと「A or 2A>GETB」や「チャージキャンセルGETB」等ですね。

ただ、私はあくまで”生”で出したいんです。
最小ケースの入力をとんでもないスピードで入力できれば、ジャンプ移行フレームをキャンセルして生でGETBが出せるはずなのです。

仮想コマコンを作る流れ

開発言語の選定

今回はC#.NETで作成しました。

正直選定というほどのことは何もしてないです。
C#.NETでとりあえず作ってみて、処理速度とか足りなければC++で作ろう。くらいの感覚で選びました。

ゲームパッドの入力情報の取り込み

仮想ゲームパッドだけでも仮想コマコンを作れますが、現在接続しているゲームパッドの入力情報を仮想ゲームパッドにマッピングしてある程度外から入力できるようにします。
(マッピング無しだと、ゲーム内のメニュー画面やオプションの変更等をキーボードでやることになるので手元が忙しくなります。)

今回はPS4のコントローラーをPCに接続して仮想ゲームパッドのマッピング元とします。

NugetからSharpDX.DirectInputをインストール

DirectInputはマウス、キーボード、ゲームパッドの入力状態を取得するAPIです。
取得しかできないところがミソで、ここで入力を書き換えられればそのままゲームパッドをコマコン化することができましたが、現実はそんなにあまくなかったです。

ゲームパッドの取得

Nugetからのインストールが済んだらusingの記述をします。

using SharpDX;
using SharpDX.DirectInput;

接続されているデバイスを検索します。
DirectInput dInput = new DirectInput();
var devices = dInput
                    .GetDevices(DeviceType.Gamepad, DeviceEnumerationFlags.AllDevices)
                    .Concat(dInput.GetDevices(DeviceType.Joystick, DeviceEnumerationFlags.AllDevices))
                    .Concat(dInput.GetDevices(DeviceClass.GameControl, DeviceEnumerationFlags.AllDevices))
                    .ToList();

if (devices.Count == 0)
{
    //デバイスが見つからない
    dInput.Dispose();
    return;
}

デバイスが見つかった場合、先頭のゲームパッドを取得する。
複数のデバイスがある方はここ調整してください。
// 先頭デバイスを取得
Guid joystickGuid = devices[0].InstanceGuid;
Joystick joystick = new Joystick(dInput, joystickGuid);

// ゲームパッドを取得
joystick.Acquire();

ゲームパッドの入力状態はGetCurrentStateで取得できます。
// ボタン押下状態取得の例
joystick.GetCurrentState().Buttons[0];

ViGEmをインストール

ViGEmは仮想ゲームパッドエミュレーターのフレームワークです。
下記よりダウンロードしてインストールしてください。

Readmeを見ると結構有名な企業も使用しているみたいですね!
よく聞く名前が連なってます!

ViGEmBus/README.md at master · ViGEm/ViGEmBus · GitHub

NugetからNefarius.ViGEm.Clientをインストール

ViGEmの.NET用のライブラリです。
非常に使いやすいライブラリなので使わない手はないです。

仮想ゲームパッドの作成

Nugetからのインストールが済んだらusingの記述をします。
2行目と3行目は作成したいゲームパッドにより適宜削除してください。※そのままでもいいです。

using Nefarius.ViGEm.Client;
using Nefarius.ViGEm.Client.Targets.Xbox360;
using Nefarius.ViGEm.Client.Targets.DualShock4;

今回はPS4コントローラーを作成します。
以下のコードで、PS4コントローラーがPCに接続されたと思います。
簡単ですね、ライブラリばんざい(*´▽`*)
var client = new ViGEmClient();
var controller = client.CreateDualShock4Controller();
controller.Connect();

コード載せといてなんですが、今回は次に説明するコードを使用するので上記接続の処理は忘れてください。

仮想ゲームパッドの入力と物理ゲームパッドのマッピング

仮想ゲームパッドの接続や入力は以下で公開されているコードを使用して行います。
同階層にXbox360コントローラー用のコードも配置されていました。

上記コードから今回使用するのに不要な処理等を省いて物理ゲームパッドの入力マッピング関数を追加したものが以下となります。
マッピングは力業です。もっといい方法があるのかもしれません。

using EnumDevicesApp;
using Nefarius.ViGEm.Client.Targets.DualShock4;
using Nefarius.ViGEm.Client.Targets;
using Nefarius.ViGEm.Client;
using SharpDX.DirectInput;

namespace ComaCon
{
    public enum DpadDirection
    {
        None,
        Northwest,
        West,
        Southwest,
        South,
        Southeast,
        East,
        Northeast,
        North,
    }

    public struct OutputControllerDualShock4InputState
    {
        public bool triangle;
        public bool circle;
        public bool cross;
        public bool square;

        public bool trigger_left;
        public bool trigger_right;

        public bool shoulder_left;
        public bool shoulder_right;

        public bool options;
        public bool share;
        public bool ps;
        public bool touchpad;

        public bool thumb_left;
        public bool thumb_right;

        public DpadDirection dPad;

        public byte thumb_left_x;
        public byte thumb_left_y;
        public byte thumb_right_x;
        public byte thumb_right_y;

        public byte trigger_left_value;
        public byte trigger_right_value;

        public bool IsEqual(OutputControllerDualShock4InputState other)
        {
            bool buttons = triangle == other.triangle
                && circle == other.circle
                && cross == other.cross
                && square == other.square
                && trigger_left == other.trigger_left
                && trigger_right == other.trigger_right
                && shoulder_left == other.shoulder_left
                && shoulder_right == other.shoulder_right
                && options == other.options
                && share == other.share
                && ps == other.ps
                && touchpad == other.touchpad
                && thumb_left == other.thumb_left
                && thumb_right == other.thumb_right
                && dPad == other.dPad;

            bool axis = thumb_left_x == other.thumb_left_x
                && thumb_left_y == other.thumb_left_y
                && thumb_right_x == other.thumb_right_x
                && thumb_right_y == other.thumb_right_y;

            bool triggers = trigger_left_value == other.trigger_left_value
                && trigger_right_value == other.trigger_right_value;

            return buttons && axis && triggers;
        }
    }

    public class OutputControllerDualShock4
    {
        private IDualShock4Controller controller;

        private OutputControllerDualShock4InputState current_state;

        public OutputControllerDualShock4(ViGEmClient emClient)
        {
            controller = emClient.CreateDualShock4Controller();
            Init();
        }

        private void Init()
        {
            controller.AutoSubmitReport = false;
        }

        public void Connect()
        {
            controller.Connect();
        }

        public void Disconnect()
        {
            controller.Disconnect();
        }

        public bool UpdateInput(OutputControllerDualShock4InputState new_state)
        {
            if (current_state.IsEqual(new_state))
            {
                return false;
            }

            DoUpdateInput(new_state);

            return true;
        }

        private void DoUpdateInput(OutputControllerDualShock4InputState new_state)
        {
            controller.SetButtonState(DualShock4Button.Triangle, new_state.triangle);
            controller.SetButtonState(DualShock4Button.Circle, new_state.circle);
            controller.SetButtonState(DualShock4Button.Cross, new_state.cross);
            controller.SetButtonState(DualShock4Button.Square, new_state.square);

            controller.SetButtonState(DualShock4Button.ShoulderLeft, new_state.shoulder_left);
            controller.SetButtonState(DualShock4Button.ShoulderRight, new_state.shoulder_right);

            controller.SetButtonState(DualShock4Button.TriggerLeft, new_state.trigger_left);
            controller.SetButtonState(DualShock4Button.TriggerRight, new_state.trigger_right);

            controller.SetButtonState(DualShock4Button.ThumbLeft, new_state.thumb_left);
            controller.SetButtonState(DualShock4Button.ThumbRight, new_state.thumb_right);

            controller.SetButtonState(DualShock4Button.Share, new_state.share);
            controller.SetButtonState(DualShock4Button.Options, new_state.options);
            controller.SetButtonState(DualShock4SpecialButton.Ps, new_state.ps);
            controller.SetButtonState(DualShock4SpecialButton.Touchpad, new_state.touchpad);

            controller.SetDPadDirection(MapDPadDirection(new_state.dPad));

            controller.SetAxisValue(DualShock4Axis.LeftThumbX, new_state.thumb_left_x);
            controller.SetAxisValue(DualShock4Axis.LeftThumbY, new_state.thumb_left_y);
            controller.SetAxisValue(DualShock4Axis.RightThumbX, new_state.thumb_right_x);
            controller.SetAxisValue(DualShock4Axis.RightThumbY, new_state.thumb_right_y);

            controller.SetSliderValue(DualShock4Slider.LeftTrigger, new_state.trigger_left_value);
            controller.SetSliderValue(DualShock4Slider.RightTrigger, new_state.trigger_right_value);

            controller.SubmitReport();

            current_state = new_state;
        }

        private DualShock4DPadDirection MapDPadDirection(DpadDirection dPad)
        {
            switch (dPad)
            {
                case DpadDirection.None: return DualShock4DPadDirection.None;
                case DpadDirection.North: return DualShock4DPadDirection.North;
                case DpadDirection.Northeast: return DualShock4DPadDirection.Northeast;
                case DpadDirection.East: return DualShock4DPadDirection.East;
                case DpadDirection.Southeast: return DualShock4DPadDirection.Southeast;
                case DpadDirection.South: return DualShock4DPadDirection.South;
                case DpadDirection.Southwest: return DualShock4DPadDirection.Southwest;
                case DpadDirection.West: return DualShock4DPadDirection.West;
                case DpadDirection.Northwest: return DualShock4DPadDirection.Northwest;
                default: throw new NotImplementedException();
            }
        }

        public OutputControllerDualShock4InputState JoyStickState2ControllerState(JoystickState jstate)
        {
            OutputControllerDualShock4InputState ControllerState = new OutputControllerDualShock4InputState();
            // ボタン
            ControllerState.square = jstate.Buttons[0]; // □
            ControllerState.cross = jstate.Buttons[1]; // ×
            ControllerState.circle = jstate.Buttons[2]; // ○
            ControllerState.triangle = jstate.Buttons[3]; // △
            ControllerState.shoulder_left = jstate.Buttons[4]; // L1
            ControllerState.shoulder_right = jstate.Buttons[5]; // R1
            ControllerState.trigger_left = jstate.Buttons[6]; // L2
            ControllerState.trigger_left_value = (byte)((jstate.RotationX) / 256); // L2押し込み量
            ControllerState.trigger_right = jstate.Buttons[7]; // R2
            ControllerState.trigger_right_value = (byte)((jstate.RotationY) / 256); // R2押し込み量
            ControllerState.share = jstate.Buttons[8]; // SHARE
            ControllerState.options = jstate.Buttons[9]; // OPTIONS
            ControllerState.ps = jstate.Buttons[12]; // PS_button
            ControllerState.touchpad = jstate.Buttons[13]; // Touchpad_push

            // スティック
            ControllerState.thumb_left_x = (byte)((jstate.X) / 256); //左スティックX
            ControllerState.thumb_left_y = (byte)((jstate.Y) / 256); //左スティックY
            ControllerState.thumb_right_x = (byte)((jstate.Z) / 256); //右スティックX
            ControllerState.thumb_right_y = (byte)((jstate.RotationZ) / 256); //右スティックY

            // 十字キー
            DpadDirection dp = new DpadDirection();
            switch (jstate.PointOfViewControllers[0])
            {
                case 0:
                    dp = DpadDirection.North;
                    break;
                case 4500:
                    dp = DpadDirection.Northeast;
                    break;
                case 9000:
                    dp = DpadDirection.East;
                    break;
                case 13500:
                    dp = DpadDirection.Southeast;
                    break;
                case 18000:
                    dp = DpadDirection.South;
                    break;
                case 22500:
                    dp = DpadDirection.Southwest;
                    break;
                case 27000:
                    dp = DpadDirection.West;
                    break;
                case 31500:
                    dp = DpadDirection.Northwest;
                    break;
                default:
                    dp = DpadDirection.None;
                    break;
            }

            ControllerState.dPad = dp;

            return ControllerState;
        }
    }
}

仮想ゲームパッドの入力テスト

テストはゲームパッドのテスト画面で行います。
L1が押下されていない場合はゲームパッドの状態を同期、L1を押下した時だけ○ボタンを押下状態に上書きして反映しています。
最後のDisconnectあたりにブレークポイントを置いたりしてテストしてみてください。

/* PS4controllerの取得 =========================================================== */
DirectInput dInput = new DirectInput();
var devices = dInput
                    .GetDevices(DeviceType.Gamepad, DeviceEnumerationFlags.AllDevices)
                    .Concat(dInput.GetDevices(DeviceType.Joystick, DeviceEnumerationFlags.AllDevices))
                    .Concat(dInput.GetDevices(DeviceClass.GameControl, DeviceEnumerationFlags.AllDevices))
                    .ToList();

if (devices.Count == 0)
{
    //デバイスが見つからない
    dInput.Dispose();
    return;
}

// 先頭デバイスを取得
Guid joystickGuid = devices[0].InstanceGuid;
Joystick joystick = new Joystick(dInput, joystickGuid);

// ゲームパッドを取得
joystick.Acquire();
/* =============================================================================== */

ViGEmClient client = new ViGEmClient();
OutputControllerDualShock4 controller = new OutputControllerDualShock4(client);
controller.Connect();

while (true)
{
    // L1をトリガーにコマコンを動作させる
    if(joystick.GetCurrentState().Buttons[4] == true)
    {
        // ゲームパッドの入力状態を取得
        JoystickState jstate = joystick.GetCurrentState();
        // ゲームパッドの入力状態を仮想ゲームパッドへ同期(未反映)
        OutputControllerDualShock4InputState ControllerState = controller.JoyStickState2ControllerState(jstate);

        // ○ボタンを押下
        ControllerState.circle = true;

        // 仮想ゲームパッドの状態を反映
        controller.UpdateInput(ControllerState);

        // 処理を抜ける
        break;
    }
    else
    {
        // ゲームパッドの入力状態を取得
        JoystickState jstate = joystick.GetCurrentState();
        // ゲームパッドの入力状態を仮想ゲームパッドへ同期(未反映)
        OutputControllerDualShock4InputState ControllerState = controller.JoyStickState2ControllerState(jstate);
        // 仮想ゲームパッドの状態を反映
        controller.UpdateInput(ControllerState);
    }
}

controller.Disconnect();

ボタン5(L1)が押下された際にボタン3(○)が押下されていますね。
これと同じボタン状態であれば成功です。

夢の生GETBを出してみる

無限ループのトリガー処理に42684268+Cの入力を設定して試してみます。
※実際には1つずつの処理が早すぎてゲーム側がゲームパッドの状態を取得しきれないので、適切な待ち時間を設定する必要があります。

// 例
// トリガーにしたL1の入力を消す
ControllerState.shoulder_left = false;
// 左を入力する
ControllerState.dPad = DpadDirection.West;
// 反映
controller.UpdateInput(ControllerState);

/*** 待ち時間を設定するとしたらここ ***/

// 下を入力する
ControllerState.dPad = DpadDirection.South;
// 反映
controller.UpdateInput(ControllerState);

 

できたぁ(*´▽`*)