Use when building Vue components that interact with Three.js, or when Three.js needs to communicate with UI. Covers strict layer separation, communication patterns, and forbidden practices.
ๆฌ้กน็ฎ้็จ ไธฅๆ ผ็ๅฑๅ็ฆปๆถๆ๏ผ
ไธคๅฑไน้ด ็ฆๆญข็ดๆฅๆไฝ๏ผๆๆ้ไฟก้่ฟ Pinia + mitt ๅฎๆใ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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 ๆธฒๆใ็ฉ็ใๅจ็ปใๆธธๆ้ป่พ | ็ฉๅฎถ็งปๅจใๅฐๅฝข็ๆใ็ธๆบๆงๅถ |
src/vue/
โโโ components/
โ โโโ hud/ # ๆธธๆๅ
HUD๏ผ่กๆกใๅฟซๆทๆ ็ญ๏ผ
โ โ โโโ HealthBar.vue
โ โ โโโ Hotbar.vue
โ โ โโโ GameHud.vue
โ โโโ menu/ # ่ๅ็ณป็ป
โ โ โโโ UiRoot.vue # ่ๅๆ น็ปไปถ
โ โ โโโ MainMenu.vue
โ โ โโโ PauseMenu.vue
โ โ โโโ SettingsMenu.vue
โ โโโ ui/ # ๅ
ฑไบซ UI ๅ
็ด
src/js/
โโโ experience.js # ๅไพๅ
ฅๅฃ
โโโ world/ # ๅบๆฏๅ
็ด
โโโ camera/ # ็ธๆบ็ณป็ป
โโโ interaction/ # ไบคไบ็ณป็ป
โโโ utils/
โโโ event-bus.js # mitt ๅฎไพ
ๅบๆฏ๏ผ็จๆทๅจ 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)
}
ๅบๆฏ๏ผ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)
})
<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>
| 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/ |
// 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>
// 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)
// 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 ็ๆ ๅต๏ผๅ ไธบๆฏ้็ฆป็้ข่งๅบๆฏใ
| ้ๆฑ | ่งฃๅณๆนๆก |
|---|---|
| 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 | ่ฟๅๅๅๆฐๆฎๆต |
| ๅฟๅไบไปถ็ๅฌๅจ | ๆ ๆณๆธ ็๏ผๅ ๅญๆณๆผ |