mcpField
T
threejs-game

threejs-game

github.com/playableintelligence开发3501.1k 次安装22 周前更新
代码仓库

关于 threejs-game

threejs-game 是一套面向 Three.js 浏览器游戏开发的实践技能,覆盖项目结构、核心模块、输入、渲染和性能规则。适合构建兼顾桌面与移动设备的游戏。

主要功能

  1. 01规定游戏目录结构与核心模块
  2. 02提供事件总线和集中状态模式
  3. 03说明 WebGL 与 WebGPU 的选择
  4. 04涵盖移动输入和安全区布局
  5. 05提供渲染性能与资源清理准则

适用场景

  1. 01搭建新的 Three.js 游戏项目
  2. 02组织游戏状态、事件和系统
  3. 03实现桌面与移动设备输入
  4. 04检查渲染性能和重启清理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/playableintelligence/game-creator --skill threejs-game

安装后在 Claude Code 中输入/threejs-game确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

core-patterns.md
input-patterns.md
tsl-guide.md
reference/
llms-full.txt
llms.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 个失败条件、1 个计分系统。
  2. 玩法清晰度 > 视觉复杂度——将 3D 视为一种风格选择,而非复杂度要求。使用简单材质、易于理解的游戏,胜过视觉复杂却令人困惑的游戏。
  3. 安全重启——游戏必须能够安全、完整地重启。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 和输入优先级模式。

添加功能时

  1. 在适当的 src/ 子目录中创建新模块
  2. 在 EventBus.js 的 Events 对象中,使用 domain:action 命名方式定义新事件
  3. 在 Constants.js 中添加配置
  4. 如有需要,在 GameState.js 中添加状态
  5. 在 Game.js 编排器中接入功能
  6. 仅通过 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 故障

评论

支持 Markdown