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.ReadValue<Vector2>();这种方式注册。
如果你要把这些配置和 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/api,Header 带Authorization: Bearer <你的Key>。注意 Model ID 要和你在控制台看到的一致,不要写错大小写。
4. 验证请求与成功结果:从 Unity 到 TaoToken 的完整链路
配置写完后,必须做一次端到端验证。验证分两层:第一层是 Unity 内部相机和光标行为是否正确,第二层是 TaoToken 请求是否返回预期结果。
先验证相机和光标。在场景里放一个 Cube 作为楼宇,挂上BillboardToCamera,把 Main Camera 拖进去。运行后按 WASD 移动,鼠标旋转视角,观察 Cube 上的标签是否始终正面朝向相机且没有倒置。然后按 Esc,鼠标应该解锁并显示,相机停止旋转;再点鼠标左键,鼠标重新锁定并隐藏,相机恢复旋转。如果标签在相机旋转时抖动,检查是不是用了Update而不是LateUpdate;如果鼠标解锁后相机还在转,检查mode判断是否生效。
再验证 TaoToken 请求。在 Unity 里写一个简单的TaoTokenClient:
using 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/api,Key 填控制台生成的 Key,Model 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/api,Key 用控制台生成的,Model ID 用文档里的,三件套写全再测。
实际项目里,我建议把 Cursor 模式切换和相机控制做成一个InteractionManager,统一管理 Browse、Pick、UI 三种状态,避免多个脚本各自改Cursor.lockState导致状态冲突。同时把 TaoToken 请求封装成异步方法,加超时和重试,避免网络波动卡住主线程。这样你的智慧城市原型就能从“能看”走到“能问、能答、能交互”。