| 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 ๅบๆฏ
function toPlaying() {
screen.value = 'playing'
isPaused.value = false
emitter.emit('ui:pause-changed', false)
emitter.emit('game:request_pointer_lock')
}
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
takeDamage(amount) {
this.health -= amount
emitter.emit('game:player-health-changed', {
health: this.health,
maxHealth: this.maxHealth
})
}
<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 ้่ฆๅๅบ
function setShadowQuality(quality) {
shadowQuality.value = quality
emitter.emit('shadow:quality-changed', quality)
saveSettings()
}
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 ็ปไปถไธญไฝฟ็จๆ ๅๅซๅ๏ผ
import emitter from '@three/utils/event-bus.js'
import { useUiStore } from '@pinia/uiStore.js'
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
import Experience from '@three/experience.js'
function handleTeleport() {
const exp = new Experience()
exp.world.player.setPosition(0, 0, 0)
}
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
import { useUiStore } from '@pinia/uiStore.js'
function onPlayerDeath() {
const ui = useUiStore()
ui.screen = 'gameOver'
}
function onPlayerDeath() {
emitter.emit('game:player-died')
}
emitter.on('game:player-died', () => {
ui.toGameOver()
})
โ ๅฟๅๅฝๆฐๆ ๆณๆธ
็
emitter.on('game:event', (data) => this.handle(data))
this._boundHandler = this.handle.bind(this)
emitter.on('game:event', this._boundHandler)
emitter.off('game:event', this._boundHandler)
โ ๅจ Vue ไธญๅฏผๅ
ฅ Three.js ็ปไปถ็ฑป
import Player from '@three/world/player.js'
const player = new Player()
emitter.emit('game:spawn-player', { x: 0, y: 0, z: 0 })
ๅ
่ฎธ็ไพๅค
็ฌ็ซ้ข่งๅบๆฏ
็จไบ UI ้ข่ง็็ฌ็ซ Three.js ๅบๆฏ๏ผๅฆ็ฎ่ค้ข่ง๏ผๅฏไปฅๅจ Vue ็ปไปถไธญๅๅปบ๏ผ
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 | ่ฟๅๅๅๆฐๆฎๆต |
| ๅฟๅไบไปถ็ๅฌๅจ | ๆ ๆณๆธ
็๏ผๅ
ๅญๆณๆผ |