この記事は、前編の続きになります。
この記事のゴールは、SwiftUIからUnityオブジェクトを操作することがターゲットになります。 具体的にはSwiftUIでCubeを10度ずつ回せるようにします。
とはいえ、普通のNativePluginを使った開発と変わりはないです。sendUnityMessageToGO
とdelegateを使った2つの方法をここでは紹介します。
(その1)sendUnityMessageToGOを使ったやり方
Cubeを回転させるためのスクリプトを作成する
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); } } }
- Unityプロジェクトをビルドします。
SwiftUIからCubeを回転させる
UnityBridge.swift
にsendMessageToGO
をラップしたメソッドを追加する
/// 指定のGame Object で呼び出し可能なメソッドを呼び出す internal func sendMessageToGO(withName: String, functionName: String, message: String){ ufw.sendMessageToGO( withName: withName, functionName: functionName, message: message) }
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()) } } } } }
- ビルドする
これだけです! あとは実行画面に追加されたボタンを押すと、Cubeが回転してくれるはずです。
(その2) NativePluginを使ったやり方
sendMessageToGO
は組むのは簡単ですが、同名メソッドを避ける必要やstringでしか値を渡せない点が不便です。
そこでDelegateを使ったメッセージをやり取りする方法がブログでは紹介されています。
Delegateを定義する
NativeCallsProxy.h
にfloat値を受け取りたいので次のようなDelegateを定義します。
#import <Foundation/Foundation.h> // float値を受け取ってvoidを返すDelegate typedef void (*RotateDelegate)(const float degree);
- 定義した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
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のメソッドを呼び出せるようにする
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); }
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); } }
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; } }
- ビルドします。
SwiftUIからCubeを回転させる
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) } }
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で紹介したsendMessageToGOと異なり、こちらの方法では文字列をパースしたりせず、型をそのままUnityとSwiftの双方で扱えるのが利点です。
まとめ
今回は、ミニマムでARKitとSwiftUIを組み合わせる例を紹介しました。 ただ、ミニマムとはいえ内容がまだまだお粗末なので、flutterとUnityを組み合わせたflutter-unity-view-widget をベースにSwiftUI版を作ってみようかなーとかなんとか考えています。