چگونه با استفاده از Phaser 3 و TypeScript، یک بازی ساده در مرورگر بسازیم؟

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 25 اسفند 1397
دسته بندی ها : جاوا اسکریپت

من یک توسعه دهنده مدافع و backend هستند، و تخصص توسعه‌دهی frontend من نسبتا ضعیف است. کمی پیش من می‌خواستم کمی خوش بگذرانم و یک بازی در مرورگر بسازم. من Phaser 3 را به عنوان یک فریم‌وورک و TypeScript را به عنوان یک زبان انتخاب کردم. معلوم شد که برای به کار انداختن این بازی باید برخی کارهای حوصله سربر انجام دهید. پس من این مقاله را نوشتم تا به افراد دیگر کمک کنم که بتوانند سریع‌تر شروع به کار کنند.

نکته: با توجه به این که این آموزش نیازمند دانشی در زمینه TypeScript است، می‌توانید نگاهی به دوره مربوطه بر روی راکت داشته باشید.

آماده‌سازی محیط

IDE

محیط توسعه‌دهی خود را انتخاب کنید. همیشه اگر بخواهید می‌توانید از Notepad خالی استفاده کنید، اما پیشنهاد می‌کنم که از یک چیز کاربردی‌تر استفاده کنید. من خودم ترجیح می‌دهم که پروژه‌ها را در Emacs توسعه دهم، و از این رو tide را نصب کرده و دستور العمل‌های مربوط به راه‌اندازی آن را دنبال کردم.

Node

اگر در حال توسعه‌دهی بر روی JavaScript بودیم، به راحتی می‌توانستیم بدون نیاز به این قدم‌های آماده‌سازی شروع به کدنویسی کنیم. گرچه، با توجه به این که ما می‌خواهیم از TypeScript استفاده کنیم، باید زیرساخت‌ها را راه‌اندازی کنیم تا توسعه‌دهی در آینده را در حد ممکن سریع کنیم. از این رو ما باید node و npm را نصب کنیم.

من وقتی که در حال نوشتن این مقاله می‌باشم، از node 10.13.0 و npm 6.4.1 استفاده می‌کنم. لطفا دقت کنید که نسخه‌های موجود در دنیای frontend خیلی سریع بروزرسانی می‌شوند؛ پس شما فقط باید آخرین نسخه‌های باثبات را بگیرید. من به شدت استفاده از nvm‌ را به جای نصب node و npm به صورت دستی پیشنهاد می‌کنم. این کار از هدر رفتن مقدار زیادی از زمان شما جلوگیری خواهد کرد.

راه‌اندازی پروژه

ساختار پروژه

ما از npm‌ برای ساخت پروژه استفاده خواهیم کرد؛ پس پروژه را شروع کنید، به یک پوشه خالی بروید، و دستور npm init را اجرا کنید. npm چندین سوال درباره ویژگی‌های پروژه شما را از شما خواهد پرسید و سپس یک فایل به نام package.json‌ خواهد ساخت. این فایل چنین ظاهری خواهد داشت

{
  "name": "Starfall",
  "version": "0.1.0",
  "description": "Starfall game (Phaser 3 + TypeScript)",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Mariya Davydova",
  "license": "MIT"
}

پکیج‌ها

با استفاده از این دستور، پکیج‌هایی که نیاز داریم را نصب کنید:

npm install -D typescript webpack webpack-cli ts-loader phaser live-server

گزینه -D (یا --save-dev)، npm را مجبور می‌کند تا به طور خودکار پکیج‌ها را به لیست dependencyها در فایل package.json اضافه کند:

"devDependencies": {
   "live-server": "^1.2.1",
   "phaser": "^3.15.1",
   "ts-loader": "^5.3.0",
   "typescript": "^3.1.6",
   "webpack": "^4.26.0",
   "webpack-cli": "^3.1.2"
 }

Webpack

Webpack کمپایلر TypeScript را اجرا کرده، و برخی از فایل‌های JavaScript نهایی را به همراه کتابخانه‌ها در یک JavaScript کوچک شده بر خواهد گرداند، تا بتوانیم آن را در صفحه خود شامل کنیم.

فایل webpack.config.js خود را نزدیک فایل project.json خود اضافه کنید:

const path = require('path');

module.exports = {
  entry: './src/app.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [ '.ts', '.tsx', '.js' ]
  },
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development'
};

در اینجا می‌بینیم که Webpack باید منابع را از src/app.ts (که به زودی آن را اضافه خواهیم کرد) شروع کرده، و همه چیز را در فایل dist/app.js جمع کند.

TypeScript

ما همچنین به یک فایل پیکربندی کوچک برای کمپایلر TypeScript (tsconfig.json) نیاز داریم، که در آن نسخه JavaScript که می‌خواهیم منابع به آن کمپایل شوند، و جایی که این منابع می‌توانند یافت شوند را توضیح می‌دهیم:

{
  "compilerOptions": {
    "target": "es5"
  },
  "include": [
    "src/*"
  ]
}

تعریفات TypeScript

TypeScript یک زبان تایپ شده به صورت استاتیک است. از این رو، این زبان نیازمند تعریفات type برای کمپایل کردن می‌باشد. در هنگام نوشتن این مقاله، تعریفات برای Phaser 3 هنوز به عنوان یک پکیج npm در دسترس نیستند؛ پس شاید نیاز باشد که آن‌ها را از مخزن رسمی دانلود کنید و فایل مورد نظر را در زیرشاخه src پروژه خود اضافه کنید.

اسکریپت‌ها

ما تقریبا راه‌اندازی پروژه را تمام کرده‌ایم. تا به اینجا باید فایل‌های package.json، webpack.config.json و tsconfig.d.ts‌ را ساخته، و فایل src/phaser.d.ts‌ را اضافه کرده باشید. آخرین کاری که باید قبل از شروع به کدنویسی انجام دهیم، این است که توضیح دهیم npm دقیقا چه کاری را باید با پروژه انجام دهد. ما بخش scripts فایل package.json‌ را به این صورت بروزرسانی کردیم:

"scripts": {
  "build": "webpack",
  "start": "webpack --watch & live-server --port=8085"
}

وقتی که شما دستور npm build را اجرا می‌کنید، فایل app.js طبق پیکربندی‌های Webpack ساخته خواهد شد. و وقتی که دستور npm start را اجرا می‌کنید، نیازی نخواهد بود که نگران روند ساخت باشید. به محض این که هر منبعی را داشته باشید، Webpack‌ برنامه را بازسازی کرده و live-server آن را در مرورگر پیشفرض مجددا بارگذاری خواهد کرد. برنامه بر روی آدرس http://127.0.0.1:8085/ میزبانی خواهد شد.

شروع کار

حال که ما زیرساختار (بخشی که من به شخصه در هنگام شروع یک پروژه از آن متنفرم) را راه‌اندازی کرده‌ایم، می‌توانیم بالاخره شروع به کدنویسی کنیم. در این قدم، ما یک کار ساده انجام خواهیم داد: کشیدن یک مستطیل به رنگ آبی تیره در مرورگر خود. استفاده از یک فریم‌وورک توسعه‌دهی بازی بزرگ، برای این کار کمی زیاده‌روی است. همچنان، ما آن را در قدم‌های بعدی نیاز خواهیم داشت.

بگذارید به طور خلاصه مفاهیم Phaser 3‌ را توضیح دهم. بازی مورد نظر نمونه‌ای از کلاس Phaser.Game (یا اولاد آن) نیست. هر بازی شامل یک یا چند نمونه از اولاد Phaser.Scene می‌باشد. هر سکانس شامل چند آبجکت، حال چه استاتیک یا دینامیک می‌باشد و نمایانگر بخش منطقی بازی است. برای مثال، بازی کوچک ما سه سکانس خواهد داشت: صفحه خوش‌آمد گویی، خود بازی و صفحه امتیاز.

بیایید شروع به کدنویسی نماییم.

در ابتدا، یک محفظه HTML حداقلی برای بازی بسازید. یک فایل به نام index.html ایجاد کنید، که شامل این کد باشد:

<!DOCTYPE html>
<html>
  <head>
    <title>Starfall</title>
    <script src="dist/app.js"></script>
  </head>
  <body>
    <div id="game"></div>
  </body>
</html>

فقط دو بخش ضروری در اینجا وجود دارند: اولین بخش ورودی script است که می‌گوید ما قرار است از فایل‌های ساخته شده خود در اینجا استفاده کنیم، و بخش دوم هم یک ورودی div است که محفظه بازی خواهد بود.

حال یک فایل به نام src/app.ts را با این کد بسازید:

import "phaser";

const config: GameConfig = {
  title: "Starfall",
  width: 800,
  height: 600,
  parent: "game"
  backgroundColor: "#18216D"
};

export class StarfallGame extends Phaser.Game {
  constructor(config: GameConfig) {
    super(config);
  }
}

window.onload = () => {
  var game = new StarfallGame(config);
};

این کد به خوبی خودش را توضیح می‌دهد. GameConfig چند ویژگی متنوع دارد.

و حال شما می‌توانید بالاخره npm start را اجرا کنید. اگر همه چیز در این قدم و قدم‌های قبلی به طور صحیح انجام شده باشد، شما باید چیزی بسیار ساده به مانند تصویر زیر را در مرورگر خود ببینید:

سقوط ستاره‌ها

ما یک برنامه ابتدایی ساخته‌ایم. حال وقت آن است که یک سکانس را اضافه کنیم، که اتفاقی در آن پیش می‌آید. بازی ما ساده خواهد بود: ستاره‌ها بر روی زمین خواهند افتاد، و هدف این خواهد بود که در حد ممکن آن‌ها را بگیریم.

برای رسیدن به این هدف، یک فایل جدید به نام gameScene.ts بسازید و این کد را به آن اضافه کنید:

import "phaser";

export class GameScene extends Phaser.Scene {

constructor() {
    super({
      key: "GameScene"
    });
  }

init(params): void {
    // TODO
  }

preload(): void {
    // TODO
  }
 
  create(): void {
    // TODO
  }

update(time): void {
    // TODO
  }
};

در اینجا Constructor شامل یک کلید می‌باشد که سکانس‌های دیگر ممکن است تحت آن این سکانس را فراخوانی کنند.

شما در اینجا ریشه‌هایی را برای چهار متد می‌بینید. بگذارید تفاوت بین آن‌ها را به طور خلاصه توضیح دهم:

  • init([params]) وقتی که سکانس شروع می‌شود، فراخوانی می‌شود. این تابع شاید پارامترهایی را بپذیرد، که از سکانس‌ها یا بازی‌های دیگر و با فراخوانی scene.start(key, [params]) منتقل می‌شوند.
  • preload() قبل از این که آبجکت سکانس ساخته شود، فراخوانی می‌شود و شامل دارایی‌های بارگذاری می‌باشد. این دارایی‌ها cache می‌شوند؛ پس وقتی که سکانس مجددا راه‌اندازی می‌شود، این دارایی‌ها مجددا بارگذاری نمی‌شوند.
  • create() وقتی که دارایی‌ها بارگذاری شده‌اند فراخوانی می‌شود، و معمولا شامل ساخت آبجکت‌های اصلی بازی (پس‌زمینه، بازیکن، موانع، دشمنان و...) می‌باشد.
  • update([time]) در هر تیک فراخوانی می‌شود و شامل بخش‌های دینامیک صفحه (هر چیزی که تکان می‌خورد، خاموش و روشن می‌شود و...) می‌باشد.

برای این که مطمئن باشیم بعدا آن را فراموش نمی‌کنیم، بیایید سریعا این خطوط را به فایل game.ts اضافه کنیم:

import "phaser";
import { GameScene } from "./gameScene";

const config: GameConfig = {
  title: "Starfall",
  width: 800,
  height: 600,
  parent: "game",
  scene: [GameScene],
  physics: {
    default: "arcade",
    arcade: {
      debug: false
    }
  },
  backgroundColor: "#000033"
};
...

حال بازی ما سکانس بازی را می‌داند. اگر پیکربندی بازی شامل لیستی از سکانس‌ها می‌باشد، اولین مورد وقتی بازی شروع شده است، راه‌اندازی می‌شود و تمام موارد دیگر هم ساخته می‌شوند، اما این موارد تا زمانی که به صراحت فراخوانی نشده‌اند، شروع نمی‌شوند.

ما همچین فیزیک arcade را در اینجا اضافه کرده‌ایم. این فیزیک برای سقوط ستاره‌ها مورد نیاز است.

حال می‌توانیم بازی خود را ارتقا دهیم.

در ابتدا، ما برخی ویژگی‌ها و آبجکت‌هایی که نیاز خواهیم داشت را تعریف می‌کنیم:

export class GameScene extends Phaser.Scene {
  delta: number;
  lastStarTime: number;
  starsCaught: number;
  starsFallen: number;
  sand: Phaser.Physics.Arcade.StaticGroup;
  info: Phaser.GameObjects.Text;
...

سپس، ما اعداد را راه‌اندازی می‌کنیم:

init(/*params: any*/): void {
    this.delta = 1000;
    this.lastStarTime = 0;
    this.starsCaught = 0;
    this.starsFallen = 0;
  }

حال چند تصویر را بارگذاری می‌کنیم:

preload(): void {
    this.load.setBaseURL(
      "https://raw.githubusercontent.com/mariyadavydova/" +
      "starfall-phaser3-typescript/master/");
    this.load.image("star", "assets/star.png");
    this.load.image("sand", "assets/sand.jpg");
  }

پس از آن، می‌توانیم کامپوننت‌های استاتیک خود را آماده کنیم. ما زمین، جایی که ستاره‌ها خواهند افتاد و متنی که امتیاز صحیح را به ما اطلاع می‌دهد، می‌سازیم:

create(): void {
    this.sand = this.physics.add.staticGroup({
      key: 'sand',
      frameQuantity: 20
    });
    Phaser.Actions.PlaceOnLine(this.sand.getChildren(),
      new Phaser.Geom.Line(20, 580, 820, 580));
    this.sand.refresh();

this.info = this.add.text(10, 10, '',
      { font: '24px Arial Bold', fill: '#FBFBAC' });
  }

یک گروه در Phaser 3، راهی برای ساخت چند آبجکت است که می‌خواهید آن‌ها را به همراه هم کنترل کنید. دو نوع آبجکت وجود دارند: استاتیک و دینامیک. همانطور که شاید حدس زده باشید، آبجکت‌های استاتیک (زمین، دیوارها، موانع مختلف) تکان نمی‌خورند، و آبجکت‌های دینامیک (کشتی‌ها، موشک‌ها) این کار را انجام می‌دهند.

ما یک گروه استاتیک از تکه‌های زمین می‌سازیم. این تکه‌ها بر روی خط قرار گرفته‌اند. لطفا دقت کنید که این خط به ۲۰ بخش مساوی (که برخلاف انتظار شما ۱۰ بخش نیستند) تقسیم شده است، و کاشی‌های زمین بر روی هر بخش در انتهای سمت چپ قرار گرفته‌اند، که مرکز کاشی در آن نقطه قرار دارد. ما همچنین باید refresh(0 را فراخوانی کنیم تا جعبه محدود کردن گروه را بروزرسانی کنیم. (در غیر این صورت تصادفات برخلاف موقعیت پیشفرض بررسی خواهند شد، که برابر با بالا سمت چپ سکانس است)

حال اگر برنامه خود را در مرورگر خود آزمایش کنید، باید چنین چیزی را ببینید:

ما بالاخره به دینامیک‌ترین بخش این سکانس رسیده‌ایم: تابع update() که ستاره‌ها سقوط می‌کنند. این تابع حدود هر ۶۰ میلی ثانیه فراخوانی می‌شود. ما می‌خواهیم هر ثانیه یک ستاره جدید را وارد کنیم. ما برای این کار از یک گروه دینامیک استفاده نخواهیم کرد؛ زیرا lifecycle هر ستاره کوتاه خواهد بود. این ستاره یا با کلیک کردن کاربر، و یا با تصادف با زمین از بین خواهد رفت. از این رو داخل تابع emitStar()، ما یک ستاره جدید را می‌سازیم و پردازش دو رویداد را اضافه می‌کنیم: onClick() و onCollision().

update(time: number): void {
    var diff: number = time - this.lastStarTime;
    if (diff > this.delta) {
      this.lastStarTime = time;
      if (this.delta > 500) {
        this.delta -= 20;
      }
      this.emitStar();
    }
    this.info.text =
      this.starsCaught + " caught - " +
      this.starsFallen + " fallen (max 3)";
  }

private onClick(star: Phaser.Physics.Arcade.Image): () => void {
    return function () {
      star.setTint(0x00ff00);
      star.setVelocity(0, 0);
      this.starsCaught += 1;
      this.time.delayedCall(100, function (star) {
        star.destroy();
      }, [star], this);
    }
  }

private onFall(star: Phaser.Physics.Arcade.Image): () => void {
    return function () {
      star.setTint(0xff0000);
      this.starsFallen += 1;
      this.time.delayedCall(100, function (star) {
        star.destroy();
      }, [star], this);
    }
  }

private emitStar(): void {
    var star: Phaser.Physics.Arcade.Image;
    var x = Phaser.Math.Between(25, 775);
    var y = 26;
    star = this.physics.add.image(x, y, "star");

star.setDisplaySize(50, 50);
    star.setVelocity(0, 200);
    star.setInteractive();

star.on('pointerdown', this.onClick(star), this);
    this.physics.add.collider(star, this.sand,
      this.onFall(star), null, this);
  }

در آخر، ما یک بازی کامل داریم! این بازی هنوز شرطی برای پیروزی ندارد. ما آن را در بخش آخر آموزش اضافه خواهیم کرد.

جمع‌بندی

معمولا هر بازی از چند سکانس تشکیل می‌شود. حتی اگر گیم‌پلی آن خیلی ساده باشد شما به یک سکانس ورودی (که حداقل شامل دکمه «Play») باشد، و یک سکانس نهایی (که نتیجه آخرین بازی شما مانند امتیاز یا آخرین مرحله‌ای که به آن رسیده‌اید را نمایش می‌دهد) نیاز دارید. بیایید این سکانس‌ها را به برنامه خود اضافه کنیم.

در این مورد، این سکانس‌ها بسیار مشابه خواهند بود؛ زیرا من نمی‌خواهم توجه زیادی به طراحی بازی کنم. بالاخره، این یک آموزش برنامه‌نویسی است.

صفحه خوش‌آمد گویی، این کد را در فایل welcomeScene.ts خواهد داشت. دقت کنید وقتی که کاربر در جایی بر روی صفحه کلیک می‌کند، یک سکانس بازی ظاهر خواهد شد.

import "phaser";

export class WelcomeScene extends Phaser.Scene {
  title: Phaser.GameObjects.Text;
  hint: Phaser.GameObjects.Text;

constructor() {
    super({
      key: "WelcomeScene"
    });
  }

create(): void {
    var titleText: string = "Starfall";
    this.title = this.add.text(150, 200, titleText,
      { font: '128px Arial Bold', fill: '#FBFBAC' });

var hintText: string = "Click to start";
    this.hint = this.add.text(300, 350, hintText,
      { font: '24px Arial Bold', fill: '#FBFBAC' });

this.input.on('pointerdown', function (/*pointer*/) {
      this.scene.start("GameScene");
    }, this);
  }
};

سکانس امتیاز ظاهر تقریبا مشابهی خواهد داشت، که در هنگام کلیک به سکانس خوش‌آمد گویی (scoreScene.ts) ختم خواهد شد.

import "phaser";

export class ScoreScene extends Phaser.Scene {
  score: number;
  result: Phaser.GameObjects.Text;
  hint: Phaser.GameObjects.Text;

constructor() {
    super({
      key: "ScoreScene"
    });
  }

init(params: any): void {
    this.score = params.starsCaught;
  }

create(): void {
    var resultText: string = 'Your score is ' + this.score + '!';
    this.result = this.add.text(200, 250, resultText,
      { font: '48px Arial Bold', fill: '#FBFBAC' });

var hintText: string = "Click to restart";
    this.hint = this.add.text(300, 350, hintText,
      { font: '24px Arial Bold', fill: '#FBFBAC' });

this.input.on('pointerdown', function (/*pointer*/) {
      this.scene.start("WelcomeScene");
    }, this);
  }
};

حال ما باید فایل اصلی برنامه خود را بروزرسانی کنیم: این سکانس‌ها را اضافه کنید و WelcomeScene را برابر با اولین مورد در لیست قرار دهید:

import "phaser";
import { WelcomeScene } from "./welcomeScene";
import { GameScene } from "./gameScene";
import { ScoreScene } from "./scoreScene";

const config: GameConfig = {
  ...
  scene: [WelcomeScene, GameScene, ScoreScene],
  ...

آیا متوجه شده‌اید که جای چه چیزی خالی است؟ درست است، ما هنوز ScoreScene را از هیچ جایی فراخوانی نمی‌کنیم. بیایید وقتی کاربر سومین ستاره را از دست می‌دهد،‌ آن را فراخوانی کنیم:

private onFall(star: Phaser.Physics.Arcade.Image): () => void {
    return function () {
      star.setTint(0xff0000);
      this.starsFallen += 1;
      this.time.delayedCall(100, function (star) {
        star.destroy();
        if (this.starsFallen > 2) {
          this.scene.start("ScoreScene",
            { starsCaught: this.starsCaught });
        }
      }, [star], this);
    }
  }

در نهایت، بازی ستاره ما به مانند یک بازی واقعی می‌باشد. این بازی شروع می‌شود، به اتمام می‌رسد و هدفی برای رسیدن دارد: این که چند ستاره را می‌توانید بگیرید؟

امیدوارم این مقاله برای شما هم به مانند من کاربردی بوده باشد. منتظر بازخوردهای شما هستیم.

سورس کد این آموزش می‌تواند در این لینک یافت شود.

منبع

مقالات پیشنهادی

چگونه یک وبسایت وردپرسی را به یک اپلیکیشن موبایل تبدیل کنیم

داشتن یک وبسایت واکنشگرا یکی از مهمترین مواردی است که دارندگان وبسایت باید به آن توجه کنند، با این کار آن ها می توانند تمام اطلاعات وبسایت را در دستگا...

طرحی بد آیکون - ۳ اشتباه و چگونگی اجتناب از آن‌ها

یک آیکون ممکن است برای بیان یک مفهوم پیچیده طراحی شده باشد، اما نباید خود از این پیچیدگی ارثی ببرد. پیچیدگی در طراحی لوگو ممکن است روی مقیاس‌پذیری آن...

آیکون های فروشگاهی و بازاریابی

در این پست لذت بخش من میخوام به شما یک مجموعه از آیکون های زیبا و ضررویه بازاریابی و فروشگاهی رو معرفی کنم که شامل +100 آیکون Swificons با 3 نوع مختلف...

چگونه با استفاده از CSS خالص، یک قلب تپنده برای ولنتاین بسازیم؟

هر سال در تاریخ ۱۴ فوریه، بسیاری از مردم با شخص خاص خود برخی کارت‌ها، شکلات‌ها، هدیه‌ها و گل‌ها را رد و بدل می‌کنند. روز عشقی که ما آن را ولنتاین می‌ن...