name: vtj-component-model description: Creates 3D components using the Experience singleton pattern. Use when building new classes in src/js/, implementing lifecycle methods, or managing Three.js objects.
vite-threejs 3D Component Model
Overview
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.
When to Use
- Creating any new class in
src/js/ - Building 3D objects, managers, controllers, or utilities that interact with the scene
- Wrapping Three.js functionality in reusable components
Component Template
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
}
}
}
Dependency Extraction Rules
只提取组件实际需要的依赖:
| 组件类型 | 典型依赖 |
|---|---|
| 渲染对象 | 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
// ... 大部分根本用不到
Lifecycle Methods
| 方法 | 必需条件 | 调用方 |
|---|---|---|
debugInit() |
有可调参数 | 构造函数,if (debug.active) |
update() |
有逐帧逻辑 | 父组件的 update() |
resize() |
响应尺寸变化 | 父组件的 resize() |
destroy() |
涉及 Object3D | 父组件的 destroy() |
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
}
Debug Access Pattern
统一使用以下方式访问调试系统:
// 在构造函数中
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判断是否启用
Common Mistakes
❌ 缺少 destroy() 方法
// BAD: 组件创建了 mesh 但没有 destroy
class BadComponent {
constructor() {
this.mesh = new THREE.Mesh(...)
this.scene.add(this.mesh)
}
// 没有 destroy() → 内存泄漏
}
❌ 调试方法命名不一致
// BAD: 使用其他命名
debuggerInit() { } // ❌
setDebug() { } // ❌
// GOOD: 统一命名
debugInit() { } // ✅
❌ 直接访问 debug.ui 而非 debug
// 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)
}
❌ 通过 update() 参数传递时间
// BAD: 通过参数传递
update(deltaTime) {
this.mesh.rotation.y += deltaTime
}
// GOOD: 通过 Experience 访问
update() {
const delta = this.experience.time.delta
this.mesh.rotation.y += delta * 0.001
}
Special Case: Independent Scene
如需创建独立于主场景的 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 单例的情况。
Quick Reference
| 规则 | 要求 |
|---|---|
| 单例访问 | this.experience = new Experience() |
| 依赖提取 | 只取所需 |
| 调试方法 | debugInit()(统一命名) |
| 调试访问 | this.debug = this.experience.debug |
| 时间访问 | this.experience.time(不通过参数) |
| destroy | 涉及 Object3D 必须实现 |
| 事件清理 | destroy() 中 emitter.off() |