Unity WebGL 嵌入 React 网页以及实现 C# 与 JS 的双向通信


前言

2025年6月10日

Unity 2022.3.54f1

nginx-1.28.0

Node.js v22.16.0

“react-unity-webgl”: “^9.8.0”,

验证将 unity webgl 嵌入 React 前端界面的可行性和效果,以及双向通信逻辑


过程中找到的网址:
为 Web 构建设置 Nginx 服务器配置:https://docs.unity3d.com/2022.3/Documentation/Manual/web-server-config-nginx.html
Unity WebGL 嵌入前端网页并通信:https://developer.unity.cn/projects/66545214edbc2a001ddb2a0b
Unity WebGL和JavaScript通信:https://developer.unity.cn/projects/66b76790edbc2a001ef504af
Unity模块嵌入React项目:https://juejin.cn/post/7028100780364152846
前端插件 React Unity WebGL:https://react-unity-webgl.dev/

webgl 输入中文解决方案, IME for Unity WebGL:https://github.com/kou-yeung/WebGLInput


一些经验

1、unity 打包 webgl ,compression format 在开发过程中选择 disabled ,最终发布时再换为 gzip 。(本人是使用 trae 开发的 react 项目,使用 npm start 启动时 gzip 的服务配置不知道怎么改,尴尬

2、gzip 配置需修改 nginx 服务器文件(路径:D:\nginx-1.28.0\conf\nginx.conf),参考 unity 官方给出的配置指南

为 Web 构建设置 Nginx 服务器配置:https://docs.unity3d.com/2022.3/Documentation/Manual/web-server-config-nginx.html

3、配置好 nginx 后,react unity webgl 中填入的的 loaderUrl、dataUrl、frameworkUrl、codeUrl ,在 disabled 和 gzip 切换时是不需要更改的。(Brotli 没试,不清楚

  const { unityProvider, isLoaded, loadingProgression, sendMessage,addEventListener,removeEventListener  } = useUnityContext({
    dataUrl: "Build/build_webgl.data",// build_webgl.data.gz 不用加.gz后缀
    frameworkUrl: "Build/build_webgl.framework.js",// build_webgl.framework.js.gz 不用加.gz后缀
    loaderUrl: "Build/build_webgl.loader.js",
    codeUrl: "Build/build_webgl.wasm",// build_webgl.wasm.gz 不用加.gz后缀
  });

4、React 向 Unity 通信时,sendMessage 的目标对象不能是子物体


一、Unity WebGL 部分

将 js.jslib 置于 Plugins 目录,Bridge.cs 挂载至场景对象。

点击 Message 按钮发送消息至 JS ,ReceiveMessage 接收 JS 消息并通过 alert 显示。

js.jslib, 置于 Plugins 目录

mergeInto(LibraryManager.library, {

  PrintString: function (str) {
    window.alert(UTF8ToString(str));
  },

  Message: function (str) {
    window.dispatchReactUnityEvent("Message",UTF8ToString(str));
  },

});

Bridge.cs,挂载至场景对象

public class Bridge: MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern void PrintString(string str);

    [DllImport("__Internal")]
    private static extern void Message(string str);

    private void OnGUI()
    {
        if (GUILayout.Button("Message"))
        {
            // 发送消息到JavaScript
            Message($"UnityToJS:[{DateTime.Now}]");
        }
    }

    /// <summary>
    /// 接收来自JavaScript的消息,注意传参类型
    /// </summary>
    /// <param name="message"></param>
    public void ReceiveMessage(string message)
    {
        PrintString($"UnityReceiveFromJS:[{message}]");
    }
}

二、Nginx 部分,配置完成后,可先单独部署 webgl 测试是否能够顺利访问网页

关键配置项 gzip_static on;

为 Web 构建设置 Nginx 服务器配置:https://docs.unity3d.com/2022.3/Documentation/Manual/web-server-config-nginx.html

nginx.conf

worker_processes 1;

events {
    worker_connections 1024;
}


http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    server {
        listen 80;
        server_name localhost;
        location / {
            root html;
            index index.html index.htm;
        }
    }

    server {
        listen 18000;
        server_name localhost;

        gzip_static on;# react-unity-webgl 能加载 gzip 的关键

        location / {
            root html/build;
            index index.html;
        }

        location ~ .+\.(data|symbols\.json)\.gz$ {
            gzip off; # Do not attempt dynamic gzip compression on an already compressed file
            add_header Content-Encoding gzip;
            default_type application/gzip;
        }
        location ~ .+\.js\.gz$ {
            gzip off; # Do not attempt dynamic gzip compression on an already compressed file
            add_header Content-Encoding gzip; # The correct MIME type here would be application/octet-stream, but due to Safari bug https://bugs.webkit.org/show_bug.cgi?id=247421, it's preferable to use MIME Type application/gzip instead.
            default_type application/javascript;
        }
        location ~ .+\.wasm\.gz$ {
            gzip off; # Do not attempt dynamic gzip compression on an already compressed file
            add_header Content-Encoding gzip;
            # Enable streaming WebAssembly compilation by specifying the correct MIME type for
            # Wasm files.
            default_type application/wasm;
        }
    }
}


三、React 部分,将 webgl 嵌入前端界面展示+双向交互

官方示例:https://react-unity-webgl.dev/docs/advanced-examples/loading-overlay
主动发消息,From React to Unity:https://react-unity-webgl.dev/docs/api/send-message
监听消息,From Unity to React:https://react-unity-webgl.dev/docs/api/event-system

App.tsx

import './App.css';
import {useEffect,useCallback} from "react";
import { Unity, useUnityContext } from "react-unity-webgl";

function App() {
  const { unityProvider, isLoaded, loadingProgression, sendMessage,addEventListener,removeEventListener  } = useUnityContext({
    dataUrl: "Build/build_webgl.data",// build_webgl.data.gz 不用加.gz后缀
    frameworkUrl: "Build/build_webgl.framework.js",// build_webgl.framework.js.gz 不用加.gz后缀
    loaderUrl: "Build/build_webgl.loader.js",
    codeUrl: "Build/build_webgl.wasm",// build_webgl.wasm.gz 不用加.gz后缀
  });

  const handleMessage = useCallback((msg: string) => {
    console.log(msg);
  }, []);

  useEffect(() => {
    addEventListener("Message", (msg: any) => handleMessage(msg.toString()));
    return () => {
      removeEventListener("Message", (msg: any) => handleMessage(msg.toString()));
    };
  }, [addEventListener, removeEventListener, handleMessage]);

  const loadingPercentage = Math.round(loadingProgression * 100);

  function handleClick() {
    sendMessage("Receive", "ReceiveMessage", "send-message");
  }

  return (
    <div className="container">
      {isLoaded === false && (
        <div className="loading-overlay">
          <p>Loading... ({loadingPercentage}%)</p>
        </div>
      )}
      <Unity className="unity" unityProvider={unityProvider} />
      <button onClick={handleClick} className="test-button">Send Message to Unity</button>
    </div>
  );
}

export default App;


App.css

.container {
  width: 100vw;
  height: 100vh;
  margin: 0;
  border: none;
}

.unity {
  width: 100%;
  height: 100%;
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  font-size: 2em;
}

.test-button {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1em;
  z-index: 100;
}

index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

index.css

html, body {
  margin: 0;
  padding: 0;
  overflow: hidden;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

### 实现 Unity 和 SSR 的集成 在探讨如何于 Unity实现服务端渲染(SSR),需理解两者的工作原理及其结合方式。Nuxt.js 提供了强大的服务端渲染能力,允许开发者构建性能优越的应用程序[^1];然而,Unity 主要用于创建三维游戏和体验,并不直接支持传统的 Web 应用的服务端渲染。 为了使 Unity 支持类似的功能,通常采用间接方法: #### 使用中间件代理请求 一种常见策略是在 Unity 项目之外设置一个基于 Node.js 或其他技术栈搭建的服务器环境,比如 Nuxt.js。此服务器负责处理页面首次加载时的内容预渲染工作。当客户端发起 HTTP 请求访问特定 URL 地址时,Web Server 将返回已经完成 HTML 结构填充后的响应给浏览器展示。此同时,Unity 可以作为前端框架的一部分被嵌入网页中,通过 WebGL 或者 WebAssembly 技术实现在浏览器内的高效图形显示效果。 对于实际的游戏逻辑部分,则继续由 C# 编写的脚本控制,而 UI 层面则可能依赖 React/Vue 这样的现代 JavaScript 框架来增强交互性和用户体验感。这种架构下,虽然严格意义上并不是传统意义上的 “服务端渲染”,但却实现了前后端分离模式下的优化加载方案。 #### 利用 Headless Chrome 渲染场景 另一种思路是利用无头浏览器如 Headless Chrome 来捕捉 Unity 构建好的 WebGL/WebAssembly 版本应用的画面帧并将其转换成图片流形式发送回用户的设备上呈现出来。这种方式能够确保即使在网络状况不佳的情况下也能提供较为流畅稳定的视觉反馈,因为大部分计算都在远程高性能机器上完成了。 不过需要注意的是,这种方法可能会带来额外的成本开销以及延迟问题,因此适用于某些特殊应用场景而非普遍推荐的做法。 ```csharp // 示例:Unity 客户端向后端 API 发送 GET 请求获取初始数据 using UnityEngine; using System.Collections; public class FetchInitialData : MonoBehaviour { void Start() { StartCoroutine(GetServerSideRenderedContent()); } IEnumerator GetServerSideRenderedContent() { using (WWW www = new WWW("http://your-nuxt-server.com/api/initial-data")) { yield return www; if (!string.IsNullOrEmpty(www.error)) { Debug.LogError($"Error fetching data from server: {www.error}"); } else { // 处理接收到的数据... string responseData = www.text; ProcessResponse(responseData); } } } private void ProcessResponse(string response) { // 解析 JSON 数据并 Unity 场景同步更新 } } ``` 上述代码展示了 Unity 如何外部 RESTful API 对话从而获得预先准备好的内容片段以便快速初始化界面组件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值