Vue 3 Integration - Ready to Use
Vue 3 is Already Integrated!
Everything is configured and works out of the box.
What's Included
1. Dependencies
package.json:
{
"dependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1"
}
}2. Vite Configuration
vite.config.ts:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()], // Vue plugin
resolve: {
alias: {
"@core": resolve(__dirname, "./src/core"),
"@game": resolve(__dirname, "./src/game"),
},
},
});3. TypeScript Support
src/shims-vue.d.ts:
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}4. HTML Container
index.html:
<style>
#ui-container {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 1000 !important;
pointer-events: none !important;
}
#ui-container > * {
pointer-events: auto !important;
}
</style>
<body>
<div id="game-container"></div>
<!-- #ui-container is created automatically by UIManager -->
</body>5. Core Systems
src/core/ui/UIManager.ts- UI layer managementsrc/core/ui/VueUIBridge.ts- Vue 3 integration- Event Bus - built-in
- Wrapper Component - automatic
How to Use
Step 1: Create Vue Component
src/game/ui/MyUI.vue:
<template>
<div class="my-ui">
<h1>{{ title }}</h1>
<p>{{ message }}</p>
<button @click="handleAction">Click me</button>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance } from "vue";
// Props (standard defineProps)
const props = defineProps<{
title: string;
message: string;
}>();
// Get globalEmit for events
const instance = getCurrentInstance();
const globalEmit = instance?.appContext.config.globalProperties.$emit;
// Emit events to Phaser
const handleAction = () => {
console.log("Button clicked!");
if (globalEmit) {
globalEmit("action-clicked");
}
};
</script>
<style scoped>
.my-ui {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 40px;
background: rgba(0, 0, 0, 0.9);
border: 2px solid white;
color: white;
font-family: monospace;
}
button {
padding: 10px 20px;
background: transparent;
border: 2px solid white;
color: white;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: white;
color: black;
}
</style>Step 2: Integrate into Phaser Scene
src/game/scenes/MyScene.ts:
import Phaser from "phaser";
import { UIManager } from "@core/ui/UIManager";
import { VueUIBridge } from "@core/ui/VueUIBridge";
import MyUI from "@game/ui/MyUI.vue"; // TypeScript sees .vue!
export class MyScene extends Phaser.Scene {
private uiManager!: UIManager;
private vueBridge!: VueUIBridge;
private score: number = 0;
create() {
// 1. Create UIManager
this.uiManager = new UIManager(this, {
containerId: "ui-container",
enableLogging: process.env.NODE_ENV === "development",
});
// 2. Create VueUIBridge
this.vueBridge = new VueUIBridge(this.uiManager);
// 3. Mount Vue component
this.vueBridge.mount("my-ui", MyUI, {
props: {
title: "My Game",
message: "Welcome!",
},
events: {
"action-clicked": () => {
console.log("Phaser received action-clicked!");
this.score += 10;
},
},
});
// 4. Show UI
this.vueBridge.show("my-ui", true); // With animation
}
update() {
// Update UI reactively!
this.vueBridge.updateProps("my-ui", {
message: `Score: ${this.score}`,
});
}
shutdown() {
// Automatic cleanup
this.vueBridge.unmountAll();
this.uiManager.shutdown();
}
}Done! Vue component works in Phaser game!
🔑 Key Concepts
1. Wrapper Component Pattern
Problem: defineProps doesn't react to external prop changes.
Solution in VueUIBridge:
const state = reactive(config.props || {});
const WrapperComponent = {
setup() {
return () => h(vueComponent, state);
// ↑
// h() recreates VNode when state changes!
},
};
const app = createApp(WrapperComponent);Why it works:
stateis a reactive objectupdateProps()changesstateh()is called again → new VNode- Vue diff → re-render!
- 100% reactivity!
2. globalEmit for Events
In Vue component:
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
const globalEmit = instance?.appContext.config.globalProperties.$emit;
const handleClick = () => {
globalEmit("my-event", { data: "value" });
// ↓
// VueUIBridge → config.events["my-event"]() → Phaser!
};IMPORTANT: Don't use defineEmits() - that's local emit!
// DOESN'T WORK:
const emit = defineEmits<{ (e: "my-event"): void }>();
emit("my-event"); // Won't reach Phaser!
// WORKS:
const globalEmit = instance?.appContext.config.globalProperties.$emit;
globalEmit("my-event"); // Reaches Phaser!3. Event Bus
Bidirectional communication:
Vue → Phaser:
// In Vue
globalEmit("button-click", { action: "start" });
// In Phaser
this.vueBridge.mount("ui", Component, {
events: {
"button-click": (data) => {
console.log(data.action); // "start"
},
},
});Phaser → Vue:
// In Phaser
this.vueBridge.updateProps("ui", {
score: 100,
health: 80,
});
// In Vue
// props update automatically!
const props = defineProps<{
score: number;
health: number;
}>();
// props.score → 1004. Lifecycle
create() {
// Initialize
this.uiManager = new UIManager(this);
this.vueBridge = new VueUIBridge(this.uiManager);
this.vueBridge.mount(...);
this.vueBridge.show(...);
}
update() {
// Reactive updates
this.vueBridge.updateProps("my-ui", { ... });
}
shutdown() {
// Automatic cleanup!
this.vueBridge.unmountAll();
this.uiManager.shutdown();
}Proven Patterns
Pattern 1: Game HUD
Reactive HUD that updates every frame:
<template>
<div class="hud">
<div class="timer">{{ formattedTime }}</div>
<div class="score">{{ score }}</div>
<div class="energy-bar" :style="{ width: energyPercent + '%' }"></div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
time: number;
score: number;
energy: number;
maxEnergy: number;
}>();
const formattedTime = computed(() => {
const m = Math.floor(props.time / 60);
const s = props.time % 60;
return `${m}:${s.toString().padStart(2, "0")}`;
});
const energyPercent = computed(() => {
return (props.energy / props.maxEnergy) * 100;
});
</script>Integration:
create() {
this.vueBridge.mount("hud", GameHUD, {
props: { time: 60, score: 0, energy: 100, maxEnergy: 100 }
});
this.vueBridge.show("hud");
}
update() {
// Updates automatically!
this.vueBridge.updateProps("hud", {
time: this.gameTime,
score: this.score,
energy: this.energy,
});
}Pattern 2: Main Menu
Menu with buttons and events:
<template>
<div class="menu">
<h1>{{ title }}</h1>
<button @click="handleStart">START</button>
<button @click="handleSettings">SETTINGS</button>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance } from "vue";
defineProps<{ title: string }>();
const instance = getCurrentInstance();
const globalEmit = instance?.appContext.config.globalProperties.$emit;
const handleStart = () => globalEmit("start-game");
const handleSettings = () => globalEmit("open-settings");
</script>Integration:
this.vueBridge.mount("main-menu", MainMenu, {
props: { title: "My Game" },
events: {
"start-game": () => this.scene.start("GameScene"),
"open-settings": () => this.scene.launch("SettingsScene"),
},
});Pattern 3: Theme Selector
Swiper for theme selection:
<template>
<div class="themes-swiper">
<div
v-for="theme in themes"
:key="theme.id"
class="theme-card"
:class="{ 'is-active': theme.id === currentThemeId }"
@click="handleThemeClick(theme.id)"
>
{{ theme.name }}
</div>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance } from "vue";
defineProps<{
currentThemeId: string;
themes: Array<{ id: string; name: string }>;
}>();
const instance = getCurrentInstance();
const globalEmit = instance?.appContext.config.globalProperties.$emit;
const handleThemeClick = (themeId: string) => {
globalEmit("change-theme", themeId);
};
</script>
<style scoped>
.themes-swiper {
display: flex;
gap: 20px;
overflow-x: auto;
justify-content: space-evenly;
}
.theme-card.is-active {
border: 3px solid white; /* Active theme */
}
</style>Integration:
this.vueBridge.mount("themes", ThemeSelector, {
props: {
currentThemeId: "minimal",
themes: [
{ id: "minimal", name: "Minimal" },
{ id: "neon", name: "Neon" },
],
},
events: {
"change-theme": (themeId: string) => {
this.applyTheme(themeId);
// Update UI for visual indication
this.vueBridge.updateProps("themes", {
currentThemeId: themeId,
});
},
},
});Technical Details
Event Flow
User clicks button in Vue
↓
handleClick() calls globalEmit("event")
↓
VueUIBridge.$emit
↓
config.events["event"]() called
↓
Phaser scene method executedProps Update Flow
Phaser: updateProps({ score: 100 })
↓
VueUIBridge: Object.assign(state, { score: 100 })
↓
state = reactive object
↓
WrapperComponent.setup() → h(Component, state)
↓
h() sees state change → creates new VNode
↓
Vue diff → re-render
↓
UI updates!Wrapper Component Pattern
Inside VueUIBridge:
const state = reactive(config.props || {});
const WrapperComponent = {
setup() {
return () => h(vueComponent, state);
// ↑
// Render function - recreates VNode
// on any state change!
},
};
const app = createApp(WrapperComponent);This guarantees:
- 100% reactivity
- Works with
defineProps - No hacks needed
- Standard Vue code
Approach Comparison
Doesn't work:
// Direct props passing
const app = createApp(MyComponent, state);
// defineProps DOESN'T see state changes!Doesn't work:
// Spread operator
const app = createApp(MyComponent, { ...state });
// Object copy → no reactivity!Works:
// Wrapper Component
const Wrapper = {
setup() {
return () => h(MyComponent, state);
},
};
const app = createApp(Wrapper);
// h() recreates VNode → reactivity!Best Practices
1. Always use getCurrentInstance()
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
const globalEmit = instance?.appContext.config.globalProperties.$emit;
const handleEvent = () => {
globalEmit("my-event", data); //
};2. Props only for display
// Correct:
const props = defineProps<{
score: number;
health: number;
}>();
// Use only for display!
// Wrong:
const emit = defineEmits(); // Won't reach Phaser!3. updateProps for updates
// In Phaser update()
update() {
this.vueBridge.updateProps("hud", {
score: this.score,
time: this.gameTime,
});
}
// Vue re-renders automatically!4. Use computed
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
time: number;
energy: number;
maxEnergy: number;
}>();
// Computed for calculations
const formattedTime = computed(() => {
const m = Math.floor(props.time / 60);
const s = props.time % 60;
return `${m}:${s.toString().padStart(2, "0")}`;
});
const energyPercent = computed(() => {
return (props.energy / props.maxEnergy) * 100;
});
</script>🐛 Troubleshooting
Problem: Component doesn't update
Cause: Props not reactive
Solution: Check that you're using VueUIBridge from @core/ui/, not creating your own! VueUIBridge uses Wrapper Component.
Problem: Events don't work
Cause: Using local emit from defineEmits
Solution: Use globalEmit:
const instance = getCurrentInstance();
const globalEmit = instance?.appContext.config.globalProperties.$emit;
const handleClick = () => {
globalEmit("my-event"); //
};Problem: UI not centered
Cause: Container CSS incorrect
Solution:
<style scoped>
.my-component {
position: fixed; /* Not absolute! */
top: 0;
left: 0;
width: 100vw; /* Not 100%! */
height: 100vh; /* Not 100%! */
display: flex;
align-items: center;
justify-content: center;
}
</style>Problem: Clicks don't work
Cause: pointer-events: none on parent
Solution:
<style scoped>
.my-component {
pointer-events: auto; /* Enable events */
}
button {
pointer-events: auto; /* And on buttons */
}
</style>Checklist
Before using, make sure:
- [x]
vueinstalled inpackage.json - [x]
@vitejs/plugin-vuein devDependencies - [x]
vite.config.tswithvue()plugin - [x]
src/shims-vue.d.tsexists - [x]
#ui-containerstyles inindex.html - [x] Import
UIManagerfrom@core/ui/UIManager - [x] Import
VueUIBridgefrom@core/ui/VueUIBridge - [x] Use
getCurrentInstance()forglobalEmit - [x] Call
shutdown()in scene
All set? Start creating UI!
📚 Additional Resources
Vue 3 is ready to use! Just import and create UI!