1. Unity 场景里 NPC 头顶标牌总是侧脸先看清问题在哪做 3D 场景交互时NPC 头顶挂一个文字提示标牌是很常见的需求任务指引、角色名字、状态气泡都靠它。但真正动手做过的开发者大概率都遇到过同一个尴尬——摄像机一转动标牌就变成一条线或者干脆背对着你文字完全看不清。这个问题的本质是标牌默认使用世界坐标朝向而摄像机是在动的两者没有建立约束关系。解决思路其实不复杂核心就两件事。第一让标牌每帧朝向摄像机这一步用Transform.LookAt就能搞定它会把物体的 forward 轴指向目标点。第二从标牌或 NPC 身上拉一条动态连线指向某个目标位置比如地面上的任务点、另一个角色、或者场景里的交互物件这一步用 DynamicLine 这类画线组件来实现。两者组合起来就是一个能跟随视角、又能动态指向的提示标牌系统。这篇文章面向的是正在做 Unity 3D 交互场景的开发者不管你是做 RPG 任务系统、展厅导览、还是工业数字孪生里的设备标注这套方案都能直接套用。我会从标牌朝向脚本写起到 DynamicLine 的连线配置再到运行验证和常见报错排查每一步都给可复制的代码和参数。你不需要有很深的图形学基础跟着做就能在场景里跑起来。需要说明的是标牌朝向和连线绘制都是纯客户端渲染逻辑不涉及任何网络请求。但如果你后续想把标牌内容做成动态的——比如从大模型接口拉取 NPC 对话文本再显示到标牌上——那就需要一个稳定的 API 接入层。这部分我会在第二节说明怎么用 TaoToken 把模型调用接进来让标牌文字真正“活”起来。2. 接入前的准备TaoToken 与 Unity 项目的对接位置在写朝向脚本之前先把“标牌文字从哪来”这件事理清楚。如果你的标牌只是静态文字那直接挂 TextMeshPro 就行不需要任何外部服务。但很多实际项目里NPC 的提示内容是需要动态生成的——比如根据玩家行为返回不同的引导语或者把语音转文字的结果显示在头顶。这时候就需要在 Unity 里发起 HTTP 请求调用模型接口。TaoToken 在这里的角色是一个模型调用接入层它提供统一的 API 地址和 Key 管理你不需要在 Unity 工程里硬编码多家厂商的地址。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 。注意 API 地址后面不加 UTM 参数直接用于代码里的 Base URL。你需要先在控制台创建一个 API Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsole 登录后进入 API Keys 页面新建一个 Key 并复制保存。这个 Key 只会完整显示一次丢了就得重建。拿到 Key 之后在 Unity 里可以用UnityWebRequest发起请求把返回的文本塞给 TextMeshPro 组件。模型选择上如果你只是做短文本生成选一个响应快的对话模型即可如果要做长文本润色或结构化输出可以选能力更强的版本。具体模型 ID 在文档里能查到 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdoc 。调用格式是标准的 OpenAI 兼容接口/v1/chat/completions请求体里带model、messages两个核心字段。这里要提醒一点Unity 的UnityWebRequest在移动端和 WebGL 平台对 HTTPS 证书的处理不一样WebGL 平台需要确保服务器 CORS 配置正确。TaoToken 的接口是标准 HTTPSPC 和移动端直接调没问题。如果你在 Editor 里测试时报证书错误检查一下系统时间是否正确以及是否用了自签名证书拦截。把 Key 和 Base URL 准备好之后就可以进入下一步——写标牌朝向脚本了。标牌文字的动态获取逻辑可以后面再加先把朝向和连线跑通视觉上能看到效果再往里填内容。3. 可复制配置Transform.LookAt 朝向脚本与 DynamicLine 连线参数这一节是核心操作部分我会给出完整的 C# 脚本和 DynamicLine 的配置参数。你可以在 Unity 里新建一个脚本文件BillboardLookAt.cs挂在标牌的根节点上。3.1 标牌朝向脚本 BillboardLookAt.csusing UnityEngine; public class BillboardLookAt : MonoBehaviour { [Header(朝向目标)] public Transform lookAtTarget; // 通常赋值为 Camera.main.transform public bool lockYAxis true; // 锁定Y轴避免标牌上下翻转 public bool reverseForward false; // 如果标牌背面朝向摄像机勾选此项 [Header(平滑参数)] public float rotationSpeed 10f; // 0表示瞬间朝向大于0为插值速度 private Transform _cam; void Start() { if (lookAtTarget null Camera.main ! null) { _cam Camera.main.transform; lookAtTarget _cam; } } void LateUpdate() { if (lookAtTarget null) return; Vector3 targetPos lookAtTarget.position; if (lockYAxis) { // 保持标牌自身Y轴不变只旋转水平方向 targetPos.y transform.position.y; } Vector3 dir targetPos - transform.position; if (dir.sqrMagnitude 0.0001f) return; Quaternion targetRot Quaternion.LookRotation( reverseForward ? dir : -dir, Vector3.up ); if (rotationSpeed 0f) { transform.rotation targetRot; } else { transform.rotation Quaternion.Slerp( transform.rotation, targetRot, Time.deltaTime * rotationSpeed ); } } }这段脚本的关键点有三个。第一用LateUpdate而不是Update因为摄像机移动通常在Update里完成LateUpdate能保证在摄像机位置确定后再计算朝向避免一帧延迟导致的抖动。第二lockYAxis参数控制是否锁定 Y 轴对于头顶标牌通常要锁定否则摄像機俯视时标牌会跟着倾斜文字看起来很难受。第三reverseForward用来处理标牌正面朝向问题不同建模工具导出的标牌正面方向可能不一致勾选后取反方向即可。把脚本挂到标牌根节点后在 Inspector 里把lookAtTarget拖入 Main Camera或者留空让脚本自动找Camera.main。运行后转动摄像机标牌应该始终正面朝向你。3.2 DynamicLine 连线配置DynamicLine 的用法是一个 Line 对应一个根节点把根节点放到合适位置然后通过脚本属性关联起点和终点。在 Unity 里新建一个空物体命名为LineRoot挂上 DynamicLine 组件或你项目里对应的画线脚本。关键参数如下参数名建议值说明StartPointNPC 头顶锚点 Transform连线起点EndPoint目标点 Transform连线终点LineColor根据状态变化任务中/已完成用不同颜色LineWidth0.02–0.05世界空间下的线宽UseWorldSpacetrue保证线在世界坐标中绘制LookAtPoint空物体 Transform用于测试朝向的参考点配置步骤在场景里建一个空物体LookAtPoint把它拖给 DynamicLine 脚本的LookAtPoint属性。然后移动这个空物体观察连线的朝向变化。这一步是验证连线逻辑是否正常的关键——如果移动空物体时连线跟着转说明朝向约束生效了。3.3 标牌与连线的关联把标牌根节点和 LineRoot 放在同一个父物体下或者用脚本在运行时建立引用。推荐的做法是在 NPC 根节点下建两个子物体Billboard挂 BillboardLookAt 和 TextMeshPro和Line挂 DynamicLine。这样 NPC 移动时标牌和连线一起跟着走不需要额外同步位置。如果你需要标牌文字动态更新可以在 BillboardLookAt 脚本里加一个SetText(string content)方法内部调用 TextMeshPro 的SetText。文字内容从模型接口获取后通过主线程调度器切回 Unity 主线程再赋值避免跨线程访问 UI 组件报错。4. 运行验证从请求到标牌显示的成功结果确认配置完成后按以下步骤验证整套系统是否正常工作。第一步在 Editor 里点击 Play。观察 Scene 视图和 Game 视图标牌应该正面朝向摄像机。用鼠标右键旋转视角标牌应该跟着转始终保持文字可读。如果标牌侧翻或倒置检查lockYAxis是否勾选以及标牌的初始旋转是否为零。第二步移动LookAtPoint空物体。在 Hierarchy 里选中它用移动工具拖拽观察 DynamicLine 绘制的连线是否从 NPC 头顶指向该点。如果连线不显示检查 DynamicLine 组件的StartPoint和EndPoint是否都已赋值材质是否丢失。第三步测试动态文字。在脚本里临时写一个协程调用 TaoToken 接口获取一段文本然后赋给标牌。请求代码示例如下using UnityEngine; using UnityEngine.Networking; using System.Collections; using System.Text; public class BillboardTextFetcher : MonoBehaviour { public BillboardLookAt billboard; private string apiKey 你的API Key; private string apiUrl https://taotoken.net/api/v1/chat/completions; [System.Serializable] public class ChatMessage { public string role; public string content; } [System.Serializable] public class ChatRequest { public string model; public ChatMessage[] messages; } [System.Serializable] public class ChatChoice { public ChatMessage message; } [System.Serializable] public class ChatResponse { public ChatChoice[] choices; } public void FetchTip(string prompt) { StartCoroutine(PostRequest(prompt)); } IEnumerator PostRequest(string prompt) { var reqBody new ChatRequest { model gpt-4o-mini, messages new ChatMessage[] { new ChatMessage { role user, content prompt } } }; string json JsonUtility.ToJson(reqBody); byte[] bodyRaw Encoding.UTF8.GetBytes(json); using (UnityWebRequest req new UnityWebRequest(apiUrl, POST)) { req.uploadHandler new UploadHandlerRaw(bodyRaw); req.downloadHandler new DownloadHandlerBuffer(); req.SetRequestHeader(Content-Type, application/json); req.SetRequestHeader(Authorization, Bearer apiKey); yield return req.SendWebRequest(); if (req.result UnityWebRequest.Result.Success) { var resp JsonUtility.FromJsonChatResponse(req.downloadHandler.text); if (resp ! null resp.choices ! null resp.choices.Length 0) { string content resp.choices[0].message.content; billboard.GetComponentTextMeshPro().SetText(content); } } else { Debug.LogError(请求失败: req.error | req.downloadHandler.text); } } } }注意model字段的值要换成你实际使用的模型 ID具体可查文档。运行后如果标牌文字成功更新为接口返回的内容说明整条链路打通了。如果返回 401检查 Key 是否正确、是否带了Bearer前缀。如果返回 404检查 URL 是否拼写正确/v1/chat/completions不能少。第四步在移动端或 WebGL 平台构建测试。移动端注意把 API Key 放在安全位置不要硬编码在客户端可反编译的脚本里。生产环境建议通过自己的后端中转客户端只调自己的服务。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错实际接入过程中报错信息往往比代码本身更让人头疼。下面列出几个高频错误和对应的排查方向。401 Unauthorized最常见的原因是 Key 错误或缺失。检查Authorization头是否写成Bearer sk-xxxx注意Bearer和 Key 之间有一个空格。另外确认 Key 没有过期或被删除。如果你在 Unity 里用了环境变量读取 Key检查打包后环境变量是否丢失。local proxy failed / connection refused这个报错通常出现在你本地配了代理工具的情况下。Unity 的UnityWebRequest默认会走系统代理如果代理配置有问题就会连接失败。解决办法是在代码里显式设置req.proxy null或者检查系统代理设置是否指向了一个不可用的端口。注意这里说的是本地开发环境的网络配置问题不涉及任何跨境网络操作。reading choices 报错 / NullReferenceException这个错误说明接口返回了数据但choices数组为空或结构不匹配。常见原因有三个一是模型 ID 写错了接口返回了错误信息而不是正常响应二是请求体 JSON 格式不对比如messages不是数组三是用了JsonUtility解析但字段名和实际返回不一致。建议先把req.downloadHandler.text打印出来看原始返回内容再对照调整解析类。OAuth 相关报错如果你用的是需要 OAuth 授权的接口报错通常提示 token 无效或 scope 不足。TaoToken 的 API Key 方式是直接 Bearer Token不涉及 OAuth 流程。如果你在代码里混用了 OAuth 的 token 获取逻辑去掉即可。确认你用的是 API Keys 页面生成的 Key而不是其他类型的凭证。标牌朝向抖动如果标牌在摄像机移动时抖动检查是否在Update里改了摄像机位置而在LateUpdate里算朝向。另外rotationSpeed设得太高也会导致插值抖动适当降低到 5–10 之间。连线不显示DynamicLine 的材质丢失是常见原因。检查 LineRenderer 的 Material 是否赋值或者 DynamicLine 脚本是否在Start里自动创建了材质。如果线宽为 0 也看不到把LineWidth设为 0.02 以上。排查时的一个通用技巧在 Unity Console 里把报错信息完整复制出来重点看 HTTP 状态码和返回体里的error.message字段。大部分问题都能从这两处定位到。6. 后续扩展与接入入口标牌朝向和连线跑通之后你可以继续扩展几个方向。一是给标牌加淡入淡出动画用 CanvasGroup 的 alpha 做插值靠近时显示、远离时隐藏。二是根据距离动态调整标牌大小用Vector3.Distance计算摄像机到 NPC 的距离映射到localScale。三是把连线做成曲线用贝塞尔曲线替代直线视觉上更柔和。如果你要把标牌文字做成动态生成需要长期稳定地调用模型接口可以了解 Coding Plan 的额度方案 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-plan 。对于需要频繁调试模型返回格式的场景可以先用模型对话页面快速验证 prompt 效果 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchat 。API Key 管理入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keys 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdoc 。回到 Unity 本身这套标牌系统的核心就是两个约束朝向约束用Transform.LookAt或Quaternion.LookRotation实现位置约束用 DynamicLine 的起终点引用实现。把这两个约束拆开调试先确保朝向正确再确保连线正确最后把文字内容接进来。每一步都单独验证比一次性全搭好再排查要快得多。