从工具包构建游戏
expo-game 模板已经包含了引擎:固定时间步长循环、键盘和触摸输入、摄像机、碰撞辅助、HUD、菜单、暂停、结果、解锁、进度保存以及一个可用的示例游戏(Coin Dash)。你的任务是替换示例内容,实现用户的游戏。请不要自行设计或编写引擎。
构建规则
- 在第一轮就编写代码。不要进行冗长的规划。用至多 10 行思路确定游戏规则,然后编辑文件。运行后修复问题。
- 保持每轮修改量小:只涉及一个文件或一个特性,然后运行
bun run typecheck。长时间推理而不写代码视为失败。 - 永远不要重新实现工具包已有的功能:循环、输入、碰撞、摄像机、HUD、屏幕、存储。
- 在添加菜单、美术或解锁之前,先让游戏实时可玩。示例已经包含了菜单。
项目
使用 omg_create_project 创建项目,template 参数设为 "expo-game"。在首次预览时,遵循 omg-app-builder 技能中的 Expo 快速路径。然后进行编辑。
需要编辑的文件(按顺序)
src/game/theme.ts:根据设计设置颜色。src/games/<your-game>/logic.ts:复制coin-dash/logic.ts。纯规则:createGame()、stepGame(game, input, dt)、finalScore()、UNLOCKS。不涉及 React、绘图或Date.now()。仅通过axisX/axisY/wasTapped(input, "p1" | "p2", ...)读取输入。使用seeded()实现随机性。src/games/<your-game>/game-view.tsx:复制coin-dash/game-view.tsx。使用World和Sprite进行绘制(彩色形状、emojilabel或react-native-svg)。使用世界单位;摄像机负责转换。src/app/play.tsx:将导入从 coin-dash 改为你的游戏。在TouchControls上设置kind(dpad、stick、steer)及其buttons(例如{ action: "a", label: "JUMP" })。保持循环、键盘、暂停和完成连接不变。src/app/index.tsx、results.tsx、unlocks.tsx:修改标题、文本以及UNLOCKS的导入。 除非用户要求,否则不要编写自动化测试。改为通过自我检查进行验证(参见“完成”部分)。
除非工具包的错误阻塞了你,否则不要编辑 src/game/loop.ts、input.ts、collision.ts、camera.ts、progress.ts。在游戏文件夹中添加辅助函数。
工具包 API(使用这些名称)
createLoop({ step })和useGameLoop(update, { paused }):更新以固定的 1/60 秒步长运行。createInput()、axisX/axisY、wasTapped、endFrame、setAction、setStick。useKeyboard(input, players, onEscape):P1 使用 WASD + Space/E;双人模式中 P2 使用方向键 + Enter。TouchControls players={1|2} kind="dpad|stick|steer" buttons={[...]}:双人模式将 P1 置于左侧,P2 置于右侧。对于分屏,渲染两个GameView,每个使用独立的摄像机(参见play.tsx)。circlesOverlap、rectsOverlap、circleRectOverlap、pushCircleOutOfRect、keepCircleInside、angleDiff、seeded、clamp、lerp。createCamera、followCamera(cam, target, dt, { velocity, lookAhead, world, viewW, viewH })。profileStore.finishRun({ gameId, score, stats }, UNLOCKS)、useProfile()、profileStore.select(group, id)、setSettings。- 屏幕:
MenuScreen、PauseOverlay、ResultsScreen、UnlocksScreen、SettingsRow、GameButton。HUD:Hud、HudStat、HudBar、Banner。
Web 陷阱
- 使用
useViewSize()(基于onLayout)测量游戏区域。在静态 Web 导出中,useWindowDimensions()在发生 resize 之前始终为 0。 - 在 Web 上,带有按钮的
Alert.alert不生效。请使用PauseOverlay或屏幕内模态框。 - 将进度保存在
profileStore中,而不是组件状态:刷新后必须保留最高分和解锁状态。 - 不要使用
setInterval或Date.now()作为游戏时间。请使用循环的dt计数。 - 不要将每帧状态放在 React 的
useState中。将其保存在游戏对象(一个 ref)中;hook 会在每帧重新绘制。
3D 游戏
该工具包是 2D 的。对于 3D 游戏(确实是需求,而非风格选择),保留循环、输入、HUD 和屏幕,仅替换 game-view.tsx。
bunx expo install three @react-three/fiber expo-gl && bun add -d @types/three
编写两个小型文件,具有相同的导出,以便 Metro 选择正确的文件:
src/components/scene.web.tsx:import { Canvas, useFrame } from "@react-three/fiber"src/components/scene.tsx:相同的代码,但使用from "@react-three/fiber/native"
放置一个 <Canvas camera={{ position: [0, 0, 4] }}>,其中包含一个 ambientLight、一个 directionalLight 和每个游戏对象一个 <mesh>(盒子、球体、平面、纯色)。在每次渲染时从游戏对象复制位置;在 useGameLoop 中运行 stepGame,而不是在 useFrame 中。保持 HUD 和控件作为常规 React Native 视图位于画布之上。画布需要一个具有尺寸(flex: 1)的父容器。
在工具包 spike 中已验证:3D 场景可在 Expo Web 中渲染,Android JS 包可以编译。未验证:在设备或 Expo Go 上渲染。在最终消息中说明这一点。
完成
运行 bun run typecheck 和 bun run build。除非用户要求,否则不要编写自动化测试。然后运行 bun run check:ui:它将以 390x844 尺寸打开每个路由,在每个路由的 .omg/ui-check/ 目录下保存一张 PNG,并报告空白页面、错误边界、控制台错误、失败请求以及被其他元素遮挡的按钮。打开每张 PNG,与请求和任何设计参考资料进行比较,修复问题,然后重新运行直到报告干净。该检查不运行游戏,因此还需要在 Expo Web 中手动检查:必须能通过键盘移动、结束、显示结果屏幕,并在刷新后保持最高分。说明你测试了哪些表面;Web 运行不能证明原生行为。发布后,以手机尺寸打开实时 URL。omg 安装栏位于游戏下方,游戏成为其上方的卡片,因此该栏不会遮盖任何游戏控件。不要移动或隐藏它。