Unity 笔记二:数字孪生智慧城市中 LookAt 与 Cursor 的协同配置到 TaoToken 1. 数字孪生智慧城市里相机 LookAt 与 Cursor 到底怎么配合做城市三维可视化项目时相机控制和光标交互往往是两个被分开处理的问题但它们在数字孪生场景里其实是一套联动系统。我先把场景说清楚你有一个智慧城市沙盘里面有楼宇、道路、管网、传感器点位用户需要既能像 RTS 一样自由浏览全局又能点击某栋楼弹出能耗面板还能一键把镜头拉近到某个路口做巡检。这时候相机不能只是简单跟随光标也不能只是显示或隐藏两者必须协同。核心检索词先摆出来Unity 数字孪生智慧城市中的 LookAt 与 Cursor 协同配置指的是用Transform.LookAt控制相机或标注物朝向目标点同时用Cursor.lockState和Cursor.visible管理鼠标状态让“自由浏览”和“精确拾取”两种模式平滑切换。它适合做城市三维可视化、园区数字孪生、交通态势大屏的开发者尤其是已经会用 Unity 基础操作、但一遇到相机翻转或光标乱跳就卡住的人。我试过在一个园区项目里最初把 LookAt 直接挂在相机上结果相机 Z 轴指向目标后整个画面倒过来了。原因在 excerpt 里也提到过LookAt默认让物体的 Z 轴前向轴指向目标而相机的 Z 轴是朝向屏幕外的所以直接 LookAt 会导致朝向与预期相反。解决办法不是不用 LookAt而是让物体朝向与摄像机朝向一致或者对相机使用“看向目标但保持 up 向量”的写法。另一个常见问题是 Cursor 状态和相机模式没有绑定。比如用户按 Esc 解锁鼠标去点 UI但相机还在用鼠标移动量旋转结果鼠标一移动视角就乱转。所以这篇会把 LookAt 的朝向修正、Cursor 的锁定/显示、以及新输入系统下的相机移动旋转串成一条可复制的配置链路最后再落到 TaoToken 的统一 Key/API 通道上做一次调用验证确保你的原型不仅能跑还能接上模型服务做智能问答或标注生成。2. TaoToken 前置统一 Key 与 API 通道准备在进入 Unity 配置之前先把 TaoToken 这一侧准备好。TaoToken 是一个面向开发者的模型调用统一入口你可以把它理解成一个“API 网关 Key 管理台”同一个 Key 可以走不同模型的对话、代码补全、Agent 任务不用在每个项目里散落一堆密钥。对于数字孪生智慧城市这种需要接大模型做语义查询、报告生成、点位描述的场景统一通道能省掉很多切换成本。你需要先拿到三件套Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api注意这个地址不带 UTM 参数直接作为请求根路径。API Key 在控制台创建建议按项目命名比如unity-digital-twin-dev方便后面轮换。Model ID 根据你要做的任务选做城市问答和标注生成可以用通用对话模型做代码辅助可以用 coding 类模型。具体动作打开https://taotoken.net/console进入控制台在 API Keys 页面点创建复制生成的 Key 并保存到本地环境变量不要硬编码进 Unity 脚本。然后打开https://taotoken.net/doc确认当前支持的模型列表和请求格式。如果你后面要做长期编码或 Agent 任务可以看https://taotoken.net/coding-plan了解套餐如果只是想先验证模型对话用https://taotoken.net/api-keys管理 Key 即可。这里给一个最小验证命令用 curl 确认 Key 和通道是通的curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: your-model-id, messages: [ {role: user, content: 用一句话描述数字孪生智慧城市中相机LookAt的作用} ] }如果返回里有choices字段和内容说明通道正常。注意不要把 Key 写进截图或公开仓库Unity 项目里建议用Environment.GetEnvironmentVariable读取或者放在不纳入版本管理的secrets.json里。这一步做完后面 Unity 里的 C# 请求才能复用同一套凭证。3. 可复制配置LookAt 朝向修正与 Cursor 状态机这一节直接给可复制的配置片段。先解决 LookAt 朝向问题。在智慧城市场景里你可能有多个需要朝向相机的对象比如楼宇标签、告警图标、巡检点标记。如果直接transform.LookAt(Camera.main.transform.position)标签的 Z 轴会指向相机但正面可能背对导致文字镜像或倒置。修正方式是让对象的前向与相机前向一致using UnityEngine; public class BillboardToCamera : MonoBehaviour { public Camera mainCamera; void LateUpdate() { if (mainCamera null) return; transform.LookAt(mainCamera.transform.position); transform.forward mainCamera.transform.forward; } }这段放在标签或图标上LateUpdate保证在相机移动之后执行避免抖动。注意transform.forward mainCamera.transform.forward这一行是关键它把对象的 Z 轴重新对齐到相机朝向解决倒向问题。接下来是 Cursor 状态机。数字孪生场景通常有三种模式浏览模式鼠标锁定相机旋转、拾取模式鼠标可见点击选择、UI 模式鼠标可见操作面板。用枚举管理比散落的 bool 更清晰public enum InteractionMode { Browse, Pick, UI } public class CursorModeController : MonoBehaviour { public InteractionMode currentMode InteractionMode.Browse; public void SetMode(InteractionMode mode) { currentMode mode; switch (mode) { case InteractionMode.Browse: Cursor.lockState CursorLockMode.Locked; Cursor.visible false; break; case InteractionMode.Pick: case InteractionMode.UI: Cursor.lockState CursorLockMode.None; Cursor.visible true; break; } } void Update() { if (Input.GetKeyDown(KeyCode.Escape)) { SetMode(InteractionMode.UI); } } }如果你用的是新输入系统把Input.GetKeyDown换成Keyboard.current.escapeKey.wasPressedThisFrame。这里要注意CursorLockMode.Locked会把鼠标锁到屏幕中心适合 FPS 式浏览CursorLockMode.Confined适合 RTS 式限制在窗口内。智慧城市大屏项目里如果用户需要拖拽地图建议用Confined而不是Locked。然后是相机控制器与 Cursor 的联动。下面这个CameraController整合了移动、旋转和模式切换参数用[Header]分组方便在 Inspector 里调using UnityEngine; using UnityEngine.InputSystem; public class CameraController : MonoBehaviour { [Header(移动设置)] public float moveSpeed 10f; [Header(旋转设置)] public float lookSpeed 2f; public float minRotation -85f; public float maxRotation 85f; [Header(模式)] public InteractionMode mode InteractionMode.Browse; private Vector2 moveInput; private Vector2 lookInput; private float yRotate 0f; private float xRotate 0f; void OnEnable() { Cursor.lockState CursorLockMode.Locked; Cursor.visible false; } void OnDisable() { Cursor.lockState CursorLockMode.None; Cursor.visible true; } void Update() { if (Keyboard.current.escapeKey.wasPressedThisFrame) { mode InteractionMode.UI; Cursor.lockState CursorLockMode.None; Cursor.visible true; } if (Mouse.current.leftButton.wasPressedThisFrame mode InteractionMode.UI) { mode InteractionMode.Browse; Cursor.lockState CursorLockMode.Locked; Cursor.visible false; } if (mode InteractionMode.Browse) { Move(); Look(); } } private void Move() { Vector3 forward transform.forward; Vector3 right transform.right; Vector3 moveDirection (forward * moveInput.y right * moveInput.x).normalized; transform.position moveDirection * moveSpeed * Time.deltaTime; } private void Look() { yRotate lookInput.x * lookSpeed; xRotate - lookInput.y * lookSpeed; xRotate Mathf.Clamp(xRotate, minRotation, maxRotation); transform.rotation Quaternion.Euler(xRotate, yRotate, 0f); } }注意minRotation和maxRotation我设成 -85 到 85而不是 -90 到 90这是为了避免万向节死锁。excerpt 里提到 -90 到 90 也可以但实际项目里 85 更稳。另外moveInput和lookInput需要通过 Input Action 事件赋值如果你还没建.inputactions资源可以在OnEnable里用controls.Camera.Move.performed ctx moveInput ctx.ReadValueVector2();这种方式注册。如果你要把这些配置和 TaoToken 的模型调用串起来可以在拾取模式下点击楼宇后把楼宇 ID 发给模型做描述生成。请求体用 JSON路径和字段保持和文档一致{ model: your-model-id, messages: [ {role: system, content: 你是智慧城市数字孪生助手根据楼宇ID生成简短描述。}, {role: user, content: 楼宇ID: B-1024, 类型: 办公楼, 能耗等级: A} ], temperature: 0.3 }这个 JSON 可以直接放进 Unity 的UnityWebRequest里Base URL 用https://taotoken.net/apiHeader 带Authorization: Bearer 你的Key。注意 Model ID 要和你在控制台看到的一致不要写错大小写。4. 验证请求与成功结果从 Unity 到 TaoToken 的完整链路配置写完后必须做一次端到端验证。验证分两层第一层是 Unity 内部相机和光标行为是否正确第二层是 TaoToken 请求是否返回预期结果。先验证相机和光标。在场景里放一个 Cube 作为楼宇挂上BillboardToCamera把 Main Camera 拖进去。运行后按 WASD 移动鼠标旋转视角观察 Cube 上的标签是否始终正面朝向相机且没有倒置。然后按 Esc鼠标应该解锁并显示相机停止旋转再点鼠标左键鼠标重新锁定并隐藏相机恢复旋转。如果标签在相机旋转时抖动检查是不是用了Update而不是LateUpdate如果鼠标解锁后相机还在转检查mode判断是否生效。再验证 TaoToken 请求。在 Unity 里写一个简单的TaoTokenClientusing UnityEngine; using UnityEngine.Networking; using System.Collections; using System.Text; public class TaoTokenClient : MonoBehaviour { private string apiKey; private string baseUrl https://taotoken.net/api; private string modelId your-model-id; void Start() { apiKey System.Environment.GetEnvironmentVariable(TAOTOKEN_API_KEY); StartCoroutine(SendRequest(用一句话描述数字孪生智慧城市中相机LookAt的作用)); } IEnumerator SendRequest(string prompt) { string json {\model\:\ modelId \,\messages\:[{\role\:\user\,\content\:\ prompt \}]}; byte[] body Encoding.UTF8.GetBytes(json); UnityWebRequest request new UnityWebRequest(baseUrl /v1/chat/completions, POST); request.uploadHandler new UploadHandlerRaw(body); 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.Log(TaoToken 返回: request.downloadHandler.text); } else { Debug.LogError(请求失败: request.error 响应: request.downloadHandler.text); } } }运行后看 Console。成功时你会看到类似{choices:[{message:{content:...}}]}的返回说明 Key、Base URL、Model ID 三件套都对。如果返回 401检查 Key 是否复制完整、有没有多余空格如果返回local proxy failed检查网络是否能直连taotoken.net如果返回里没有choices检查 Model ID 是否在文档列表里。验证通过后你可以把拾取到的楼宇信息拼进 prompt让模型生成描述并显示在 UI 面板上。这样数字孪生场景就不只是静态展示而是能根据用户点击动态生成语义信息。如果你要做更复杂的 Agent 任务比如自动巡检报告可以走https://taotoken.net/coding-plan了解长期方案如果只是验证模型对话用https://taotoken.net/api-keys管理 Key 就够了。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来排。第一个高频错误是 401 Unauthorized。在 Unity 里通常表现为request.error返回HTTP/1.1 401 Unauthorized响应体里可能有invalid api key。原因一般是 Key 没读到、Key 过期、或者 Header 拼写错误。检查Authorization是不是Bearer加空格再加 Key检查环境变量是否在 Unity 启动前设置好。如果你在 Editor 里改了环境变量需要重启 Unity 才能读到。第二个是local proxy failed。这个报错通常出现在请求根本没到达 TaoToken 服务端而是被本地网络层拦住了。检查你的 Base URL 是不是写成了https://taotoken.net/api不要多加斜杠或路径。检查系统代理设置是否干扰了 Unity 的UnityWebRequest。如果你在公司网络里确认防火墙允许对taotoken.net的 HTTPS 出站。这个错误和 Key 无关先排网络再排凭证。第三个是reading choices相关错误比如Cannot read property choices of undefined或 C# 里反序列化后choices为 null。这通常说明返回的 JSON 结构和你预期的不一样。可能原因Model ID 写错导致服务端返回错误对象而不是正常响应请求体里messages格式不对比如 role 写成了user以外的值或者temperature传了字符串而不是数字。建议先把request.downloadHandler.text完整打印出来看服务端到底返回了什么。第四个是 OAuth 相关报错。如果你在配置 Claude Code 或类似工具时看到 OAuth 失败注意 TaoToken 的 API 通道用的是 Bearer Key不是 OAuth 流程。如果你在settings.json或auth.json里配置确保字段是apiKey或api_key而不是oauthToken。对于 Claude Code 类工具Base URL 填https://taotoken.net/apiKey 填控制台生成的 KeyModel ID 填文档里的模型名。三件套缺一不可只填 Base URL 不填 Key 会直接 401。还有一个容易忽略的问题Cursor 锁定后 UI 按钮点不到。这是因为CursorLockMode.Locked把鼠标锁在屏幕中心UI 射线检测不到。解决办法是在打开 UI 面板前调用SetMode(InteractionMode.UI)把lockState设为None并visible true。关闭面板后再切回Browse。如果你用CursorLockMode.Confined鼠标可以在窗口内移动但不会移出窗口适合需要拖拽但又不想完全锁定的场景。最后检查 LookAt 的坐标系。Unity 是左手坐标系Z 轴向前Y 轴向上。LookAt会让 Z 轴指向目标但如果你对象的模型本身朝向是反的就需要额外旋转 180 度。可以在BillboardToCamera里加一个offsetRotation参数在LateUpdate最后乘上去transform.rotation * Quaternion.Euler(0, 180f, 0);这样即使模型导入时朝向不对也能通过配置修正不用改模型文件。6. 继续接入用 TaoToken 做智慧城市语义交互相机和光标配好之后下一步是让场景“会说话”。数字孪生智慧城市的价值不只是看而是能问。比如用户点击一栋楼场景把楼宇 ID、类型、能耗等级发给 TaoToken模型返回一段描述或建议显示在侧边面板。这个链路复用你前面验证过的TaoTokenClient只需要把 prompt 换成动态拼接的楼宇信息。如果你要做代码辅助比如自动生成相机控制脚本或输入配置可以用https://taotoken.net/coding-plan了解长期编码方案。如果只是偶尔调用模型对话做验证用https://taotoken.net/api-keys管理 Key配合https://taotoken.net/doc查请求格式就够了。Claude Code 类工具接入时Base URL 用https://taotoken.net/apiKey 用控制台生成的Model ID 用文档里的三件套写全再测。实际项目里我建议把 Cursor 模式切换和相机控制做成一个InteractionManager统一管理 Browse、Pick、UI 三种状态避免多个脚本各自改Cursor.lockState导致状态冲突。同时把 TaoToken 请求封装成异步方法加超时和重试避免网络波动卡住主线程。这样你的智慧城市原型就能从“能看”走到“能问、能答、能交互”。