尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity接入DeepSeek流式传输实战:从API调用到逐字输出

Unity接入DeepSeek流式传输实战:从API调用到逐字输出 做 Unity 项目的朋友最近应该都在聊怎么把大模型塞进游戏里。我折腾了一段时间把 DeepSeek 的 API 接进了 Unity 工程并且实现了数据流式传输——就是那种 AI 对话不是干等好几秒然后一次性蹦出来而是像 ChatGPT 网页版一样逐字往外冒的效果。这篇东西就聊聊我完整趟完这条路之后的技术细节和踩坑记录。先说结论Unity 里直接调大模型 API 并不复杂真正麻烦的是“流式传输”这件事。Unity 的UnityWebRequest封装得比较“整块”想做真正的流式读取需要换思路。我会从最基础的 API 调用讲到流式方案的底层原理再给出一套能直接跑进项目的完整实现最后把我在实测中遇到的坑和优化手段一并倒出来。1. 为什么非要在 Unity 里接 DeepSeek以及流式到底解决了什么1.1 不只是一个“聊天机器人”很多人一听在游戏里接 AI第一反应是做 NPC 对话。这当然是最直接的应用场景但实际接到项目里之后你会发现可用范围比想象中大得多游戏内的新手引导助手、帮助玩家生成自定义任务、战斗结束后的对局分析与打法建议、甚至用 AI 帮你写剧情草稿再人工润色。Unity 本身就是内容生成工具把大模型当成一个“实时内容生成后端”能玩的花样非常多。DeepSeek 在这一轮里被大家盯上主要是三点价格便宜、中文效果好、API 兼容 OpenAI 的格式。意味着你之前调 OpenAI 接口的代码逻辑换一下base_url和 key 基本就能复用。对于个人开发者或者中小团队来说这是非常务实的选择。1.2 流式传输不是“锦上添花”而是体验底线我最早偷懒用的是非流式调用发一个请求过去等个三五秒然后整段内容一次性吐出来。放在后台工具脚本里问题不大但一旦面向玩家体验就很糟糕。玩家会怀疑是不是卡死了、网络是不是断了尤其对话内容一长等待时间线性拉长。流式传输的本质是让服务端边生成边把内容推给你客户端拿到一段就显示一段。玩家看到的是“正在打字”的 AI而不是“正在转圈的加载”。这种体验上的差距在交互型应用里就是天壤之别。从技术角度说DeepSeek 官方对 OpenAI 兼容接口的流式实现走的是 SSEServer-Sent Events协议也叫 server-send-events。它建立在 HTTP 基础之上服务端可以持续往同一个连接里写数据客户端不需要轮询。理解这一点是接下来所有实现的前提。2. 接入前的准备工作环境、账号和基础调用格式2.1 开发环境与账号准备我这边用的 Unity 版本是 2021.3 LTS虽然 Demo 代码大部分在 Unity 2020 上也能用但建议至少 2021 以上原因后面讲纹理和编码处理时会提到。申请 API key 这一步没啥好说的平台上注册后拿一串 key 就行了。注意这串 key 要保管好别硬编码在最终包里——至少放到一个配置文件里最好是启动时从远程拉取哪怕只是个人项目也应该有这个意识。这里有一个分工建议业务逻辑层放在 C# 脚本里但网络层不要直接塞进 MonoBehaviour尽量拆成一个独立的 HTTP 请求工具类。这既方便单独测试也避免场景重载时请求生命周期失控。2.2 先跑通一次非流式请求确认链路是通的工程里我先写了一个最原始的UnityWebRequest调用把请求体发到 DeepSeek 的接口拿到完整 JSON 再说。DeepSeek 的接口地址是https://api.deepseek.com/chat/completions请求体和 OpenAI 格式一致{ model: deepseek-chat, messages: [ { role: user, content: 你好简单介绍一下你自己 } ], stream: false }注意stream: false意味着非流式返回返回 JSON 里choices[0].message.content就是完整回答。这一步先把链路调通能拿到正确结果再做流式改造——我见过不少人一上来直接上流式结果分不清问题是在网络层还是解析层排查起来非常痛苦。2.3 遇到最常规的一个坑Unity 编辑器里的平台限制在 Unity 编辑器里有个很隐蔽的问题需要提前说UnityWebRequest跑 http 协议会有安全限制Windows 编辑器下默认禁止 HTTP 明文请求直接给你一个 SSL 相关的报错。DeepSeek 接口是 HTTPS 的所以没这个问题。但如果你在公司内网做调试用的中转服务是 HTTP就得去 Player Settings 里把Allow downloads over HTTP挑成Always allowed。这个配置项小但卡住过不少人。非流式请求能通之后我已经能确定网络没问题、key 没问题、模型参数格式没问题。接下来才是真正的重头戏——流式传输。3. SSE 协议基本原理和 Unity 里实现流式的方案选型3.1 SSE 报文到底长什么样SSE 和普通 HTTP 响应最直观的区别在 Content-Type。普通响应的Content-Type是application/json流式响应是text/event-stream。响应体不是一整块 JSON而是一连串事件每个事件由若干行key: value组成事件之间用空行分隔。在 DeepSeek 的流式返回里核心字段是data:data: {id:chatcmpl-xxx,choices:[{delta:{role:assistant,content:},index:0}]} data: {id:chatcmpl-xxx,choices:[{delta:{content:你好},index:0}]} data: {id:chatcmpl-xxx,choices:[{delta:{content:我是},index:0}]} data: [DONE]每一条data:后面跟着的是一段独立的 JSON增量内容就在choices[0].delta.content里。到最后会有一个特殊的data: [DONE]表示结束。理解了这套报文格式解析代码就只是一层薄薄的壳了。3.2 Unity 里三条流式实现路线对比这一步是我花时间最多的部分把三种方案实际写出来跑过一遍之后才定下来最终的路线。方案原理优点缺点UnityWebRequestDownloadHandler等待服务器关闭连接后才拿完整数据接入简单少量代码不是真正的流式无法做到逐 token 展示HttpClientStreamReader底层分块读取 HTTP 响应体边读边解析真正的逐 token 处理支持取消、超时控制代码清晰需要自己管理线程注意跨线程回调裸TcpClient手写 HTTP 解析直接在 TCP 层解析报文最底层控制没有任何中间依赖开发量大HTTP/1.1 的 chunked 编码要自己处理容易出边界 bug我第一次验证流式效果时用的是HttpClient后来在真机上跑了好几个版本最终也一直用它。原因很简单UnityWebRequest压根没给流式读取的接口它在DownloadHandler层面把数据缓存在内存里只有在请求生命周期结束连接断开后才能拿到完整内容。有人说我用UnityWebRequest也能收到多次回调其实那是把DownloadHandlerBuffer与协程轮询配合的伪流式数据的实际到达时间和内容连续性与真正的流式有明显差异体验不够稳定。HttpClient方案里我搭配了StreamReader.ReadLineAsync()这可以在服务端每推送一 event 时立刻返回一行data:解析进度和生成进度是同步的这才是真正的流式。3.3 为什么 Unity 主线程限制让流式实现多了不少活Unity 的规则大家都知道引擎 API比如 UI 文本更新只能在主线程调用网络回调往往在子线程。用HttpClient发起异步请求后拿到数据回调时的线程上下文不一定是主线程所以不能直接在回调里改Text.text。我在工程里做了一个很轻量的MainThreadDispatcher工具类网络子线程拿到增量文本后把它塞进一个ConcurrentQueuestring主线程在Update()里统一取队列并刷新 UI。这个模式简单可靠也比UnityMainThreadDispatcher组件少一层依赖。4. 完整实现Unity 流式接入 DeepSeek 的实战代码4.1 数据模型定义我习惯在工程里单独建一个ChatModels.cs用来放请求和响应的数据结构。Unity 里可以直接用Newtonsoft.Json在 Package Manager 里搜com.unity.nuget.newtonsoft-json装官方移植版也可以直接用JsonUtility。不过流式场景里需要解析数据分段建议直接用JsonSerializer灵活、性能好后面会讲为什么要这样做。请求实体就是标准 OpenAI 格式[Serializable] public class ChatRequest { public string model deepseek-chat; public ListChatMessage messages new ListChatMessage(); public bool stream true; public float temperature 0.7f; } [Serializable] public class ChatMessage { public string role; public string content; public ChatMessage(string role, string content) { this.role role; this.content content; } }Stream 响应这边我只需要解析choices[0].delta.content用一个轻量的 DTO 接住就可以[Serializable] public class StreamChunk { public ListStreamChoice choices; } [Serializable] public class StreamChoice { public StreamDelta delta; } [Serializable] public class StreamDelta { public string content; }4.2 核心方法论HttpClient 流式请求封装为了让代码能直接在别人的项目里复用我写了一个不依赖 MonoBehaviour 的DeepSeekClient类。构造函数接收 API key并对外暴露三个事件OnChunkReceived每收到一个增量片段、OnComplete请求完成、OnError出错。public class DeepSeekClient { private readonly HttpClient _httpClient; private readonly string _apiKey; public event Actionstring OnChunkReceived; public event Actionstring OnComplete; // 传入完整拼接结果 public event Actionstring OnError; public DeepSeekClient(string apiKey) { _apiKey apiKey; _httpClient new HttpClient(); _httpClient.Timeout TimeSpan.FromSeconds(60); } public async Task StreamChatAsync(string userMessage, ListChatMessage history null) { var request new ChatRequest(); request.messages new ListChatMessage(); if (history ! null) request.messages.AddRange(history); request.messages.Add(new ChatMessage(user, userMessage)); var requestJson JsonConvert.SerializeObject(request); using var httpRequest new HttpRequestMessage(HttpMethod.Post, https://api.deepseek.com/chat/completions); httpRequest.Headers.Add(Authorization, $Bearer {_apiKey}); httpRequest.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(text/event-stream)); httpRequest.Content new StringContent(requestJson, Encoding.UTF8, application/json); using var response await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead); if (!response.IsSuccessStatusCode) { var errorBody await response.Content.ReadAsStringAsync(); OnError?.Invoke($HTTP {(int)response.StatusCode}: {errorBody}); return; } using var stream await response.Content.ReadAsStreamAsync(); using var reader new StreamReader(stream, Encoding.UTF8); var builder new StringBuilder(); string line; while ((line await reader.ReadLineAsync()) ! null) { if (string.IsNullOrWhiteSpace(line)) continue; if (!line.StartsWith(data:)) continue; var jsonData line.Substring(5).Trim(); if (jsonData [DONE]) { OnComplete?.Invoke(builder.ToString()); return; } var chunk JsonConvert.DeserializeObjectStreamChunk(jsonData); if (chunk?.choices ! null chunk.choices.Count 0) { var delta chunk.choices[0].delta; if (!string.IsNullOrEmpty(delta?.content)) { builder.Append(delta.content); OnChunkReceived?.Invoke(delta.content); } } } } }这段代码几个关键点解释一下HttpCompletionOption.ResponseHeadersRead是最关键的一行。它告诉HttpClient只等响应头到达就立即返回不要等整块 body 下载完。少了这个参数await会一直挂到流结束又回到非流式。StreamReader.ReadLineAsync()按行读取 SSE 的data:数据。每一行都是一个事件返回时机与服务端推送时机基本同步。用StringBuilder累积完整回答最终在[DONE]时一次性输出方便后续做 token 数统计或保存记录。4.3 增量 JSON 解析里的细节解析时有个很常见的现象SSE 的data:行几乎是瞬时分片的拿到的 JSON 字符串虽然每行是独立的但里面可能包含转义字符、超长中文字符串等。用JsonConvert.DeserializeObjectStreamChunk解析此段数据时理论上每一行都是完整 JSON实践中实测验证如此。不过我遇到过一种情况服务端在极端情况下可能会把一个事件的数据跨行拆开或者空行被意外挤压。出于稳妥我在解析层做了一个“当前缓冲 等待完整闭合”的防御private StringBuilder _lineBuffer new StringBuilder(); private StreamChunk TryParseChunk(string rawData) { _lineBuffer.Append(rawData); var text _lineBuffer.ToString(); // 简单判断包含完整的选择器结构才尝试解析 if (!text.Contains(\choices\)) return null; try { var chunk JsonConvert.DeserializeObjectStreamChunk(text); _lineBuffer.Clear(); return chunk; } catch (JsonException) { // 半包等待下一段拼进来 return null; } }这个缓冲逻辑意义在于宁可多等几毫秒绝不让解析异常导致整个流中断。此外要提防JsonConvert的默认日期解析在一些 Unity 版本里会对字符串内容做奇怪的时间转换遇到异常直接 catch 住就好。4.4 主线程回调工具我在场景里挂了一个MainThreadDispatcher单例作用是收集子线程推送的文本并排队public class MainThreadDispatcher : MonoBehaviour { public static MainThreadDispatcher Instance { get; private set; } private readonly ConcurrentQueuestring _chunkQueue new ConcurrentQueuestring(); private readonly ConcurrentQueuestring _completeQueue new ConcurrentQueuestring(); private readonly ConcurrentQueuestring _errorQueue new ConcurrentQueuestring(); public event Actionstring OnChunkReceived; public event Actionstring OnComplete; public event Actionstring OnError; private void Awake() { if (Instance null) Instance this; } public void EnqueueChunk(string chunk) _chunkQueue.Enqueue(chunk); public void EnqueueComplete(string result) _completeQueue.Enqueue(result); public void EnqueueError(string error) _errorQueue.Enqueue(error); private void Update() { while (_chunkQueue.TryDequeue(out var chunk)) OnChunkReceived?.Invoke(chunk); while (_completeQueue.TryDequeue(out var result)) OnComplete?.Invoke(result); while (_errorQueue.TryDequeue(out var error)) OnError?.Invoke(error); } }这样 Unity 主线程Update()里自动取出队列内容刷新 UI完全不触碰跨线程 API 的雷区。5. 实测表现从等待加载到逐字输出体验差距有多大5.1 实测数据我在同一台机器、同一段 prompt 下分别测了非流式和流式两种方式。测试 prompt 是让 DeepSeek 写一份 500 字左右的游戏剧情简介。测试配置就是上面那段代码Unity 2021.3Windows 10。非流式按下按钮后界面“请求中”状态维持了大约 6.2 秒然后整段文字一次性显示。流式按下按钮后大约 1.4 秒出现第一个字符后续每个片段间隔极短约 4.8 秒内全部展示完毕。注意两者总完成时间差别不大但是“首次响应时间”TTFTTime To First Token从 6.2 秒降到了 1.4 秒用户在 1 秒多后就看到文字开始跳动了主观等待感大幅缩小。这个测试告诉我们流式传输不是让 AI 生成得更快而是让“等待结果”的感觉更快结束本质上是把等待时间切碎并分摊到接收过程中。这个思路放在任何面向用户的内容生成场景里都成立。5.2 刷新频率与节流逐字刷新的情况下如果每收到一个 chunk 就直接改 UI 文本在某些低端安卓机上会出现 Text 重建频繁导致的卡顿。实测在 PC 上无感但在 Pico 4 这类安卓头显上就明显掉帧。我的处理是在MainThreadDispatcher里加一个累积字符串 刷新间隔控制private string _pendingChunk ; private float _lastFlushTime; private void Update() { while (_chunkQueue.TryDequeue(out var chunk)) { _pendingChunk chunk; } if (Time.time - _lastFlushTime 0.08f !string.IsNullOrEmpty(_pendingChunk)) { OnFlushedChunk?.Invoke(_pendingChunk); // 每 80ms 批量刷新一次 _pendingChunk ; _lastFlushTime Time.time; } }80ms 的刷新间隔肉眼几乎察觉不到但 UI 压力降低了十倍以上。5.3 成本控制从设计阶段就要想DeepSeek 的 API 是按 token 计费的流式返回时响应里的usage字段只在非流式模式出现。流式模式为了逐字快响并不会提前告知总 token 数。这意味着你如果完全依赖流式接口单次请求的成本统计会缺失——但可以通过拼接完整回答自己做字数估算或者用非流式的 usage 字段做对账。我实测过中文字数到 token 数的大致比例1 个汉字约等于 1.2~1.5 token和上下文窗口、特殊字符有关。游戏对话频繁的玩家一天可能发起上百次请求如果回答都很长这个成本不能忽略。体量小的时候我会在前端做“最大生成长度”限制把max_tokens设为 256~512 之间保证对话简短、成本可控。6. Unity 接入 DeepSeek 流式传输的常见坑完整排查链路6.1 坑一子线程异常不弹到主线程请求“莫名失败”我最早跑流式代码时在StreamChatAsync里忘了包 try-catch结果发出请求后 UI 上什么都没发生编辑器控制台也完全没日志。后来查了半天发现是HttpClient.SendAsync抛的异常发生在子线程中Unity 的主线程日志系统默认捕获不到。这种情况下的表现就是“点击按钮后石沉大海”非常劝退。我的做法是在入口处把整个流程包成 try-catch并把异常信息塞进OnError回调public async Task StreamChatAsync(string userMessage, ListChatMessage history null) { try { // 网络请求主流程 } catch (Exception ex) { OnError?.Invoke(ex.Message); } }只要养成这个习惯后面所有暗坑都能第一时间暴露在 UI 上而不是沉到编辑器日志深处。6.2 坑二中文乱码与编码不一致有一次在 Windows 编辑器下跑得挺正常打包到 Android 真机后返回的中文变成了 Unicode 转义形式的字符串看起来像\u4f60\u597d。排查下来是StreamReader没指定 UTF-8在某些 Android 运行时下默认编码行为不一致。解决办法很简单创建StreamReader时第二参数强制传Encoding.UTF8using var reader new StreamReader(stream, Encoding.UTF8);这算是个小细节但跨平台项目里这类问题最容易在最后阶段冒出来。6.3 坑三HttpClient 在部分 Android 机型上的 TLS 限制实测发现部分老旧 Android 设备系统 API level 可能偏低内置的 TLS 版本较低连 HTTPS 接口时可能出现 “The SSL connection could not be established” 之类的异常。如果你的目标设备包含比较老的安卓机建议在打包设置里启用可用的现代 TLS 支持或者干脆在接口层做应答失败后的二级降级方案改用一条兼容性更好的UnityWebRequest非流式调用作为兜底。流式失败是偶发场景时界面不应直接报错而是切到“请求中”状态然后一次性展示完整回答。6.4 坑四协程与 async/await 的混用冲突Unity 异步支持已经成熟了但老项目里大量存在那种用协程处理请求的代码yield return new WaitForSeconds()配UnityWebRequest.SendWebRequest()。把协程和async/await混在一个MonoBehaviour生命周期里会碰到一个典型问题协程无法直接await一个Task。我最初的方案是在协程里yield return new WaitUntil(() task.IsCompleted)看起来能跑但没法写超时控制而且 task 在IsCompleted前出现异常也不会自动抛出。最终我干脆把请求逻辑全部放在独立类里用StartCoroutine(DoRequest().AsCoroutine())的方式包装Task或者直接抛弃协程改用异步方法 状态机。项目越老越建议在这条路径上做干净彻底的替换。6.5 坑五连续快速发送请求导致回调错乱玩家短时间内连续点击发送或者同一会话多个并发请求时如果没有做请求 ID 隔离会出现“第二个问题的回答跑到第一个问题 UI 里”这种诡异体验。我加了一个简单的自增 requestId 参数在回调事件里带回去UI 层比对当前显示的是不是最新请求不是就直接丢弃public event Actionint, string OnChunkReceived; // requestId, delta // UI 层 private void HandleChunk(int requestId, string delta) { if (requestId ! _currentRequestId) return; _chatText.text delta; }这个坑在做多轮对话时非常值得提前防住。7. 工程化落地把流式对话做成正式功能的优化建议7.1 系统架构分层与替换扩展演示项目可以一个脚本写完但要做成正式功能我会把系统拆成三层接入层DeepSeekClient、业务层对话状态机 历史管理、表现层UI 刷新、音频朗读。接入层和业务层之间用接口隔离这样以后如果从 DeepSeek 切换到别的模型或者公司内部部署的私有模型只需要换一个IChatClient实现UI 完全不受影响。7.2 多轮对话的记忆管理每个会话请求必须把历史消息都带上去否则多轮对话会失忆。但历史消息无限增长会让 token 数快速膨胀成本也不可控。我的方式是维护一个“对话窗口”最近 8 轮消息全部携带超过之后把更早的消息压缩成一条摘要塞进system消息。摘要由 DeepSeek 自己生成用一个低成本的deepseek-chat调用完成。这样长对话既有上下文连续性又不会让单次请求的 token 数爆炸。7.3 超时、重试与离线兜底大模型服务虽然稳定但玩家网络环境千奇百怪必须做三重保护60 秒无响应整体超时、遇到网络错误自动重试一次指数退避、连续两次失败则走本地兜底回复比如“我这边网络有点卡请稍后再试”。这里的重点是兜底文案也要设计成符合角色设定的口吻而不是直接抛出技术错误码。游戏里的 AI 角色的“人设”不能因为一个异常就被击穿。7.4 本地部署的进阶方向如果项目对数据隐私要求很高或者大量调用导致 API 成本实在控不住还有一条路本地部署 DeepSeek 模型。基于 vLLM 加载量化后的轻量模型在内网提供兼容 OpenAI 格式的接口Unity 端代码可以原样复用。本地部署以后流式传输依然适用因为 vLLM 本身就支持 SSE 流式输出。不过硬件门槛和运维成本需要先估算清楚这里只做提醒不展开具体部署步骤。7.5 语音朗读与流式的配合游戏 NPC 对话如果配上语音流式还能自动解决“什么时候开始读”的问题——第一个 token 到达后就可以让语音播报开始收到的文字增量同步供语音合成使用。实现在本地 TTS 引擎上把一次性等待改成了边收边合成玩家的感官体验会再上一个台阶。我在一个 Demo 里试过这个玩法效果比“先显示完再朗读”要自然得多值得往这个方向投入。8. 一个容易被忽略的问题流式场景下的 token 统计与用量对账8.1 流式响应不回传 usage怎么对账非流式接口的响应体里会带一个usage字段包含prompt_tokens和completion_tokens方便做费用核算。但流式接口为了逐字快传响应体里每一条 chunk 里都没有 usage只有[DONE]之前也不会汇总统计。这意味着你如果全走流式平台后台能看到每个 key 的调用量和计费但你在业务层没法精确到具体某一次会话的 token 消耗。解决办法是前端自己做估算。我用的方法是拿到完整回答后做一次字符长度换算并在 UI 日志里记录每轮对话的估算 token 数。准确率虽不如官方 usage但做成本预警已经够用。注意这不影响计费本身——计费由服务端核算前端对账只是为了自己心里有数。8.2 控制 single-turn 生成长度DeepSeek API 的参数里max_tokens控制单次输出最大 token 数。你不设置时模型可能会按默认上限生成很长的回答。游戏场景里大部分对话响应不应该超过 200~300 token。我统一设置max_tokens 512在某些特定场景比如剧情生成按需单独调大。这个参数对成本和响应速度都影响显著别嫌麻烦每个场景都值得独立配置。9. 我最终落地时保留的架构与后续规划写完这一整套方案后我回头把代码重新整理了一遍最后落地的结构比最初演示版复杂一些但每一层都有清晰的职责ChatModels.cs所有请求/响应 DTO单纯数据结构。DeepSeekClient.cs只负责 HTTP 通信和事件回调不碰 Unity API。MainThreadDispatcher.cs负责线程调度和 UI 刷新节流。ChatSession.cs管理多轮对话历史、token 预算、请求 ID 冲突。UIChatPanel.cs负责可视化输出对接按钮、文本、语音播放。这套结构最大的好处是整体可以脱离 Unity 编辑器做单元测试。网络请求层的事我用了一个简单的 C# 控制台测试程序去验证流式行为确认无误后再挂回 Unity 场景。省下了大量在编辑器里反复点击调试的时间。后续我准备扩展的方向有两个。第一个是在流式输出期间支持“打断”玩家点一下聊天框当前请求立刻取消并保留已生成内容让 AI 根据新输入继续回答。这需要在HttpClient层传入CancellationTokenUnity 里用CancellationTokenSource管理。第二个是在 NPC 对话表现层做更精细的情绪状态机流式接收的同时根据内容关键词驱动角色的表情和动作切换让 AI 对话不只是“文字冒泡”而是真正的“角色演出”。Unity 接 DeepSeek 这件事技术上的门槛其实已经降得很低了难点集中在流式传输的正确选型、跨线程处理、以及工程化的边界控制。希望这篇记录能帮你少走一些弯路。这里额外分享一个我自己的习惯——写流式请求时先在命令行用curl调一遍原生 SSE 接口确认服务端响应正常后再动 Unity 代码。别小看这一步它能帮你把网络层问题和 Unity 层问题快速切分开。
返回列表