VRChatのOSCで実際にゲームが動かせるコントローラーギミックを作る

## はじめに
OSC経由でローカルPCへゲームコントローラー入力を飛ばすギミックを試しに作成してみました
かなりざっくりとした説明になっていますので、各実装での不明箇所は末尾の参考資料を確認してください！！


実際に動かしている様子

https://x.com/surarin_vr/status/1930964352871670036

TITAN One+キャプチャーボードを使用してNintendo switchを操作しています
PCからSwitchを操作する方法については今回の記事のメインではないため、こちらは割愛します（参考資料のみリンクあり）

上記動画ではTopazChatをかませて画面表示しているため1秒程度のラグがありますが、
ローカルで画面を見る分にはちゃんとゲームできる程度の遅延で動きます。
体感4-5フレーム遅延程度...?


## 使用ツール
* VRChat

* Unity 2022.3.22
アバターギミック作成用、VCCのアバタープロジェクトを想定


* Visual Studio 2022f1
OSC受信のための.NETコンソールアプリ作成

* Protokol
OSC信号確認

* ViGEmBus
仮想コントローラ作成

## アバターにギミックを仕込む
今回はアバターにコントローラーを仕込み、Contactsをつかって各ボタンの入力を取得していきます

### Parameterの変数追加
![スクリーンショット 2025-06-06 192158.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/f0dc188b-b152-424b-ae1a-320ee7d042ec.png)
ボタン1個ごとに入力をとるため、アバターで使用しているParameterにボタン個数分の変数を追加していきます
すぐ下に出てくるContactsでのみ使用する変数となります
intである意味は特にありません。何も考えずに作っていました。。。

### Contact Receiverの追加
![スクリーンショット 2025-06-06 192253.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/c3c27fcc-d619-4fb6-a044-0264f03143fa.png)
ボタンに触ると入力が取れるように、各ボタンの位置にContact Receiverを仕込みます。
主に設定したのは赤枠の部分となります。
### Collision tags
指で触れると反応するようにFingerを追加します。

### Receiver
今回は触れている間は1という入力が欲しいため、Receiver TypeはConstantに設定をします
Parameterには先ほど作成した変数を各ボタンごとに使用します


## VRChatのOSC設定
ここでProtokolというツールを使用してVRCから送られるOSC信号の確認をします

https://hexler.net/protokol

Protokolを起動し、VRChatから送られてくる9001番ポートをEnableで受信できる状態にしておきます

![スクリーンショット 2025-06-06 195708.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/14f3fdd1-492c-4e81-901d-4218f614ade1.png)
VRC側の設定からOSCをEnabledにすると…　

![スクリーンショット 2025-06-06 195751.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/2c2638e5-67b6-426d-831e-8252e74ee6dd.png)
すごい勢いで信号が送られてきます。

タイムスタンプを見るとわかりますが、このデフォルトのままでは不必要な大量のメッセージが送られてしまっています。

このままでは後から作成する**OSC受信アプリの負荷増加**、**コントローラ入力の遅延**に繋がってしまうため出力する変数の設定を行います。

OSCで出力する変数の管理は下記のjsonファイルで行われています。
```
C:\Users\(ユーザ名)\AppData\LocalLow\VRChat\VRChat\OSC\(ユーザID)\Avatars\(アバターID).json
```

デフォルトではすべてが吐き出されていますが、今回はコントローラに関係する変数以外を全部消し飛ばしてしまいましょう
サンプルとして自分のコードを一部記載します
```json:アバターjsonファイル
{
  "id": "省略",
  "name": "省略",
  "hash": "省略",
  "parameters": [
    {
      "name": "SELECTint",
      "input": {
        "address": "/avatar/parameters/SELECTint",
        "type": "Int"
      },
      "output": {
        "address": "/avatar/parameters/SELECTint",
        "type": "Int"
      }
    },
    ---------------省略-------------
    {
      "name": "Aint",
      "input": {
        "address": "/avatar/parameters/Aint",
        "type": "Int"
      },
      "output": {
        "address": "/avatar/parameters/Aint",
        "type": "Int"
      }
    }
  ]
}

```

jsonファイルの編集が終わったら、VRChatで再度OSCのEnabledをOFF→ONしてみると…

![スクリーンショット 2025-06-06 195851.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/144d91c7-3395-4e45-ba62-41701ff22a2c.png)

必要な変数だけが送られてくるようになりました。
これでVRChat側で行うことは完了です。

## OSCを受信するコンソールアプリの作成
VRChatからコントローラ用の変数がOSCで送られてくるようになったので、今度はそれを実際のコントローラ入力へと変換するためのアプリを作成します

ViGEmで作成した仮想コントローラを動かすためにはViGEmBusのインストールが必要となります。

https://github.com/nefarius/ViGEm.NET

今回は.NETのコンソールアプリとして作成をします
ライブラリの依存関係上.NET Framework4.5.2を使用してプロジェクトを作成します

![スクリーンショット 2025-06-06 202427.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/5093299e-2136-4d5d-ab7c-c651e053b994.png)

プロジェクトが作成できたらプロジェクト→NuGetパッケージの管理より必要なパッケージをインストールします
* Nefarius.ViGEm.Client
* Rug.Osc

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/63684bd9-c27d-4dfb-bd53-4a1f20f0e19e.png)

それでは実際にOSCを受信してコントローラー信号を出力するアプリのプログラムを書きましょう。半分くらいChatGPTに整形させました　AI様様
**変数のDictionary部分は自分がVRChat内で設定した値に書き換えてください！**

```cs:Program.cs
using System;
using System.Collections.Generic;

using Rug.Osc;
using Nefarius.ViGEm.Client;
using Nefarius.ViGEm.Client.Targets;
using Nefarius.ViGEm.Client.Targets.Xbox360;

using System.Threading;

namespace ConsoleApp1
{

    internal class Program
    {
        static ViGEmClient client;
        static IXbox360Controller controller;

        static readonly Dictionary<string, Xbox360Button> addressToButton = new Dictionary<string, Xbox360Button>
        {
            { "/avatar/parameters/Aint", Xbox360Button.A },
            { "/avatar/parameters/Bint", Xbox360Button.B },
            { "/avatar/parameters/Xint", Xbox360Button.X },
            { "/avatar/parameters/Yint", Xbox360Button.Y },
            { "/avatar/parameters/UPint", Xbox360Button.Up },
            { "/avatar/parameters/DOWNint", Xbox360Button.Down },
            { "/avatar/parameters/LEFTint", Xbox360Button.Left },
            { "/avatar/parameters/RIGHTint", Xbox360Button.Right },
            { "/avatar/parameters/STARTint", Xbox360Button.Start },
            { "/avatar/parameters/SELECTint", Xbox360Button.Back }
        };

        static void Main(string[] args)
        {
            // ViGEm初期化
            client = new ViGEmClient();
            controller = client.CreateXbox360Controller();
            controller.Connect();
            Console.WriteLine("仮想XInputコントローラーが接続されました");

            // OSC 受信ポートの設定
            var listener = new OscReceiver(9001); // VRChat送信はデフォルトで9001番
            listener.Connect();
            Console.WriteLine("OSCポート9001で待機中...");

            // OSC受信ループ
            while (true)
            {
                if (listener.State == OscSocketState.Connected)
                {
                    if (listener.TryReceive(out OscPacket packet))
                    {
                        if (packet is OscMessage message)
                        {
                            if (message[0].GetType() != typeof(int))
                            {
                                continue;
                            }
                            HandleMessage(message);
                        }
                    }
                }

                Thread.Sleep(1); // 負荷軽減
            }
        }

        static void HandleMessage(OscMessage message)
        {
            Xbox360Button btn;
            if (addressToButton.TryGetValue(message.Address, out btn))
            {
                int value;
                if (message.Count > 0 && message[0] is int)
                {
                    value = (int)message[0];
                    bool pressed = value == 1;
                    ControlButton(btn, pressed);
                    Console.WriteLine($"OSC受信: {message.Address} → {(pressed ? "押下" : "解放")}");
                }
            }
        }

        static void ControlButton(Xbox360Button BTN, bool flag)
        {
            controller.SetButtonState(BTN, flag);
        }
    }

}
```

完成したらデバッグからアプリを実行してみましょう！
コントローラーの接続音がしたら正常に動作しています。
Protokolが起動したままだとポートが競合するため、落としてからアプリを起動することを忘れずに

![スクリーンショット 2025-06-06 211300.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/56de6d78-115f-4b04-8231-6ebf15498543.png)

VRChat内でコントローラーに触れてみてアプリのログが吐き出されていたら正常に動作しています
実際にコントローラーが動いているかwindowsのコントローラー設定から見てみましょう

![スクリーンショット 2025-06-06 211536.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4107306/1168a776-30a6-4b7e-a83b-cbe9011dcce4.png)

無事触っている時だけ入力が出ていれば成功です！
ここの確認でいかにラグがなく入力が取れているか確認ができると思います。

もし触れてから入力までの遅延が多い場合はjsonファイルでのOSC信号設定がうまくいっていない可能性がありますので見直してみてください。
アバターを更新するたびにjsonファイルはリセットされてしまいます。

## 参考資料
VRChat公式　OSC資料
https://docs.vrchat.com/docs/osc-overview

VRCのOSCが「わからん」から「ちょっとわかる」になれる記事
https://note.com/33linn/n/n2f60b5155ee3

VRChatでOSCやーる（Python3.9、Windows10）
https://qiita.com/SatoshiGachiFujimoto/items/7f43954c7cbbe8c25dbe

【VRChat】Contactsで物を出し入れする【Avatar Dynamics】
https://note.com/yui0471/n/n54beffc6df97#c2bc999d-d1cb-4a3b-a99f-e0e1ebde04d5

ViGEm.NETを使って仮想ゲームコントローラを作る
https://hideb3.hatenablog.com/entry/2021/12/31/112049

とりあえずパソコンからニンテンドースイッチを操作する方法
https://miyabiproject.com/nintendoswitch-and-pc-with-titanone/