はじめに
この記事は,農工大アドベントカレンダー Advent Calendar 2024 12日目の記事となります.
私が所属するサークルでのゲーム開発時にQuest上のUnityとESP32でOSC通信を担当したのでその実装を話していきます.
本記事ではQuest -> ESP32の一方向通信のみを行います.双方向通信やESP32->Questがしたい方は別の記事を参照してください.
開発環境
- Windows11
使用環境
- Meta Quest 3
- ESP32 DEVKIT V1
- Unity 2022.3.36f1
- OSCJack 2.0.0
サンプルリポジトリ
本記事で実装する内容は以下のリポジトリに置いてあります.
Unity
ライブラリ準備
OSCJackというライブラリを用います.
OscCoreというライブラリも存在しますが,少なくとも私の環境ではOscCoreはQuestからOSC通信が飛ばせなかったのでOscJackを使用します.
READMEのHow To Installを参考にインストールしていきます.
Edit
>Project Settings
>PackageManager
を開いて以下を追加します.
Name: Keijiro
URL: https://registry.npmjs.com
Scope: jp.keijiro
そうしたらWindow
>PackageManager
を開き,Packages
をMy Registries
に変更します.
スクロールしていくとOSC Jackがあると思うのでそれをインストールします.
これでインストールができました.
OSCメッセージの送信
コンポーネントでも送信できるようですが,今回はスクリプトを用いて送信を行います.
IPアドレスとポートを指定して,スペースキーかプライマリボタンが押されたときにアドレスを/sample
として文字列を送信するスクリプトを適当な場所に配置します.今回はAssets/Scripts/
直下にSendMessage.csという名前で置きます.
今回は文字列を送信しますが,intやfloatなども送信できるようです.
using UnityEngine;
using UnityEngine.XR;
using OscJack;
// OSCメッセージを送信するクラス
public class SendMessage : MonoBehaviour
{
[SerializeField] private string ipAddress = "ESP's ipaddress"; // OSCメッセージを受信するデバイスのIPアドレス
[SerializeField] private int port = 8000; // 送信先のポート番号
private OscClient _client; // OSC通信を行うためのクライアント
private const XRNode ControllerNode = XRNode.RightHand; // XRコントローラ(右手デバイス)を指定
// コンポーネントが有効化されたときに呼び出される
private void OnEnable()
{
_client = new OscClient(ipAddress, port); // 指定したIPアドレスとポート番号でOSCクライアントを初期化
}
// コンポーネントが無効化されたときに呼び出される
private void OnDisable()
{
_client.Dispose(); // OSCクライアントを解放
}
private void Update()
{
// Spaceキーが押された場合
if (Input.GetKeyDown(KeyCode.Space))
{
// Debug.Log("Space key was pressed."); // ログを出力.適宜コメントアウトを外してください.
_client.Send("/sample", "Hello, I am Windows!"); // OSCでメッセージを送信
}
// コントローラのプライマリボタン(例: OculusのAボタン)が押された場合
else if (InputDevices.GetDeviceAtXRNode(ControllerNode) // 指定されたXRデバイス(右手コントローラ)の状態を取得
.TryGetFeatureValue(CommonUsages.primaryButton, out bool primaryButtonValue) && primaryButtonValue)
{
_client.Send("/sample", "Hello, I am Quest3!"); // OSCでメッセージを送信
}
}
}
このスクリプトをシーン上に配置してみましょう.
適当なシーンでCreateEmptyしてOSCSenderというオブジェクトを作って先ほどのSendMessage.cs
をアタッチしましょう.
これでUnityから送信ができるようになりました.
テスト
本当に送信できているかを確認してみましょう.この節はただのテストなので飛ばしていただいても構いません.
送信処理
ループバックアドレスに対してOSC通信を飛ばすことで,自分自身でOSC通信を受け取ります.
先ほど配置したOSCSender
のSendMessage
のIPAdress
を127.0.0.1
にします.
これでゲームを再生してスペースボタンを押すと,自分自身に対してOSCメッセージが送信されます.
受信処理
Unityで受信処理を書いてもいいのですが,せっかく通信を行うので,別の言語で受け取るところを見たいところです.「他の言語で受信処理を試してみたい」という方はPythonでの受信テストを参照ください.一方で,「後で結局ESPで受け取るんだし,環境構築などの手間を省きたいのでUnityで受信したい」という方はUnityでの受信テストを参照ください.
Pythonでの受信テスト
次のスクリプトを動かしましょう.python-osc
というライブラリをインストールしてください.
from pythonosc import dispatcher
from pythonosc import osc_server
# メッセージを処理する関数
def message_handler(address, *args):
print(f"受信したアドレス: {address}")
print(f"受信したデータ: {args}")
# ディスパッチャを作成してハンドラーを登録
dispatcher = dispatcher.Dispatcher()
dispatcher.map("/sample", message_handler) # "/sample"アドレスに対応するハンドラーを登録
# サーバーを設定
ip = "127.0.0.1" # 受信するIPアドレス
port = 8000 # 受信するポート番号
server = osc_server.BlockingOSCUDPServer((ip, port), dispatcher)
print(f"OSCサーバーが {ip}:{port} で開始されました...")
server.serve_forever() # サーバーを起動
実行し,Unityエディタでゲームを再生してスペースボタンを押すと受信が確認できると思います.
Unityでの受信テスト
受信するのみなので,コンポーネントを使って実装します.
Assets
> Create
> ScriptableObjects
> OSC Jack
> Connection
で新たにOSC Connection
を作成してください.今回はAssets/
直下に配置します.
Host
が127.0.0.1
で,Port
が8000
であることを確認してください.
次に,わかりやすさのために受信したメッセージを表示したいと思うので,テキストを配置してください.ReceivedText
という名前で中央に配置しておきます.
最後に,CreateEmptyしてOSCReceiverというオブジェクトを作ってEventReceiver
というスクリプトをアタッチしてください.
Connection
を先ほどのOSC Connection
,OSC Adress
を\sample
,Data Type
をString
にして,String Event
にReceivedText
のTextMeshProUGUI
>text
を指定してください.
この状態でゲームを再生してスペースボタンを押すとテキストが変わると思います.
また,Window
>OSC Monitor
からもOSCの受信が確認できます.
ESP32
Unityの準備ができたのでESP32で受信処理を書いていきましょう.
次のコードを書き込んでください.
#include <WiFi.h>
#include <WiFiUdp.h>
// WiFi接続のためのSSIDとパスワード
const char* ssid = "SSID";
const char* password = "PASSWORD";
const int localPort = 8000; // 受信するポート番号
const int ledPin = 2; // LEDのピン番号
WiFiUDP udp;
// OSCメッセージを表す構造体
struct OscMessage {
bool isInt = false;
bool isFloat = false;
bool isString = false;
int intValue = 0;
float floatValue = 0.0f;
char stringValue[256] = {0};
};
void setup() {
Serial.begin(115200);
// WiFi接続
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi");
// UDPの初期化
Serial.println(WiFi.localIP());
udp.begin(localPort);
Serial.printf("Listening on UDP port %d\n", localPort);
// LEDを出力モードに
pinMode(ledPin, OUTPUT);
Serial.println("setup finished");
}
void loop() {
// 受信バッファを設定
char incomingPacket[255];
// UDPでOSCメッセージを受信
int packetSize = udp.parsePacket();
if (packetSize) {
int len = udp.read(incomingPacket, 255);
if (len > 0) {
incomingPacket[len] = '\0'; // パケットの末尾をNULLで終了
}
// OSCメッセージを解析
OscMessage message = parseOscMessage(incomingPacket, len);
// 受信したらLEDを点灯
ledOn();
}
}
// LEDを点灯させる
void ledOn() {
digitalWrite(ledPin, HIGH);
delay(250);
digitalWrite(ledPin, LOW);
delay(250);
}
// 4バイト境界に揃えるためのパディング計算
int padSize(int size) {
return (size + 3) & ~3; // 4バイト境界に揃える
}
// 受信メッセージを解析して構造体を返す
OscMessage parseOscMessage(char* packet, int packetSize) {
OscMessage message;
// アドレス部分の抽出
String address = String(packet);
Serial.println("Received OSC address: " + address);
// アドレス部分の終わりを見つける(4バイト境界に揃える)
int addressEnd = padSize(address.length() + 1);
// 型タグの位置を見つける
int typeTagPos = addressEnd;
// 型タグが存在し、"i"(整数)であることを確認
if (packet[typeTagPos] == ',' && packet[typeTagPos + 1] == 'i') {
// 値の位置を取得して整数として読み取る(型タグの4バイト後が値の開始位置)
int valuePos = padSize(typeTagPos + 2);
memcpy(&message.intValue, packet + valuePos, sizeof(message.intValue));
message.intValue = ntohl(message.intValue); // ネットワークバイトオーダーをホストバイトオーダーに変換
message.isInt = true;
Serial.println("Received int value: " + String(message.intValue));
}
// 型タグが存在し、"f"(浮動小数点)であることを確認
else if (packet[typeTagPos] == ',' && packet[typeTagPos + 1] == 'f') {
int valuePos = padSize(typeTagPos + 2);
uint32_t networkOrderValue;
memcpy(&networkOrderValue, packet + valuePos, sizeof(networkOrderValue));
networkOrderValue = ntohl(networkOrderValue);
memcpy(&message.floatValue, &networkOrderValue, sizeof(message.floatValue));
message.isFloat = true;
Serial.println("Received float value: " + String(message.floatValue));
}
// 型タグが存在し、"s"(文字列)であることを確認
else if (packet[typeTagPos] == ',' && packet[typeTagPos + 1] == 's') {
int valuePos = padSize(typeTagPos + 2);
strcpy(message.stringValue, (char*)(packet + valuePos));
message.isString = true;
Serial.println("Received string value: " + String(message.stringValue));
}
else {
Serial.println("Unexpected or missing type tag");
}
return message;
}
setup関数内でWi-Fiに接続し,loop関数内で受信の処理を行っています.Wi-Fiに接続し終わるとIPアドレスをシリアルモニターに表示するので,それをUnity側で設定してください.
parseOscMessage関数を自作しているためバグがある可能性があります.今回は使用していませんが,OSC受信用のライブラリもあるようなのでそれも検討してください.
動作確認
ESP32を実行して,シリアルモニタに出力されたIPアドレスをUnityのOSCsender
のIPAddress
に入力してください.
そうしたらUnityでゲームをスタートさせてスペースキーを押してください.それでシリアルモニタにメッセージが表示されたら成功です.
上手くいかない場合,同じWi-Fiにつながっているかや,IPアドレスが間違っていないかなどを確認してください.
Quest->ESP32
最後に,QuestにUnityをビルドしてみましょう.この記事を読んでいる人は大抵セットアップ済みだと思うので,VRに対応させる部分は今回は省略します.サンプルリポジトリや他記事を参考にしてください.
ビルドできたら,実行して右手のプライマリボタン(QuestのAボタン)を押してみてください.それでESP32のシリアルモニタにHello, I am Quest3!
というメッセージが届けばOKです.
おわりに
今回の開発時,QuestからESP32に通信しようとしたときに,シリアル通信が上手くいかず,OSC通信に渋々乗り換えてOscCoreで動かなかったとき絶望しました.OscJackでうまくいって良かったです.先人様に大感謝...
参考記事