)
1. 為什么我決定用 Codex 寫一個觸屏能玩的俄羅斯方塊俄羅斯方塊這個游戲幾乎每個寫前端的人都動過手。但真正把它寫到「手機上能順暢玩」的程度坑比想象中多得多。我試過直接讓 AI 生成一版結果在電腦上跑得好好的一放到手機瀏覽器就出問題手指滑動沒反應、按鈕點一下觸發(fā)兩次、方塊下落速度跟屏幕刷新率綁死導致越玩越快。這次我換了個思路用 Codex 從零開始把「觸屏手機也能玩」當成第一優(yōu)先級來寫。核心要解決三件事HTML5 Canvas 的渲染要適配不同 DPR 的屏幕、觸摸手勢要區(qū)分「點擊旋轉」和「下滑硬降」、游戲循環(huán)要用時間戳驅動而不是幀數(shù)驅動。這三個點搞定移動端可玩性基本就穩(wěn)了。這篇文章會給出完整的 Codex 提示詞、Canvas 初始化配置、觸屏事件綁定代碼以及我在手機瀏覽器實測旋轉、下落、消行的過程。適合已經(jīng)會一點 JavaScript、想用 AI 快速做出可玩小游戲的人。你不需要懂游戲引擎只要跟著步驟把代碼貼進去就能在手機上玩到自己寫的俄羅斯方塊。先說清楚 Codex 在這里扮演什么角色。它不是幫你「一鍵生成整個游戲」的魔法按鈕而是一個能理解具體技術術語的結對伙伴。你描述得越精確——比如「10列×20行、每格30px、用 SRS 旋轉系統(tǒng)」——它生成的代碼就越接近能直接用的狀態(tài)。模糊地說「做個俄羅斯方塊」出來的東西大概率要返工。2. 前置準備TaoToken 接入 Codex 的配置與 API Key 獲取在開始寫游戲之前得先把 Codex 跑起來。我用的方式是走 TaoToken 的 API 接入這樣在命令行里就能直接調(diào)用不用來回切網(wǎng)頁。整個流程分三步拿 Key、配環(huán)境變量、驗證連通。2.1 獲取 API Key打開 TaoToken 控制臺進入 API Keys 頁面創(chuàng)建一個新的 Key。創(chuàng)建時建議給它起個能認出來的名字比如codex-tetris方便后面區(qū)分不同項目的用量。Key 只在創(chuàng)建時完整顯示一次復制后先存到安全的地方。拿到 Key 之后不要直接寫死在代碼里。我用的是環(huán)境變量方式在~/.zshrc或~/.bashrc里加一行export TAOTOKEN_API_KEYsk-你的key然后source ~/.zshrc讓它生效。這樣做的原因是后面 Codex 的配置文件里只需要引用變量名不會把明文 Key 提交到 git 里。2.2 配置 Codex 的 auth.jsonCodex 的認證信息放在~/.codex/auth.json。如果你之前沒配過這個文件可能不存在手動創(chuàng)建即可。內(nèi)容結構如下{ OPENAI_API_KEY: sk-你的key, OPENAI_BASE_URL: https://taotoken.net/api }注意 Base URL 這里填的是https://taotoken.net/api不要帶多余的路徑。Model ID 在調(diào)用時指定常用的編碼模型填gpt-5-codex或你賬號下可用的對應模型名。這三個要素——Base URL、Key、Model ID——缺一不可后面排查 401 錯誤時也是先查這三項。2.3 驗證連通性配好之后用一條最簡單的請求確認能通。在終端里執(zhí)行curl https://taotoken.net/api/v1/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY如果返回一個包含模型列表的 JSON說明 Key 和網(wǎng)絡都沒問題。如果返回 401先檢查 Key 有沒有復制完整、有沒有多余空格如果返回連接超時檢查 Base URL 是不是寫成了帶/v1的完整路徑導致重復。這一步看起來簡單但很多人卡在這里。我踩過的坑是把 Key 寫進了auth.json但忘了export環(huán)境變量結果 Codex 讀的是空值。兩個地方都要對缺一個都不行。3. 可復制配置Canvas 初始化與觸屏事件綁定這一節(jié)是整篇文章的核心。我會把 Codex 生成的配置拆成三塊Canvas 的 DPR 適配、游戲循環(huán)的時間戳驅動、觸摸手勢的判定邏輯。每一塊都給出可直接復制的代碼。3.1 Canvas 的 DPR 適配配置移動端 Canvas 最常見的問題是「畫出來模糊」。原因是 CSS 像素和物理像素不是 1:1尤其在 Retina 屏上。解決辦法是根據(jù)devicePixelRatio放大 Canvas 的實際分辨率再用 CSS 把它縮回視覺尺寸。function setupCanvas(canvas, cssWidth, cssHeight) { const dpr window.devicePixelRatio || 1; // 實際分辨率 CSS 尺寸 × DPR canvas.width cssWidth * dpr; canvas.height cssHeight * dpr; // CSS 尺寸保持視覺大小 canvas.style.width cssWidth px; canvas.style.height cssHeight px; const ctx canvas.getContext(2d); // 縮放上下文讓后續(xù)繪制按 CSS 像素坐標走 ctx.scale(dpr, dpr); return ctx; } // 主畫布10列 × 20行每格30px const gameCtx setupCanvas( document.getElementById(game-canvas), 300, 600 );這段配置的關鍵在ctx.scale(dpr, dpr)。調(diào)用之后你后面所有fillRect(x, y, 30, 30)里的坐標都按 CSS 像素算不用自己乘 DPR。Codex 生成這段時我特意在提示詞里寫了「用 devicePixelRatio 適配高分屏」它就把 scale 這步帶上了。3.2 游戲循環(huán)的時間戳驅動很多教程用setInterval或者按幀計數(shù)來控制下落這在 60Hz 和 120Hz 屏幕上表現(xiàn)完全不同。正確做法是用requestAnimationFrame拿到時間戳自己算距離上次下落過了多久。let lastDropTime 0; let dropInterval 800; // 初始下落間隔毫秒 function gameLoop(timestamp) { if (gameOver || paused) return; // 距離上次自動下落是否超過間隔 if (timestamp - lastDropTime dropInterval) { moveDown(); lastDropTime timestamp; } render(); requestAnimationFrame(gameLoop); } // 啟動 requestAnimationFrame((ts) { lastDropTime ts; gameLoop(ts); });dropInterval隨等級遞減公式用經(jīng)典的Math.pow(0.8 - (level - 1) * 0.007, level - 1) * 1000最低不低于 50ms。這樣無論屏幕刷新率多少下落速度都是一致的。3.3 觸摸手勢的判定邏輯觸屏操作要區(qū)分三種意圖短按旋轉、快速下滑硬降、水平滑動左右移。判定依據(jù)是位移距離和持續(xù)時間。let touchStartX 0; let touchStartY 0; let touchStartTime 0; gameCanvas.addEventListener(touchstart, (e) { e.preventDefault(); const touch e.touches[0]; touchStartX touch.clientX; touchStartY touch.clientY; touchStartTime Date.now(); }, { passive: false }); gameCanvas.addEventListener(touchend, (e) { e.preventDefault(); const touch e.changedTouches[0]; const dx touch.clientX - touchStartX; const dy touch.clientY - touchStartY; const dt Date.now() - touchStartTime; const absDx Math.abs(dx); const absDy Math.abs(dy); if (absDx 10 absDy 10 dt 300) { // 短按旋轉 rotatePiece(1); } else if (absDy absDx dy 50) { // 快速下滑硬降 hardDrop(); } else if (absDx absDy absDx 30) { // 水平滑動左右移 moveHorizontal(dx 0 ? 1 : -1); } }, { passive: false });這里{ passive: false }很重要。移動端瀏覽器默認把 touch 事件當被動監(jiān)聽preventDefault()會失效導致頁面跟著滾動。加上這個選項才能阻止默認行為。3.4 虛擬按鈕的綁定除了手勢底部還放了一排虛擬按鈕方便不習慣滑動的用戶。綁定用touchstart而不是click響應更快。const btnMap { btn-left: () moveHorizontal(-1), btn-right: () moveHorizontal(1), btn-rotate: () rotatePiece(1), btn-down: () softDrop(), btn-drop: () hardDrop(), btn-hold: () holdCurrentPiece(), }; Object.entries(btnMap).forEach(([id, handler]) { const btn document.getElementById(id); btn.addEventListener(touchstart, (e) { e.preventDefault(); handler(); }, { passive: false }); });按鈕的 CSS 里要加touch-action: manipulation和-webkit-tap-highlight-color: transparent去掉點擊高亮和 300ms 延遲。4. 驗證請求手機瀏覽器實測旋轉、下落與消行代碼寫完得在真機上驗證。我用的是手機 Chrome通過局域網(wǎng)訪問電腦起的靜態(tài)服務。這一步的目的是確認三件事方塊能正常旋轉、下落速度穩(wěn)定、消行邏輯正確。4.1 起一個本地服務在項目目錄下執(zhí)行python3 -m http.server 8080然后在手機瀏覽器輸入電腦的局域網(wǎng) IP 加端口比如http://192.168.1.100:8080。注意手機和電腦要在同一個 Wi-Fi 下。4.2 驗證旋轉進入游戲后點一下屏幕中央方塊應該順時針轉 90 度。連續(xù)點四次應該回到初始朝向。如果轉不動檢查rotatePiece里的isValidPosition判斷——可能是旋轉后的形狀越界了SRS 的踢墻數(shù)據(jù)沒生效。我在實測時遇到過一次「貼墻旋轉失敗」原因是踢墻偏移的 y 軸方向搞反了。SRS 數(shù)據(jù)里 dy 為正表示向上代碼里要用y - dy而不是y dy。改過來之后貼左墻和貼右墻都能正常旋轉。4.3 驗證下落與消行讓方塊自然下落觀察速度是否均勻。然后手動堆幾行故意留一行快滿等一個長條方塊下來消掉。消行時應該看到閃爍動畫然后上面的行整體下移。驗證消行的關鍵是看clearLines函數(shù)。它從底部往上掃遇到滿行就splice刪掉然后在頂部unshift補空行。如果消行后出現(xiàn)錯位多半是splice的索引沒處理好——刪一行之后后面的行號會變所以要從下往上刪。4.4 驗證觸屏手勢在畫布上快速下滑方塊應該直接落到底部并鎖定。水平滑動方塊應該左右移動一格。短按方塊旋轉。三種手勢互不干擾。如果下滑被識別成了旋轉檢查dy 50這個閾值。手指滑動距離太短會被當成點擊。反過來如果水平滑動沒反應可能是absDx 30設得太高調(diào)低到 20 試試。5. 本篇常見錯誤排查401、local proxy failed 與 OAuth 報錯配置和運行過程中最容易卡住的是認證和網(wǎng)絡問題。這一節(jié)把幾個高頻報錯和對應解法列出來。5.1 401 Unauthorized這是最常見的。原因通常是三個Key 不對、Base URL 不對、Model ID 不對。先確認auth.json里的OPENAI_API_KEY和OPENAI_BASE_URL都填了。Base URL 必須是https://taotoken.net/api不要帶/v1。然后確認調(diào)用時指定的 Model ID 是你賬號下可用的。三者都對還報 401就去控制臺重新生成一個 Key 試試。5.2 local proxy failed這個報錯通常出現(xiàn)在 Codex 嘗試走本地代理但連不上時。檢查你的環(huán)境變量里有沒有殘留的HTTP_PROXY或HTTPS_PROXY設置。如果有先unset掉再重試。另外確認auth.json里的 Base URL 是完整的https://taotoken.net/api不要寫成相對路徑。5.3 reading choices 報錯這個錯誤一般出現(xiàn)在響應格式不符合預期時。Codex 期望返回的 JSON 里有choices字段如果返回的是錯誤信息或者空對象就會報這個。先確認請求的 endpoint 是/v1/chat/completions或對應的編碼接口再確認 Model ID 拼寫正確。5.4 OAuth 相關報錯如果你用的是需要 OAuth 的接入方式報錯時先檢查 token 有沒有過期。重新走一遍授權流程拿到新的 token 后更新到配置文件里。注意 OAuth token 和 API Key 是兩套東西不要混用。5.5 觸屏相關的問題如果手機上按鈕點不動先檢查有沒有加{ passive: false }。如果頁面跟著手指滾動在touchstart和touchmove里都調(diào)preventDefault()。如果按鈕有 300ms 延遲加touch-action: manipulation。6. 從單機到聯(lián)機用 Codex 繼續(xù)擴展你的俄羅斯方塊單機版跑通之后如果想繼續(xù)加功能Codex 也能接著幫你寫。我試過讓它加 Hold 暫存、下一方塊預覽、甚至 WebSocket 聯(lián)機對戰(zhàn)都能生成可用的代碼。6.1 加 Hold 暫存Hold 的邏輯是按 C 鍵把當前方塊存起來如果之前存過就交換。關鍵是每次鎖定前只能用一次用一個canHold標志控制。function holdCurrentPiece() { if (!currentPiece || !canHold) return; if (holdPiece) { const temp holdPiece; holdPiece { type: currentPiece.type }; currentPiece createTetromino(temp.type); } else { holdPiece { type: currentPiece.type }; currentPiece getNextPiece(); } canHold false; updatePreview(); }鎖定新方塊時把canHold重置為true。6.2 加下一方塊預覽用一個小的 Canvas 畫下一個方塊的形狀。Codex 生成的drawPreview函數(shù)會自動居中繪制不用自己算偏移。6.3 加 WebSocket 聯(lián)機聯(lián)機部分需要一個 Node.js 服務端。核心邏輯是兩個玩家匹配到同一個房間一方消行時給對手發(fā)「垃圾行」一方游戲結束另一方獲勝。// 服務端轉發(fā)垃圾行 case lines_cleared: const garbageLines msg.count 4 ? 4 : msg.count - 1; if (garbageLines 0) { forwardToOpponent(ws, { type: garbage, count: garbageLines }); } break;客戶端收到garbage消息后在棋盤底部加一行灰色方塊隨機留一個缺口。6.4 長期編碼建議如果你打算持續(xù)用 Codex 做這類項目可以考慮用 Coding Plan 的方式管理調(diào)用。它的好處是額度更穩(wěn)定適合需要反復迭代的場景。配置方式跟單次調(diào)用一樣只是計費模式不同。6.5 驗證模型輸出寫完一段邏輯不確定對不對可以把代碼貼到模型對話里讓它 review。比如問「這段消行邏輯有沒有邊界問題」它通常能指出splice索引或者空行補充的疏漏。整個項目從零到能玩我用了一個下午。大部分時間花在調(diào)觸屏手勢的閾值和 SRS 踢墻數(shù)據(jù)的方向上。Codex 省掉的是查文檔和寫樣板代碼的時間但邏輯對不對、手感好不好還是得自己在真機上試。建議你先把單機版跑通再逐步加功能每加一個就在手機上驗證一次這樣出問題容易定位。