デコシノニッキ

ホロレンジャーの戦いの記録

Unity as a Libraryと SwiftUI で作るARアプリ (後編)

この記事は、前編の続きになります。

この記事のゴールは、SwiftUIからUnityオブジェクトを操作することがターゲットになります。 具体的にはSwiftUIでCubeを10度ずつ回せるようにします。

とはいえ、普通のNativePluginを使った開発と変わりはないです。sendUnityMessageToGOdelegateを使った2つの方法をここでは紹介します。

(その1)sendUnityMessageToGOを使ったやり方

Cubeを回転させるためのスクリプトを作成する

  1. Manipulator.csというスクリプトを作って、Cubeにこれをアタッチします。
using UnityEngine;

public class Manipulator: MonoBehaviour
{
    /// <summary>
    /// sendMessageToGOを使ってメッセージを受け取る場合、
    /// 引数はstring型である必要がある。
    /// </summary>
    /// <param name="message"></param>
    public void Rotate(string message)
    {
        if (float.TryParse(message, out var degree))
        {
            this.transform.Rotate(Vector3.up * degree);
        }
    }
}
  1. Unityプロジェクトをビルドします。

SwiftUIからCubeを回転させる

  1. UnityBridge.swiftsendMessageToGOをラップしたメソッドを追加する
    /// 指定のGame Object で呼び出し可能なメソッドを呼び出す
    internal func sendMessageToGO(withName: String, functionName: String, message: String){
        ufw.sendMessageToGO(
                withName: withName,
                functionName: functionName,
                message: message)
    }
  1. ContentView.swiftに回転させるボタンを追加する
struct ContentView: View {
    var body: some View {
        ZStack{
            UnityView()

            HStack{
                Button(action: {
                    UnityBridge.getInstance().sendMessageToGO(withName: "Cube", functionName: "Rotate", message: "10")
                }){
                    Image(systemName: "rotate.right")
                            .frame(width: 60, height: 60)
                            .imageScale(.large)
                            .background(Color.black)
                            .foregroundColor(.white)
                            .clipShape(Circle())
                }

                Spacer()

                Button(action: {
                    UnityBridge.getInstance().sendMessageToGO(withName: "Cube", functionName: "Rotate", message: "-10")
                }){
                    Image(systemName: "rotate.left")
                            .frame(width: 60, height: 60)
                            .imageScale(.large)
                            .background(Color.black)
                            .foregroundColor(.white)
                            .clipShape(Circle())
                }
            }
        }
    }
}
  1. ビルドする

これだけです! あとは実行画面に追加されたボタンを押すと、Cubeが回転してくれるはずです。

(その2) NativePluginを使ったやり方

sendMessageToGOは組むのは簡単ですが、同名メソッドを避ける必要やstringでしか値を渡せない点が不便です。 そこでDelegateを使ったメッセージをやり取りする方法がブログでは紹介されています。

Delegateを定義する

  1. NativeCallsProxy.hにfloat値を受け取りたいので次のようなDelegateを定義します。
#import <Foundation/Foundation.h>

// float値を受け取ってvoidを返すDelegate
typedef void (*RotateDelegate)(const float degree);
  1. 定義したDelegateをUnityから登録してもらうためのメソッドSetRotateDelegateを定義します。
#import <Foundation/Foundation.h>

// float値を受け取ってvoidを返すDelegate
typedef void (*RotateDelegate)(const float degree);

// NativeCallsProtocol は iOS側からUnityメソッドに登録する型
@protocol NativeCallsProtocol
@required
- (void) onUnityStateChange:(const NSString*) state;
// Unityのメソッドを登録する
- (void) setRotateDelegate:(RotateDelegate) delegate;
@end

__attribute__ ((visibility("default")))
@interface FrameworkLibAPI : NSObject
// UnityFrameworkLoadの後に呼び出す
// iOS側はこのメソッドから、デリゲートを登録する
+(void) registerAPIforNativeCalls:(id<NativeCallsProtocol>) aApi;

@end
  1. NativeCallsProxy.mmに実装を追加します。
// Unity側からiOSのメソッドを伝達する部分
// apiに登録されているデリゲートを呼び出している
extern "C" {
  void
  sendUnityStateUpdate(const char* state)
  {
      const NSString* str = @(state);
      [api onUnityStateChange: str];
  }

  void
  setRotateDelegate(RotateDelegate delegate)
  {
      [api setRotateDelegate: delegate];
  }
}

NativePluginからUnityのメソッドを呼び出せるようにする

  1. HostNativeAPI.csにDllを呼び出すためのメソッドを追加する
public class HostNativeAPI 
{
    public delegate void RotateDelegate(float degree);

    [DllImport("__Internal")]
    public static extern void sendUnityStateUpdate(string state);

    [DllImport("__Internal")]
    public static extern void setRotateDelegate(RotateDelegate rotateDelegate);
}
  1. UnityNativeAPI.csを追加します。

NativePluginから呼び出すメソッドはstaticではないとダメという制約があるためGameObjectを直接回転させるようなことはできません。そのため、回転リクエストがあったことをDelegateで飛ばすようにしています。

using System;
using AOT;

public static class UnityNativeAPI
{
    public static Action<float> OnRotate;

    [MonoPInvokeCallback(typeof(HostNativeAPI.RotateDelegate))]
    public static void Rotate(float degree)
    {
        OnRotate?.Invoke(degree);
    }
}
  1. Manipulator.csというスクリプトを作ってこれをCubeにアタッチします。(※sendMessageToGOで作ったものとは別物だと思ってください)
using System;
using UnityEngine;

public class Manipulator: MonoBehaviour
{
    private void Start()
    {
        if (Application.platform == RuntimePlatform.IPhonePlayer) 
        {
            HostNativeAPI.setRotateDelegate(UnityNativeAPI.Rotate);
            UnityNativeAPI.OnRotate += Rotate;
        }
    }

    public void Rotate(float degree)
    {
        this.transform.Rotate(Vector3.up * degree);
    }
    
    private void OnDestroy()
    {
        UnityNativeAPI.OnRotate -= Rotate;
    }
}
  1. ビルドします。

SwiftUIからCubeを回転させる

  1. API.csに実装を追加する。

NativeCallsProxyでsetRotateDelegateを追加定義したため、これを実装する。 APIオブジェクトでUnityから渡されたメソッドをデリゲートに登録しておく。また、このメソッドをSwiftから呼び出せるようにrotateメソッドを定義する。

import Foundation
import UnityFramework

class API: NativeCallsProtocol {
    
    internal weak var bridge: UnityBridge!
    
    private var rotateCallback: RotateDelegate!
    
    internal func onUnityStateChange(_ state: String) {
        switch (state) {
            case "ready":
                self.bridge.unityGotReady()
            default:
                return
        }
    }
    
    internal func setRotateDelegate(_ delegate: RotateDelegate!) {
        self.rotateCallback = delegate
    }
    
    public func rotate(_ value: Float) {
        self.rotateCallback(value)
    }
}
  1. ContentView.swiftに回転させるボタンを追加する
struct ContentView: View {
    var body: some View {
        ZStack{
            UnityView()

            HStack{
                Button(action: {
                    UnityBridge.getInstance().api.rotate(10)
                }){
                    Image(systemName: "rotate.right")
                            .frame(width: 60, height: 60)
                            .imageScale(.large)
                            .background(Color.black)
                            .foregroundColor(.white)
                            .clipShape(Circle())
                }

                Spacer()

                Button(action: {
                    UnityBridge.getInstance().api.rotate(-10)
                }){
                    Image(systemName: "rotate.left")
                            .frame(width: 60, height: 60)
                            .imageScale(.large)
                            .background(Color.black)
                            .foregroundColor(.white)
                            .clipShape(Circle())
                }
            }
        }
    }
}
  1. ビルドする

その1で紹介したsendMessageToGOと異なり、こちらの方法では文字列をパースしたりせず、型をそのままUnityとSwiftの双方で扱えるのが利点です。

まとめ

今回は、ミニマムでARKitとSwiftUIを組み合わせる例を紹介しました。 ただ、ミニマムとはいえ内容がまだまだお粗末なので、flutterとUnityを組み合わせたflutter-unity-view-widget をベースにSwiftUI版を作ってみようかなーとかなんとか考えています。

[デコシノニッキ]は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、Amazonアソシエイト・プログラムの参加者です。」