Unity集成DeepSeek API实战:跨平台网络通信与WebGL避坑指南

Unity集成DeepSeek API实战:跨平台网络通信与WebGL避坑指南 1. 项目概述当Unity引擎遇上DeepSeek API最近在捣鼓一个挺有意思的项目想把DeepSeek的AI能力直接集成到Unity游戏或者交互应用里。想法很美好让游戏里的NPC能真正“听懂人话”或者做个AI驱动的虚拟助手。但实际一上手Unity这个游戏引擎和DeepSeek这类云端大语言模型API的“跨界合作”真是踩坑无数。这不像在Python脚本里调个包那么简单Unity的环境、线程模型、平台差异尤其是WebGL和网络请求的异步处理每一个环节都可能成为拦路虎。这篇笔记就记录了我从环境搭建到功能实现过程中遇到的那些让人头秃的BUG和最终的解决方案希望能给同样想走这条路的朋友们省点时间。简单说这个项目的核心就是在Unity中通过HTTP请求调用DeepSeek的Chat Completion API处理返回的流式或非流式响应并将结果整合到游戏逻辑中。目标用户可以是独立游戏开发者、教育应用创作者或者任何想在实时3D交互环境中引入自然语言对话能力的从业者。无论你是Unity新手还是老鸟只要涉及网络API集成这些坑大概率都会碰到。2. 环境准备与初始配置的深坑万事开头难在Unity里搞外部服务集成第一步配置环境就能劝退不少人。这里的关键在于让Unity能顺畅地发送HTTP请求并处理JSON而Unity原生对现代C#网络库的支持有些“特立独行”。2.1 Unity网络栈的选择与NuGet的“水土不服”Unity默认的UnityWebRequest虽然能用但处理复杂的REST API请求、尤其是像DeepSeek这样的流式响应时灵活性和易用性不如.NET标准的HttpClient。很多开发者包括我第一反应是去用System.Net.Http.HttpClient。在普通的.NET项目里通过NuGet安装Microsoft.AspNet.WebApi.Client这类包来增强HTTP能力是常规操作。但在Unity里直接打开Visual Studio的NuGet管理器安装十有八九会失败。问题表象在Unity关联的Visual Studio项目中通过NuGet安装网络库后回到Unity编辑器要么报大量编译错误找不到命名空间要么在构建尤其是WebGL平台时出现诡异的链接错误。根因分析Unity使用自己定制化的.NET运行时通常是.NET Standard 2.1或.NET Framework的子集并且有一套独立的程序集解析和编译流程。直接从NuGet安装的包其依赖的.dll文件可能不兼容Unity的运行时环境或者包含了Unity不支持的高级API。更麻烦的是不同Unity版本如2021 LTS vs 2022 LTS和不同构建平台Windows Standalone vs WebGL所需的程序集差异巨大。解决方案放弃传统的NuGet拥抱专为Unity设计的包管理方案。使用Unity的Package Manager (UPM) 安装官方或社区包对于HTTP客户端可以尝试搜索com.unity.nuget.newtonsoft-json用于JSON序列化和com.unity.nuget.system-net-http。但注意这些包可能版本较旧。使用Git URL直接安装对于更成熟的解决方案比如原笔记中提到的NuGetForUnity插件这是最靠谱的路径。具体操作访问其GitHub仓库如https://github.com/GlitchEnzo/NuGetForUnity。在Unity的Package Manager中点击“”号选择“Add package from git URL...”。粘贴仓库的URL有时需要带.git后缀。安装后Unity编辑器顶部菜单栏会出现“NuGet”选项。通过这个专用的NuGet客户端你可以搜索并安装System.Net.Http、Newtonsoft.Json等包它会自动处理Unity环境下的兼容性问题。注意即使通过NuGetForUnity安装了System.Net.Http在WebGL平台下HttpClient的许多同步方法和部分异步模式仍然不可用。WebGL本质上运行在浏览器沙箱中所有网络请求都必须通过JavaScript的XMLHttpRequest或Fetch API转发这导致了根本性的限制。因此从项目初期就要为WebGL平台规划备选方案。2.2 API密钥管理与项目安全拿到DeepSeek的API密钥后新手常犯的错误是把它硬编码在C#脚本里然后一不小心就把代码上传到了公开的Git仓库。这相当于把家门钥匙放在了路边。安全方案使用Unity的Resources或StreamingAssets不推荐用于敏感信息对于学习项目可以将配置放在Resources文件夹下的一个JSON或文本文件中通过Resources.LoadTextAsset读取。但Resources内的所有文件在构建时会被打包容易被反编译提取。StreamingAssets文件夹的内容在构建后原样存在同样不安全。使用ScriptableObject配置资产中等安全创建一个ApiConfigScriptableObject里面包含apiKey和baseUrl等字段。在编辑器中为其赋值并确保该.asset文件被添加到.gitignore中。这样密钥不会出现在代码里但.asset文件如果被误提交风险依旧。最佳实践环境变量或外部配置文件开发阶段在本地机器上设置系统环境变量如DEEPSEEK_API_KEY。在C#中使用System.Environment.GetEnvironmentVariable读取。绝对不要将包含环境变量值的脚本提交。运行时/构建后对于需要分发给用户的客户端如PC游戏密钥绝不能存放在客户端内。正确的架构是自建一个简单的后端代理服务器。你的Unity客户端向自己的服务器发送请求由服务器持有API密钥并转发请求给DeepSeek。这样密钥完全与客户端隔离。对于WebGL由于所有代码运行在用户浏览器中这个代理方案更是必须的。实操步骤我采用ScriptableObject环境变量结合的方式。创建ScriptableObject其中包含一个bool字段useEnvironmentVariable。在Awake或Start方法中判断useEnvironmentVariable。若为真则从环境变量读取若为假则使用ScriptableObject中一个用于临时测试的debugApiKey此字段务必在提交前清空。在Unity Editor中可以通过菜单项快速打开这个配置资产方便切换。// 示例代码片段ApiConfiguration.cs using UnityEngine; [CreateAssetMenu(fileName ApiConfig, menuName Configs/Api Configuration)] public class ApiConfiguration : ScriptableObject { public string baseUrl https://api.deepseek.com/v1; public bool useEnvironmentVariable true; public string debugApiKey ; // 仅用于临时测试切勿提交真实密钥 public string GetApiKey() { if (useEnvironmentVariable) { string keyFromEnv System.Environment.GetEnvironmentVariable(DEEPSEEK_API_KEY); if (!string.IsNullOrEmpty(keyFromEnv)) { return keyFromEnv; } else { Debug.LogWarning(环境变量 DEEPSEEK_API_KEY 未设置将使用Debug Key如果存在。); } } // 安全警告此debugKey仅应在绝对必要时使用且绝不能提交至版本库。 return debugApiKey; } }3. 核心通信层的BUG与突围环境配好了密钥也有了接下来就是让Unity和DeepSeek“对话”。这里是BUG的重灾区主要集中在异步编程、JSON处理和流式响应上。3.1 UnityWebRequest的异步之殇与HttpClient的替代一开始我用了Unity自带的UnityWebRequest代码写起来像是这样IEnumerator SendChatRequest(string message) { string url https://api.deepseek.com/v1/chat/completions; string jsonBody JsonUtility.ToJson(new RequestData { model deepseek-chat, messages ... }); byte[] bodyRaw Encoding.UTF8.GetBytes(jsonBody); UnityWebRequest request new UnityWebRequest(url, POST); request.uploadHandler new UploadHandlerRaw(bodyRaw); request.downloadHandler new DownloadHandlerBuffer(); request.SetRequestHeader(Content-Type, application/json); request.SetRequestHeader(Authorization, Bearer apiKey); yield return request.SendWebRequest(); if (request.result ! UnityWebRequest.Result.Success) { Debug.LogError($Error: {request.error}); yield break; } string jsonResponse request.downloadHandler.text; // 解析jsonResponse... }遇到的BUG超时处理不直观UnityWebRequest有timeout属性但当超时发生时request.result可能是ConnectionError错误信息不明确难以区分是网络超时还是其他连接问题。流式响应(SSE)支持极差DeepSeek API支持以stream: true参数返回服务器发送事件Server-Sent Events。UnityWebRequest的DownloadHandler是一次性获取全部数据的无法实时处理分块返回的数据流。虽然可以用DownloadHandlerScript自己实现但复杂度陡增。与Unity协程(yield)的深度绑定所有逻辑必须包裹在IEnumerator中并用yield return调用当需要组合多个异步操作或进行复杂错误处理时代码会变成“回调地狱”的协程版本可读性差。解决方案拥抱C#的async/await与条件编译既然UnityWebRequest在流式处理和代码优雅度上不足而HttpClient在部分平台受限那就采用条件编译为不同平台选择最佳方案。对于Windows/Mac/Linux独立平台、Android、iOS使用System.Net.Http.HttpClient配合async/await。代码清晰流式处理方便。对于WebGL平台回退到基于UnityWebRequest的封装或者使用更高级的第三方库如Best HTTP/2资产商店插件并放弃原生的流式响应改为轮询或非流式请求。// 示例使用HttpClient处理流式响应非WebGL平台 #if !UNITY_WEBGL using System.Net.Http; using System.Threading.Tasks; public async Taskstring GetChatResponseStreamingAsync(string message) { using var httpClient new HttpClient(); httpClient.DefaultRequestHeaders.Authorization new System.Net.Http.Headers.AuthenticationHeaderValue(Bearer, apiKey); var requestData new { model deepseek-chat, messages new[] { new { role user, content message } }, stream true }; string jsonBody JsonConvert.SerializeObject(requestData); // 使用Newtonsoft.Json var content new StringContent(jsonBody, Encoding.UTF8, application/json); var response await httpClient.PostAsync(https://api.deepseek.com/v1/chat/completions, content, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); var stream await response.Content.ReadAsStreamAsync(); using var reader new StreamReader(stream); while (!reader.EndOfStream) { string line await reader.ReadLineAsync(); if (line.StartsWith(data: )) { string jsonData line.Substring(6); if (jsonData [DONE]) break; // 解析jsonData中的delta content并更新UI... Debug.Log($收到流式数据块: {jsonData}); } } return Stream finished; } #endif实操心得async/await在Unity中需要非常小心。Unity的主线程不是自由的线程池上下文直接在某些回调如按钮点击事件中await一个HttpClient请求然后去更新GameObject的Text组件可能会遇到“非主线程”错误。你需要用UnitySynchronizationContext或者将更新操作派发到主线程。一个简单的方法是使用MainThreadDispatcher这样的单例组件或者在新版Unity中在await后使用await Task.Yield();并检查Thread.CurrentThread.ManagedThreadId。3.2 JSON序列化的“字段失踪”案Unity自带的JsonUtility速度快、内存效率高但它有个巨大的限制只支持标注了[Serializable]的普通类Plain Old C# Object的公共字段。它不支持属性Property、字典、多态类型等复杂结构。DeepSeek API返回的JSON结构嵌套较深用JsonUtility解析时内部嵌套的对象如果没定义成[Serializable]类数据就会直接丢失。BUG场景你定义了一个ChatResponse类里面有一个ListChoice字段。Choice类里有一个Message字段。你确保所有类都加了[Serializable]。但解析后Choice列表始终为null或为空。检查发现是因为Message类中的属性如public string role { get; set; }没有被JsonUtility识别。解决方案放弃JsonUtility使用功能全面的Newtonsoft.Json即Json.NET。通过前面提到的NuGetForUnity安装Newtonsoft.Json包。在代码中使用JsonConvert.SerializeObject和JsonConvert.DeserializeObjectT。它的兼容性极好支持属性、字典、自定义命名规则通过[JsonProperty(content)]、忽略空值等。// 使用Newtonsoft.Json定义数据模型 public class ChatCompletionRequest { [JsonProperty(model)] public string Model { get; set; } deepseek-chat; [JsonProperty(messages)] public ListChatMessage Messages { get; set; } [JsonProperty(stream)] public bool Stream { get; set; } false; } public class ChatMessage { [JsonProperty(role)] public string Role { get; set; } // user, assistant, system [JsonProperty(content)] public string Content { get; set; } } // 序列化和反序列化 string jsonToSend JsonConvert.SerializeObject(request); ChatCompletionResponse response JsonConvert.DeserializeObjectChatCompletionResponse(jsonReceived);注意事项Newtonsoft.Json比JsonUtility更耗内存和CPU。对于每帧需要处理大量小型JSON的极端性能场景如网络游戏JsonUtility仍是首选。但对于AI API调用这种低频、数据量不大的操作Newtonsoft.Json的开发效率和功能优势是决定性的。4. 平台特异性问题尤其是WebGL这座大山如果你做的项目需要发布到网页端那么WebGL平台将是所有问题的放大器。Unity WebGL的初始化慢、网络请求限制、线程限制等问题会集中爆发。4.1 “Unity WebGL初始化很久”与首屏优化用户反馈点开网页黑屏时间长达十几秒甚至几十秒进度条卡在“初始化...”或“加载中...”。这不仅仅是网络加载资源慢更是Unity WebGL运行时一个庞大的WASM模块编译和初始化的过程。解决方案启用引擎代码剥离Engine Code Stripping在Player Settings - Publishing Settings - WebGL中设置“Code Stripping”为“High”或“Full”。这会移除Unity引擎中未使用的模块显著减小构建后的.wasm文件大小。但要注意过度剥离可能导致运行时找不到某些功能而崩溃需要测试。使用增量式压缩格式将“Compression Format”设置为“Brotli”或“Gzip”。现代浏览器都支持能减少下载体积。同时确保你的Web服务器正确配置了对应的MIME类型和压缩传输。拆分Asset Bundles不要把所有资源都打包进主包。将首屏不需要的模型、场景、音频等放到Asset Bundle中在游戏运行时按需异步加载。显示自定义加载界面Unity默认的加载进度条信息有限。可以自己制作一个精美的HTML/CSS/JS加载页面通过unityInstance.SetProgress回调来更新自定义进度条提升等待体验。在Unity项目的Template模板中修改index.html可以实现。优化DeepSeek集成代码不要在Awake或Start中立即发起API测试请求。将AI模块的初始化放在游戏主逻辑加载完成之后或者由玩家首次触发时再加载。4.2 WebGL下的网络请求限制与CORS在WebGL中所有网络请求都受浏览器同源策略Same-Origin Policy和CORS跨源资源共享规则的限制。直接从前端网页向api.deepseek.com发送请求如果DeepSeek的服务器没有配置允许你的网页域名Access-Control-Allow-Origin请求就会被浏览器拦截。BUG现象在编辑器和PC端构建版运行正常一到WebGL版本网络请求就失败浏览器控制台报错“Access to fetch at ‘https://api.deepseek.com/...’ from origin ‘http://localhost:...’ has been blocked by CORS policy”。解决方案如前所述必须使用后端代理。搭建一个简单的后端服务可以用Node.js Express、Python Flask、C# ASP.NET Core等部署在你自己可控的服务器上。该服务提供一个接口如POST /api/proxy/chat。Unity WebGL客户端向你的代理服务接口发送请求。代理服务收到请求后添加DeepSeek的API密钥转发请求给真正的DeepSeek API。代理服务将DeepSeek的响应原样返回给Unity客户端。这样对于浏览器而言请求是发往同源的你的服务器避开了CORS限制。API密钥也安全地保存在服务器端。简易Node.js代理示例// server.js (使用Express) const express require(express); const axios require(axios); require(dotenv).config(); // 用于从.env文件读取API_KEY const app express(); app.use(express.json()); app.post(/api/proxy/chat, async (req, res) { try { const response await axios({ method: post, url: https://api.deepseek.com/v1/chat/completions, headers: { Authorization: Bearer ${process.env.DEEPSEEK_API_KEY}, Content-Type: application/json, }, data: req.body, responseType: stream, // 如果需要支持流式这里很重要 }); // 将DeepSeek的响应头如Content-Type和流式数据转发给客户端 res.setHeader(Content-Type, response.headers[content-type]); response.data.pipe(res); } catch (error) { console.error(Proxy error:, error); res.status(500).json({ error: Internal proxy server error }); } }); app.listen(3000, () console.log(Proxy server running on port 3000));在Unity中你的请求地址就从https://api.deepseek.com/v1/chat/completions改为了http://你的服务器地址:3000/api/proxy/chat。4.3 WebGL中的线程与异步操作阻塞主线程在WebGL中多线程System.Threading.Thread受到严格限制Thread.Sleep()、某些同步的HttpClient方法会导致整个浏览器标签页卡死。async/await虽然可以用但其背后的线程池行为与标准.NET不同。注意事项避免在WebGL中使用任何阻塞式同步调用。将所有I/O操作网络、文件读取都改为异步async/await或基于UnityWebRequest的协程。即使使用async/await长时间运行的CPU密集型计算也会阻塞主线程因为WebGL本质是单线程。如果AI响应处理涉及复杂计算考虑将计算拆分成小块用yield return null分散到多帧执行或者探索Web Worker的可能性在Unity WebGL中较复杂。5. 性能、内存与异常处理实战集成外部API尤其是像大语言模型这种可能返回大量文本的服务性能、内存和稳定性必须提前考虑。5.1 流式响应与UI实时更新的性能陷阱为了实现打字机效果我们使用流式响应stream: true。每次收到一个数据块data: {...}就解析出新的delta内容追加到UI的Text组件上。BUG如果AI回复很长比如1000字每秒可能收到几十个数据块。每收到一个块就直接textComponent.text deltaContent;在WebGL或移动端上会导致严重的UI卡顿。因为每次修改text属性Unity UI系统都会触发网格重建Mesh rebuild和布局计算。解决方案缓冲更新不要每帧更新UI。设置一个StringBuilder来累积当前帧内收到的所有delta内容。在Update方法中或者使用Coroutine每隔0.05-0.1秒将累积的内容一次性更新到UI上。使用TextMeshPro如果还没用立刻换成TextMeshPro (TMP)。它的性能优于旧版UI Text尤其是在频繁更新文本时。分帧处理如果一次更新的内容量巨大比如一次性收到了非流式的完整长回复可以考虑将长字符串拆分成若干段通过协程分几帧追加到UI上避免单帧卡顿。// 示例缓冲式流式UI更新 using TMPro; using System.Text; using System.Collections.Generic; public class StreamTextDisplay : MonoBehaviour { public TMP_Text displayText; private StringBuilder _pendingText new StringBuilder(); private bool _needsUpdate false; // 在收到流式数据块时调用 public void AppendStreamDelta(string delta) { _pendingText.Append(delta); _needsUpdate true; } void Update() { if (_needsUpdate) { displayText.text _pendingText.ToString(); _pendingText.Clear(); _needsUpdate false; // 可以在这里触发打字机音效等 } } }5.2 内存泄漏未释放的请求与回调无论是UnityWebRequest还是HttpClient如果使用不当都会导致内存泄漏。UnityWebRequest必须调用request.Dispose()或者使用using语句块。更好的方法是使用UnityWebRequest的简化版UnityWebRequest.Post或Get它们返回的请求对象在协程结束后会自动管理但显式处理是好习惯。HttpClient普遍认为应该将其作为单例长期复用而不是每次请求都new一个。频繁创建和销毁HttpClient会导致底层socket来不及释放耗尽端口资源。但在Unity中特别是考虑到不同场景的加载和卸载一个更安全的方法是为AI管理器创建一个单独的HttpClient实例并在该管理器组件的整个生命周期内使用它当组件被销毁时OnDestroy调用httpClient.Dispose()。更大的陷阱是事件和回调。如果你为异步请求注册了回调事件但在请求完成或对象销毁时没有取消注册那么回调函数及其持有的对象引用将无法被垃圾回收。// 错误示例回调可能导致内存泄漏 public class LeakyBehaviour : MonoBehaviour { private SomeAsyncService _service; void Start() { _service new SomeAsyncService(); _service.OnDataReceived HandleData; // 注册事件 } void HandleData(string data) { /* ... */ } void OnDestroy() { // 忘记取消注册如果_service生命周期更长它仍持有对当前GameObject的HandleData方法的引用阻止其被回收。 // _service.OnDataReceived - HandleData; } }解决方案在OnDestroy或OnDisable中务必取消注册所有事件监听器并释放持有的非托管资源如网络请求、文件流。5.3 全面的异常处理与重试机制网络请求天生脆弱超时、服务器错误5xx、速率限制429、临时网络中断。一个健壮的系统必须有完善的异常处理和重试逻辑。关键策略区分异常类型HttpRequestException/UnityWebRequest网络错误可能是临时网络问题适合重试。TaskCanceledException/ 操作取消用户主动取消或超时需根据业务逻辑决定是否重试。API返回的错误状态码如400 Bad Request, 401 Unauthorized, 429 Too Many Requests这些是业务逻辑错误通常不应简单重试。429需要等待一段时间响应头中可能有Retry-After。实现指数退避重试对于可重试的错误不要立即重试而是等待一段时间且每次重试的间隔指数级增加避免加重服务器负担或陷入死循环。public async Taskstring SendRequestWithRetryAsync(FuncTaskstring requestFunc, int maxRetries 3) { int retryCount 0; while (true) { try { return await requestFunc(); } catch (HttpRequestException ex) when (retryCount maxRetries) { retryCount; int delay (int)Math.Pow(2, retryCount) * 1000; // 指数退避2秒4秒8秒... Debug.LogWarning($请求失败{delay}ms后重试 ({retryCount}/{maxRetries})。错误: {ex.Message}); await Task.Delay(delay); } catch (Exception ex) { // 其他不可重试的异常如API密钥错误、JSON解析错误 Debug.LogError($请求失败且不可重试: {ex.Message}); throw; } } }超时设置为HttpClient设置合理的Timeout属性例如TimeSpan.FromSeconds(30)。对于UnityWebRequest设置其timeout属性。流式请求的超时需要特殊处理可能需要设置一个更长的总超时或者基于活动状态收到数据的时间间隔来判断。6. 调试、监控与后期优化功能跑通只是第一步要让体验流畅还需要深入的调试和监控。6.1 Unity编辑器与浏览器开发者工具联调编辑器内调试使用Debug.Log输出关键信息请求URL、发送的数据、接收的原始JSON前几百个字符。对于流式响应可以每收到N个块就Log一次。使用Unity的Profiler窗口监控CPU、内存和网络占用特别是在频繁更新UI时。WebGL调试构建为Development Build并启用“Debug Symbols”。在浏览器中按F12打开开发者工具。Console查看Debug.Log输出的信息会转发到浏览器控制台以及JavaScript错误。Network这是最重要的面板。查看所有从你的WebGL页面发出的网络请求。检查请求头特别是Authorization、请求体、响应状态码、响应头和响应体。对于流式请求你会看到请求类型是EventStream可以实时看到分块到达的数据。Performance / Memory录制一段时间内的性能查看是否有内存泄漏JS堆或WASM内存持续增长、长任务阻塞主线程。6.2 设计状态机与用户反馈AI请求是异步的可能成功、失败、超时、被用户取消。UI需要清晰地向用户反馈当前状态。设计一个简单的请求状态机public enum ApiRequestState { Idle, // 空闲 Sending, // 发送中 Streaming, // 流式接收中 Finished, // 完成成功 Error, // 错误 Cancelled // 用户取消 }在UI上根据状态显示不同的元素Idle显示输入框和发送按钮。Sending禁用发送按钮显示“思考中...”的加载动画。Streaming持续更新文本光标闪烁。Error显示错误提示和重试按钮。Cancelled显示“已取消”提示。实现取消功能对于HttpClient可以使用CancellationTokenSource。对于UnityWebRequest可以调用request.Abort()。private CancellationTokenSource _cts; public async void SendMessage(string text) { // 取消之前的请求 _cts?.Cancel(); _cts new CancellationTokenSource(); CurrentState ApiRequestState.Sending; try { var response await _httpClient.PostAsync(url, content, _cts.Token); // ... 处理响应 } catch (TaskCanceledException) { CurrentState ApiRequestState.Cancelled; } catch (Exception ex) { CurrentState ApiRequestState.Error; ErrorMessage ex.Message; } } public void CancelCurrentRequest() { _cts?.Cancel(); }6.3 针对移动端与弱网的优化如果项目需要发布到手机网络环境更复杂。超时时间调整移动网络延迟高且不稳定适当增加超时时间例如60秒。请求压缩确保请求体JSON和响应体没有被Gzip压缩虽然DeepSeek服务器可能已经做了。对于自建代理可以启用压缩以减少数据传输量。离线检测与队列在发送请求前检查Application.internetReachability。如果网络不可用可以将请求加入队列等网络恢复后自动发送并给用户提示。流量敏感模式在设置中提供“省流模式”选项关闭流式响应一次性接收完整回复减少HTTP连接的开销和数据包数量。整个Unity与DeepSeek集成的过程就是一个不断与不同层面的“墙”碰撞并寻找缝隙的过程。从开发环境的配置兼容性到运行时各平台的网络限制再到用户体验的流畅度打磨每一步都需要细致的考量。最深的体会是不要试图用一个方案解决所有平台的问题条件编译和分层设计是关键。对于WebGL尽早确立代理服务器方案能避免后期大量重构。对于性能时刻警惕主线程阻塞和UI频繁更新。把这些坑踩过一遍之后剩下的就是享受在Unity世界里创造智能交互的乐趣了。