كيفية بناء لعبة بسيطة في المتصفح باستخدام Phaser 3 و TypeScript

الصورة من قبل فيل بوتا على Unsplash

أنا مدافع عن مطور ومطور خلفي ، وخبرتي في تطوير الواجهة الأمامية ضعيفة نسبياً. منذ فترة كنت أرغب في الحصول على بعض المتعة وجعل لعبة في المتصفح ؛ لقد اخترت Phaser 3 كإطار عمل (يبدو شائعًا هذه الأيام) و TypeScript كلغة (لأنني أفضل الكتابة الثابتة على الديناميكية). اتضح أنك بحاجة إلى القيام ببعض الأشياء الممله لجعل كل شيء يعمل ، لذلك كتبت هذا البرنامج التعليمي لمساعدة الآخرين مثلي على البدء بشكل أسرع.

إعداد البيئة

IDE

اختر بيئة التطوير الخاصة بك. يمكنك دائمًا استخدام المفكرة القديمة العادية إذا كنت ترغب في ذلك ، لكنني اقترح استخدام شيء أكثر فائدة. بالنسبة لي ، أنا أفضل تطوير مشاريع الحيوانات الأليفة في Emacs ، لذلك قمت بتثبيت المد واتبعت التعليمات لإعداده.

العقدة

إذا كنا نعمل على تطوير جافا سكريبت ، فسوف نكون على ما يرام لبدء الترميز دون كل خطوات الإعداد هذه. ومع ذلك ، فنحن نرغب في استخدام TypeScript ، يتعين علينا إعداد البنية التحتية لجعل التطوير المستقبلي بأسرع ما يمكن. وبالتالي نحن بحاجة إلى تثبيت العقدة و npm.

أثناء كتابة هذا البرنامج التعليمي ، استخدم العقدة 10.13.0 و npm 6.4.1. يرجى ملاحظة أن الإصدارات الموجودة في الواجهة الأمامية تعمل على تحديث سريع للغاية ، لذلك يمكنك ببساطة أخذ أحدث الإصدارات الثابتة. أوصي بشدة باستخدام nvm بدلاً من تثبيت العقدة و npm يدويًا ؛ سيوفر لك الكثير من الوقت والأعصاب.

إعداد المشروع

هيكل المشروع

سوف نستخدم npm لبناء المشروع ، لذلك لبدء المشروع ، انتقل إلى مجلد فارغ وقم بتشغيل npm init. سوف تطرح لك npm عدة أسئلة حول خصائص المشروع الخاص بك ثم تقوم بإنشاء ملف package.json. سيبدو شيئا من هذا القبيل:

{
  "الاسم": "Starfall" ،
  "الإصدار": "0.1.0" ،
  "الوصف": "لعبة Starfall (Phaser 3 + TypeScript)" ،
  "الرئيسي": "index.js" ،
  "نصوص": {
    "test": "echo \" خطأ: لم يتم تحديد اختبار \ "&& exit 1"
  }،
  "المؤلف": "ماريا دافيدوفا" ،
  "الترخيص": "MIT"
}

حزم

قم بتثبيت الحزم التي نحتاجها باستخدام الأمر التالي:

npm تثبيت -D typescript webpack webpack-cli ts-loader phaser live-server

الخيار -D (a.k.a.sav-dev) يجعل npm يضيف هذه الحزم إلى قائمة التبعيات في package.json تلقائيًا:

"devDependencies": {
   "الخادم المباشر": "^ 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 وجمع مجموعة من ملفات JS الناتجة بالإضافة إلى المكتبات في JS مصغر واحد حتى نتمكن من تضمينه في صفحتنا.

أضف webpack.config.js بالقرب من project.json:

const مسار = تتطلب ('مسار') ؛
module.exports = {
  الإدخال: './src/app.ts' ،
  وحدة: {
    قواعد: [
      {
        اختبار: /\.tsx؟$/ ،
        استخدام: "ts-loader" ،
        استبعاد: / node_modules /
      }
    ]
  }،
  حل: {
    الامتدادات: ['.ts' ، '.tsx' ، '.js']
  }،
  انتاج: {
    اسم الملف: 'app.js' ،
    path: path.resolve (__ dirname، 'dist')
  }،
  الوضع: "التنمية"
}؛

نرى هنا أن حزمة الويب يجب أن تبدأ في الحصول على المصادر بدءًا من src / app.ts (والتي سنضيفها قريبًا جدًا) وجمع كل شيء في ملف dist / app.js.

نسخة مطبوعة على الآلة الكاتبة

نحتاج أيضًا إلى ملف تكوين صغير لبرنامج التحويل البرمجي لـ TypeScript (tsconfig.json) ، حيث نوضح إصدار JS الذي نريد أن يتم تجميع المصادر إليه ومكان العثور على هذه المصادر:

{
  "compilerOptions": {
    "الهدف": "es5"
  }،
  "تتضمن": [
    "SRC / *"
  ]
}

تعريفات TypeScript

TypeScript هي لغة مكتوبة بشكل ثابت. لذلك ، يتطلب تعريفات النوع من أجل الترجمة. في وقت كتابة هذا البرنامج التعليمي ، لم تكن تعريفات Phaser 3 متاحة بعد كحزمة npm ، لذا فقد تحتاج إلى تنزيلها من المستودع الرسمي ووضع الملف في الدليل الفرعي src لمشروعك.

نصوص

لقد انتهينا تقريبًا من إعداد المشروع. في هذه اللحظة ، يجب أن تكون قد قمت بإنشاء package.json و webpack.config.js و tsconfig.json وإضافة src / phaser.d.ts. آخر ما نحتاج إلى القيام به قبل البدء في كتابة التعليمات البرمجية هو شرح ما تفعله بالضبط npm مع المشروع. نقوم بتحديث قسم البرامج النصية من package.json كما يلي:

"نصوص": {
  "build": "webpack" ،
  "start": "webpack - watch & live-server --port = 8085"
}

عندما تقوم بتنفيذ npm build ، سيتم بناء ملف app.js وفقًا لتكوين webpack. وعندما تقوم بتشغيل npm start ، لن تضطر إلى إزعاج عملية الإنشاء: بمجرد أن تقوم بحفظ أي مصدر ، ستعيد webpack إنشاء التطبيق وسيقوم الخادم المباشر بإعادة تحميله في المستعرض الافتراضي لديك. سيتم استضافة التطبيق على http://127.0.0.1:8085/.

ابدء

الآن وقد أنشأنا البنية التحتية (الجزء الذي أكرهه شخصيًا عند بدء مشروع) ، يمكننا أخيرًا البدء في الترميز. في هذه الخطوة ، سنفعل شيئًا بسيطًا: ارسم مستطيلًا أزرق داكن في نافذة المتصفح. باستخدام إطار تطوير لعبة كبيرة لهذا هو القليل من ... هممم ... مبالغة. ومع ذلك ، سنحتاج إليها في الخطوات التالية.

اسمحوا لي أن أشرح بإيجاز المفاهيم الرئيسية لبرنامج Phaser 3. اللعبة هي مثيل لفئة Phaser.Game (أو سليلها). تحتوي كل لعبة على مثيل واحد أو أكثر من أحفاد Phaser.Scene. يحتوي كل مشهد على عدة كائنات ، إما ثابتة أو ديناميكية ، ويمثل جزءًا منطقيًا من اللعبة. على سبيل المثال ، ستحتوي لعبتنا التافهة على ثلاثة مشاهد: شاشة الترحيب ، اللعبة نفسها ، وشاشة النتائج.

لنبدأ الترميز.

أولاً ، قم بإنشاء حاوية HTML أضيق الحدود للعبة. قم بإنشاء ملف index.html ، والذي يحتوي على الكود التالي:



  <رئيس>
    <عنوان> Starfall 
    
  
  
    
  

لا يوجد هنا سوى جزأين أساسيين: الأول هو إدخال نصي يوضح أننا سنستخدم ملفنا المدمج هنا ، والثاني هو إدخال div والذي سيكون حاوية الألعاب.

الآن قم بإنشاء ملف src / app.ts بالشفرة التالية:

استيراد "phaser" ؛
const config: GameConfig = {
  العنوان: "Starfall" ،
  العرض: 800
  الارتفاع: 600
  الأصل: "لعبة"
  backgroundColor: "# 18216D"
}؛
فئة التصدير StarfallGame يمتد Phaser.Game {
  مُنشئ (التكوين: GameConfig) {
    السوبر (التكوين)؛
  }
}
window.onload = () => {
  var game = StarfallGame (config) جديد ؛
}؛

هذا الرمز هو التفسير الذاتي. تمتلك GameConfig الكثير من الخصائص ، يمكنك التحقق منها هنا.

والآن يمكنك تشغيل أخيرا npm start. إذا كان كل شيء قد تم بشكل صحيح في هذا والخطوات السابقة ، يجب أن ترى شيئًا بسيطًا مثل هذا في متصفحك:

نعم ، هذه شاشة زرقاء.

جعل سقوط النجوم

لقد أنشأنا تطبيق الابتدائية. الآن حان الوقت لإضافة مشهد حيث سيحدث شيء ما. ستكون لعبتنا بسيطة: النجوم ستسقط على الأرض ، والهدف سيكون التقاط أكبر عدد ممكن.

لتحقيق هذا الهدف ، قم بإنشاء ملف جديد ، gameScene.ts ، وأضف الكود التالي:

استيراد "phaser" ؛
فئة التصدير GameScene يمتد Phaser.Scene {
البناء() {
    ممتاز({
      المفتاح: "GameScene"
    })؛
  }
الحرف الأول (params): باطل
    // لكى يفعل
  }
التحميل المسبق (): باطل
    // لكى يفعل
  }
  
  create (): باطل
    // لكى يفعل
  }
التحديث (الوقت): باطل
    // لكى يفعل
  }
}؛

يحتوي المنشئ هنا على مفتاح يمكن تحته المشاهد الأخرى بهذا المشهد.

ترى هنا بذرة لأربعة طرق. اسمحوا لي أن أشرح باختصار الفرق بين ذلك الحين:

  • يطلق على الحرف ([params]) عندما يبدأ المشهد ؛ قد تقبل هذه الوظيفة المعلمات ، والتي يتم تمريرها من مشاهد أو لعبة أخرى عن طريق استدعاء scene.start (مفتاح ، [params])
  • يسمى التحميل المسبق () قبل إنشاء كائنات المشهد ، ويحتوي على مواد تحميل ؛ يتم تخزين هذه الأصول مؤقتًا ، لذلك عند إعادة تشغيل المشهد ، لا يتم إعادة تحميلها
  • يسمى create () عندما يتم تحميل الأصول وعادة ما يحتوي على إنشاء كائنات اللعبة الرئيسية (الخلفية ، اللاعب ، العقبات ، الأعداء ، إلخ.)
  • يسمى التحديث ([time]) كل علامة ويحتوي على الجزء الديناميكي من المشهد - كل ما يتحرك ، ومضات ، وما إلى ذلك.

للتأكد من أننا لا ننسى ذلك لاحقًا ، دعنا نضيف الأسطر التالية بسرعة في اللعبة.

استيراد "phaser" ؛
استيراد {GameScene} من "./gameScene" ؛
const config: GameConfig = {
  العنوان: "Starfall" ،
  العرض: 800
  الارتفاع: 600
  الأصل: "لعبة" ،
  المشهد: [GameScene] ،
  الفيزياء: {
    الافتراضي: "الممرات" ،
    ممر: {
      تصحيح: خطأ
    }
  }،
  backgroundColor: "# 000033"
}؛
...

تعرف لعبتنا الآن على مشهد اللعبة. إذا احتوى تكوين اللعبة على قائمة من المشاهد ، فعندئذٍ تبدأ أول لعبة عندما تبدأ اللعبة ، ويتم إنشاء جميع المشاهد الأخرى ولكن لا يتم بدءها حتى يتم استدعاءها صراحة.

لقد أضفنا أيضا فيزياء الممرات هنا. مطلوب لجعل نجومنا تقع.

الآن يمكننا وضع اللحم على عظام مشهد لعبتنا.

أولاً ، نعلن بعض الخصائص والأشياء التي سنحتاجها:

فئة التصدير GameScene يمتد Phaser.Scene {
  دلتا: رقم
  lastStarTime: رقم ؛
  stars القبض: عدد؛
  starsFallen: رقم ؛
  الرمال: Phaser.Physics.Arcade.StaticGroup؛
  معلومات: Phaser.GameObjects.Text؛
...

ثم ، نقوم بتهيئة الأرقام:

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

الآن ، نقوم بتحميل بضع صور:

التحميل المسبق (): باطل
    this.load.setBaseURL (
      "https://raw.githubusercontent.com/mariyadavydova/" +
      "starfall-phaser3-نسخة مطبوعة على الآلة الكاتبة / الماجستير /")؛
    this.load.image ("star" ، "stocks / star.png") ؛
    this.load.image ("الرمال" ، "الأصول / sand.jpg") ؛
  }

بعد ذلك ، يمكننا إعداد مكونات ثابتة لدينا. سنخلق الأرض ، حيث تقع النجوم ، والنص الذي يبلغنا بالنتيجة الحالية:

create (): باطل
    this.sand = this.physics.add.staticGroup ({
      مفتاح: "الرمال" ،
      الإطارالكمية: 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 هي طريقة لإنشاء مجموعة من الكائنات التي تريد التحكم بها معًا. هناك نوعان من الكائنات: ثابت وديناميكي. كما قد تتخيل ، الكائنات الثابتة لا تتحرك (الأرض والجدران والعقبات المختلفة) ، في حين أن الكائنات الديناميكية تؤدي المهمة (ماريو ، السفن ، الصواريخ).

نخلق مجموعة ثابتة من القطع الأرضية. يتم وضع هذه القطع على طول الخط. يرجى ملاحظة أن الخط مقسم إلى 20 قسمًا متساويًا (وليس 19 كما قد تتوقع) ، ويتم وضع البلاط الأرضي على كل قسم في الطرف الأيسر مع وجود مركز البلاط في تلك المرحلة (آمل أن يشرح ذلك الأقسام أعداد). يتعين علينا أيضًا الاتصال بـ update () لتحديث مربع إحاطة المجموعة (وإلا ، سيتم التحقق من التصادمات في مقابل الموقع الافتراضي ، وهو أعلى يسار المشهد).

إذا قمت بفحص تطبيقك في المتصفح الآن ، فسترى شيئًا مثل هذا:

تطور الشاشة الزرقاء

لقد وصلنا أخيرًا إلى الجزء الأكثر ديناميكية من هذا المشهد - وظيفة () (تحديث) ، حيث تقع النجوم. وتسمى هذه الوظيفة في مكان ما حول مرة واحدة في 60 مللي ثانية. نريد أن تنبعث نجمة جديدة تسقط كل ثانية. لن نستخدم مجموعة ديناميكية لهذا ، لأن دورة حياة كل نجم ستكون قصيرة: سيتم تدميرها إما عن طريق نقرة المستخدم أو عن طريق التصادم مع الأرض. لذلك داخل وظيفة emitStar () نقوم بإنشاء نجم جديد وإضافة معالجة حدثين: onClick () و onCollision ().

التحديث (الوقت: العدد): باطل
    var diff: number = time - this.lastStarTime؛
    إذا (فرق> هذا. دلتا) {
      this.lastStarTime = الوقت ؛
      if (this.delta> 500) {
        this.delta - = 20 ؛
      }
      this.emitStar ()؛
    }
    this.info.text =
      this.starsCaught + "اشتعلت -" +
      this.starsFallen + "سقط (بحد أقصى 3)"؛
  }
onClick الخاص (نجمة: Phaser.Physics.Arcade.Image): () => void {
    وظيفة الإرجاع () {
      star.setTint (0x00ff00)؛
      star.setVelocity (0، 0)؛
      this.starsCaught + = 1؛
      this.time.delayedCall (100 ، دالة (نجمة) {
        star.destroy ()؛
      } ، [نجمة] ، هذا) ؛
    }
  }
onFall الخاص (نجمة: Phaser.Physics.Arcade.Image): () => void {
    وظيفة الإرجاع () {
      star.setTint (0xff0000)؛
      this.starsFallen + = 1؛
      this.time.delayedCall (100 ، دالة (نجمة) {
        star.destroy ()؛
      } ، [نجمة] ، هذا) ؛
    }
  }
emitStar () الخاص:
    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.physics.add.collider (نجمة ، هذه. الرمال ،
      this.onFall (نجمة) ، لاغية ، هذا) ؛
  }

أخيرًا ، لدينا لعبة! ليس لديها شرط الفوز بعد. سنضيفه في الجزء الأخير من البرنامج التعليمي.

أنا سيئة في اصطياد النجوم ...

التفاف كل شيء

عادة ، تتكون اللعبة من عدة مشاهد. حتى لو كانت طريقة اللعب بسيطة ، فأنت بحاجة إلى مشهد افتتاحي (يحتوي على الأقل على زر "تشغيل!") ومشهد إغلاق (يُظهر نتيجة جلسة لعبتك ، مثل النتيجة أو المستوى الأقصى الذي تم الوصول إليه). دعنا نضيف هذه المشاهد إلى تطبيقنا.

في حالتنا ، ستكون متشابهة إلى حد كبير ، حيث لا أريد أن أبدي الكثير من الاهتمام للتصميم الجرافيكي للعبة. بعد كل شيء ، هذا البرنامج التعليمي البرمجة.

سيكون مشهد الترحيب الرمز التالي في welcomeScene.ts. لاحظ أنه عندما ينقر المستخدم في مكان ما على هذا المشهد ، سوف يظهر مشهد اللعبة.

استيراد "phaser" ؛
فئة التصدير WelcomeScene يمتد Phaser.Scene {
  العنوان: Phaser.GameObjects.Text؛
  تلميح: Phaser.GameObjects.Text؛
البناء() {
    ممتاز({
      المفتاح: "WelcomeScene"
    })؛
  }
create (): باطل
    var titleText: string = "Starfall"؛
    this.title = this.add.text (150 ، 200 ، titleText ،
      {font: '128px Arial Bold'، fill: '#FBFBAC'})؛
var hintText: string = "انقر للبدء"؛
    this.hint = this.add.text (300 ، 350 ، hintText ،
      {font: '24px Arial Bold'، fill: '#FBFBAC'})؛
this.input.on ('pointerdown' ، الدالة (/ * المؤشر * /) {
      this.scene.start ( "GameScene")؛
    }، هذه)؛
  }
}؛

سيبدو مشهد النتيجة كما هو تقريبا ، مما يؤدي إلى مشهد الترحيب عند النقر (scoreScene.ts).

استيراد "phaser" ؛
فئة التصدير ScoreScene يمتد Phaser.Scene {
  النتيجة: العدد
  النتيجة: Phaser.GameObjects.Text؛
  تلميح: Phaser.GameObjects.Text؛
البناء() {
    ممتاز({
      المفتاح: "ScoreScene"
    })؛
  }
الحرف الأول (المعلمات: أي): باطل
    this.score = params.starsCaught؛
  }
create (): باطل
    var resultText: string = 'نتيجتك' + this.score + '!'؛
    this.result = this.add.text (200 ، 250 ، resultText ،
      {font: '48px Arial Bold'، fill: '#FBFBAC'})؛
var hintText: string = "انقر لإعادة التشغيل"؛
    this.hint = this.add.text (300 ، 350 ، hintText ،
      {font: '24px Arial Bold'، fill: '#FBFBAC'})؛
this.input.on ('pointerdown' ، الدالة (/ * المؤشر * /) {
      this.scene.start ( "WelcomeScene")؛
    }، هذه)؛
  }
}؛

نحتاج إلى تحديث ملف التطبيق الرئيسي الآن: أضف هذه المشاهد وجعل WelcomeScene ليكون الأول في القائمة:

استيراد "phaser" ؛
استيراد {WelcomeScene} من "./welcomeScene" ؛
استيراد {GameScene} من "./gameScene" ؛
استيراد {ScoreScene} من "./scoreScene" ؛
const config: GameConfig = {
  ...
  المشهد: [WelcomeScene ، GameScene ، ScoreScene] ،
  ...

هل لاحظت ما هو مفقود؟ صحيح ، نحن لا ندعو ScoreScene من أي مكان حتى الآن! دعنا نسميها عندما غاب اللاعب عن النجمة الثالثة:

onFall الخاص (نجمة: Phaser.Physics.Arcade.Image): () => void {
    وظيفة الإرجاع () {
      star.setTint (0xff0000)؛
      this.starsFallen + = 1؛
      this.time.delayedCall (100 ، دالة (نجمة) {
        star.destroy ()؛
        if (this.starsFallen> 2) {
          this.scene.start ( "ScoreScene"
            {starsCaught: this.starsCaught})؛
        }
      } ، [نجمة] ، هذا) ؛
    }
  }

أخيرًا ، تبدو لعبة Starfall وكأنها لعبة حقيقية - حيث تبدأ وتنتهي ، بل لها هدف للأرشفة (كم عدد النجوم التي يمكنك التقاطها؟).

آمل أن يكون هذا البرنامج التعليمي مفيدًا لك كما كان بالنسبة لي عندما كتبت عليه :) أي ردود فعل في غاية الامتنان!

يمكن العثور على الكود المصدري لهذا البرنامج التعليمي هنا.