控制項(玩家可見標誌——禁止反向出貨)
在為任何使用 WASD、方向鍵、追蹤攝影機或飛行器的遊戲編寫移動/轉向/飛行代碼之前,請完整閱讀此文件。請 不要 跳過此部分,只打開 racing-kart 或 fps——這些類型檔案假定你已經了解這些標誌。反向 A/D 是載具和飛行示範中最常見的出貨阻礙。
與 building-games(循環、攝影機、3D 方向)和 building-games/references/input.md(按鍵按下狀態、遊戲手把、觸控)配對使用。此技能負責 左/右/上對玩家意味著什麼 以及 你如何證明它。
0. 硬性規則(若違反則建置失敗)
- 玩家可見的左/右是鐵律。 從 追蹤 / 後方 攝影機觀看,且載具 向前 移動時:
- A / ← → 機頭(或傾斜)在畫面上 向左轉
- D / → → 機頭(或傾斜)在畫面上 向右轉
- 絕不要將 FPS 橫向移動重複用作載具轉向。 FPS 的 "D → +右向量" 是在地面平面上的 位置 移動。載具的 A/D 是 偏航(或滾轉)速率,而不是橫向偏移。混合它們是反向 A/D 的第一大原因。
- 在聲稱完成之前,你必須執行控制自我測試(§5)。 僅截圖的 QA 是不夠的。如果 A 向右轉,翻轉轉向/滾轉符號,重新測試,然後出貨——不要發明新的座標故事。
1. 共享的 3D 基礎(此處定義通用於所有情況)
three.js:右手系,+Y 向上,網格面向 +Z,攝影機看向 −Z。
僅偏航基準方向在 XZ 平面上(地面載具、步行者、大多數街機載具):
// yaw = 0 朝向世界 −Z;+yaw 是繞 +Y 逆時針(機頭朝 −X 移動)
forward = (-sin(yaw), 0, -cos(yaw))
right = ( cos(yaw), 0, -sin(yaw)) // = normalize(cross(forward, worldUp))
使用 在後方 的追蹤攝影機(攝影機接近 position - forward * dist):
| 玩家所見 | 世界(此基準) | 輸入 |
|---|---|---|
| 機頭向左 | +yaw | A / ← 必須產生 +yaw(或對飛機而言,等效的向左傾斜) |
| 機頭向右 | −yaw | D / → 必須產生 −yaw |
如果你的基準不同,請保持 一組 一致——但上方的 玩家可見 行是強制的。
2. 類型映射
2a. FPS / 步行(橫向移動,非轉向)
W = +前進, S = −前進, D = +右, A = −右 // 位置,非偏航
滑鼠:yaw -= movementX * sens; pitch -= movementY * sens; 限制 pitch
身體偏航用於觀看;移動僅使用偏航(不要對行走套用 pitch)。
2b. 地面 / 水面載具(卡丁車、腳踏車、水上摩托車、船、坦克、探測車、雪地摩托車)
使用 heading/yaw 和前進 speed 的街機式主體:
// 輸入(按住按鍵 → 每幀動作)
let steer = 0; // -1..+1,玩家可見
if (keys.has('KeyA') || keys.has('ArrowLeft')) steer += 1; // 左
if (keys.has('KeyD') || keys.has('ArrowRight')) steer -= 1; // 右
// 可選:steer = clamp(steer + gamepadX, -1, 1) 使用相同符號慣例
// 積分(speedFactor ~ 0..1 來自 |speed|)
const reverse = speed >= 0 ? 1 : -1; // 倒車時左轉仍然感覺向左
yaw += steer * turnRate * speedFactor * reverse * dt;
// 沿著航向移動
const fx = -Math.sin(yaw), fz = -Math.cos(yaw);
position.x += fx * speed * dt;
position.z += fz * speed * dt;
經典錯誤(不要複製):
// 錯誤——這就是在現實中導致 A/D 反向出貨的原因
if (KeyA) steer -= 1;
if (KeyD) steer += 1;
yaw += steer * turnRate * dt; // A → −yaw → 在追蹤攝影機下機頭向右
如果你已經寫了 KeyA → steer--,請 交換按鍵映射 或 在積分時取反一次(yaw += -steer * …)——然後執行 §5。不要翻轉兩次(按鍵 + 積分 + 傾斜網格)。
2c. 固定翼飛行(飛機、滑翔機、遙控飛機)
| 輸入 | 動作 | 玩家預期 |
|---|---|---|
| A / ← | 向左滾轉(副翼) | 左翼向下 / 向左傾斜 |
| D / → | 向右滾轉 | 右翼向下 / 向右傾斜 |
| W / ↑ | 俯仰(選擇一種方案並在 HUD 上標註) | 通常是機頭向下 或 拉起——保持一致 |
| S / ↓ | 反向俯仰 | |
| Q / E | 偏航 / 方向舵(可選) | Q 左,E 右 |
- A/D 不是橫向移動,也不是 "帶有 FPS 符號的地面轉向"。
- 在載具的 局部前進軸 上套用滾轉,其符號需與 按 A 向左傾斜 匹配。如果網格傾斜方向錯誤,請翻轉滾轉套用上的 一個 符號(或 A/D 到滾轉的映射),而不是整個基準。
- 協調轉彎:正滾轉應產生一個在追蹤/外部攝影機下與傾斜方向匹配的轉彎。
2d. 直升機 / 無人機 / 6DOF
在開始覆蓋層上記錄方案。至少:
- 離散按鍵上的油門/高度 不能 在按一次後 "始終向上" 黏住(使用按住狀態或明確的升/降)。
- 橫向移動/偏航:在載具的水平框架(玩家可見)中,A 左,D 右。
2e. 2D 橫向捲軸 / 平台遊戲
D / → 在畫面上 向右 移動;A / ← 在畫面上 向左 移動。重力僅在類型明確為翻轉時才反向。
3. 攝影機必須一致
- 追蹤攝影機:
desired = craftPos + up*height + forward*(-followDist);插值;lookAt(craft)。 - 為移動和攝影機 一次性 計算
forward——不要在攝影機路徑中使用相反的偏航符號重新建構。 - 除錯順序:(1)按鍵註冊 →(2)符號正確(§5)→(3)攝影機一致。
4. 輸入管道(簡要說明)
- 使用
keydown/keyup事件上的Set並使用event.code來追蹤按鍵;在blur/visibilitychange時清除。在遊戲循環中使用 dt 移動,而不是在按鍵處理程式中移動。 - 將鍵盤 + 觸控 + 遊戲手把統一為 動作(
throttle、steer、pitch、roll……)。請參閱building-games/references/input.md。 - 觸控:左搖桿 = 移動/轉向,右搖桿 = 動作;目標區域 ≥44px。
5. 強制控制自我測試(在 "完成" 之前)
僅截圖 不 足以應對任何帶有 A/D 的載具。
5a. 玩家可見檢查清單(每個載具 / 飛行建置)
在 向前移動(速度 > 小 epsilon)時,追蹤攝影機在後:
| 按住 | 必須在 ~0.5 秒內觀察到 |
|---|---|
| A | 機頭或傾斜在畫面上 向左 移動 |
| D | 機頭或傾斜在畫面上 向右 移動 |
| W(地面) | 速度增加 / 沿面向方向移動 |
| S(地面) | 煞車或倒車(按設計) |
如果 A 失敗:翻轉轉向/滾轉符號 一次,重新測試 A 和 D。
5b. 最小測試介面(實作此介面)
公開一個小型鉤子,以便你(以及自動化 QA)可以在不猜測私有閉包的情況下證明符號:
// 例如 src/game/controlsTest.ts —— 僅開發/QA 使用即可
export type ControlsProbe = {
getYaw: () => number; // 弧度;或 getHeading()
getSpeed: () => number;
/** 注入按住狀態的動作而不是真實按鍵;兩者都保持套用直到你
* 更改它們,所以 §5c 可以跨影格按住一個按鍵並在結束時清除。 */
setSteer?: (v: number) => void; // -1..1,與生產環境相同的符號
setKeys?: (codes: string[]) => void; // 按住直到下次呼叫;`[]` 清除
};
declare global {
interface Window {
__controlsTest?: ControlsProbe;
}
}
當 import.meta.env.DEV 或設定了 ?qa=1 標記時,從遊戲循環中連接 window.__controlsTest。
5c. 自動化冒煙測試(執行它)
使用預先安裝的 agent-browser CLI 驅動 §5b 的探針——這是第一步,而不是手寫腳本。一個擲出的 eval 會以非零退出;一個僅為 falsy 的不會,因此請透過擲出錯誤來斷言。作為一個 batch 執行——一次 CLI 呼叫,而不是每個動詞一次——從 stdin 接收 JSON;--bail 在第一個失敗步驟時停止並以非零退出。
agent-browser batch --bail <<'JSON' # find 的標籤區分大小寫:從 `snapshot -i` 複製
[["open","http://127.0.0.1:8080/"],
["find","text","Start","click"],
["eval","if (!window.__controlsTest?.setKeys) throw Error('no §5b probe: add setKeys')"],
["eval","__controlsTest.setKeys(['KeyW'])"],
["wait","600"],
["eval","if (__controlsTest.getSpeed() <= 0.1) throw Error('W: no move')"],
["eval","(async () => { const t = __controlsTest, y0 = t.getYaw(); t.setKeys(['KeyW','KeyA']); await new Promise(r => setTimeout(r, 300)); t.setKeys(['KeyW']); const d = t.getYaw() - y0, w = Math.atan2(Math.sin(d), Math.cos(d)); if (w < -0.05) throw Error('A turns RIGHT — inverted, got ' + w.toFixed(2)); if (w <= 0.05) throw Error('A barely turned (' + w.toFixed(2) + ') — hold longer or check the sim is running'); return 'A ok' })()"],
["eval","__controlsTest.setKeys([])"],
["screenshot","/workspace/screenshots/controls.png"],
["close"]]
JSON
透過探針按住按鍵,而不是使用 keydown —— agent-browser keydown 不會到達 §4 的 Set,因此一個正確的遊戲會被讀取為損壞。你的探針上沒有 setKeys?添加它(§5b),或者自己分派按鍵事件——瀏覽器-QA 參考資料的 Keys 註釋說明了機制和形式。
按住狀態位於 一個 eval 內部:分布在多個指令中時,它持續的時間就是它們花費的時間,並且 Math.atan2(Math.sin(d), Math.cos(d)) 中的 ±π 包裝會將轉彎超過 π 的載具讀取為向另一個方向轉彎。IIFE 是使步驟可重新執行的原因——一個裸的 const d = … 在第二次執行時會因為 "已經聲明" 而失敗——而 async 是讓按住狀態在頁面時間中運行的原因。對 D 重複,使用 'KeyD' 並鏡像兩個比較(w > 0.05 是反向,w >= -0.05 是幾乎沒有),使用單引號以便 JSON 不需要轉義;對於飛機,斷言 roll:A ⇒ 向左傾斜。
A turns RIGHT 是符號錯誤:翻轉 一個 符號並重新執行。
A barely turned 不是——載具以正確的方式移動,只是在 300 毫秒內少於 0.05 弧度,這在船隻或重型探測車上會發生;增加按住時間,或檢查模擬是否正在運行,並且不要翻轉任何東西。任何其他非零退出——沒有探針,守護行程卡住——意味著檢查從未運行:首先閱讀訊息。在第一次流程之前閱讀瀏覽器-QA 參考資料的 AGENTS.md 連結——動詞、參數形狀和回退方案都在那裡。
5d. 不要做的事
- 不要 僅測試 "D 增加某個內部變數"。
- 不要 使用 FPS "D → +X 當面向 −Z 時" 作為載具通過條件。
- 不要 在修復時同時翻轉網格傾斜、攝影機和轉向——更改 一個 符號,重新測試。
6. 完成標準(控制項)
- 在編寫移動/轉向/飛行代碼之前 已打開 此技能。
- 已選擇類型映射(§2)且開始畫面 / HUD 標籤與之匹配。
- 追蹤攝影機 A/D 玩家可見測試 已通過(§5a)。
-
window.__controlsTest(或等效)在開發/QA 中可用並已使用至少一次。 - 相對於滾轉/轉向輸入,沒有反向的傾斜網格。
- 按鍵是按住狀態 + dt 縮放;沒有來自單次空格鍵按下的黏性推力,除非是有意為之並已標記。
如果有任何方框未勾選,則遊戲 尚未 完成。