0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】Apple Watch → iPhone(iOS)→ Unity で加速度データをリアルタイム連携する方法

Posted at

~WCSession + Swift + @_cdecl + Objective-C++ + UnitySendMessage~

はじめに

Unity で Apple Watch の加速度データを使いたい場合、
「watchOS → iOS → Unity」という 3 つの技術スタックを跨ぐ必要があります。

しかし、watchOS・iOS(Swift)・Objective-C++・Unity(C#) の連携方法はネット上に情報がほとんどなく、
断片的で、わかりづらいと思います。

そこでこの記事は、次のような人のためにまとめました:

  • Unity で Apple Watch の加速度データを使いたい
  • WatchConnectivity と Unity をつなぐ方法が分からない
  • Swift → C# のブリッジが難しすぎて詰んだ

私が実際に構築した通信フローは次の通りです:

  1. Apple Watch (CoreMotion)
    ↓ WCSession.sendMessage
  2. iPhone (Swift)
    ↓ @_cdecl ブリッジ関数
  3. Objective-C++ (.mm)
    ↓ UnitySendMessage
  4. Unity (C#)

この三段ブリッジが動けば、Apple Watch を Unity製ゲーム/アプリの入力デバイス として使えます。

実際に動いているプロジェクトの手順とコードを可能な限り公開します。
この記事だけで再現できるはずです。

:checkered_flag: ゴール(何ができるようになる?)

  • Apple Watch の加速度(x, y, z)を30fpsで取得
  • WatchConnectivity を使って iPhone へ送信
  • Swift → Objective-C++ → Unity(C#) にリアルタイム連携
  • Unity内で Vector3 として扱える
  • ゲーム/アプリの操作などに使える

⚙️ 開発環境

項目 内容
Unity 6.0 (2025)
Xcode 16.3
iPhone iOS 26.1
Apple Watch watchOS 26.1
Swift Swift 6

:page_facing_up: 必要なファイル一覧

先に、この記事で使用するファイル名一覧を記載します。
これをもとに順序立てて説明します。

  • watchOS側 UI:ContentView.swift
  • watchOS側 加速度送信:MotionSenderWrapper.swift
  • iOS側ブリッジ(Swift):WatchBridge.swift
  • Unity側 受信:SensorReceiver.cs

1. Apple Watch 側の実装(watchOS)

1-1. UI(送信開始/停止ボタン・現在値表示)

まずは、watchOS アプリのメイン UI の例です。
下記 ContentView.swiftのように実装しました。
加速度値の表示と、送信のON/OFF切り替えができます。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @StateObject private var motionSender = MotionSenderWrapper()

    var body: some View {
        VStack(spacing: 10) {
            Text(motionSender.isSending ? "📡 加速度送信中" : "📴 送信停止中")
                .font(.headline)
            Text("x: \(motionSender.x, specifier: "%.2f")")
            Text("y: \(motionSender.y, specifier: "%.2f")")
            Text("z: \(motionSender.z, specifier: "%.2f")")

            Button(action: {
                motionSender.toggleSending()
            }) {
                Text(motionSender.isSending ? "送信OFF" : "送信ON")
                    .padding()
                    .background(motionSender.isSending ? Color.red : Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .padding()
    }
}

実行すると、下記のように表示されます。

AppleWatch.jpg

1-2. 加速度取得 & iPhone へ送信(30fps)

次に、加速度を送信する処理を実装します。
下記 MotionSenderWrapper.swiftのように実装しました。
Apple Watch の加速度(x, y, z)を30fpsで送信します。
CoreMotion と WatchConnectivity を組み合わせて、iPhone に連携します。

MotionSenderWrapper.swift
import Foundation
import CoreMotion
import WatchConnectivity

class MotionSenderWrapper: NSObject, ObservableObject, WCSessionDelegate {
    private let motion = CMMotionManager()
    private let session = WCSession.default

    @Published var x: Double = 0
    @Published var y: Double = 0
    @Published var z: Double = 0
    @Published var isSending = false

    override init() {
        super.init()
        session.delegate = self
        session.activate()
    }

    func toggleSending() {
        isSending.toggle()

        if isSending { start() }
        else { motion.stopAccelerometerUpdates() }
    }

    private func start() {
        motion.accelerometerUpdateInterval = 1.0 / 30.0

        motion.startAccelerometerUpdates(to: .main) { data, _ in
            guard let a = data?.acceleration else { return }

            self.x = a.x
            self.y = a.y
            self.z = a.z

            let dict: [String: Any] = [
                "ax": a.x,
                "ay": a.y,
                "az": a.z
            ]

            self.session.sendMessage(dict, replyHandler: nil, errorHandler: nil)
        }
    }
}

2. iPhone(Swift)側の実装

Apple Watch から送られてくる加速度データを iPhone 側で受信し、
Unity へ橋渡しするために WatchConnectivity + @_cdecl ブリッジ を利用します。

2-1. WCSessionDelegate で Apple Watch のデータを受信

iPhone アプリ側では、Apple Watch の sendMessage(dict) を受け取るために
WCSessionDelegate を実装します。

受信したデータは最終的にUnityへ渡せる形式へ橋渡しする必要があります。

(※ ここでは WatchConnectivity の受信部分は要点のみ記述し、
Unity に値を送る要となるブリッジ関数に焦点を当てます。)

2-2. Swift → Unity の橋渡し:@_cdecl による C関数ブリッジ

Swift から Objective-C++(Unity)を直接呼び出すことはできません。
そのため、Swift側でC関数としてエクスポート可能な関数を定義し、
Objective-C++ 側から参照できるようにします。

以下のように @_cdecl を用いることで Swift 関数を C関数として公開できます。

WatchBridge.swift
@_cdecl("WatchReceiverBridge")
public func WatchReceiverBridge(_ ax: Double, _ ay: Double, _ az: Double) {
    UnitySendAccelerometer(ax, ay, az)
}
  • "WatchReceiverBridge"
    → Objective-C++ から呼び出される C関数名

  • (ax, ay, az)
    → Watch から送られた加速度値

Swift 側はこのブリッジ関数を通して Unity へデータを渡します。

2-3. 実際のデータ受信→Unity送信までの流れ

  1. Apple Watch が {ax, ay, az} を sendMessage(dict) で送る
  2. iPhone の WCSessionDelegate が受信
  3. Swift で値を取り出して WatchReceiverBridge(ax, ay, az) を呼ぶ
  4. @_cdecl が Objective-C++ の InitWatchReceiverBridge と接続
  5. UnitySendMessage を通じて C# のメソッドへ届く

3. Objective-C++(Unity)側の実装

iPhone(Swift)と Unity(C#)の橋渡しを行う最小構成は、
UnityAppController.mm に2行追加するだけで実現できます。

3-1. UnityAppController.mm に追加する2行

🔧 追加①:外部 C 関数の宣言

ファイル上部に追加します:

UnityAppController.mm
extern "C" void InitWatchReceiverBridge(void);

🔧 追加②:Unity 起動時にブリッジを初期化

didFinishLaunchingWithOptionsの先頭で 1 行追加します:

UnityAppController.mm
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    ::printf("-> applicationDidFinishLaunching()\n");

    InitWatchReceiverBridge();   // ← ここが最重要

3-2. なぜこの2行だけでいいのか?

  • UnitySendMessage を直接 UnityAppController.mm に書く必要はない

  • Swift → @_cdecl → InitWatchReceiverBridge → UnitySendMessage
    というルートが別ファイルに存在するため

  • UnityAppController.mm の役割は “ブリッジの初期化” のみ

そのため、UnityAppController.mm の編集量は最小限で済みます。

4. Unity(C#)側の受信処理

Unity 側では、Objective-C++ → UnitySendMessage から送られてきた、
CSV形式の加速度値を受信し、Vector3 に変換します。

実装は下記の通りです。

SensorReceiver.cs
using UnityEngine;

public class SensorReceiver : MonoBehaviour
{
    public static Vector3 Accel;

    // Objective-C++ → UnitySendMessage → ここに届く
    public void OnWatchAccel(string csv)
    {
        var sp = csv.Split(',');
        Accel = new Vector3(
            float.Parse(sp[0]),
            float.Parse(sp[1]),
            float.Parse(sp[2])
        );
    }

    void Update()
    {
        Debug.Log("Watch Accel: " + Accel);
    }
}

ゲーム内のどこからでも
SensorReceiver.Accel
を参照すれば、リアルタイムの加速度値が利用できます。

5. Xcode 側で必要な設定(watchOS / iOS / Unity の接続)

watchOS・iOS・Unity を同時に使う構成上、
Xcode 側でいくつかの設定が必要です。
ここでは、watchOS・iOS・Unity の3つを接続するために必要な
「ファイルの配置(ターゲット設定)」と
「Xcode 側の Build Settings / Capabilities」などをまとめて整理します。

📌 Swift ファイルを追加(上記、WatchBridge.swift)

  • ビルドターゲットをUnityFrameworkで追加します

UnityFramework に追加する理由
Unity の iOSビルドは Unity-iPhone プロジェクトを再生成するため、 Unity-iPhone にのみ追加した Swift ファイルは消えてしまう可能性があります。

📌 Identify and Type 設定

  • Unity-iPhoneのProject Formatを必要なら変更します
    ※私の場合はXcode 16.3 です。

📌 Signing & Capabilities 確認

  • Unity-iPhoneとWatchAppのSigningが正しく設定されていることを確認しましょう
    ※私はApple Developer Program に登録しています

📌 WatchApp の Build Settings 変更

  • Supported Platformsを"watchOS"にします
  • Architecturesを"Standard Architectures (arm64, armv7k, arm64_ …) "にします

arm64_32 は Apple Watch Series 4 以降の CPU(S4~S9)向けのアーキテクチャです。

※ここまでやったら、下記のように表示されているはずです

図1.png

さぁ続けましょう。

📌 WatchApp の Capability 追加

  • Health Kit を追加します
     - HealthKit Background Delivery をチェック
  • Background Modes を追加します
     - Workout processing をチェック

図2.png

HealthKit を有効にすると、Watchアプリ起動直後または最初の送信開始時にユーザーへの「ヘルスケアデータ利用許可」ダイアログが表示されます。

📌 WatchApp の Info > Description 追記

  • Privacy - Health Share Usage Description を追記します
     - 「ヘルスデータを共有するために使用します」とでも書いておきましょう
  • Privacy - Health Update Usage Description を追記します
     - 「ワークアウトのために使用します」とでも書いておきましょう

以上です。
あとはビルドし、iPhoneとAppleWatchへインストールするだけです。

6. よくあるハマりポイント(重要)

Apple Watch × iOS × Unity の 3 連携は、通常の iOS ビルドよりも複雑で、
原因不明のビルド失敗通信が動かない といったトラブルが頻発します。

ここでは、実際の開発で遭遇しやすいトラブルとその対処方法をまとめます。

❌ 6-1. Unity を再ビルドしたら Swift ファイルが消える

原因

Unity-iPhone 側に Swift ファイルを置いていると、
Unity の iOSビルド再生成で プロジェクト丸ごと上書きされる。

症状

  • Xcode 内で Swift ファイルが消えている
  • SwiftBridging が突然機能しなくなる

対処

  • Swift ファイルは UnityFramework ターゲットに追加する
  • もしくは PostProcessBuild で毎回自動コピーする

❌ 6-2. UnitySendMessage が C# に届かない

原因

  • GameObject 名が UnitySendMessage の第1引数と一致していない
  • C# メソッド名の文字が一致していない
  • C# メソッドが public でない
  • スクリプトをアタッチした GameObject がシーンに存在しない

対処

UnitySendMessage("SensorReceiver", "OnWatchAccel", "1,2,3");
             ↑ここ   ↑ここ

GameObject 名・メソッド名を Unity 内の実物と完全一致 させる。

❌ 6-3. WatchConnectivity が反応しない

原因

  • watchOS と iOS のどちらかで session.activate() を呼び忘れている
  • iPhone 側アプリが一度もフォアグラウンドで実行されていない
  • Watch と iPhone のペアリングが不安定

対処

  • watchOS / iOS の両方で WCSession.default.activate() を必ず呼ぶ
  • iPhone アプリを一度フォアグラウンド起動
  • 再ペアリングで改善することもある

❌ 6-4. Apple Watch アプリのインストールが失敗する(Wi-Fi接続が原因)

原因

Apple Watch はアプリインストール時に
iPhoneとAppleWatchのWi-Fi 接続を必要とする ケースが多い。
Wi-Fi がオフの場合、インストールが失敗しやすい。

症状

  • Xcode で Installing... のまま固まる
  • “Unable to install” と表示される
  • Watch 側のアプリアイコンが延々と待機状態

対処

  • iPhoneとAppleWatchを 必ず Wi-Fi に接続しておく(これで成功率が一気に上がる)
  • 公衆Wi-Fiだと失敗率が高いので注意
  • ペアリングを一度オフ→オンすると改善する場合もある

❌ 6-5. 理由不明でビルドが失敗する(watchOS / iOS)

原因

watchOS + iOS + Unity の複数ターゲット構成では、
ビルドキャッシュの不整合が発生しやすい。

症状

  • エラー内容が意味不明
  • Xcode のログに一般的な Swift / ObjC の原因が書かれていない
  • “Build Failed” のみ表示
  • “File not found” だが存在している、など矛盾したエラー

対処(最重要)

  • Clean Build Folder(Shift + Command + K)
  • Xcode の再起動
  • iPhone・Watch の再起動
  • Provisioning の再同期

大抵は クリーン → 再ビルド で直ります。

📝 これは watchOS を含むビルドで非常に頻繁に起こります。
深く悩むより、まずクリーンが最速の解決策。

❌ 6-6. 加速度の更新が遅延する・止まる

原因

  • 送信レートが高すぎてバッファが詰まる
  • WorkoutSession を開始していないためバックグラウンドが制限
  • Watch 側のバッテリーセーブモード
  • CoreMotion のメインスレッド負荷が高い

対処

  • 送信レートは 20〜30fps が最適
  • UI更新とデータ送信の処理を分離
  • バックグラウンド維持が必要なら WorkoutSession を開始
  • iPhone アプリをいったんフォアグラウンドにして安定化

❌ 6-7. HealthKit が突然使えなくなる

原因

  • watchOS 側と iOS 側の Signing & Capabilities がズレている
  • Developer Program の権限周りが変更
  • HealthKit の Entitlements が片方にしか設定されていない
  • Info.plist の Privacy 設定が不足している

対処

  • iOS / watchOS 両方が同じ Team(開発者アカウント)に設定されていること
  • Capability が両方のターゲットに設定されていること
  • Info の Privacy 設定(Share/Update Usage Description)が記述されていること

❌ 6-8. Unity側 C# が起動する前に Swift が送信してアプリが落ちる

原因

UnitySendMessage の受信側(C# のオブジェクト)が
Unity 起動直後にはまだ存在していないため。

症状

  • アプリ起動直後にクラッシュ
  • ログに SendMessage: object SensorReceiver not foundのようなエラーが出る

対処

  • Swift 側で UnityReady フラグ を持たせる
  • Unity 起動後、C# から Swift 側へ “初期化完了” 連絡を送る
  • それまでは WatchReceiverBridge() を呼ばない

「6. よくあるハマりポイント」はここまで。
watchOS × iOS × Unity の構成は複雑で、トラブルは多いですが、
トラブルにはよくある典型パターンが存在すると感じています。

7. まとめ

本記事では、

  • Apple Watch(watchOS)で CoreMotion を使って加速度を取得する
  • WatchConnectivity で iPhone(iOS)に値を送る
  • iOS 側の Swift で @_cdecl を使って C 関数ブリッジを用意する
  • UnityAppController.mm に 2 行だけ追加して初期化する
  • UnitySendMessage で Unity(C#)のメソッドへ渡す
  • Xcode 上で watchOS / iOS / UnityFramework のターゲットや Capability を調整する

という一連の流れを通して、
Apple Watch の加速度を Unity 側で Vector3 として扱えるようにするまで の手順をまとめました。

watchOS・iOS・Unity を同時に扱う構成は情報が少なく、
どこでハマっているのか分からない場面が多くありました。

この記事の内容は、その際に試しながら組み上げていった構成と手順を整理したものです。
同じように Apple Watch のセンサー情報を Unity で扱いたい方にとって、
少しでも助けになれば幸いです。

ここまで読んでいただき、ありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?