Creates 3D components using the Experience singleton pattern. Use when building new classes in src/js/, implementing lifecycle methods, or managing Three.js objects.
All 3D components in this project follow a class-based singleton pattern. Components access shared resources through the Experience singleton and implement standardized lifecycle methods.
Core principle: Extract only what you need, implement all required lifecycle methods, always clean up resources.
src/js/import Experience from '@/js/experience.js'
import emitter from '@/js/utils/event-bus.js'
import * as THREE from 'three'
export default class YourComponent {
constructor(options = {}) {
// 1. 获取 Experience 单例
this.experience = new Experience()
// 2. 按需提取依赖(仅取所需,避免冗余)
this.scene = this.experience.scene
this.resources = this.experience.resources
this.debug = this.experience.debug
// 3. 组件参数
this.params = {
enabled: options.enabled ?? true,
// ... 其他参数
}
// 4. 组件状态
this.mesh = null
// 5. 初始化
this._init()
// 6. 调试面板(必须在 debug.active 条件下调用)
if (this.debug.active) {
this.debugInit()
}
}
_init() {
// 创建 3D 对象并添加到场景
this.mesh = new THREE.Mesh(/* ... */)
this.scene.add(this.mesh)
}
debugInit() {
// 调试面板 - 详见 vtj-debug-panel skill
}
update() {
// 每帧更新逻辑
// 时间通过 this.experience.time 访问,不通过参数传递
}
resize() {
// 窗口尺寸变化时调用
}
destroy() {
// 清理资源 - 涉及 Object3D 的组件必须实现
if (this.mesh) {
this.scene.remove(this.mesh)
this.mesh.geometry?.dispose()
this.mesh.material?.dispose()
this.mesh = null
}
}
}
只提取组件实际需要的依赖:
| 组件类型 | 典型依赖 |
|----------|----------|
| 渲染对象 | scene, resources |
| 交互组件 | scene, camera.instance, iMouse |
| 动画组件 | scene, time |
| 调试组件 | debug |
| UI 相关 | sizes, canvas |
// ✅ GOOD: 只取所需
this.scene = this.experience.scene
this.resources = this.experience.resources
// ❌ BAD: 提取全部
this.scene = this.experience.scene
this.camera = this.experience.camera
this.renderer = this.experience.renderer
this.time = this.experience.time
this.sizes = this.experience.sizes
this.iMouse = this.experience.iMouse
this.debug = this.experience.debug
// ... 大部分根本用不到
| 方法 | 必需条件 | 调用方 |
|------|----------|--------|
| debugInit() | 有可调参数 | 构造函数,if (debug.active) |
| update() | 有逐帧逻辑 | 父组件的 update() |
| resize() | 响应尺寸变化 | 父组件的 resize() |
| destroy() | 涉及 Object3D | 父组件的 destroy() |
任何涉及 Object3D 的组件必须实现 destroy() 方法。
destroy() {
// 1. 移除事件监听
emitter.off('some:event', this._handler)
// 2. 从场景移除对象
if (this.mesh) {
this.scene.remove(this.mesh)
}
// 3. 销毁几何体
this.mesh?.geometry?.dispose()
// 4. 销毁材质
if (this.mesh?.material) {
if (Array.isArray(this.mesh.material)) {
this.mesh.material.forEach(m => m.dispose())
} else {
this.mesh.material.dispose()
}
}
// 5. 销毁纹理(如果组件自己创建的)
this.texture?.dispose()
// 6. 销毁子组件
this.childComponent?.destroy()
// 7. 清空引用
this.mesh = null
}
统一使用以下方式访问调试系统:
// 在构造函数中
this.debug = this.experience.debug
// 检查是否激活
if (this.debug.active) {
this.debugInit()
}
// 在 debugInit() 中创建面板
debugInit() {
this.debugFolder = this.debug.ui.addFolder({
title: 'Component Name',
expanded: false,
})
this.debugFolder.addBinding(this.params, 'someValue', {
label: '参数名称',
min: 0,
max: 1,
})
}
注意:
debugInit()(不是 debugInit 或 setDebug)this.debug.ui 访问 Tweakpane 实例this.debug.active 判断是否启用// BAD: 组件创建了 mesh 但没有 destroy
class BadComponent {
constructor() {
this.mesh = new THREE.Mesh(...)
this.scene.add(this.mesh)
}
// 没有 destroy() → 内存泄漏
}
// BAD: 使用其他命名
debuggerInit() { } // ❌
setDebug() { } // ❌
// GOOD: 统一命名
debugInit() { } // ✅
// BAD: 直接提取 ui
this.debug = this.experience.debug.ui // ❌
// GOOD: 提取 debug 对象
this.debug = this.experience.debug // ✅
// 然后通过 this.debug.ui 访问面板
// BAD: 绑定事件但没清理
constructor() {
emitter.on('game:event', this.handler.bind(this))
}
// 没有在 destroy() 中 emitter.off()
// GOOD: 保存引用并清理
constructor() {
this._boundHandler = this.handler.bind(this)
emitter.on('game:event', this._boundHandler)
}
destroy() {
emitter.off('game:event', this._boundHandler)
}
// BAD: 通过参数传递
update(deltaTime) {
this.mesh.rotation.y += deltaTime
}
// GOOD: 通过 Experience 访问
update() {
const delta = this.experience.time.delta
this.mesh.rotation.y += delta * 0.001
}
如需创建独立于主场景的 3D 视图(如 UI 预览),不使用 Experience 单例:
// 参考 src/js/components/skin-preview-scene.js
class IndependentScene {
constructor(canvas) {
// 创建独立的 scene/camera/renderer
this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(...)
this.renderer = new THREE.WebGLRenderer({ canvas })
}
}
这是唯一允许不使用 Experience 单例的情况。
| 规则 | 要求 |
|------|------|
| 单例访问 | this.experience = new Experience() |
| 依赖提取 | 只取所需 |
| 调试方法 | debugInit()(统一命名) |
| 调试访问 | this.debug = this.experience.debug |
| 时间访问 | this.experience.time(不通过参数) |
| destroy | 涉及 Object3D 必须实现 |
| 事件清理 | destroy() 中 emitter.off() |
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer