1. 项目概述:当Unity3D遇上WEBGL,我们到底在解决什么?
如果你做过Unity3D的WEBGL项目,大概率会和我一样,在某个深夜对着浏览器控制台里一片红色的报错信息陷入沉思。Unity3D WEBGL发布,听起来很美——无需安装,点开即玩,跨平台体验。但真把带点“后台交互”或“复杂UI”的项目丢进去,各种水土不服就来了。其中最典型、最让人头疼的两个拦路虎,就是数据库连接和字体显示。
这俩问题,本质上暴露了WEBGL运行环境的特殊性。它不是传统的桌面应用,也不是服务端程序,而是一个运行在浏览器沙箱里的“客人”。这个客人权限有限:它不能直接访问本地文件系统,不能随意发起网络请求(尤其是跨域请求),更不能像在编辑器或PC端那样,用熟悉的System.Data.SqlClient去直连数据库。字体问题也一样,你在Unity编辑器里用得好好的字体,打包成WEBGL后,可能直接变成“豆腐块”(□)或者直接消失,因为浏览器压根没加载到对应的字体文件。
所以,这个“实战”要解决的,不是简单的功能实现,而是思维模式的转换。我们需要从“Unity怎么用”切换到“WEBGL环境下,Unity能怎么用”。接下来,我会结合我踩过的无数个坑,把这两个问题的来龙去脉、解决思路和可直接复用的代码,掰开揉碎了讲清楚。无论你是想做一个带数据存档的WEBGL小游戏,还是做一个需要展示动态文本信息的3D看板,这篇内容都能给你一套经过验证的解决方案。
2. 核心问题一:WEBGL环境下的数据库连接策略
在PC端,你可能习惯这样写:new SqlConnection(connectionString),然后Open(),执行命令。但在WEBGL里,这条路直接被堵死。浏览器的安全策略(同源策略、CORS)和WEBGL本身的限制,决定了Unity无法直接建立到远程数据库的TCP连接。
2.1 为什么不能直连?理解限制的本质
首先得明白,WEBGL构建出来的应用,其网络通信能力是基于浏览器的XMLHttpRequest或Fetch API的。这意味着:
- 协议限制:通常只能使用HTTP/HTTPS协议,而像SQL Server、MySQL默认使用的1433、3306端口是私有数据库协议端口,浏览器无法直接发起这类请求。
- 同源策略(CORS):即使你的数据库服务神奇地提供了HTTP接口,如果它部署的域名和你的WEBGL页面域名不同,浏览器会拦截这次请求,除非数据库服务端明确设置了允许你域名访问的CORS头。
- 安全性:让前端代码直接包含数据库IP、用户名和密码是极度危险的行为,相当于把钥匙挂在门上。
所以,解决方案的核心思想是:引入一个中间层。让Unity WEBGL客户端不再直接面对数据库,而是与一个自己搭建的后端服务器API进行通信,由这个后端服务器去安全地操作数据库。
2.2 主流解决方案:前后端分离架构
这是目前最标准、最安全的做法。架构非常简单清晰:
Unity WEBGL客户端 (前端) <--[HTTP/HTTPS, WebSocket]--> 后端服务器API (中间层) <--> 数据库你的Unity代码里,不再有任何数据库连接字符串,只有向后端API发起请求的逻辑。
后端选型:这里灵活性很大。你可以用任何你熟悉的后端技术栈:
- Node.js + Express:轻量快捷,JavaScript/TypeScript全栈。
- Python + Flask/Django:开发效率高,生态丰富。
- C# + ASP.NET Core:如果你团队主力是C#,可以无缝衔接,共享一些模型定义。
- Java Spring Boot:适合大型、需要复杂业务逻辑的项目。
通信方式:通常使用RESTful API或GraphQL。对于实时性要求高的场景(如多人游戏状态同步),可以结合WebSocket。
2.3 实战代码示例:Unity中发起HTTP请求
假设我们有一个简单的需求:玩家在游戏结束时提交分数。后端已经提供了一个POST /api/score的接口。
首先,在Unity中,我们使用UnityWebRequest类。这是Unity封装好的、兼容WEBGL的HTTP请求工具。
using UnityEngine; using UnityEngine.Networking; using System.Collections; using System.Text; public class ScoreManager : MonoBehaviour { // 你的后端API地址 private string apiBaseUrl = "https://your-backend-server.com/api"; public void SubmitScore(int score, string playerName) { StartCoroutine(SubmitScoreCoroutine(score, playerName)); } IEnumerator SubmitScoreCoroutine(int score, string playerName) { // 1. 构造请求数据(通常为JSON) ScoreData data = new ScoreData { score = score, playerName = playerName, timestamp = System.DateTime.UtcNow.ToString("o") }; string jsonData = JsonUtility.ToJson(data); // 2. 创建POST请求 using (UnityWebRequest request = new UnityWebRequest(apiBaseUrl + "/score", "POST")) { byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonData); request.uploadHandler = new UploadHandlerRaw(bodyRaw); request.downloadHandler = new DownloadHandlerBuffer(); // 3. 设置请求头,告诉服务器我们发送的是JSON request.SetRequestHeader("Content-Type", "application/json"); // 如果需要认证,可以在这里添加Token等 // request.SetRequestHeader("Authorization", "Bearer " + authToken); // 4. 发送请求并等待 yield return request.SendWebRequest(); // 5. 处理响应 if (request.result == UnityWebRequest.Result.Success) { Debug.Log("Score submitted successfully: " + request.downloadHandler.text); // 解析返回的JSON,例如获取排名信息 // var response = JsonUtility.FromJson<ScoreResponse>(request.downloadHandler.text); } else { Debug.LogError("Failed to submit score: " + request.error + "\nResponse: " + request.downloadHandler?.text); // 根据HTTP状态码进行更细致的错误处理 if (request.responseCode == 401) { // 未授权,可能需要重新登录 } } } } // 定义上传的数据结构 [System.Serializable] private class ScoreData { public int score; public string playerName; public string timestamp; } }关键点与避坑指南:
- 必须使用协程(Coroutine):
UnityWebRequest.SendWebRequest()是异步操作,必须配合yield return在协程中调用,否则会阻塞主线程。 - 正确处理请求生命周期:使用
using语句包裹UnityWebRequest对象,确保请求结束后相关资源被正确释放,避免内存泄漏。这在WEBGL中尤为重要。 - 错误处理要完备:不要只看
request.error,还要检查request.result和request.responseCode。网络超时、服务器错误、数据格式错误等都需要不同的处理逻辑。 - 注意JSON序列化:Unity自带的
JsonUtility对于简单结构很好用,但嵌套复杂或需要处理字典时可能力不从心。可以考虑引入第三方库如Newtonsoft.Json(需兼容WEBGL),或者在数据结构设计上做些妥协。 - CORS问题:如果你的Unity WEBGL页面运行在
localhost或127.0.0.1,而后端API在另一个端口(如localhost:5000),依然会遇到CORS错误。务必在后端服务器配置中正确设置CORS响应头。例如在ASP.NET Core中需要在Program.cs中添加策略。
2.4 备选方案与高级话题
- WebSocket实时通信:对于聊天、实时对战等场景,
UnityWebRequest就不够用了。可以使用第三方WebSocket库(如NativeWebSocket),它们通常提供了对WEBGL的良好支持。核心是建立长连接,进行双向通信。 - GraphQL:如果数据交互复杂,需要灵活查询,可以考虑让后端提供GraphQL端点。Unity端可以使用类似
UnityGraphQL的客户端库来构建和发送查询请求,能有效减少请求次数,精确获取所需数据。 - 静态数据与本地存储:对于一些只读的、非敏感的基础数据(如游戏配置、物品属性),可以打包成JSON或Scriptable Object,直接包含在构建资源中。对于玩家本地进度,可以使用
PlayerPrefs(WEBGL下实际存储在浏览器的IndexedDB中)或直接操作UnityEngine.Application.persistentDataPath路径下的文件(同样受浏览器沙箱限制,但可用于缓存)。
重要提示:在任何情况下,绝对不要尝试在Unity WEBGL代码中拼接SQL语句并发送给后端。后端API应接收结构化的、明确含义的参数(如
itemId,actionType),由后端进行业务逻辑处理和安全的数据库操作,防止SQL注入攻击。
3. 核心问题二:WEBGL中的字体显示崩溃与修复
字体问题通常在你第一次把带有TextMeshPro或旧版UI Text的项目发布到WEBGL时爆发。症状包括:文字不显示、显示为方块(□)、字体粗细异常、或者直接导致运行时错误。
3.1 问题根源:字体资源加载机制差异
在PC或移动平台,Unity可以“看到”并直接加载系统字体或项目内的字体文件(.ttf, .otf)。但在WEBGL中:
- 动态字体(Dynamic Font)失效:Unity旧版UI Text使用的“Arial”等动态字体,依赖于运行平台的系统字体。而浏览器沙箱环境无法直接枚举或访问宿主操作系统的字体库,因此会回退到浏览器默认的有限字体集,常常导致找不到字体。
- 字体文件引用丢失:TextMeshPro(TMP)使用的字体是资产文件(.asset)。当你为TMP创建字体资产时,它关联了一个或多个字体源文件(.ttf)。如果这个关联在构建过程中没有被正确处理,或者字体源文件没有被包含在构建里,WEBGL运行时就会加载失败。
- 字体图集生成失败:TMP的核心原理是为字体生成纹理图集。在WEBGL构建过程中,这个生成步骤可能因为环境差异(如缺少某些依赖)而出错,导致生成的图集是空的或损坏的。
3.2 终极解决方案:使用TextMeshPro并正确配置
对于新项目,强烈建议直接使用TextMeshPro(TMP),它是Unity官方推荐的UI文本解决方案,功能强大,对WEBGL支持也更好。但需要正确配置。
步骤一:确保导入TMP Essentials资源包在Unity Editor中,通过Window -> TextMeshPro -> Import TMP Essential Resources,确保导入了核心资源。这个操作通常会在项目初始化时做一次。
步骤二:为WEBGL创建/配置TMP字体资产这是最关键的一步。你不能直接使用为编辑器环境创建的字体资产。
- 准备字体源文件:将你需要的
.ttf或.otf字体文件放入项目的Resources文件夹或任意Resources子文件夹下。例如Assets/Resources/Fonts/MyFont.ttf。Resources文件夹下的资源会被Unity强制包含在构建中。 - 创建字体资产:
- 打开
Window -> TextMeshPro -> Font Asset Creator。 - 在
Source Font File中选择你放在Resources下的字体文件。 - 调整字符集(Character Set)。对于WEBGL,为了减小包体,不要使用“Dynamic”。建议选择“Custom Characters”或“ASCII”。如果你需要显示中文,选择“Unicode Range (Hex)”并输入常用汉字范围(如
0x4E00-0x9FFF),或者更精确地输入你游戏中会用到的所有字符。 - 调整其他参数(如图集尺寸、字体大小),点击“Generate Font Atlas”。预览无误后,点击“Save”或“Save as…”将字体资产保存到项目目录(不要保存在Resources里,保存字体资产本身)。
- 打开
- 应用字体资产:在你的TMP Text组件上,将新创建的字体资产拖拽到“Font Asset”字段。
步骤三:检查并修改TMP设置(重要)打开Edit -> Project Settings -> TextMesh Pro。
- 确保“Default Font Asset”是你为WEBGL创建的那个字体资产。
- 查看“Font Assets”列表,确认你需要的字体资产在其中。
3.3 实战代码示例:运行时动态加载字体(备用方案)
有些场景下,你可能需要从服务器动态加载自定义字体(比如用户自定义字体)。这可以通过UnityWebRequest加载字体文件,然后动态创建TMP字体资产来实现。注意:此操作消耗较大,需谨慎使用。
using TMPro; using UnityEngine; using UnityEngine.Networking; using System.Collections; public class DynamicFontLoader : MonoBehaviour { public TMP_Text targetText; // 需要应用字体的TMP文本组件 public string fontUrl = "https://your-cdn.com/fonts/MyCustomFont.ttf"; // 字体文件URL IEnumerator Start() { // 1. 下载字体文件 using (UnityWebRequest request = UnityWebRequest.Get(fontUrl)) { request.downloadHandler = new DownloadHandlerBuffer(); yield return request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError("Failed to download font: " + request.error); yield break; } // 2. 获取字体数据 byte[] fontData = request.downloadHandler.data; // 3. 动态创建字体资产(这是一个简化示例,实际过程更复杂) // 注意:在WEBGL中,直接使用byte[]创建Font可能受限。 // 更可靠的做法是:将字体数据保存为临时文件(在Application.persistentDataPath), // 然后使用TMP的FontAssetCreator API或AssetBundle来加载。 // 以下代码仅为思路演示,可能需要结合AssetBundle或插件实现。 Debug.Log("Font downloaded, size: " + fontData.Length + " bytes"); // ... 后续复杂的字体处理与创建TMP Font Asset的代码 ... // 由于涉及TMP内部API和平台差异,此处省略具体实现。 // 一个可行的思路是:将字体数据发送到后端,后端生成对应的TMP字体AssetBundle,再下载加载。 } } }动态字体加载的注意事项:
- 性能与兼容性:运行时创建字体资产非常消耗CPU和内存,尤其在移动端或性能受限的WEBGL环境,可能导致卡顿甚至崩溃。应尽量避免,或仅在绝对必要时使用。
- 备用字体:始终为TMP Text组件设置一个在构建时包含的、可靠的备用字体资产。这样即使动态加载失败,文字也不会消失。
- 字体授权:确保你拥有动态分发该字体的合法授权。
3.4 常见字体问题排查清单
当你的WEBGL版本出现字体问题时,请按此清单检查:
文字完全消失/显示方块:
- [ ] 检查TMP字体资产是否被正确赋值给Text组件。
- [ ] 检查该字体资产引用的源字体文件(.ttf)是否在
Resources文件夹内。 - [ ] 在
Project Settings -> TextMesh Pro中检查字体资产列表。 - [ ] 打开浏览器开发者工具(F12)的“网络(Network)”选项卡,查看是否有加载字体文件(.ttf)或字体图集纹理(.png?)的请求失败(404或跨域错误)。
字体模糊或边缘有锯齿:
- [ ] 检查TMP字体资产的“Atlas Resolution”。对于WEBGL,通常需要更高的分辨率(如1024x1024或2048x2048)来保证清晰度,但这会增加包体大小和内存占用,需要权衡。
- [ ] 检查TMP Text组件上的“Font Size”和“Auto Sizing”设置,确保渲染尺寸合适。
构建后字体变化/回退到默认字体:
- [ ] 确认你为WEBGL平台专门创建并配置了字体资产,而不是直接使用编辑器环境下可用的系统字体创建的资产。
- [ ] 清理构建缓存(
Build Settings->Build按钮下的Clear Build或删除Library文件夹中的相关缓存),然后重新构建。
使用旧版UI Text的字体问题:
- [ ]终极建议:升级到TextMeshPro。如果暂时无法升级,在UI Text组件上,将“Font”设置为一个已导入项目的
.ttf字体文件(同样需放在Resources下),并将“Font Style”设置为“Normal”,避免使用“Bold”或“Italic”(这些样式在动态字体失效时可能无法正确合成)。
- [ ]终极建议:升级到TextMeshPro。如果暂时无法升级,在UI Text组件上,将“Font”设置为一个已导入项目的
4. WEBGL项目构建与部署的专项优化
解决了数据库和字体两大难题,并不意味着项目就能顺畅运行。WEBGL平台有其独特的性能特点和限制,需要在构建和部署环节做针对性优化。
4.1 构建设置(Player Settings)关键项
打开File -> Build Settings,选择WebGL平台,点击Player Settings:
分辨率与呈现(Resolution and Presentation):
- Default Canvas Width/Height:设置初始画布大小。建议与你的游戏设计分辨率一致,或设为0以使用HTML模板中的设置。
- WebGL Template:选择一个合适的模板。
Minimal模板最干净,Default包含进度条等UI。你可以自定义模板来更好地与你的网页集成。
其他设置(Other Settings):
- Color Space:对于大多数项目,
Gamma就够了,性能更好。如果需要更精确的HDR或线性光照计算,才选择Linear,但这会显著增加着色器编译时间和内存占用。 - Auto Graphics API:取消勾选,并确保只保留了
WebGL 2.0(如果支持)。移除WebGL 1.0可以减小构建大小,并确保使用更现代的图形功能。 - Strip Engine Code:务必勾选。这会移除项目未使用的Unity引擎模块代码,极大减小
.wasm和.js代码文件体积。 - Enable Exceptions:设置为
None或Explicitly Thrown Only。Full会在生成的代码中加入大量异常处理逻辑,导致代码体积暴增和性能下降。这意味着你需要更小心地处理代码中的潜在错误,避免未捕获的异常导致游戏崩溃。 - Data Caching:勾选。这会将资源缓存到浏览器的IndexedDB中,玩家第二次访问时加载速度会快很多。
- Color Space:对于大多数项目,
发布设置(Publishing Settings):
- Compression Format:选择
Brotli。这是目前压缩率最高、浏览器支持良好的格式,能显著减少网络传输量。确保你的Web服务器(如Nginx)配置了支持Brotli压缩。 - Decompression Fallback:勾选。这会在不支持Brotli的旧浏览器上使用Gzip备用,提高兼容性。
- Compression Format:选择
4.2 资源优化与加载策略
WEBGL应用的初始加载速度至关重要,玩家没有耐心等待几十MB的资源下载。
纹理优化:
- 使用合适的压缩格式:WEBGL主要支持
ASTC(需设备支持)、ETC2(需WebGL 2.0)和回退到RGBA32。在Texture Import Settings中,为WebGL平台选择ASTC或ETC 2.0,并设置合适的压缩质量。对于UI纹理,可以考虑使用Crunch压缩。 - 控制纹理尺寸:非必要的纹理坚决缩小。一个2048x2048的纹理压缩后可能还有几MB,而512x512的纹理可能只有几百KB,视觉差异在很多时候并不明显。
- 使用合适的压缩格式:WEBGL主要支持
音频优化:
- 将长背景音乐设置为
Streaming(流式加载),避免一次性载入内存。 - 将短音效(如点击、爆炸声)的加载类型设为
Decompress On Load,并选择合适的压缩格式(如Vorbis),在内存和CPU解压开销间取得平衡。
- 将长背景音乐设置为
代码分包与异步加载:
- 使用
Addressable Assets System或AssetBundle。将游戏按场景、功能模块拆分成多个包。启动时只加载核心包,其他包在需要时(如进入新关卡前)异步加载。这能极大缩短首屏加载时间。 - 对于
Addressables,构建时选择Build Script: Built-In Shader Bundle选项,可以将着色器单独打包,避免重复。
- 使用
4.3 部署服务器配置要点
即使构建文件完美,服务器配置不当也会导致游戏无法运行或体验极差。
- MIME类型:确保你的Web服务器为Unity WEBGL生成的文件类型配置了正确的MIME类型。这是最常见的问题之一。
.wasm->application/wasm.data->application/octet-stream或application/x-unitydata.js->application/javascript.symbols.json->application/json- 以Nginx为例,在配置文件中添加:
location ~ .wasm$ { add_header Content-Type application/wasm; } location ~ .data$ { add_header Content-Type application/octet-stream; }
- HTTP压缩:如前所述,启用
Brotli和Gzip压缩。在Nginx中需要加载对应的模块并进行配置。 - 缓存策略:为
.data、.wasm等较大的静态资源设置较长的缓存时间(如1年),并在文件名中嵌入哈希值(Unity构建时自动完成),实现增量更新。为.html文件设置较短的缓存或不缓存。 - 跨域问题(CORS):如果你的游戏资源(如AssetBundle、配置文件)存放在另一个域名(CDN)下,需要在该CDN的响应头中设置
Access-Control-Allow-Origin: *或你的游戏页面域名。
5. 调试与问题排查实战指南
WEBGL的调试比原生平台更麻烦,因为你面对的是浏览器的黑盒。掌握正确的调试方法能事半功倍。
5.1 浏览器开发者工具是你的主战场
- 控制台(Console):查看
Debug.Log的输出、JavaScript错误和警告。Unity的日志会在这里打印。注意:大量频繁的Debug.Log在WEBGL中会有性能开销,发布前建议使用条件编译#if !UNITY_WEBGL || UNITY_EDITOR来移除。 - 网络(Network):这是排查资源加载、API请求失败问题的核心。查看所有请求的状态码(200成功,404未找到,403禁止访问,500服务器错误,CORS错误等)。重点关注
.wasm、.data、字体文件、以及你发起的API请求是否成功加载。 - 源代码(Sources):你可以看到Unity生成的JavaScript代码(
.js文件),虽然可读性差,但可以设置断点,对于追踪复杂的逻辑流或崩溃点有时有奇效。查找错误堆栈中提到的文件名和行号。 - 性能(Performance) & 内存(Memory):录制一段时间内的运行时性能,分析帧时间、函数调用耗时,查找性能瓶颈(是否是某个复杂Shader?是否是某段密集的JavaScript计算?)。检查内存使用情况,防止内存泄漏。
5.2 Unity端调试技巧
- 开发构建(Development Build):在
Build Settings中勾选Development Build和Autoconnect Profiler。这样构建出的版本包含调试符号,并且会自动连接回Unity Editor的Profiler和Console窗口,你可以像在编辑器中一样进行性能分析和查看日志,极其方便。 - 自定义日志处理:可以创建一个脚本,重定向
Application.logMessageReceived事件,将日志不仅输出到控制台,也通过HTTP发送到你自己的日志服务器,便于收集线上用户的错误信息。public class WebGLLogger : MonoBehaviour { void OnEnable() { Application.logMessageReceived += HandleLog; } void OnDisable() { Application.logMessageReceived -= HandleLog; } void HandleLog(string logString, string stackTrace, LogType type) { if (type == LogType.Exception || type == LogType.Error) { // 将 logString 和 stackTrace 通过 UnityWebRequest 发送到你的错误收集API // StartCoroutine(SendLogToServer(logString, stackTrace)); } } } - 模拟延迟与弱网:在浏览器开发者工具的“网络(Network)”选项卡中,可以设置节流(Throttling)来模拟3G、4G或自定义的网络延迟和带宽,测试游戏在弱网环境下的表现和资源加载逻辑是否健壮。
5.3 典型错误与解决方案速查表
| 错误现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
游戏黑屏,控制台报TypeError或WebAssembly相关错误 | .wasm文件MIME类型错误或加载失败 | 1. 检查服务器.wasm文件的MIME类型是否为application/wasm。2. 检查网络面板,确认 .wasm文件是否成功下载(状态码200)。3. 清理浏览器缓存并硬刷新(Ctrl+F5)。 |
| 游戏卡在加载进度条,或进度条走完后白屏 | .data文件或其他资源文件加载失败、缺失或跨域 | 1. 检查网络面板,看是否有.data、.js或其他资源请求报错(404, 403, CORS)。2. 确认所有构建生成的文件都已完整上传到服务器。 3. 检查服务器CORS配置。 |
| 字体显示为方块(□) | TMP字体资产未正确包含或加载 | 1. 确认字体源文件(.ttf)在Resources文件夹内。2. 检查TMP Text组件引用的字体资产是否正确。 3. 重新为WEBGL平台生成字体资产。 |
| 点击按钮无反应,或部分UI功能失效 | 浏览器事件系统与Unity交互问题 | 1. 检查UI元素的Raycast Target是否被意外禁用。 2. 确认没有其他全屏透明的UI面板挡住了点击。 3. 对于输入框(TMP Input Field),WEBGL下可能需要额外处理焦点事件,有时在移动端虚拟键盘弹出时会有异常。 |
| 游戏运行缓慢,帧率低 | 性能瓶颈 | 1. 使用开发构建连接Profiler,分析CPU/GPU耗时。 2. 检查Draw Call数量,合并静态UI和场景物体。 3. 检查是否有每帧执行的昂贵操作(如不必要的FindObject、Instantiate/Destroy)。 4. 降低纹理分辨率、简化Shader。 |
UnityWebRequest网络请求失败 | CORS问题、服务器错误、网络超时 | 1. 查看网络面板中该请求的详细状态码和响应头。 2. 确认后端API服务器已正确配置CORS。 3. 在Unity代码中增加超时处理和更详细的错误日志。 |
6. 从理论到实践:一个简单的积分榜案例
让我们把上面所有的知识点串联起来,实现一个完整的WEBGL小功能:从服务器获取并显示玩家积分榜。
架构:
- 前端:Unity WEBGL应用,使用TMP显示文字,使用
UnityWebRequest与后端通信。 - 后端:一个简单的Node.js + Express服务器,提供
GET /api/leaderboardAPI,从数据库(如SQLite或MongoDB)查询数据。 - 数据库:SQLite(用于演示)。
步骤简述:
后端搭建(Node.js示例):
// server.js const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const cors = require('cors'); const app = express(); const port = 3000; app.use(cors()); // 启用CORS,允许所有域名访问(生产环境应指定具体域名) app.use(express.json()); let db = new sqlite3.Database('./scores.db'); // 创建表(如果不存在) db.run(`CREATE TABLE IF NOT EXISTS leaderboard ( id INTEGER PRIMARY KEY AUTOINCREMENT, playerName TEXT NOT NULL, score INTEGER NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`); // 获取积分榜API app.get('/api/leaderboard', (req, res) => { const limit = parseInt(req.query.limit) || 10; db.all(`SELECT playerName, score FROM leaderboard ORDER BY score DESC LIMIT ?`, [limit], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(rows); // 返回JSON数组,如 [{"playerName":"Alice","score":1000}, ...] }); }); // 提交分数API(供其他部分调用) app.post('/api/score', (req, res) => { const { playerName, score } = req.body; db.run(`INSERT INTO leaderboard (playerName, score) VALUES (?, ?)`, [playerName, score], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, id: this.lastID }); }); }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); });Unity前端实现:
- 创建一个UI Canvas,包含一个
ScrollView,里面用Content存放积分榜条目。 - 创建一个
LeaderboardEntry.prefab预制体,包含两个TMP Text组件(排名、玩家名、分数)。 - 编写
LeaderboardManager.cs脚本,负责调用API、解析数据、动态生成条目。
// LeaderboardManager.cs (简化版) using UnityEngine; using UnityEngine.Networking; using TMPro; using System.Collections.Generic; public class LeaderboardManager : MonoBehaviour { public string apiUrl = "http://localhost:3000/api/leaderboard"; public GameObject entryPrefab; public Transform contentParent; void Start() { StartCoroutine(FetchLeaderboard()); } IEnumerator FetchLeaderboard() { using (UnityWebRequest request = UnityWebRequest.Get(apiUrl)) { yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { string jsonResponse = request.downloadHandler.text; // 使用简单的JSON解析,例如JsonUtility或第三方库 // 假设返回格式: [{"playerName":"Tom","score":1500}, ...] LeaderboardDataList dataList = JsonUtility.FromJson<LeaderboardDataList>("{\"entries\":" + jsonResponse + "}"); PopulateLeaderboard(dataList.entries); } else { Debug.LogError("Leaderboard fetch failed: " + request.error); } } } void PopulateLeaderboard(List<LeaderboardData> entries) { // 清空现有条目 foreach (Transform child in contentParent) { Destroy(child.gameObject); } for (int i = 0; i < entries.Count; i++) { GameObject entryObj = Instantiate(entryPrefab, contentParent); LeaderboardEntryUI ui = entryObj.GetComponent<LeaderboardEntryUI>(); if (ui != null) { ui.SetData(i + 1, entries[i].playerName, entries[i].score); } } } [System.Serializable] private class LeaderboardDataList { public List<LeaderboardData> entries; } [System.Serializable] private class LeaderboardData { public string playerName; public int score; } }- 创建一个UI Canvas,包含一个
字体处理:确保
LeaderboardEntry.prefab中使用的TMP Text组件,其字体资产是按照3.2节所述方法为WEBGL专门创建并包含在构建中的。构建与部署:按照第4章的优化建议进行构建。将构建出的
Build文件夹内容上传到你的Web服务器(如Nginx、Apache)。确保Node.js后端服务器也在运行且网络可达。
通过这个案例,你将完整走通从后端数据存储、API提供,到前端Unity WEBGL请求数据、处理响应、使用TMP安全显示文字的全流程。过程中遇到的任何CORS、字体缺失、资源加载问题,都可以依据前面的章节进行排查。这不仅仅是解决两个孤立的问题,更是掌握了一套应对WEBGL平台特殊性的完整开发方法论。