vite-threejs State Management (Pinia + mitt)
Overview
ๆฌ้กน็ฎไฝฟ็จ ๅ้้็ถๆ็ฎก็๏ผ
- Pinia๏ผๆไน
ๅ็ถๆ๏ผVue ๅ Three.js ๅๅๅๆญฅ
- mitt๏ผๅณๆถไบไปถ้็ฅ๏ผ่ทจๅฑ้ไฟก
ๆ ธๅฟๅๅ๏ผPinia ็ฎก็ถๆ๏ผmitt ็ฎกไบไปถใ็ถๆๅๆด้่ฟ Pinia๏ผ้็ฅ Three.js ้่ฟ mitt emitใ
When to Use
- ้่ฆๅจ Vue UI ๅ Three.js ไน้ดๅ
ฑไบซ็ถๆ
- ้่ฆไป UI ่งฆๅ 3D ๅบๆฏ่กไธบ
- ้่ฆไป 3D ๅบๆฏ้็ฅ UI ๆดๆฐ
- ็ฎก็ๆธธๆ่ฎพ็ฝฎใ็จๆทๅๅฅฝ็ญๆไน
็ถๆ
ๅ้้ๆถๆ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Vue UI Layer โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ Components โโโโโโโโโโโบโ Pinia Stores โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโ
โ emitter.emit()
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ mitt Event Bus โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ emitter.on()
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Three.js Layer โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ Components โโโโโโโโโโโบโ Experience โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Pinia Store ๆจกๅผ
Store ๅฎไน
// src/pinia/settingsStore.js
import emitter from '@three/utils/event-bus.js'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSettingsStore = defineStore('settings', () => {
// ็ถๆ
const shadowQuality = ref('high')
// Action๏ผไฟฎๆน็ถๆ + ้็ฅ Three.js
function setShadowQuality(quality) {
shadowQuality.value = quality
emitter.emit('shadow:quality-changed', quality) // ้็ฅ Three.js
saveSettings() // ๆไน
ๅ
}
return { shadowQuality, setShadowQuality }
})
ๅจ Vue ไธญไฝฟ็จ
<script setup>
import { useSettingsStore } from '@pinia/settingsStore.js'
const settings = useSettingsStore()
function handleQualityChange(quality) {
settings.setShadowQuality(quality)
}
</script>
<template>
<select v-model="settings.shadowQuality" @change="handleQualityChange">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</template>
ๅจ Three.js ไธญไฝฟ็จ
// ๆนๅผไธ๏ผ็ๅฌ mitt ไบไปถ๏ผๆจ่๏ผ
import emitter from './utils/event-bus.js'
emitter.on('shadow:quality-changed', (quality) => {
this.updateShadowQuality(quality)
})
// ๆนๅผไบ๏ผ็ดๆฅ่ฏปๅ Pinia๏ผ้่ฆๆถ๏ผ
import { useSettingsStore } from '@pinia/settingsStore.js'
const settings = useSettingsStore()
const quality = settings.shadowQuality
mitt Event Bus
ไบไปถๆป็บฟๅฎไน
// src/js/utils/event-bus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter
ไบไปถๅฝๅ่ง่
| ๅ็ผ |
ๆฅๆบ |
็จ้ |
็คบไพ |
ui: |
Vue UI |
UI ็ถๆๅๅ |
ui:pause-changed |
game: |
Vue/Three.js |
ๆธธๆ้ป่พไบไปถ |
game:create_world |
settings: |
Pinia Store |
่ฎพ็ฝฎๅๆด้็ฅ |
settings:environment-changed |
core: |
Experience |
ๆ ธๅฟ็ณป็ปไบไปถ |
core:ready, core:resize |
shadow: |
ๆธฒๆ็ธๅ
ณ |
้ดๅฝฑ่ฎพ็ฝฎ |
shadow:quality-changed |
ไบไปถไฝฟ็จๆจกๅผ
// ๅ้ไบไปถ๏ผVue/Pinia ๅฑ๏ผ
emitter.emit('game:create_world', { seed, terrain, trees })
emitter.emit('ui:pause-changed', true)
emitter.emit('settings:environment-changed', { skyMode: 'HDR' })
// ็ๅฌไบไปถ๏ผThree.js ๅฑ๏ผ
emitter.on('game:create_world', ({ seed, terrain, trees }) => {
this.world.reset({ seed, terrain, trees })
})
emitter.on('ui:pause-changed', (paused) => {
this.isPaused = paused
})
// ็งป้ค็ๅฌ๏ผdestroy ๆถ๏ผ
this._boundHandler = this.handleEvent.bind(this)
emitter.on('some:event', this._boundHandler)
// ...
emitter.off('some:event', this._boundHandler)
้ไฟกๆจกๅผ
ๆจกๅผไธ๏ผUI โ Three.js๏ผๆๅธธ็จ๏ผ
// 1. Pinia Store ไธญๅฎไน action
function setEnvFogDensity(value) {
envFogDensity.value = value
emitter.emit('settings:environment-changed', { fogDensity: value })
saveSettings()
}
// 2. Three.js ็ปไปถ็ๅฌ
emitter.on('settings:environment-changed', (patch) => {
if (patch.fogDensity !== undefined) {
this.updateFog(patch.fogDensity)
}
})
ๆจกๅผไบ๏ผThree.js โ UI
// 1. Three.js ไธญๅ้ไบไปถ
emitter.emit('game:player-health-changed', { health: 80, maxHealth: 100 })
// 2. Vue ็ปไปถ็ๅฌ
import emitter from '@three/utils/event-bus.js'
import { onMounted, onUnmounted, ref } from 'vue'
const health = ref(100)
onMounted(() => {
emitter.on('game:player-health-changed', handleHealthChange)
})
onUnmounted(() => {
emitter.off('game:player-health-changed', handleHealthChange)
})
function handleHealthChange({ health: newHealth }) {
health.value = newHealth
}
ๆจกๅผไธ๏ผ็ถๆๅๆญฅ๏ผๅๅ๏ผ
// Pinia Store ๆๆ็ถๆ
const isPaused = ref(false)
function toPauseMenu() {
isPaused.value = true
emitter.emit('ui:pause-changed', true)
}
function toPlaying() {
isPaused.value = false
emitter.emit('ui:pause-changed', false)
}
// Three.js ๅฑๅๅบ
emitter.on('ui:pause-changed', (paused) => {
this.isPaused = paused
})
็ฐๆ Stores
| Store |
ๆไปถ |
่่ดฃ |
useUiStore |
uiStore.js |
ๅฑๅน็ถๆใ่ๅๅฏผ่ชใไธ็็ฎก็ |
useSettingsStore |
settingsStore.js |
ๆธธๆ่ฎพ็ฝฎใๆไน
ๅใThree.js ้็ฅ |
useHudStore |
hudStore.js |
HUD ็ถๆ๏ผ่ก้ใๅฟซๆทๆ ็ญ๏ผ |
useSkinStore |
skinStore.js |
็ฎ่ค้ๆฉ็ถๆ |
็ฐๆไบไปถ
core: ็ณป็ปไบไปถ
| ไบไปถ |
ๆฐๆฎ |
่งฆๅๆถๆบ |
core:ready |
- |
ๆๆ่ตๆบๅ ่ฝฝๅฎๆ |
core:resize |
- |
็ชๅฃๅฐบๅฏธๅๅ |
core:tick |
- |
ๆฏๅธงๆดๆฐ |
ui: UI ไบไปถ
| ไบไปถ |
ๆฐๆฎ |
่งฆๅๆถๆบ |
ui:pause-changed |
boolean |
ๆๅ/ๆขๅค |
game: ๆธธๆไบไปถ
| ไบไปถ |
ๆฐๆฎ |
่งฆๅๆถๆบ |
game:create_world |
{ seed, terrain, trees } |
ๅๅปบๆฐไธ็ |
game:reset_world |
{ seed, terrain, trees } |
้็ฝฎไธ็ |
game:request_pointer_lock |
- |
่ฏทๆฑ้ๅฎ้ผ ๆ |
settings: ่ฎพ็ฝฎไบไปถ
| ไบไปถ |
ๆฐๆฎ |
่งฆๅๆถๆบ |
settings:environment-changed |
{ skyMode?, sunIntensity?, ... } |
็ฏๅข่ฎพ็ฝฎๅๆด |
settings:postprocess-changed |
{ speedLines } |
ๅๆๅค็ๅๆด |
settings:camera-rig-changed |
{ fov, bobbing } |
็ธๆบ่ฎพ็ฝฎๅๆด |
settings:chunks-changed |
{ viewDistance?, unloadPadding? } |
ๅบๅ่ฎพ็ฝฎๅๆด |
settings:front-view-changed |
{ enabled } |
ๅ่งๅพๅผๅ
ณ |
settings:mouse-sensitivity-changed |
number |
้ผ ๆ ็ตๆๅบฆๅๆด |
shadow: ้ดๅฝฑไบไปถ
| ไบไปถ |
ๆฐๆฎ |
่งฆๅๆถๆบ |
shadow:quality-changed |
string |
้ดๅฝฑ่ดจ้ๅๆด |
Common Mistakes
โ ็ดๆฅๅจ Vue ไธญๆไฝ Three.js
// BAD: Vue ็ปไปถ็ดๆฅๆไฝ Three.js
import Experience from '@three/experience.js'
function handleClick() {
const exp = new Experience()
exp.world.player.setPosition(0, 0, 0) // โ ็ดๆฅๆไฝ
}
// GOOD: ้่ฟ mitt ไบไปถ
function handleClick() {
emitter.emit('game:player-teleport', { x: 0, y: 0, z: 0 })
}
โ ๅฟ่ฎฐๆธ
็ไบไปถ็ๅฌ
// BAD: ๆฒกๆๆธ
็
constructor() {
emitter.on('some:event', (data) => this.handle(data))
}
// GOOD: ไฟๅญๅผ็จๅนถๆธ
็
constructor() {
this._boundHandler = this.handle.bind(this)
emitter.on('some:event', this._boundHandler)
}
destroy() {
emitter.off('some:event', this._boundHandler)
}
โ ๅจ 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.emit('pauseChanged', true)
emitter.emit('CreateWorld', data)
// GOOD: ไฝฟ็จ่ง่ๅ็ผ
emitter.emit('ui:pause-changed', true)
emitter.emit('game:create_world', data)
โ ไบไปถๆฐๆฎ็ปๆไธไธ่ด
// BAD: ๆๆถไผ ๅฏน่ฑก๏ผๆๆถไผ ๅๅงๅผ
emitter.emit('settings:fog-changed', 0.01)
emitter.emit('settings:fog-changed', { density: 0.01 })
// GOOD: ็ปไธไฝฟ็จๅฏน่ฑก๏ผๅฏๆฉๅฑ๏ผ
emitter.emit('settings:environment-changed', { fogDensity: 0.01 })
Quick Reference
| ๅบๆฏ |
ไฝฟ็จ |
| ๆไน
ๅ็ถๆ |
Pinia Store |
| UI โ Three.js ้็ฅ |
Pinia action + emitter.emit() |
| Three.js โ UI ้็ฅ |
emitter.emit() |
| Three.js ่ฏปๅ็ถๆ |
Pinia Store ๆ emitter.on() |
| ไบไปถๆธ
็ |
destroy() ไธญ emitter.off() |
| ไบไปถๅ็ผ |
็จ้ |
core: |
็ณป็ป็บง๏ผready/resize/tick๏ผ |
ui: |
UI ็ถๆๅๅ |
game: |
ๆธธๆ้ป่พ |
settings: |
่ฎพ็ฝฎๅๆด |
shadow: |
ๆธฒๆ็ธๅ
ณ |