当前位置: 首页 > news >正文

在 Vue 中嵌入 Unity WebGL 并实现双向通信

本文专为前端开发者撰写,将深入介绍如何在 Vue.js 项目中,通过 <iframe> 集成 Unity WebGL 应用,并建立一套基于 postMessage 的高级双向通信架构。本文不仅会展示 Vue 端的代码实现,还将独家揭秘如何改造 Unity 默认生成的 index.html 文件,将其从一个静态页面升级为一个功能强大的“通信桥梁”,最终实现 Vue 与 Unity 之间的完美解耦和高效交互。

一、 前言

在现代 Web 开发中,我们常常需要将 Unity 开发的复杂 3D 场景嵌入到 Vue 应用中。一个常见的痛点是前后端的紧密耦合。本文将为你呈现一套业界领先的解决方案,其核心在于改造 Unity 的 index.html,构建一个“通信桥梁”,让你告别直接调用 API 的脆弱模式,进入一个更优雅、更易维护的集成世界。

二、 核心架构:解耦的“通信桥梁”模式

我们集成的核心思想,不是让 Vue 直接与 Unity 对话,而是通过一个中间层——一个被精心改造过的 Unity index.html 文件——来充当“通信桥梁”

  • vue 应用 (父窗口): 我们的主应用,负责 2D UI 和业务逻辑。
  • 通信桥梁 (<iframe> 中的 index.html): 这是 Unity 团队交付给我们的 index.html,但它经过了改造。它的职责是:
  1. 接收 Vue 的指令: 监听来自 Vue 的 postMessage,并将其翻译成 Unity 能听懂的 unityInstance.SendMessage() API调用。
  2. 转发 Unity 的事件: 监听来自 Unity 内部的事件调用,并将其翻译成 postMessage 发送给 Vue。
  • Unity 应用 (在 <iframe> 中运行): 纯粹的 3D 引擎,专注于 3D 逻辑。

通过这种方式,Vue 和 Unity 互相不知道对方的技术细节,它们只通过标准的 postMessage “信件”与中间的桥梁沟通,实现了完美的解耦。

三、揭秘“通信桥梁”:如何改造 Unity 的 index.html

这是整个架构的精髓所在。Unity 默认生成的 index.html 是一个孤岛,我们需要通过以下改造,将它变为通信枢纽。

Unity 内部可以通过调用全局 JavaScript 函数来对外“喊话”。默认情况下,这些函数是空的,我们需要让它们把话传出去。

改造前 (Unity 原始代码)

//原代码是 Unity 构建出的标准模板,<!DOCTYPE html>
<html lang="en-us">
<head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>Unity Web Player | YangShanGang</title>
</head>
<body style="text-align: center; padding: 0; border: 0; margin: 0;"><canvas id="unity-canvas" width=3200 height=1800 tabindex="-1" style="width: 3200px; height: 1800px; background: #231F20"></canvas><script src="Build/YSG.loader.js"></script><script>var unityInstance;if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {var meta = document.createElement('meta');meta.name = 'viewport';meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';document.head.appendChild(meta);var canvas = document.querySelector("#unity-canvas");canvas.style.width = "100%";canvas.style.height = "100%";canvas.style.position = "fixed";document.body.style.textAlign = "left";}function loadUnity() {createUnityInstance(document.querySelector("#unity-canvas"), {arguments: [],dataUrl: "Build/YSG.data",frameworkUrl: "Build/YSG.framework.js",codeUrl: "Build/YSG.wasm",streamingAssetsUrl: "StreamingAssets",companyName: "DefaultCompany",productName: "YangShanGang",productVersion: "0.1.0",}).then((instance) => {unityInstance = instance;console.log(" Unity 实例已加载");// 绑定全屏按钮(如果有的话)var fullscreenButton = document.querySelector("#unity-fullscreen-button");if (fullscreenButton) {fullscreenButton.onclick = () => {unityInstance.SetFullscreen(1);};}}).catch((message) => {console.error("❌ Unity 加载失败: ", message);});}document.addEventListener("DOMContentLoaded", loadUnity);//________________________function HtmlCurrentPlotIndex(index) {}function GetCursorMoveData(jsonData) {if (unityInstance) {unityInstance.SendMessage("MockCursor", "GetCursorMoveData", jsonData);}}</script>
</body>
</html>

核心目标:实现应用解耦与双向通信

原始的 index.html 只能单向地被外部调用(通过全局 JS 函数),并且无法主动通知外部发生了什么。下面的修改实现了两个关键目标。

  • 建立从 Unity 到 Vue 的消息通道 (postMessage 发送)
  • 建立从 Vue 到 Unity 的消息通道 (postMessage 接收)

这使得 Vue 应用和 Unity 应用可以作为两个独立的实体进行开发,它们之间通过一个标准化的、类似 API 的方式进行对话,大大降低了耦合度。

实现 “Unity → Vue” 的消息转发

这是最重大的修改之一。

原始状态: 在原始文件中,所有以 Html 开头的函(如 HtmlCurrentPlotIndex, HtmlSendEventId)都是空函数体。

    // 原始代码function HtmlCurrentPlotIndex(index) {// (这里是空的)}function HtmlSendEventId(index) {// (这里是空的)}

这意味着,当 Unity 内部调用这些函数时,什么都不会发生。Unity 只是在“喊话”,但没人把它的话传出去。

修改后:将它们变成了消息转发器。

    // 您的修改后代码function HtmlCurrentPlotIndex(index) {window.parent.postMessage({name: 'HtmlCurrentPlotIndex', // 自定义消息名称,用于Vue中识别data: index                  // 传递的参数}, '*');}function HtmlSendEventId(index) {window.parent.postMessage({name: 'HtmlSendEventId',data: index}, '*');}

修改逻辑: 我们为所有 Unity 需要对外发送事件的 Html... 函数都填充了 window.parent.postMessage 的实现,使其成为一个消息转发器。

实现 “Vue → Unity” 的指令接收与分发

这是让架构变得非常优雅和解耦的关键。

原始状态: 原始文件只能通过直接调用全局函数(如 GotoShip('id-123'))来与 Unity 交互。这意味着前端必须知道所有这些全局函数的具体名称。

修改后:在 index.html 中添加了一个全局的 message 事件监听器,使其成为一个指令接收和分发中心。

    // 新增代码window.addEventListener('message', function(event) {let data = event.data;// 如果是 unityCommand 类型的消息if (data.type === 'unityCommand') {if (typeof window[data.method] === 'function') {window[data.method](data.param); // 调用相应的方法并传递参数}}// ... (还可以处理其他类型的消息)});

这段代码使 index.html 成为了一个指令分发中心。它监听所有来自 Vue 的消息,如果消息类型是 unityCommand,它就会根据消息中的 method 字段,动态地执行页面中已经存在的同名全局函数(如 GotoShip、SetVolume 等),从而触发对 unityInstance.SendMessage 的最终调用。

修改逻辑:

  • 监听消息: 这段代码会监听所有来自父窗口(Vue)的 postMessage 请求。
  • 协议解析: 它检查消息是否符合您定义的协议,即 data.type === 'unityCommand'。
  • 动态调用: 如果协议匹配,它会取出 data.method(例如,字符串 "GotoShip")和 data.param,然后动态地在当前 window 对象上查找并执行同名函数,即 window['GotoShip'](data.param)。
  • 执行指令: GotoShip 函数内部再调用 unityInstance.SendMessage,最终将指令送达 Unity 场景。
增加 Unity 加载完成的通知

为了让 Vue 知道何时可以安全地与 Unity 通信,我们需要在 Unity 实例加载完成后,主动通知 Vue。

原始状态: 原始文件加载完 Unity 实例后,只是在控制台打印一条日志。外部应用无法确切知道 Unity 何时准备就绪。

修改后:在 createUnityInstance 的 .then() 回调中,增加了一条 postMessage。

    // 修改后代码.then((instance) => {unityInstance = instance;// console.log(" Unity 实例已加载");window.top.postMessage({name: 'LoadSceneOver'}, '*')})

修改逻辑: 当 Unity 实例成功加载后,立刻向父窗口(Vue)发送一个名为 LoadSceneOver 的消息。Vue 应用在监听到这个消息后,就可以安全地开始向 Unity 发送初始化指令或执行其他依赖 Unity 的操作,避免了在 Unity 未准备好时调用而出错的问题。

修改后的整体代码:

<!DOCTYPE html>
<html lang="en-us"><head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>Unity Web Player | YangShanGang</title><style>html,body {width: 100%;height: 100%;padding: 0;margin: 0;overflow: hidden;}#unity-container {position: absolute;width: 100% !important;height: 100% !important;overflow: hidden;}#unity-container.unity-desktop {left: 50%;top: 50%;transform: translate(-50%, -50%)}#unity-container.unity-mobile {position: fixed;width: 100% !important;height: 100% !important;}#unity-canvas {background: #231F20;width: 100% !important;height: 100% !important;}.unity-mobile #unity-canvas {width: 100% !important;height: 100% !important;}</style>
</head>
<body style="text-align: center; padding: 0; border: 0; margin: 0;"><canvas id="unity-canvas" tabindex="-1" style="width: 1920px; height: 1120px; background: #231F20"></canvas><script src="Build/YSG.loader.js"></script><script>var unityInstance;if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {var meta = document.createElement('meta');meta.name = 'viewport';meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';document.head.appendChild(meta);var canvas = document.querySelector("#unity-canvas");canvas.style.width = "100%";canvas.style.height = "100%";canvas.style.position = "fixed";document.body.style.textAlign = "left";}function loadUnity() {createUnityInstance(document.querySelector("#unity-canvas"), {arguments: [],dataUrl: "Build/YSG.data",frameworkUrl: "Build/YSG.framework.js",codeUrl: "Build/YSG.wasm",streamingAssetsUrl: "StreamingAssets",companyName: "DefaultCompany",productName: "YangShanGang",productVersion: "0.1.0",}).then((instance) => {unityInstance = instance;// console.log(" Unity 实例已加载");window.top.postMessage({name: 'LoadSceneOver'}, '*')// 绑定全屏按钮(如果有的话)var fullscreenButton = document.querySelector("#unity-fullscreen-button");if (fullscreenButton) {fullscreenButton.onclick = () => {unityInstance.SetFullscreen(1);};}}).catch((message) => {// console.error("❌ Unity 加载失败: ", message);});}document.addEventListener("DOMContentLoaded", loadUnity);window.addEventListener('message', function(event) {let data = event.data;// 根据 data.name 来区分不同的消息类型if (data.name === 'loadInit') {// 处理初始化if (typeof window[data.method] === 'function') {window[data.method](data.param); // 调用传递的方法并传参}}// 如果是 unityCommand 类型的消息if (data.type === 'unityCommand') {if (typeof window[data.method] === 'function') {window[data.method](data.param); // 调用相应的方法并传递参数}}// 针对其他消息类型(例如警告数据处理)if (data.name === 'warnProps') {setTimeout(() => {if (typeof window[data.method] === 'function') {window[data.method](data.param);}}, 1000); // 延时处理,避免冲突}// 根据不同的消息执行不同的处理if (data.name === 'callEvent') {if (typeof window[data.method] === 'function') {window[data.method](data.param);}}if (data.name === 'callOverId') {if (typeof window[data.method] === 'function') {window[data.method](data.param);}}});function HtmlCurrentPlotIndex(index) {console.log(" unity当前情节索引-------------:" + index);window.parent.postMessage({name: 'HtmlCurrentPlotIndex', // 自定义消息名称,用于Vue中识别data: index // 传递的参数(例如点击的UI名称)}, '*');}function HideOrShowHeatPointData(jsonData) {// console.log(jsonData, 'yjshu哈哈哈哈哈');if (unityInstance) {unityInstance.SendMessage("SceneManager", "HideOrShowHeatPointData", jsonData);// console.log(" unity热力图显隐---------" + jsonData);}}
//.....</script>
</body></html>

四、 步骤一:在 Vue 中嵌入 Unity 应用

集成 Unity 应用的第一步是让它在我们的页面中正确地显示出来。

1.文件放置

首先,将 Unity 团队提供的整个 WebGL 构建文件夹(通常包含一个 index.html 和其他资源目录)直接复制到你的 Vue 项目的 public 目录下。这样做可以确保在项目启动后,该文件夹内的所有内容都能作为静态资源被直接访问。下面是我的项目中unity给到的文件结构

    - public/
- UnityBuild/      <-- 将 Unity 构建文件夹放在这里
- index.html
- Build/
- StreamingAssets/
- ...
- favicon.ico
- src/
- ...

2.使用 <iframe> 作为容器

在你的 Vue 组件中,使用 <iframe> 标签来加载 Unity 的入口 HTML 文件。<iframe> 是最理想的选择,因为它创建了一个独立的沙箱环境,可以有效避免 CSS 样式污染和 JavaScript 变量冲突。

    <template><div class="container"><!-- 你的其他 Vue UI 组件可以放在这里 --><Header class="header" /><!-- 用于承载 Unity 的 Iframe --><iframeref="unityFrame":src="unityUrl"class="unity-iframe"frameborder="0"></iframe></div></template><script setup lang="ts">import { ref } from 'vue';// Unity 应用的访问路径,相对于 public 目录const unityUrl = ref('/UnityBuild/index.html'); // 创建一个 ref 来获取 iframe DOM 元素const unityFrame = ref<HTMLIFrameElement | null>(null);</script><style scoped>.container {position: relative;width: 100vw;height: 100vh;overflow: hidden;}.header {position: absolute;top: 0;left: 0;width: 100%;z-index: 10; /* 确保 UI 在 iframe 上方 */}.unity-iframe {position: absolute;top: 0;left: 0;width: 100%;height: 100%;border: none; /* 移除边框 */}</style>

五、 步骤二:接收来自 Unity 的消息 (Unity → Vue)

当 Unity 场景中发生特定事件时(例如,一段介绍动画播放到某个节点),它会通过 window.parent.postMessage() 向我们的 Vue 应用发送消息。我们需要做的就是监听这些消息。

1.注册全局消息监听器

最佳实践是在组件挂载 (onMounted) 时添加监听器,并在组件卸载 (onUnmounted) 时移除它,以防止内存泄漏。

    import { ref, onMounted, onUnmounted } from 'vue';import { Store} from '@/store'; // 假设你使用 Piniaconst store = Store();// 消息处理函数const handleMessage = (event: MessageEvent) => {// 安全起见,可以检查消息来源是否可信// if (event.origin !== "http://your-expected-origin.com") return;const { name, data } = event.data;// 使用 switch 根据消息名称分发任务switch (name) {case 'LoadSceneOver':console.log("Unity 场景已加载完毕!");// 可以在此时进行一些初始化调用break;case 'HtmlCurrentPlotIndex':console.log(`Unity 正在讲解图表: ${data}`);store.setActiveChart(data); // 更新状态管理中的当前图表break;case 'HtmlSendEventId':console.log(`接收到 Unity 预警事件 ID: ${data}`);store.showWarningPopup(data); // 触发显示预警弹窗break;// ...可以处理更多其他类型的消息default:break;}};onMounted(() => {// 注册一个全局的 message 事件监听器window.addEventListener('message', handleMessage);});onUnmounted(() => {// 移除监听器,避免内存泄漏window.removeEventListener('message', handleMessage);});

核心解读:

  1. window.addEventListener('message', ...) 是浏览器提供的标准 API,用于监听跨窗口消息。

  2. event.data 就是 Unity 发送过来的消息对象。我们通常会约定一个格式,比如 { name: '消息类型', data: '具体数据' },这样在 handleMessage 函数中就可以通过 name 来判断如何处理 data。四、 步骤三:向 Unity 发送指令。(我们监听 window 的 message 事件,并根据 event.data 中约定的 name 字段来执行不同的前端逻辑。)

六、 步骤三:向 Unity 发送指令

反向通信——从 Vue 控制 Unity 场景——是实现动态交互的核心。不同于直接调用 Unity 的 API,我们将采用一种更为优雅的 postMessage 机制,以实现彻底的前后端解耦。

前端通信接口的约定

我们与 Unity 侧约定,所有从 Vue 发往 Unity 的指令都将通过 postMessage 发送,并遵循以下数据结构:

    interface UnityCommand {type: 'unityCommand'; // 一个固定的标识符method: string;       // 要调用的方法名param?: any;          // 传递的参数}

在 Pinia Store 中封装发送函数

为了方便在项目的任何组件中调用,我们将发送逻辑封装在状态管理(如 Pinia)中。

    // src/store/store.tsimport { defineStore } from 'pinia';export const useUnityStore = defineStore('unity', {state: () => ({unityFrame: null as HTMLIFrameElement | null,}),actions: {setUnityFrame(frame: HTMLIFrameElement) {this.unityFrame = frame;},/*** 向 Unity 发送指令* @param methodName 要在 Unity 的 index.html 中调用的全局函数名* @param parameter 要传递的参数,如果是对象会自动转为 JSON 字符串*/sendToUnity(methodName: string, parameter?: any) {if (!this.unityFrame || !this.unityFrame.contentWindow) {console.warn('Unity iframe 尚未初始化!');return;}try {// 如果参数是对象,则字符串化const paramToSend = typeof parameter === 'object' && parameter !== null? JSON.stringify(parameter): parameter;// 使用 postMessage 发送标准格式的指令this.unityFrame.contentWindow.postMessage({type: 'unityCommand',method: methodName,param: paramToSend},'*' // 注意:生产环境应指定为 Unity 应用的域名);} catch (error) {console.error('发送消息到 Unity Iframe 失败:', error);}},},});

注意: 在你的主组件 (index.vue) 的 onMounted 中,需要调用 store.setUnityFrame(unityFrame.value) 来初始化 unityFrame。

onMounted(() => {// store.warningId = 1;//测试预警播报store.setUnityFrame(unityFrame.value)shipType()window.addEventListener('message', handleMessage);//注册一个全局的 message 事件监听器
})

在业务逻辑中调用

现在,从任何组件中向 Unity 发送指令都变得非常简单和清晰

<template><!-- ... --><div class="controls"><button @click="focusOnShip('Ship_007')">聚焦 007 号船</button><button @click="toggleHeatmap(true)">显示热力图</button></div></template><script setup lang="ts">import { Store} from '@/store'; // 假设你使用 Piniaconst store = Store();function focusOnShip(shipId: string) {// 我们只需要知道方法名是 'GotoShip'store .sendToUnity('GotoShip', shipId);}function toggleHeatmap(show: boolean) {// 我们只需要知道方法名是 'HideOrShowHeatPointData'const params = { isShow: true};store .sendToUnity('HideOrShowHeatPointData', String(params .isShow);}</script>

核心解读:

  • 前端的视角: 作为前端开发者,我们完全不需要关心 Unity 的内部实现。我们的通信接口就是 unityStore.sendToUnity。我们只需查阅与 Unity 团队约定好的“方法名”列表,就可以自由地控制 3D 场景了。
  • 架构的优雅: 这种基于 postMessage 的双向通信模型,让 Vue 和 Unity 成了两个通过标准化信件(消息对象)沟通的独立应用,大大提升了项目的可维护性和扩展性。
http://www.dtcms.com/a/352003.html

相关文章:

  • 有 100W 个数,有一个函数是可以高效查找并删除某个数,问应该用什么数据结构去存这 100W 个数
  • 文献阅读笔记【雷达信号分选】:基于机器学习的雷达信号分选方法综述
  • 在python 代码中调用rust 源码库操作步骤
  • Excel跨sheet检索提取信息
  • 最简洁yolov8 C++配置教程
  • Leetcode+Java+dpI
  • 汇智焕彩,聚势创新 - openKylin 2.0 SP2正式发布!
  • 企业云办公安全指南:如何构建高效无忧的云办公环境?
  • 在Godot中为您的游戏添加并控制游戏角色的完整技术指南
  • 集成电路学习:什么是MobileNet
  • 在数据同步过程中,RustFS如何平衡RDMA的高吞吐和金融级数据校验的开销?
  • 深分页优化:高效解决方案全解析
  • golang7 数组切片
  • RocketMQ 消息存储机制-消息刷盘
  • TorchInductor - Autotune
  • 【牛客刷题】链表指定区间反转:两种高效解法详解,轻松掌握面试高频题!
  • 互联网医院品牌定位与差异化策略
  • 地下蚁国 全DLC(Empires of the Undergrowth)免安装中文版 在蚂蚁的世界里建立你的帝国
  • 马斯克的「巨硬」: MacroHard
  • 机器学习模型可解释库的介绍:Shapash (一)
  • TRO冻结卷土重来?Keith律所代理Ronald Kuang版权再维权
  • Spring Bean 生命周期高阶用法:从回调到框架级扩展
  • Android系统学习2——Android.Utils.Log模块讨论
  • Android 系统属性添加篇
  • StandardScaler()进行0,1标准化时fit_transform与transform的区别
  • MIME类型与文件上传漏洞 - 网络安全视角
  • 【Jetson】基于llama.cpp部署gpt-oss-20b(推理与GUI交互)
  • 多地闭店上热搜,中产的白月光无印良品怎么了?
  • 项目管理进阶——项目经理任职资格评定聘任及考核管理办法
  • 支持向量机(SVM)核心概念总结