vtj-ui-integration

star 144

Integrates Vue UI with Three.js while maintaining strict separation. Use when building HUDs, menus, or handling UI-to-scene communication via events.

hexianWeb By hexianWeb schedule Updated 2/2/2026

name: vtj-ui-integration description: Integrates Vue UI with Three.js while maintaining strict separation. Use when building HUDs, menus, or handling UI-to-scene communication via events.

vite-threejs UI Integration (Vue ↔ Three.js)

Overview

本项目采用 严格的层分离架构

  • Vue 层:UI、用户输入、菜单、HUD
  • Three.js 层:3D 场景、渲染、游戏逻辑

两层之间 禁止直接操作,所有通信通过 Pinia + mitt 完成。

When to Use

  • 创建需要与 3D 场景交互的 Vue 组件
  • 从 Three.js 更新 UI 状态
  • 处理用户输入并影响 3D 场景
  • 管理游戏状态(暂停、菜单、设置)

架构图

┌─────────────────────────────────────────────────────────────┐
│                         Vue UI Layer                         │
│  ┌─────────────────┐          ┌─────────────────┐           │
│  │   Components    │◄────────►│   Pinia Stores  │           │
│  │   (src/vue/)    │          │   (src/pinia/)  │           │
│  └─────────────────┘          └────────┬────────┘           │
└────────────────────────────────────────┼────────────────────┘
                                         │ emitter.emit()
                                         ▼
┌─────────────────────────────────────────────────────────────┐
│                      mitt Event Bus                          │
│                  (src/js/utils/event-bus.js)                 │
└─────────────────────────────────────────────────────────────┘
                                         │ emitter.on()
                                         ▼
┌─────────────────────────────────────────────────────────────┐
│                      Three.js Layer                          │
│  ┌─────────────────┐          ┌─────────────────┐           │
│  │   Components    │◄────────►│   Experience    │           │
│  │   (src/js/)     │          │   (singleton)   │           │
│  └─────────────────┘          └─────────────────┘           │
└─────────────────────────────────────────────────────────────┘

职责分离

职责 示例
Vue 界面渲染、用户输入、菜单导航 HUD、暂停菜单、设置面板
Three.js 3D 渲染、物理、动画、游戏逻辑 玩家移动、地形生成、相机控制

Vue 层 (src/vue/)

src/vue/
├── components/
│   ├── hud/           # 游戏内 HUD(血条、快捷栏等)
│   │   ├── HealthBar.vue
│   │   ├── Hotbar.vue
│   │   └── GameHud.vue
│   ├── menu/          # 菜单系统
│   │   ├── UiRoot.vue       # 菜单根组件
│   │   ├── MainMenu.vue
│   │   ├── PauseMenu.vue
│   │   └── SettingsMenu.vue
│   └── ui/            # 共享 UI 元素

Three.js 层 (src/js/)

src/js/
├── experience.js      # 单例入口
├── world/             # 场景元素
├── camera/            # 相机系统
├── interaction/       # 交互系统
└── utils/
    └── event-bus.js   # mitt 实例

通信模式

模式一:UI → Three.js(最常用)

场景:用户在 UI 操作,需要影响 3D 场景

// 1. Pinia Store 定义 action
// src/pinia/uiStore.js
function toPlaying() {
  screen.value = 'playing'
  isPaused.value = false
  emitter.emit('ui:pause-changed', false)  // 通知 Three.js
  emitter.emit('game:request_pointer_lock')
}

// 2. Three.js 组件监听
// src/js/world/player.js
constructor() {
  this._handlePause = this._handlePause.bind(this)
  emitter.on('ui:pause-changed', this._handlePause)
}

_handlePause(paused) {
  this.isPaused = paused
}

destroy() {
  emitter.off('ui:pause-changed', this._handlePause)
}

模式二:Three.js → UI

场景:3D 场景状态变化,需要更新 UI

// 1. Three.js 中发送事件
// src/js/world/player.js
takeDamage(amount) {
  this.health -= amount
  emitter.emit('game:player-health-changed', { 
    health: this.health, 
    maxHealth: this.maxHealth 
  })
}

// 2. Vue 组件监听
// src/vue/components/hud/HealthBar.vue
<script setup>
import emitter from '@three/utils/event-bus.js'
import { onMounted, onUnmounted, ref } from 'vue'

const health = ref(100)
const maxHealth = ref(100)

function handleHealthChange({ health: h, maxHealth: max }) {
  health.value = h
  maxHealth.value = max
}

onMounted(() => {
  emitter.on('game:player-health-changed', handleHealthChange)
})

onUnmounted(() => {
  emitter.off('game:player-health-changed', handleHealthChange)
})
</script>

模式三:设置变更

场景:用户修改设置,Three.js 需要响应

// 1. Pinia Store
// src/pinia/settingsStore.js
function setShadowQuality(quality) {
  shadowQuality.value = quality
  emitter.emit('shadow:quality-changed', quality)
  saveSettings()
}

// 2. Three.js Renderer 响应
// src/js/renderer.js
emitter.on('shadow:quality-changed', (quality) => {
  this.updateShadowMapSize(quality)
})

Vue 组件模板

带事件监听的组件

<script setup>
import emitter from '@three/utils/event-bus.js'
import { onMounted, onUnmounted, ref } from 'vue'

// 状态
const value = ref(0)

// 事件处理函数(必须保存引用以便清理)
function handleEvent(data) {
  value.value = data.value
}

// 生命周期
onMounted(() => {
  emitter.on('game:some-event', handleEvent)
})

onUnmounted(() => {
  emitter.off('game:some-event', handleEvent)  // 必须清理!
})

// 发送事件到 Three.js
function triggerAction() {
  emitter.emit('ui:some-action', { param: 'value' })
}
</script>

菜单组件

<script setup>
import { useUiStore } from '@pinia/uiStore.js'

const ui = useUiStore()

function handlePlay() {
  ui.toPlaying()  // Pinia action 内部会 emit 事件
}

function handleSettings() {
  ui.toSettings('mainMenu')
}
</script>

<template>
  <div class="menu">
    <button @click="handlePlay">Play</button>
    <button @click="handleSettings">Settings</button>
  </div>
</template>

现有 Stores

Store 文件 职责
useUiStore uiStore.js 屏幕状态、菜单导航、世界管理
useSettingsStore settingsStore.js 游戏设置、持久化、Three.js 通知
useHudStore hudStore.js HUD 状态(血量、快捷栏等)
useSkinStore skinStore.js 皮肤选择状态

事件命名规范

详见 vtj-state-management skill,此处简要回顾:

前缀 来源 示例
ui: Vue UI ui:pause-changed
game: 游戏逻辑 game:create_world
settings: 设置变更 settings:environment-changed
core: 系统级 core:ready, core:resize

路径别名

Vue 组件中使用标准别名:

// ✅ GOOD: 使用别名
import emitter from '@three/utils/event-bus.js'
import { useUiStore } from '@pinia/uiStore.js'

// ❌ BAD: 相对路径
import emitter from '../../../js/utils/event-bus.js'
别名 路径
@ src/
@ui src/vue/
@ui-components src/vue/components/
@pinia src/pinia/
@three src/js/

Common Mistakes

❌ 在 Vue 中直接操作 Three.js

// BAD: 直接操作 Three.js 实例
import Experience from '@three/experience.js'

function handleTeleport() {
  const exp = new Experience()
  exp.world.player.setPosition(0, 0, 0)  // ❌ 违反层分离
}

// GOOD: 通过事件通信
function handleTeleport() {
  emitter.emit('game:player-teleport', { x: 0, y: 0, z: 0 })
}

❌ 忘记清理事件监听

<script setup>
// BAD: 没有 onUnmounted 清理
onMounted(() => {
  emitter.on('game:event', handleEvent)  // 内存泄漏!
})

// GOOD: 完整生命周期
onMounted(() => {
  emitter.on('game:event', handleEvent)
})

onUnmounted(() => {
  emitter.off('game:event', handleEvent)
})
</script>

❌ 在 Three.js 中直接修改 Pinia

// BAD: Three.js 直接写 Pinia
import { useUiStore } from '@pinia/uiStore.js'

function onPlayerDeath() {
  const ui = useUiStore()
  ui.screen = 'gameOver'  // ❌ 跨层直接操作
}

// GOOD: 通过事件让 Vue 层处理
function onPlayerDeath() {
  emitter.emit('game:player-died')
}

// Vue 层响应
emitter.on('game:player-died', () => {
  ui.toGameOver()
})

❌ 匿名函数无法清理

// BAD: 匿名函数
emitter.on('game:event', (data) => this.handle(data))  // 无法 off

// GOOD: 保存引用
this._boundHandler = this.handle.bind(this)
emitter.on('game:event', this._boundHandler)
// ...
emitter.off('game:event', this._boundHandler)

❌ 在 Vue 中导入 Three.js 组件类

// BAD: 导入并实例化 Three.js 组件
import Player from '@three/world/player.js'

const player = new Player()  // ❌ Vue 不应该创建 3D 组件

// GOOD: 只通过事件/状态交互
emitter.emit('game:spawn-player', { x: 0, y: 0, z: 0 })

允许的例外

独立预览场景

用于 UI 预览的独立 Three.js 场景(如皮肤预览)可以在 Vue 组件中创建:

// src/js/components/skin-preview-scene.js
// 这是独立场景,不使用 Experience 单例
class SkinPreviewScene {
  constructor(canvas) {
    this.scene = new THREE.Scene()
    this.camera = new THREE.PerspectiveCamera(...)
    this.renderer = new THREE.WebGLRenderer({ canvas })
  }
}
<!-- src/vue/components/menu/SkinSelector.vue -->
<script setup>
import SkinPreviewScene from '@three/components/skin-preview-scene.js'
import { onMounted, onUnmounted, ref } from 'vue'

const canvasRef = ref(null)
let preview = null

onMounted(() => {
  preview = new SkinPreviewScene(canvasRef.value)
})

onUnmounted(() => {
  preview?.destroy()
})
</script>

这是唯一允许 Vue 直接操作 Three.js 的情况,因为是隔离的预览场景。

Quick Reference

需求 解决方案
UI 触发 3D 动作 Pinia action → emitter.emit()
3D 更新 UI emitter.emit() → Vue emitter.on()
共享状态 Pinia Store
即时通知 mitt 事件
Vue 读取 3D 状态 通过 Pinia(3D 写入 Pinia 或 emit 事件)
禁止 原因
Vue 直接操作 Experience 破坏层分离
Vue 导入 3D 组件类 职责混淆
Three.js 直接写 Pinia 违反单向数据流
匿名事件监听器 无法清理,内存泄漏
Install via CLI
npx skills add https://github.com/hexianWeb/Third-Person-MC --skill vtj-ui-integration
Repository Details
star Stars 144
call_split Forks 37
navigation Branch main
article Path SKILL.md
More from Creator