AgentSkillsCN

vtj-component-model

使用 Experience 单例模式创建 3D 组件。当您需要在 src/js/ 中构建新类、实现生命周期方法,或管理 Three.js 对象时,可选用本技能。

SKILL.md
--- frontmatter
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

javascript
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
javascript
// ✅ 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() 方法

javascript
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

统一使用以下方式访问调试系统

javascript
// 在构造函数中
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()(不是 debugInitsetDebug
  • 通过 this.debug.ui 访问 Tweakpane 实例
  • 通过 this.debug.active 判断是否启用

Common Mistakes

❌ 缺少 destroy() 方法

javascript
// BAD: 组件创建了 mesh 但没有 destroy
class BadComponent {
  constructor() {
    this.mesh = new THREE.Mesh(...)
    this.scene.add(this.mesh)
  }
  // 没有 destroy() → 内存泄漏
}

❌ 调试方法命名不一致

javascript
// BAD: 使用其他命名
debuggerInit() { }      // ❌
setDebug() { }       // ❌

// GOOD: 统一命名
debugInit() { }   // ✅

❌ 直接访问 debug.ui 而非 debug

javascript
// BAD: 直接提取 ui
this.debug = this.experience.debug.ui // ❌

// GOOD: 提取 debug 对象
this.debug = this.experience.debug // ✅
// 然后通过 this.debug.ui 访问面板

❌ 事件监听未清理

javascript
// 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() 参数传递时间

javascript
// 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 单例

javascript
// 参考 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()