この記事は、Unity Advent Calendar 2025 11日目の記事です。
自己紹介
Graffity株式会社でUnityエンジニアをしていますsadaです。
まえがき
VisionProでは基本的にはハンドジェスチャー・ゲームパッド、一部の空間アクセサリを用いて操作することができます。
自作のデバイスを接続してVisionProで利用しようとするとBLEなどの無線を用いて繋ぐ必要があります。
BLEとはBluetoothLowEnergyのことでBluetoothの規格の一種で、主にIoT端末で使用される規格です。
今回BLEを用いてVisionPro上で動作する自作デバイスを作ってみました。
事前調査
そもそもVisionOSでBLEが使えるのか?
CoreBluetooth(BLEのフレームワーク)自体はVisionOSで利用可能です。
ただし、利用可能な形式がCentralというモードのみ対応となっています。
In watchOS, tvOS, and visionOS, you can’t advertise services using a CBPeripheralManager object because support for doing so is unavailable.
https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager より引用
今回自作デバイスをPeripheral、VisionProをCentralとして使う想定であるため問題なく利用ができます。
Unityで使えるプラグインはあるのか?
Unityで利用可能なBLEのプラグインはいくつか存在します。
アセットストアで提供されているものや、
github上で公開されているOSSもいくつか存在します。
ただ、どれもVisionOSには対応しておらず、またPeripheralの機能も含まれていることからそのままでは動作しません。
上記のOSSをフォークしてVIsionOSで利用可能なものに修正するのも一つの手ではありますが、今回はCoreBluetoothのCentralの機能のみを用いるネイティブプラグインを作成することにしました。
実装
実装環境
| 項目 | Version等 |
|---|---|
| Unity | 6000.0.62f1 |
| Xcode | 26.1.1 |
| PolySpatial | 2.4.3 |
ネイティブプラグイン実装
今回CoreBluetoothのCentralの機能のみを外出しするようなネイティブプラグインを作成しました。
作成したネイティブプラグインのコード・Unity側のネイティブプラグインを呼び出すコード・サンプル実装は以下のgithubで公開していますので参考にしてください。
Unity側実装
作成した.hファイルと.mmファイルをPlugins/VisonOS以下に配置し、プラグインを利用するスクリプトを追加してビルドするだけではBluetoothの利用はできません。Cameraやマイクの利用確認と同じように使用許諾の実装を入れる必要があります。
また、CoreBluetoothのライブラリもそのままでは利用できないので利用するように設定を変える必要があります。
どちらもUnityでXcodeProjectのビルド後Xcode上でInfo.plistやFrameworkの追加をすればよいのですが、以下のようにPostProcessBuildでビルド成果物を書き換える手法の方を用いると自動化できて便利です。
using System.IO;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Callbacks;
# endif
#if UNITY_VISIONOS && UNITY_EDITOR
using UnityEditor.iOS.Xcode;
#endif
namespace VisionOS.BLE.Editor
{
/// <summary>
/// visionOSビルド後にBluetooth利用説明とCoreBluetooth.framework追加を行うポストプロセス。
/// </summary>
public static class BlePostprocess
{
#if UNITY_VISIONOS && UNITY_EDITOR
private const string UsageDescription = "used to communicate with Bluetooth devices.";
[PostProcessBuild(900)]
public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
{
if (target != BuildTarget.VisionOS)
{
return;
}
AddUsageDescription(pathToBuiltProject);
AddBluetoothFramework(pathToBuiltProject);
}
private static void AddUsageDescription(string pathToBuiltProject)
{
var plistPath = Path.Combine(pathToBuiltProject, "Info.plist");
var plist = new PlistDocument();
plist.ReadFromFile(plistPath);
plist.root.SetString("NSBluetoothAlwaysUsageDescription", UsageDescription);
plist.WriteToFile(plistPath);
}
private static void AddBluetoothFramework(string pathToBuiltProject)
{
var projPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
var proj = new PBXProject();
proj.ReadFromFile(projPath);
var targetGuid = proj.GetUnityMainTargetGuid();
proj.AddFrameworkToProject(targetGuid, "CoreBluetooth.framework", /*weak*/ false);
proj.WriteToFile(projPath);
}
#endif
}
}
動作
使用する自作デバイス
今回自作デバイスとしてSeeed Studio XIAO ESP32C6というマイコンを使用します。ブレッドボード上にマイコンとLED、スイッチを配置しただけの簡易なデバイスです。

デモ実装として以下のような簡単な動作を行うように実装しました。
- BLE Peripheralとして動作
- ペアリングされたら0,1,2のコマンドを受け付けて動作を行う
- 0:LED消灯
- 1:LED点灯
- 2:LED点滅
- ボタンを押すことでBLE通信を行いCentral端末へ
PINGの文字列を送信
実装コード
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
constexpr uint8_t BUTTON_PIN = D5;
constexpr uint8_t LED_PIN = D6;
constexpr uint32_t DEBOUNCE_MS = 50;
constexpr uint32_t BLINK_INTERVAL_MS = 500;
// UUID
static const char *SERVICE_UUID = "12345678-1234-5678-1234-56789abcdef0";
static const char *CHAR_UUID = "12345678-1234-5678-1234-56789abcdef1";
enum class LedMode { Off = 0, On = 1, Blink = 2 };
volatile bool g_button_event = false;
volatile uint32_t g_last_isr_ms = 0;
LedMode g_mode = LedMode::Blink;
int g_led_state = LOW;
uint32_t g_last_blink_ms = 0;
BLECharacteristic *g_char = nullptr;
// 割り込みハンドラ
void IRAM_ATTR onButton() {
uint32_t now = millis();
if (now - g_last_isr_ms < DEBOUNCE_MS) return;
g_last_isr_ms = now;
g_button_event = true;
}
void applyMode() {
switch (g_mode) {
case LedMode::On:
g_led_state = HIGH;
break;
case LedMode::Off:
g_led_state = LOW;
break;
case LedMode::Blink:
default:
g_led_state = LOW;
g_last_blink_ms = millis();
break;
}
digitalWrite(LED_PIN, g_led_state);
}
class LedCharCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) override {
String val = pCharacteristic->getValue();
if (val.length() == 0) return;
char c = val[0];
switch (c) {
case '0': g_mode = LedMode::Off; break;
case '1': g_mode = LedMode::On; break;
case '2': g_mode = LedMode::Blink; break;
default: return;
}
applyMode();
}
};
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
applyMode();
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButton, FALLING);
// BLE初期化
BLEDevice::init("XIAO-LED");
BLEServer *server = BLEDevice::createServer();
BLEService *service = server->createService(SERVICE_UUID);
g_char = service->createCharacteristic(
CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY
);
g_char->setCallbacks(new LedCharCallbacks());
g_char->setValue("2"); // 初期モード: Blink
service->start();
BLEAdvertising *adv = BLEDevice::getAdvertising();
adv->addServiceUUID(SERVICE_UUID);
adv->setScanResponse(true);
adv->setMinPreferred(0x06);
adv->setMaxPreferred(0x12);
BLEDevice::startAdvertising();
}
void loop() {
// ボタンイベントが来たらPINGを送信
if (g_button_event) {
g_button_event = false;
if (g_char) {
g_char->setValue("PING");
g_char->notify();
}
}
// Blinkモードのときだけ点滅処理
if (g_mode == LedMode::Blink) {
uint32_t now = millis();
if (now - g_last_blink_ms >= BLINK_INTERVAL_MS) {
g_last_blink_ms = now;
g_led_state = (g_led_state == LOW) ? HIGH : LOW;
digitalWrite(LED_PIN, g_led_state);
}
}
delay(5);
}
動作している様子
このように、VisionProから自作デバイス、自作デバイスからVisionProへの双方向通信ができました。
まとめ
VisionOSではBLEの利用は可能です。ただし、Centralの機能のみが利用可能で、Peripheralとしての機能は利用が不可となっています。
UnityでBLEを利用するためのプラグインはいくつか存在しますが、どれもVisionOSには対応していません。
既存のOSSをフォークするか、自作でCentralのみの機能に絞ったネイティブプラグインを作成することで、VisionOSでBLEが利用可能になります。
今回はデモとして簡易なデバイスを用いましたが、この手法を流用することで自作のコントローラデバイスをVisionProに接続したり、様々なセンサデバイスをVisionProから利用することが可能になります。
