vtj-component-model

star 144

Creates 3D components using the Experience singleton pattern. Use when building new classes in src/js/, implementing lifecycle methods, or managing Three.js objects.

hexianWeb By hexianWeb schedule Updated 2/2/2026

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()(不是 debugInitsetDebug
  • 通过 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()
Install via CLI
npx skills add https://github.com/hexianWeb/Third-Person-MC --skill vtj-component-model
Repository Details
star Stars 144
call_split Forks 37
navigation Branch main
article Path SKILL.md
More from Creator