ساخت Game تحت وب با Vue و Phaser

۱۴۰۵/۰۸/۱۳ | چهارشنبه، ۱۳ آبان
دوره آموزشی ویژه 4 اپیزود اول تمام دوره های آموزشی ویدیویی رایگان میباشد دانلود رایگان

دورهٔ جامع CSS از صفر

آموزش CSS از صفر در ۲۲ ویدیو: مدل جعبه، انتخاب‌گر، فلکس‌باکس، گرید، واکنش‌گرایی، متغیرها، انیمیشن و معماری، تا پروژهٔ کامل صفحهٔ محصول؛ کد کنار خروجی ...

مدرس دوره : تیم 22 اپیزود ویدیویی 03 ساعت و 33 دقیقه و 59 ثانیه آموزش ویدیویی
متن پرونده شماره 482 - ویو جی اس

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 + PhaserVue (UI)Menus, HUD, ModalsLeaderboard, SettingsPhaser (Game)Canvas, PhysicsSprites, ScenesPinia 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 بسیار فعال است و منابع آموزشی زیادی وجود دارد.
برچسب‌های این مقاله
  • Vue
  • Phaser
  • Game Development
  • HTML5 Games
  • Game Framework
  • Canvas Games
٪۴۱
دوره اختصاصی INZIP

دورهٔ جامع HTML از صفر

آموزش حرفه ای HTML از صفر در ۱۶ ویدیو: سند، متن، لینک، تصویر، جدول، فرم، معنایی‌سازی، دسترس‌پذیری و سئو، تا پروژهٔ کامل صفحهٔ فروشگاه محصول.

تماشای آنلاین + دانلود دسترسی آنی و همیشگی به‌روزرسانی رایگان کیفیت Full HD
۹۹۰,۰۰۰ تومان ۵۹۰,۰۰۰ تومان سود شما ۴۰۰,۰۰۰ تومان
مشاهده سریال