Phaser محبوبترین فریمورک game development در جاوااسکریپت است. در این مقاله، یاد میگیریم چگونه Phaser را با Vue ادغام کنیم و یک game کامل بسازیم. از ساختار پروژه تا physics، animations و leaderboard را پوشش میدهیم.
چرا Phaser با Vue
Phaser یک game framework قدرتمند با پشتیبانی از 2D graphics، physics، animations، sprites، tilemaps و بسیاری قابلیتهای دیگر است. اگرچه Vue اصلی برای ساخت game نیست، اما ترکیب Vue و Phaser یک معماری فوقالعاده ایجاد میکند.
Vue برای UI اطراف game عالی است: main menu، game over screens، leaderboard، settings panel، HUD. Phaser برای رندر game و منطق بازی مسئول است. این جداسازی مسئولیتها باعث میشود کد تمیزتر و قابل نگهداریتر باشد.
اپلیکیشنهای موفقی مثل Vampire Survivors و بسیاری از .io games با Phaser ساخته شدهاند. اگر game ایدهای در ذهن دارید، Phaser احتمالاً میتواند آن را پیادهسازی کند. برای 3D games باید سراغ Three.js یا Babylon.js بروید.
راهاندازی پروژه
شروع پروژه Vue با Phaser ساده است. فقط Phaser را نصب کنید و در component مناسب init کنید.
npm install phaser
// src/game/config.js
import Phaser from "phaser"
import MainScene from "./scenes/MainScene"
import GameScene from "./scenes/GameScene"
import GameOverScene from "./scenes/GameOverScene"
export function createGameConfig(parent) {
return {
type: Phaser.AUTO,
parent,
width: 800,
height: 600,
backgroundColor: "#1a0f0f",
physics: {
default: "arcade",
arcade: {
gravity: { y: 300 },
debug: false
}
},
scene: [MainScene, GameScene, GameOverScene],
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
}
}
}
// src/components/PhaserGame.vue
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue"
import Phaser from "phaser"
import { createGameConfig } from "@/game/config"
const gameContainer = ref(null)
let game = null
onMounted(() => {
const config = createGameConfig(gameContainer.value)
game = new Phaser.Game(config)
// Make game instance accessible globally for Vue-Phaser communication
window.gameInstance = game
})
onBeforeUnmount(() => {
game?.destroy(true)
delete window.gameInstance
})
defineExpose({
getGame: () => game
})
</script>
<template>
<div ref="gameContainer" class="phaser-game"></div>
</template>
<style scoped>
.phaser-game {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.phaser-game canvas {
max-width: 100%;
max-height: 100%;
}
</style>
Phaser از event system داخلی خودش استفاده میکند که با Vue reactivity متفاوت است. برای ارتباط بین Vue و Phaser، از event emitter یا یک Pinia store مشترک استفاده کنید.
اولین Scene
Scene واحد اصلی سازماندهی در Phaser است. هر صفحه game (menu، gameplay، game over) یک scene است.
// src/game/scenes/MainScene.js
import Phaser from "phaser"
export default class MainScene extends Phaser.Scene {
constructor() {
super({ key: "MainScene" })
}
preload() {
// Load assets
this.load.image("logo", "/game/logo.png")
this.load.image("button", "/game/button.png")
}
create() {
const { width, height } = this.scale
// Background
this.add.rectangle(0, 0, width, height, 0x1a0f0f).setOrigin(0)
// Title
this.add.text(width / 2, height / 3, "بازی من", {
fontSize: "64px",
color: "#F095A5",
fontFamily: "Vazirmatn"
}).setOrigin(0.5)
this.add.text(width / 2, height / 3 + 70, "یک ماجراجویی هیجانانگیز", {
fontSize: "20px",
color: "#ffffff"
}).setOrigin(0.5)
// Play button
const playButton = this.add.text(width / 2, height / 2 + 50, "شروع بازی", {
fontSize: "32px",
color: "#ffffff",
backgroundColor: "#660000",
padding: { x: 30, y: 15 }
}).setOrigin(0.5).setInteractive()
playButton.on("pointerover", () => {
playButton.setStyle({ backgroundColor: "#8B2635" })
playButton.setScale(1.1)
})
playButton.on("pointerout", () => {
playButton.setStyle({ backgroundColor: "#660000" })
playButton.setScale(1)
})
playButton.on("pointerdown", () => {
this.cameras.main.fadeOut(500, 0, 0, 0)
this.time.delayedCall(500, () => {
this.scene.start("GameScene")
})
})
}
}
محصول اورجینال INZIP
دانلود پوسته وردپرس باریستا؛ قالب کافیشاپ، رُستری قهوه و فروشگاه قهوه دوزبانه با منوی رومیزی، رزرو آنلاین میز با کد پیامکی و ووکامرس. طراحی اختصاصی inzip.ir
تحویل آنی
آپدیت رایگان
بدون قفل لایسنس
پشتیبانی واقعی
٪۲۶
تخفیف ویژه
۱,۰۳۰,۰۰۰ تومان
Scene بازی اصلی
GameScene جایی است که منطق بازی اجرا میشود. physics، input، animations و game loop در اینجا هستند.
// src/game/scenes/GameScene.js
import Phaser from "phaser"
import { useGameStore } from "@/stores/game"
export default class GameScene extends Phaser.Scene {
constructor() {
super({ key: "GameScene" })
}
preload() {
this.load.image("player", "/game/player.png")
this.load.image("enemy", "/game/enemy.png")
this.load.image("coin", "/game/coin.png")
this.load.image("platform", "/game/platform.png")
this.load.audio("jump", "/game/sounds/jump.mp3")
this.load.audio("collect", "/game/sounds/collect.mp3")
}
create() {
this.gameStore = useGameStore()
this.gameStore.resetScore()
const { width, height } = this.scale
// Platforms
this.platforms = this.physics.add.staticGroup()
this.platforms.create(width / 2, height - 20, "platform").setScale(2, 1).refreshBody()
this.platforms.create(200, 400, "platform")
this.platforms.create(600, 300, "platform")
this.platforms.create(400, 200, "platform")
// Player
this.player = this.physics.add.sprite(100, 450, "player")
this.player.setBounce(0.2)
this.player.setCollideWorldBounds(true)
// Player animations
this.anims.create({
key: "idle",
frames: [{ key: "player", frame: 0 }],
frameRate: 10
})
// Coins
this.coins = this.physics.add.group({
key: "coin",
repeat: 11,
setXY: { x: 12, y: 0, stepX: 70 }
})
this.coins.children.iterate((coin) => {
coin.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8))
})
// Enemies
this.enemies = this.physics.add.group()
this.spawnEnemy()
// Spawn enemy every 5 seconds
this.time.addEvent({
delay: 5000,
callback: this.spawnEnemy,
callbackScope: this,
loop: true
})
// Collisions
this.physics.add.collider(this.player, this.platforms)
this.physics.add.collider(this.coins, this.platforms)
this.physics.add.collider(this.enemies, this.platforms)
this.physics.add.overlap(this.player, this.coins, this.collectCoin, null, this)
this.physics.add.collider(this.player, this.enemies, this.hitEnemy, null, this)
// Input
this.cursors = this.input.keyboard.createCursorKeys()
this.wasd = this.input.keyboard.addKeys({
up: Phaser.Input.Keyboard.KeyCodes.W,
left: Phaser.Input.Keyboard.KeyCodes.A,
down: Phaser.Input.Keyboard.KeyCodes.S,
right: Phaser.Input.Keyboard.KeyCodes.D
})
// Camera fade in
this.cameras.main.fadeIn(500)
}
update() {
const leftPressed = this.cursors.left.isDown || this.wasd.left.isDown
const rightPressed = this.cursors.right.isDown || this.wasd.right.isDown
const upPressed = this.cursors.up.isDown || this.wasd.up.isDown
if (leftPressed) {
this.player.setVelocityX(-200)
this.player.setFlipX(true)
} else if (rightPressed) {
this.player.setVelocityX(200)
this.player.setFlipX(false)
} else {
this.player.setVelocityX(0)
}
if (upPressed && this.player.body.touching.down) {
this.player.setVelocityY(-400)
this.sound.play("jump", { volume: 0.3 })
}
}
spawnEnemy() {
const x = Phaser.Math.Between(50, this.scale.width - 50)
const enemy = this.enemies.create(x, 50, "enemy")
enemy.setBounce(1)
enemy.setCollideWorldBounds(true)
enemy.setVelocity(Phaser.Math.Between(-100, 100), 20)
}
collectCoin(player, coin) {
coin.disableBody(true, true)
this.sound.play("collect", { volume: 0.5 })
this.gameStore.addScore(10)
// Spawn new coins when all collected
if (this.coins.countActive(true) === 0) {
this.coins.children.iterate((c) => {
c.enableBody(true, c.x, 0, true, true)
})
}
}
hitEnemy(player, enemy) {
this.physics.pause()
player.setTint(0xff0000)
this.time.delayedCall(1000, () => {
this.cameras.main.fadeOut(500, 0, 0, 0)
this.time.delayedCall(500, () => {
this.scene.start("GameOverScene", { score: this.gameStore.score })
})
})
}
}
Vue-Phaser Communication
برای ارتباط بین Vue (UI) و Phaser (game logic)، از Pinia store مشترک استفاده میکنیم.
// stores/game.js
import { defineStore } from "pinia"
import { ref, computed } from "vue"
import { useLocalStorage } from "@vueuse/core"
export const useGameStore = defineStore("game", () => {
const score = ref(0)
const highScores = useLocalStorage("highScores", [])
const isPlaying = ref(false)
const isPaused = ref(false)
const sound = ref(true)
const music = ref(true)
const difficulty = useLocalStorage("difficulty", "normal")
const topScore = computed(() =>
highScores.value.length > 0
? Math.max(...highScores.value.map(s => s.score))
: 0
)
function addScore(points) {
score.value += points
}
function resetScore() {
score.value = 0
}
function saveScore(playerName) {
highScores.value.push({
name: playerName,
score: score.value,
date: new Date().toISOString(),
difficulty: difficulty.value
})
highScores.value.sort((a, b) => b.score - a.score)
highScores.value = highScores.value.slice(0, 10) // Top 10
}
function startGame() {
isPlaying.value = true
isPaused.value = false
resetScore()
}
function endGame() {
isPlaying.value = false
}
function pauseGame() {
isPaused.value = !isPaused.value
const game = window.gameInstance
if (game) {
if (isPaused.value) {
game.scene.pause("GameScene")
} else {
game.scene.resume("GameScene")
}
}
}
return {
score, highScores, isPlaying, isPaused, sound, music, difficulty,
topScore,
addScore, resetScore, saveScore,
startGame, endGame, pauseGame
}
})
HUD با Vue
HUD (Heads-Up Display) اطلاعات بازی مثل score، lives و زمان را نمایش میدهد. بهتر است با Vue بسازیم تا از reactivity آن بهره ببریم.
<!-- components/GameHUD.vue -->
<script setup>
import { useGameStore } from "@/stores/game"
import { storeToRefs } from "pinia"
const store = useGameStore()
const { score, topScore, isPaused } = storeToRefs(store)
</script>
<template>
<div class="game-hud">
<div class="hud-top">
<div class="score">
<div class="label">امتیاز</div>
<div class="value">{{ score.toLocaleString() }}</div>
</div>
<div class="high-score">
<div class="label">بالاترین</div>
<div class="value">{{ topScore.toLocaleString() }}</div>
</div>
<button @click="store.pauseGame" class="pause-btn">
{{ isPaused ? "ادامه" : "توقف" }}
</button>
</div>
<div v-if="isPaused" class="pause-overlay">
<div class="pause-menu">
<h2>بازی متوقف شد</h2>
<button @click="store.pauseGame">ادامه بازی</button>
<button @click="$emit(\"restart\")">شروع مجدد</button>
<button @click="$emit(\"menu\")">منوی اصلی</button>
</div>
</div>
</div>
</template>
<style scoped>
.game-hud {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 10;
}
.hud-top {
display: flex;
justify-content: space-around;
padding: 20px;
pointer-events: auto;
}
.score, .high-score {
background: rgba(102, 0, 0, 0.8);
color: white;
padding: 10px 20px;
border-radius: 8px;
text-align: center;
}
.label {
font-size: 12px;
opacity: 0.7;
}
.value {
font-size: 24px;
font-weight: bold;
}
.pause-btn {
background: #660000;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
pointer-events: auto;
}
.pause-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
pointer-events: auto;
}
.pause-menu {
background: #1a0f0f;
padding: 40px;
border-radius: 12px;
text-align: center;
color: white;
}
.pause-menu button {
display: block;
width: 200px;
margin: 10px auto;
padding: 12px;
background: #660000;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
}
</style>
معماری Vue + Phaser Vue (UI) Menus, HUD, Modals Leaderboard, Settings Phaser (Game) Canvas, Physics Sprites, Scenes Pinia Store = پل ارتباطی
Leaderboard
Leaderboard با Vue به راحتی قابل پیادهسازی است. از همان store که score را ذخیره میکنیم استفاده میکنیم.
<!-- components/Leaderboard.vue -->
<script setup>
import { useGameStore } from "@/stores/game"
import { storeToRefs } from "pinia"
const store = useGameStore()
const { highScores } = storeToRefs(store)
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString("fa-IR")
}
</script>
<template>
<div class="leaderboard">
<h2>🏆 جدول بهترینها</h2>
<div v-if="highScores.length === 0" class="empty">
هنوز هیچ امتیازی ثبت نشده
</div>
<table v-else>
<thead>
<tr>
<th>رتبه</th>
<th>بازیکن</th>
<th>امتیاز</th>
<th>سختی</th>
<th>تاریخ</th>
</tr>
</thead>
<tbody>
<tr
v-for="(score, index) in highScores"
:key="index"
:class="{ top: index < 3 }"
>
<td>
<span v-if="index === 0">🥇</span>
<span v-else-if="index === 1">🥈</span>
<span v-else-if="index === 2">🥉</span>
<span v-else>{{ index + 1 }}</span>
</td>
<td>{{ score.name }}</td>
<td>{{ score.score.toLocaleString() }}</td>
<td>{{ score.difficulty }}</td>
<td>{{ formatDate(score.date) }}</td>
</tr>
</tbody>
</table>
</div>
</template>
بهترین شیوهها
برای ساخت game های موفق با Phaser، این نکات را رعایت کنید. اول، preload تمام assets را قبل از start game. دوم، از asset packs استفاده کنید تا بارگذاری سریعتر باشد.
// Loading screen با progress bar
preload() {
const width = this.scale.width
const height = this.scale.height
const progressBox = this.add.rectangle(width / 2, height / 2, 320, 50)
.setStrokeStyle(2, 0x660000)
const progressBar = this.add.rectangle(
width / 2 - 150, height / 2, 1, 30, 0x660000
).setOrigin(0, 0.5)
this.load.on("progress", (value) => {
progressBar.width = 300 * value
})
// Load assets
this.load.image("player", "player.png")
this.load.audio("music", "music.mp3")
}
سوم، object pooling برای performance. بهجای create و destroy مکرر، objects را recycle کنید. چهارم، physics debug را در development فعال کنید. پنجم، به mobile touch controls فکر کنید. ششم، از tweens برای animations استفاده کنید نه direct manipulation.
هفتم، به memory management توجه کنید. در unmount تمام resources را آزاد کنید. هشتم، از sound sprites استفاده کنید. نهم، برای multiplayer، Colyseus یا Socket.IO گزینههای خوبی هستند. دهم، PWA کردن game باعث میشود offline هم کار کند.
ساخت game یکی از لذتبخشترین پروژههای برنامهنویسی است. با Phaser میتوانید ایدههایتان را سریع به بازی تبدیل کنید. از ساده شروع کنید و به تدریج قابلیت اضافه کنید. community Phaser بسیار فعال است و منابع آموزشی زیادی وجود دارد.
٪۴۱
دوره اختصاصی INZIP
آموزش حرفه ای HTML از صفر در ۱۶ ویدیو: سند، متن، لینک، تصویر، جدول، فرم، معناییسازی، دسترسپذیری و سئو، تا پروژهٔ کامل صفحهٔ فروشگاه محصول.
تماشای آنلاین + دانلود
دسترسی آنی و همیشگی
بهروزرسانی رایگان
کیفیت Full HD
۹۹۰,۰۰۰ تومان
۵۹۰,۰۰۰ تومان
سود شما ۴۰۰,۰۰۰ تومان
مشاهده سریال