如果你按照我之前写的关于从 SwiftUI APP启动 Unity 游戏的帖子进行操作,那你就能成功地将Unity 游戏集成到SwiftUI 项目中,并通过操控按钮进行下载或卸载。
然而,我们的 Swifty 之旅并不能就此结束,因为如果不能在两个APP之间进行通信,那几乎就等于没用。你可能需要从 iOS 向 Unity 端发送一些游戏或玩家信息以正确初始化游戏,或者,你可能要在完成游戏后将玩家的分数发送回本机APP。
这都能通过 Unity 框架实现,我们只需稍加设置,就可以轻松创建双向通信。让我们往下看吧!
注意: 我们将接着上次的讲解,所以如果你还没有看本系列的第一部分,记得一定要补上,因为第一部分所包含的项目都已经设置完成了。
我们的目标?
为了实现 iOS APP和 Unity 游戏之间的通信,我们需要制作一些比单个按钮APP稍复杂的东西,但操作仍要简单,只有这样,才不会被游戏机制分心。
因此,对于 iOS →Unity 通信 ,我们要采用以下这个简单的思路:我们的 Unity 游戏将包含一个球,这个球可以根据我们从本机端发送的消息而改变颜色。在本地 iOS 端,我们将实现三个用不同颜色名称标记的按钮: 红色 、 绿色 和 蓝色 。每个按钮都将启动同一个Unity 游戏。但是,每个按钮向游戏发送的消息内容将会不同,因此如果用户点击 红色 按钮,球的颜色将为红色,蓝色按钮使球变为蓝色,依此类推。
对于 Unity → iOS 通信, 我们将反其道而行。我们将在 Unity 游戏中添加一个按钮,通过每次按下按钮时从 Unity 向 iOS 发送的消息来跟踪它被按下的次数。
这些例子很简单,虽不代表真正的游戏,但足以让我们清楚建立通信的原理。现在我们知道该怎么做了,那就开始吧!
iOS → Unity 通信
先创建一个新的 Unity 项目。在该项目中,添加一个 Quit Game
按钮并使其只调用 Application.Unload()
,这与我们在上一篇文章中所做的完全相同。如有需要,你可以重新使用之前已经具备退出按钮功能的 Unity 项目。
接下来,添加一个球体游戏对象并将其命名为 球 。
我们这个闪亮的球目前并没有什么特别之处,但一旦我们赋予它一些功能,那性质就不一样了。因此,创建一个名为 BallBehavior.cs 的新脚本并将其附加到我们的 Ball 游戏对象中。这个脚本需要有以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BallBehavior : MonoBehaviour
{
void SetBallColor(string color)
{
if (color == "red") GetComponent<Renderer>().material.color = Color.red;
else if (color == "blue") GetComponent<Renderer>().material.color = Color.blue;
else if (color == "green") GetComponent<Renderer>().material.color = Color.green;
else GetComponent<Renderer>().material.color = Color.white;
}
}
不管你信不信,这段代码是 Unity 端建立 iOS → Unity 通信所需的全部代码。本机APP只需使用 UnityFramework SDK来调用 SetBallColor
方法就行。
现在我们需从Unity 导出这个游戏,并将其导出到名为 UnityBallExport 的文件夹中。
注意,每次导出 Unity 游戏时,都必须重复集成步骤:
-
将导出的项目的 Unity-iPhone.xcodeproj 文件 拖到 主要iOS APP的 XCode 工作区
-
(重新)在 XCode 中导入 UnityFramework.framework 库,
-
选择 Data 文件夹并选中 UnityFramework 旁边的 Target Membership 框 。
有关如何执行此操作的更多信息,可参阅上一篇文章的 将 Unity 与 iOS 连接 的部分。
现在是时候修改我们的 SwiftyUnity 项目了。我们的主要目标是从 iOS APP中调用我们刚刚在 Unity 端执行的 SetBallColor
方法。回想一下,我们从 ContentView.swift 文件中加载 Unity 游戏,我们将在演示完Unity 游戏后立即设置球的颜色。
import SwiftUI
struct ContentView: View {
var body: some View {
Button(action: {
Unity.shared.show()
// Implement a way to call Unity's SetBallColor() method here
}) {
Text("Launch Unity!")
}
}
}
需要考虑到的关键一点是,当我们发送消息时,游戏可能尚未初始化,这将导致消息丢失,这就是为什么我们将实施一种缓存机制来存储任意待处理的消息,以便在游戏加载完全后发送。
打开 Unity.swift 文件,这是我们游戏的主要入口点。添加一个名为 UnityMessage 的新结构,它将包含我们向 Unity 发送消息所需的所有数据。
import Foundation
import UnityFramework
class Unity: UIResponder, UIApplicationDelegate {
private struct UnityMessage {
let objectName: String?
let methodName: String?
let messageBody: String?
}
private var cachedMessages = [UnityMessage]()
. . .
func sendMessage(
_ objectName: String,
methodName: String,
message: String
) {
let msg: UnityMessage = UnityMessage(
objectName: objectName,
methodName: methodName,
messageBody: message
)
// Send the message right away if Unity is initialized, else cache it
if isInitialized {
ufw?.sendMessageToGO(
withName: msg.objectName,
functionName: msg.methodName,
message: msg.messageBody
)
} else {
cachedMessages.append(msg)
}
}
}
现在我们有一种方法向Unity发送消息,即使用 sendMessage
调用 Unity 框架 sendMessageToGO
方法(GO 代表游戏对象)。如你所见,该APP将检查 Unity 是否已经初始化,如果已完成初始化,则会立即发送消息,否则,消息将被缓存,以便稍后发送。
我们现在需要处理发送和清理缓存消息的机制。考虑到这一点,你的 Unity.swift 文件应如下所示:
import Foundation
import UnityFramework
class Unity: UIResponder, UIApplicationDelegate {
// The structure for Unity messages
private struct UnityMessage {
let objectName: String?
let methodName: String?
let messageBody: String?
}
private var cachedMessages = [UnityMessage]() // Array of cached messages
static let shared = Unity()
private let dataBundleId: String = "com.unity3d.framework"
private let frameworkPath: String = "/Frameworks/UnityFramework.framework"
private var ufw : UnityFramework?
private var hostMainWindow : UIWindow?
private var isInitialized: Bool {
ufw?.appController() != nil
}
func show() {
if isInitialized {
showWindow()
} else {
initWindow()
}
}
func setHostMainWindow(_ hostMainWindow: UIWindow?) {
self.hostMainWindow = hostMainWindow
}
private func initWindow() {
if isInitialized {
showWindow()
return
}
guard let ufw = loadUnityFramework() else {
print("ERROR: Was not able to load Unity")
return unloadWindow()
}
self.ufw = ufw
ufw.setDataBundleId(dataBundleId)
ufw.register(self)
ufw.runEmbedded(
withArgc: CommandLine.argc,
argv: CommandLine.unsafeArgv,
appLaunchOpts: nil
)
sendCachedMessages() // Added this line
}
private func showWindow() {
if isInitialized {
ufw?.showUnityWindow()
sendCachedMessages() // Added this line
}
}
private func unloadWindow() {
if isInitialized {
cachedMessages.removeAll() // Added this line
ufw?.unloadApplication()
}
}
private func loadUnityFramework() -> UnityFramework? {
let bundlePath: String = Bundle.main.bundlePath + frameworkPath
let bundle = Bundle(path: bundlePath)
if bundle?.isLoaded == false {
bundle?.load()
}
let ufw = bundle?.principalClass?.getInstance()
if ufw?.appController() == nil {
let machineHeader = UnsafeMutablePointer<MachHeader>.allocate(capacity: 1)
machineHeader.pointee = _mh_execute_header
ufw?.setExecuteHeader(machineHeader)
}
return ufw
}
// Main method for sending a message to Unity
func sendMessage(
_ objectName: String,
methodName: String,
message: String
) {
let msg: UnityMessage = UnityMessage(
objectName: objectName,
methodName: methodName,
messageBody: message
)
// Send the message right away if Unity is initialized, else cache it
if isInitialized {
ufw?.sendMessageToGO(
withName: msg.objectName,
functionName: msg.methodName,
message: msg.messageBody
)
} else {
cachedMessages.append(msg)
}
}
// Send all previously cached messages, if any
private func sendCachedMessages() {
if cachedMessages.count >= 0 && isInitialized {
for msg in cachedMessages {
ufw?.sendMessageToGO(
withName: msg.objectName,
functionName: msg.methodName,
message: msg.messageBody
)
}
cachedMessages.removeAll()
}
}
}
extension Unity: UnityFrameworkListener {
func unityDidUnload(_ notification: Notification!) {
ufw?.unregisterFrameworkListener(self)
ufw = nil
hostMainWindow?.makeKeyAndVisible()
}
}
我在此文件中添加了注释,以便你可以和上一个比较已添加哪些内容。如你所见, sendCachedMessages
method 确保在游戏初始化后发送缓存的消息(如果有的话),因此不会丢失任何内容。
现在让我们回到 ContentView.swift 以便我们可以通过创建好的红、蓝和绿色按钮来对这个功能加以使用。我添加了视图修饰符只是为了让按钮看起来更漂亮一些,没有其他意思。
import SwiftUI
private struct ButtonViewModifier: ViewModifier {
var color: Color
func body(content: Content) -> some View {
content.frame(width: 200, height: 50).background(color)
}
}
private struct TextViewModifier: ViewModifier {
func body(content: Content) -> some View {
content.font(.title).foregroundColor(Color.white)
}
}
struct ContentView: View {
var body: some View {
VStack {
Spacer()
Button(action: {
Unity.shared.show()
Unity.shared.sendMessage(
"Ball",
methodName: "SetBallColor",
message: "red"
)
}) {
Text("Red").modifier(TextViewModifier())
}
.modifier(ButtonViewModifier(color: Color.red))
Spacer()
Button(action: {
Unity.shared.show()
Unity.shared.sendMessage(
"Ball",
methodName: "SetBallColor",
message: "blue"
)
}) {
Text("Blue").modifier(TextViewModifier())
}
.modifier(ButtonViewModifier(color: Color.blue))
Spacer()
Button(action: {
Unity.shared.show()
Unity.shared.sendMessage(
"Ball",
methodName: "SetBallColor",
message: "green"
)
}) {
Text("Green").modifier(TextViewModifier())
}
.modifier(ButtonViewModifier(color: Color.green))
Spacer()
}
}
}
这些按钮中的每一个都调用了 Unity.shared.sendMessage
方法。第一个参数是我们在 Unity 中的游戏对象的名称,在本例中是 Ball
。第二个参数是附加到游戏对象的脚本中方法的名称,即 SetBallColor
。 第三个参数是我们要发送的消息,也就是颜色名称。记住,消息 必须是字符串类型 ,因此如果你想发送不同的类型,你需要将其封装在一个字符串中并在 Unity 端展开。
让我们通过点击按钮来测试一下:
没毛病!球收到信息并改变颜色,它们分别根据你按下的按钮,变为红色、蓝色或绿色。我们的 iOS → Unity 通信到此结束。
Unity → iOS 通信
以相反的方式发送消息有点棘手,但这影响不大!让我们重新审视我们的目标:我们需要通过在每次按下按钮时向 iOS 发送一条消息来跟踪 Unity 游戏中按钮被按下的次数。
那我们就先创建一个新的 Unity 项目并向场景中添加一个按钮。此外,在 Assets
文件夹内创建一个新文件夹并将其命名为 Plugins
.
我们暂时离开 Unity,因为我们需要在 iOS 端做一些配置。在 SwiftyUnity 项目中,添加一个名为 NativeCallProxy 的新 Objective-C 文件。
当 XCode创建一个 Objective-C 桥接标头时,点击 Create Bridging Header 按钮并让 XCode 配置所有内容,以便也可以从项目中访问 Objective-C 代码。
重要提示: XCode 自动创建了一个名为 NativeCallProxy.m 的文件,但我们希望再扩展为 .mm 。因此,将文件重命名为 NativeCallProxy.mm 。
到目前为止,我们在 iOS 项目中有 2 个重要的新文件: NativeCallProxy.mm 和 SwiftyUnity-Bridging-Header.h 。
再添加一个文件并将其命名为 NativeCallProxy.h 。编辑这些文件,使其具有以下内容:
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <UnityFramework/NativeCallProxy.h>
#import <Foundation/Foundation.h>
@protocol NativeCallsProtocol
@required
- (void) sendMessageToMobileApp:(NSString*)message;
// other methods
@end
__attribute__ ((visibility("default")))
@interface FrameworkLibAPI : NSObject
+(void) registerAPIforNativeCalls:(id<NativeCallsProtocol>) aApi;
@end
#import <Foundation/Foundation.h>
#import "NativeCallProxy.h"
@implementation FrameworkLibAPI
id<NativeCallsProtocol> api = NULL;
+(void) registerAPIforNativeCalls:(id<NativeCallsProtocol>) aApi
{
api = aApi;
}
@end
extern "C"
{
void sendMessageToMobileApp(const char* message)
{
return [api sendMessageToMobileApp:[NSString stringWithUTF8String:message]];
}
}
我们将用 sendMessageToMobileApp
方法建立Unity → iOS 通信。我们将通过按下我们在 Unity 场景中创建的按钮进行调用,并在本机端为该事件放置一个侦 听器。
现在是有趣的部分:我们需要将 NativeCallProxy.h 和 NativeCallProxy.mm 移动到 Unity 项目中,因为我们将从 UnityFramework 调用它们!不过,我们也将在 XCode 中保留 SwiftyUnity-Bridging-Header.h 。
因此,让我们将 NativeCallProxy.h 和 NativeCallProxy.mm 移到我们预先在 Unity 项目中创建的 Plugins
文件夹中。我们在 iOS 项目中不需要这些,因为无论如何它们都会被打包到导出的 Unity 项目中,所以可以随意从 SwiftyUnity 项目中删除它们。
是时候开始连接了。让我们创建一个名为 ButtonBehavior.cs 的 C# 脚本并用以下代码对其进行填充:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine.UI;
using UnityEngine;
public class NativeAPI {
[DllImport("__Internal")]
public static extern void sendMessageToMobileApp(string message);
}
public class ButtonBehavior : MonoBehaviour
{
private int pressCount;
void Start () {
pressCount = 0;
}
public void ButtonPressed()
{
pressCount++;
NativeAPI.sendMessageToMobileApp("The button has been tapped " + pressCount.ToString() + " times!");
}
}
“魔法”就发生在 ButtonPressed()
方法内部。它用将在本机 iOS 端接收到的消息调用 NativeAPI.sendMessageToMobileApp()
方法。
现在 Unity 端要做的就是将此脚本与按钮本身连接起来,所以我们要将 ButtonBehavior.cs 脚本添加到按钮并连接 ButtonPressed 侦听器。
就是这样!下面让我们建立这个游戏。我们将其命名为 UnityButtonExport 并将其集成到我们的 XCode 工作区中,就像我们之前所做的一样。注意,每次将 Unity 游戏导出为 iOS 项目时,你必须:
- 将导出项目的 Unity-iPhone.xcodeproj 文件 拖到 主 iOS APP的 XCode 工作区,
- (重新)在 XCode 中导入 UnityFramework.framework 库,
- 选择 Data 文件夹并选中 UnityFramework 旁边的 Target Membership 框 。
重要提示: 这次除了常用的配置外,我们还需要一点其他配置。还记得Unity 游戏 Plugins
文件夹中的 NativeCallProxy 文件吗?现在你将会在导出的 Unity-iPhone 项目中看到它们。
你需要选择 Unity-iPhone项目 Libraries/Plugins
文件夹里面的 NativeCallProxy.h , 改变UnityFramework从 Project 到 Public 的目标成员。不要忘记这一步!
最后一步是将APP中的侦听器连接到 sendMessageToMobileApp
方法。让我们创建一个示例视图模型类,它将为我们的 Unity 消息注册一个侦听器。创建一个新的 Swift 文件,将其命名为 ViewModel.swift ,并使用以下代码对其进行填充:
import Foundation
class ViewModel: NSObject, ObservableObject, NativeCallsProtocol {
override init() {
super.init()
NSClassFromString("FrameworkLibAPI")?.registerAPIforNativeCalls(self)
}
func sendMessage(toMobileApp message: String) {
print(message)
}
}
我们还将简化我们的 ContentView.swift ,因此它只启动游戏:
import SwiftUI
import UnityFramework
struct ContentView: View {
let viewModel = ViewModel()
var body: some View {
VStack {
Button(action: {
Unity.shared.show()
}) {
Text("Launch Unity")
}
}
}
}
做了这么多工作,终于到了测试通信的时候了。运行 iOS 项目并点击“按我!” 按钮。来自 Unity 的消息应该打印到你的 XCode 日志中。
注意,你必须使用 物理 iPhone 设备 (我在下面的示例 gif 中也使用了一个,我只是将其流式传输到屏幕上)。
演示网址
呼!这项工程真是不小。但你却可以将此设置应用于任何你所需要类型的通信! 这些例子虽然非常简单,但是有了这个基础,你可以进行更复杂的逻辑,例如在每一帧上发送或接收消息,甚至为游戏使用自定义蓝牙控制器,这些将在 iOS 端处理并传递给Unity。
希望你会喜欢我们的内容。愿你在 Unity 冒险中玩得开心!
原文作者 Dino Trnka
原文链接 https://medium.com/mop-developers/communicate-with-a-unity-game-embedded-in-a-swiftui-ios-app-1cefb38ff439