关于 threejs-game
threejs-game 是一套面向 Three.js 浏览器游戏开发的实践技能,覆盖项目结构、核心模块、输入、渲染和性能规则。适合构建兼顾桌面与移动设备的游戏。
主要功能
- 01规定游戏目录结构与核心模块
- 02提供事件总线和集中状态模式
- 03说明 WebGL 与 WebGPU 的选择
- 04涵盖移动输入和安全区布局
- 05提供渲染性能与资源清理准则
适用场景
- 01搭建新的 Three.js 游戏项目
- 02组织游戏状态、事件和系统
- 03实现桌面与移动设备输入
- 04检查渲染性能和重启清理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ playableintelligence/ game-creator --skill threejs-game安装后在 Claude Code 中输入/threejs-game确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
core-patterns.mdinput-patterns.mdtsl-guide.mdreference/llms-full.txtllms.txt详细介绍
Three.js 游戏开发
你是一名资深 Three.js 游戏开发者。构建 3D 浏览器游戏时,请遵循这些明确的模式。
参考:有关 Three.js 的官方 LLM 文档,请参阅
reference/llms.txt(快速指南)和reference/llms-full.txt(完整 API + TSL)。如果这些文件与本技能存在冲突,请优先采用其中的模式。
性能说明
- 每一步都要认真完成。质量比速度更重要。
- 不要跳过验证步骤——它们能及早发现问题。
- 修改文件前,请先阅读完整上下文。
- 先分析性能,再进行优化。瓶颈很少出现在你预想的位置。
参考文件
详细参考资料见此目录中的配套文件:
core-patterns.md— 完整的 EventBus、GameState、Constants 和 Game.js 编排器代码tsl-guide.md— Three.js 着色语言参考(NodeMaterial 类、何时使用 TSL)input-patterns.md— 陀螺仪输入、虚拟摇杆、统一模拟输入 InputSystem、输入优先级系统
如需了解经过测量并提供优化前后证据的性能优化模式,请参阅 threejs-perf 技能(skills/threejs-perf/SKILL.md)。
技术栈
- 渲染器:Three.js(
[email protected]+,ESM 导入) - 构建工具:Vite
- 语言:游戏模板使用 JavaScript(而非 TypeScript)——TypeScript 可选
- 包管理器:npm
项目设置
搭建新的 Three.js 游戏时:
mkdir <game-name> && cd <game-name>
npm init -y
npm install three@^0.183.0
npm install -D vite
创建 vite.config.js:
import { defineConfig } from 'vite';
export default defineConfig({
root: '.',
publicDir: 'public',
server: { port: 3000, open: true },
build: { outDir: 'dist' },
});
在 package.json 脚本中添加:
{
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
现代导入模式
Vite / npm(默认——用于我们的模板)
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
Import Maps / CDN(独立 HTML 游戏,无需构建步骤)
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
</script>
使用 import maps 发布无需构建工具的单个 HTML 文件。在 import map URL 中固定版本。
必需架构
每个 Three.js 游戏都必须使用此目录结构:
src/
├── core/
│ ├── Game.js # Main orchestrator - init systems, render loop
│ ├── EventBus.js # Singleton pub/sub for all module communication
│ ├── GameState.js # Centralized state singleton
│ └── Constants.js # ALL config values, balance numbers, asset paths
├── systems/ # Low-level engine systems
│ ├── InputSystem.js # Keyboard/mouse/gamepad input
│ ├── PhysicsSystem.js # Collision detection
│ └── ... # Audio, particles, etc.
├── gameplay/ # Game mechanics
│ └── ... # Player, enemies, weapons, etc.
├── level/ # Level/world building
│ ├── LevelBuilder.js # Constructs the game world
│ └── AssetLoader.js # Loads models, textures, audio
├── ui/ # User interface
│ └── ... # Game over, overlays
└── main.js # Entry point - creates Game instance
核心原则
- 优先实现核心循环——实现一个摄像机、一个场景和一个游戏循环。在添加视觉润色之前,先加入玩家输入和终止条件(胜利/失败)。初始范围应保持精简:1 个机制、1 个失败条件、1 个计分系统。
- 玩法清晰度 > 视觉复杂度——将 3D 视为一种风格选择,而非复杂度要求。使用简单材质、易于理解的游戏,胜过视觉复杂却令人困惑的游戏。
- 安全重启——游戏必须能够安全、完整地重启。
GameState.reset()必须恢复到干净状态。清理时释放几何体/材质/纹理。重启后不得残留过期引用或泄漏的监听器。
核心模式(不可妥协)
每个 Three.js 游戏都需要以下四个核心模块。完整实现代码见 core-patterns.md。
1. EventBus 单例
所有模块间通信都必须通过 EventBus(core/EventBus.js)。模块之间不得为了通信而直接相互导入。提供 on、once、off、emit 和 clear 方法。事件采用 domain:action 命名方式(例如 player:hit、game:over)。完整实现见 core-patterns.md。
2. 集中式 GameState
使用一个单例(core/GameState.js)保存所有游戏状态。系统从中读取状态,事件对其进行更新。必须包含 reset() 方法,以便重启时恢复到干净状态。完整实现见 core-patterns.md。
3. Constants 文件
所有魔法数字、平衡数值、资源路径和配置都必须放入 core/Constants.js。游戏逻辑中不得硬编码数值。按领域组织:PLAYER_CONFIG、ENEMY_CONFIG、WORLD、CAMERA、COLORS、ASSET_PATHS。完整实现见 core-patterns.md。
4. Game.js 编排器
Game 类(core/Game.js)负责初始化所有内容并运行渲染循环。使用 renderer.setAnimationLoop()——这是 Three.js 官方模式(可正确处理 WebGPU 异步行为,并在标签页隐藏时暂停)。在 init() 中设置渲染器、场景、摄像机、系统、UI 和事件监听器。完整实现见 core-patterns.md。
渲染器选择
WebGLRenderer(默认——适用于所有游戏模板)
浏览器兼容性最高。成熟可靠,大多数示例和教程都使用它。我们的模板默认使用 WebGLRenderer。
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
WebGPURenderer(需要 TSL 或计算着色器时使用)
自定义基于节点的材质(TSL)、计算着色器和高级渲染都需要它。注意:导入路径会改为 'three/webgpu',且初始化是异步的。
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init();
何时选择 WebGPU:需要 TSL 自定义着色器、计算着色器或基于节点的材质时使用。否则,请使用 WebGL。TSL 详情请参阅 tsl-guide.md。
Play.fun 安全区域
游戏在移动 Safari 的 Play.fun 控制面板中运行时,SDK 会在游戏 iframe 的 document.documentElement 上设置 CSS 自定义属性:
--ogp-safe-top-inset— Play.fun 页眉气泡下方的空间(移动设备上约为 68px)--ogp-safe-bottom-inset— Safari 底部控件上方的空间(移动设备上约为 148px)
在控制面板之外运行时(桌面设备、独立运行),两者默认为 0px。
常量
// In Constants.js — reads SDK CSS vars with static fallbacks
function _readSafeInsets() {
const s = getComputedStyle(document.documentElement);
return {
top: parseInt(s.getPropertyValue('--ogp-safe-top-inset')) || 0,
bottom: parseInt(s.getPropertyValue('--ogp-safe-bottom-inset')) || 0,
};
}
const _insets = _readSafeInsets();
export const SAFE_ZONE = {
TOP_PX: Math.max(75, _insets.top),
BOTTOM_PX: _insets.bottom,
TOP_PERCENT: 8,
};
CSS 规则
所有 .overlay 元素(游戏结束界面、暂停界面、菜单)都必须使用 CSS 变量设置内边距:
.overlay {
padding-top: max(20px, 8vh, var(--ogp-safe-top-inset, 0px));
padding-bottom: var(--ogp-safe-bottom-inset, 0px);
}
位于底部的 UI(摇杆、动作按钮)也必须考虑底部安全区域:
#joystick-zone {
bottom: max(20px, 3vh, var(--ogp-safe-bottom-inset, 0px));
}
.bottom-hud {
margin-bottom: var(--ogp-safe-bottom-inset, 0px);
}
检查事项
- 顶部或底部安全区域内不得有文本、按钮或交互元素
- 游戏结束界面应在可用区域(两处安全区域之间)居中显示内容,而不是在整个视口中居中
- 分数显示、标题和重新开始按钮均应清晰可见,不被浏览器界面遮挡
- 位于底部的控件(摇杆、动作按钮)不得被 Safari 底部栏裁切
注意:3D 画布本身会渲染在浏览器界面后方,这没有问题——游戏应延伸并填满整个视口。只有 HTML 覆盖层 UI 需要偏移以避开安全区域。场景内的 3D 元素(HUD 纹理、悬浮文字)应避开屏幕空间顶部 8% 的区域和底部安全区域。
性能规则
- 使用
renderer.setAnimationLoop(),不要手动调用requestAnimationFrame。标签页隐藏时它会暂停,并能正确处理 WebGPU 异步操作。 - 限制 delta time:使用
Math.min(clock.getDelta(), 0.1),避免性能失控 - 限制像素比:使用
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)),避免高 DPI 屏幕导致 GPU 过载 - 对象池:在高频循环中重复使用
Vector3、Box3和临时对象,以尽量减少垃圾回收。避免逐帧分配对象——应预先分配并重复使用。 - 首次实现时禁用阴影——只有在确有需要,且已在移动设备上测试过时才启用阴影贴图。动态阴影是开销最高的渲染功能。
- 减少绘制调用——独特材质和几何体越少,绘制调用就越少。尽可能合并静态几何体。重复对象使用实例化网格。实例化网格模式请参阅
skills/threejs-perf/(绘制调用减少约 9,000×,渲染 CPU 速度提升约 57×)。 - 优先使用简单材质——使用
MeshBasicMaterial或MeshStandardMaterial。除非确有需要,否则避免使用MeshPhysicalMaterial、自定义着色器或复杂的材质配置。 - 默认不使用后期处理——首次实现时跳过泛光、SSAO、运动模糊和其他后期处理通道。这些效果会严重影响移动设备性能。只有在游戏玩法稳定且性能预算允许时才添加。
- 控制几何体和材质数量——使用 10 种独特材质的游戏,渲染速度会快于使用 100 种材质的游戏。外观相同的对象应重复使用材质。
- 渲染器使用
powerPreference: 'high-performance' - 正确释放资源:移除对象时,对几何体、材质和纹理调用
.dispose() - 视锥体剔除:交由 Three.js 处理(默认启用),但自定义几何体应设置包围球
资源加载
- 将静态资源放在 Vite 的
/public/目录中 - 3D 模型使用 GLB 格式(体积更小,且为单个文件)
- 使用
THREE.TextureLoader和来自three/addons的GLTFLoader - 通过回调在 UI 中显示加载进度
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
function loadModel(path) {
return new Promise((resolve, reject) => {
loader.load(
path,
(gltf) => resolve(gltf.scene),
undefined,
(error) => reject(error),
);
});
}
输入处理(移动端优先)
除非另有明确说明,所有游戏都必须同时支持桌面端和移动端。权衡取舍时,将 60% 的精力分配给移动端,40% 分配给桌面端。根据游戏概念,为每款游戏选择最合适的移动端输入方式:
| 游戏类型 | 主要移动端输入方式 | 备用方式 |
|---|---|---|
| 弹珠/倾斜/平衡 | 陀螺仪(DeviceOrientation) | 虚拟摇杆 |
| 跑酷/无尽 | 点击区域(左半屏/右半屏) | 滑动手势 |
| 解谜/回合制 | 点击目标(最小 44px) | 拖放 |
| 射击/瞄准 | 虚拟摇杆 + 点击开火 | 双摇杆 |
| 平台跳跃 | 虚拟方向键 + 跳跃按钮 | 倾斜控制移动 |
统一的 Analog InputSystem
使用专用的 InputSystem,将键盘、陀螺仪和触控合并为统一的模拟输入接口。游戏逻辑读取 moveX/moveZ(-1..1),无需知道输入来源。键盘输入始终作为覆盖输入启用;在移动端,系统会初始化陀螺仪(iOS 13+ 需请求权限),或回退到虚拟摇杆。完整实现请参阅 input-patterns.md,其中包括 GyroscopeInput、VirtualJoystick 和输入优先级模式。
添加功能时
- 在适当的
src/子目录中创建新模块 - 在
EventBus.js的 Events 对象中,使用domain:action命名方式定义新事件 - 在
Constants.js中添加配置 - 如有需要,在
GameState.js中添加状态 - 在
Game.js编排器中接入功能 - 仅通过 EventBus 与其他系统通信
发布前验证清单
在认为游戏已完成前,请确认:
- 核心循环正常 — 玩家可以开始游戏、进行游戏、输掉/赢得游戏并看到结果
- 重启正常且干净 —
GameState.reset()会恢复干净状态,并释放所有 Three.js 资源 - 触控 + 键盘输入 — 游戏在移动端(陀螺仪/摇杆/点击)和桌面端(键盘/鼠标)均可运行
- 画布自适应 — 渲染器会在窗口大小改变时调整尺寸,并更新摄像机纵横比
- 所有数值都在 Constants 中 — 游戏逻辑中没有硬编码的魔法数字
- 仅使用 EventBus — 通信时不直接跨模块导入
- 资源清理 — 从场景中移除时,释放几何体、材质和纹理
- 不使用后期处理 — 除非确有需要且已在移动端测试
- 禁用阴影 — 除非确有需要且预算允许
- 限制增量时间的移动 — 每帧都使用
Math.min(clock.getDelta(), 0.1) - 静音切换 — 可以静音/取消静音;遵循
isMuted状态 - 遵循安全区 — 所有 HTML 覆盖层 UI 都使用
var(--ogp-safe-top-inset)/var(--ogp-safe-bottom-inset)遵循 Play.fun 安全区;底部控件需偏移至底部安全区上方 - 构建通过 —
npm run build成功且没有错误 - 控制台无错误 — 游戏运行时没有未捕获异常或 WebGL 故障