كيفية كتابة sapper في Phaser وتشغيل مهمة اختبار مطور HTML5

مساء الخير أيها الزملاء الأعزاء!

اسمي ألكساندر ، أنا مطور ألعاب HTML5.

في إحدى الشركات التي أرسلت سيرتي الذاتية ، طُلب مني إكمال مهمة اختبار. وافقت ، وبعد يوم واحد ، أرسلت نتيجة تطوير اللعبة وفقًا لـ TOR HTML5.



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

سوف تقدم هذه المقالة فكرة عن مقدار العمل الكافي لإكمال مهمة الاختبار المتوسطة لموضع HTML5 الخاص بالمطور بنجاح. قد تكون المادة أيضًا موضع اهتمام أي شخص يريد التعرف على إطار عمل Phaser. وإذا كنت تعمل بالفعل مع Phaser والكتابة في JS - تعرف على كيفية تطوير مشروع في TypeScript.

لذلك ، تحت القط هناك الكثير من كود TypeScript!

مقدمة


نعطي بيانا موجزا للمشكلة.

  1. سنقوم بتطوير لعبة HTML5 بسيطة - sapper الكلاسيكية.
  2. كأدوات رئيسية سنستخدم phaser 3 و typescript و webpack.
  3. سيتم تصميم اللعبة لسطح المكتب وتشغيلها في المتصفح.

نحن نقدم روابط للمشروع النهائي.

روابط إلى العرض التوضيحي والمصدر

واسترجع ميكانيكا القاتل ، فجأة إذا نسي شخص ما قواعد اللعبة. ولكن نظرًا لأن هذه الحالة غير مرجحة ، يتم وضع القواعد تحت المفسد :)

قواعد سابر
يتكون الملعب من خلايا مرتبة في طاولة. افتراضيًا ، عندما تبدأ اللعبة ، يتم إغلاق جميع الخلايا. يتم وضع القنابل في بعض الخلايا.

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

إذا لم يكن هناك قنبلة في الخلية ، فسيتم عرض رقم بداخلها ، مما يشير إلى عدد القنابل الموجودة في الخلايا المجاورة بالنسبة للفتح الحالي. إذا لم تكن هناك قنابل في مكان قريب ، فإن الخلية تبدو فارغة.

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

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


1. التحضير


1.1 قالب المشروع


قم بتنزيل قالب مشروع phaser الافتراضي . هذا هو القالب الموصى به من مؤلف الإطار ويقدم لنا بنية الدليل التالية:
index.html وصفحة HTML إطلاق اللعبة
webpack /base.jsبناء التكوين لبيئة الاختبار
prod.jsبناء التكوين للإنتاج
src /الأصول /أصول اللعبة (العفاريت ، الأصوات ، الخطوط)
index.jsنقطة الدخول
بالنسبة index.js ، لسنا بحاجة إلى ملف index.js الحالي ، لذلك احذفه. ثم قم بإنشاء الدليل /src/scripts/ ثم ضع ملف index.ts الفارغ فيه. سنضيف كل نصوصنا إلى هذا المجلد.
تجدر الإشارة أيضًا إلى أنه عند إنشاء مشروع للإنتاج ، سيتم إنشاء دليل dist في الجذر ، حيث سيتم وضع إصدار الإصدار.

1.2 بناء التكوين


سوف نستخدم webpack للتجميع. نظرًا لأن القالب قد تم إعداده في الأصل للعمل مع JavaScript ، ونكتب في TypeScript ، فنحن بحاجة إلى إجراء تغييرات صغيرة على تكوين أداة التجميع.

في webpack/base.js أضف مفتاح entry ، الذي يشير إلى نقطة الدخول عند إنشاء مشروعنا ، وكذلك تكوين ts-loader الذي يصف قواعد إنشاء البرامج النصية TS:

 // webpack/base.js //... module.exports = { entry: './src/scripts/index.ts', // ... resolve: { extensions: [ '.ts', '.tsx', '.js' ] }, module: { rules: [{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }, //... 

سنحتاج أيضًا إلى إنشاء ملف tsconfig.json في جذر المشروع. بالنسبة لي يحتوي على المحتوى التالي:

 { "compilerOptions": { "module": "commonjs", "lib": [ "dom", "es5", "es6", "es2015", "es2017", "es2015.promise" ], "target": "es5", "skipLibCheck": true }, "exclude": ["node_modules", "dist"] } 

1.3 تركيب وحدات


قم بتثبيت جميع التبعيات من package.json وإضافة الوحدات النمطية للوحدات النمطية للصفيف و ts:

 npm i npm i typescript --save-dev npm i ts-loader --save-dev 

الآن المشروع جاهز لبدء التطوير. لدينا 2 أوامر تحت تصرفنا تم تعريفها بالفعل في خاصية scripts في ملف package.json .

  1. إنشاء مشروع للتصحيح وفتح في مستعرض من خلال خادم محلي

     npm start 
  2. تشغيل الإنشاء للبيع ووضع الإصدار الإصدار في المجلد dist /

     npm run build 

1.4 تحضير الأصول


يتم تنزيل جميع أصول هذه اللعبة بأمانة من OpenGameArt (الإصدار 61 × 61) ولديها أفضل التراخيص المسماة Feel free للاستخدام ، والتي تخبرنا بها الصفحة مع الحزمة بعناية). بالمناسبة ، الكود المعروض في المقالة لديه نفس الرخصة! ؛)

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

من خلال Phaser JSONArray الناتجة ، Phaser JSONArray أطلس تنسيق Phaser JSONArray في برنامج TexturePacker (يوجد أكثر من إصدار مجاني ، لم أحصل على عمل حتى الآن) spritesheet.json ملفات spritesheet.json و spritesheet.json في دليل src/assets/ project.



2. خلق مشاهد


2.1 نقطة الدخول


نبدأ التطوير من خلال إنشاء نقطة الإدخال الموضحة في تهيئة webpack.

 // src/scripts/index.ts import * as Phaser from "phaser"; new Phaser.Game({ type: Phaser.AUTO, parent: "minesweeper", width: window.innerWidth, height: window.innerHeight, backgroundColor: "#F0FFFF", scene: [] }); 

نظرًا لأن اللعبة التي لدينا مصممة لسطح المكتب وستملأ الشاشة بالكامل ، فإننا نستخدم بجرأة العرض والارتفاع الكامل للمتصفح لحقول width height .
حقل scene حاليًا مجموعة فارغة وسنصلحها!

2.2 بداية المشهد


قم src/scripts/scenes/StartScene.ts فئة المشهد الأول في ملف src/scripts/scenes/StartScene.ts :

 export class StartScene extends Phaser.Scene { constructor() { super('Start'); } public preload(): void { } public create(): void { } } 

من أجل الميراث الصحيح لـ Phaser.Scene نقوم بتمرير اسم المشهد كمعلمة إلى مُنشئ الفئة الأصل.

سيجمع هذا المشهد بين وظيفة التحميل المسبق للموارد وشاشة البدء ، ودعوة المستخدم إلى اللعبة.

عادةً ما يمر المشاهد في مشاريعي بمشاهدتين قبل وصوله إلى البداية ، بهذا الترتيب:

 Boot => Preload => Start 

لكن في هذه الحالة ، تكون اللعبة بسيطة جدًا ، وهناك عدد قليل جدًا من الأصول بحيث لا يوجد سبب لوضع التحميل المسبق في مشهد منفصل ، بل وأكثر من ذلك للقيام ببرنامج تحميل Boot الأولي.

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

  1. الحصول على كل من ملفات png و json atlas باستخدام:

     // StartScene.ts const spritesheetPng = require("./../../assets/spritesheet.png"); const spritesheetJson = require("./../../assets/spritesheet.json"); // ... 

  2. قم preload طريقة preload لمشهد البداية:

     // StartScene.ts // ... public preload(): void { this.load.atlas("spritesheet", spritesheetPng, spritesheetJson); } // ... 


2.3 نصوص المشهد الانطلاق


هناك 2 الأشياء المتبقية للقيام في مشهد البداية:

  1. أخبر اللاعب كيف تبدأ اللعبة
  2. ابدأ اللعبة بمبادرة من اللاعب

لتحقيق النقطة الأولى ، نقوم أولاً بإنشاء تعدادين في بداية ملف المشهد لوصف النصوص وأنماطها:

 // StartScene.js enum Texts { Title = 'Minesweeper HTML5', Message = 'Click anywhere to start' } enum Styles { Color = '#008080', Font = 'Arial' } //... 

ثم قم بإنشاء كلا النصين ككائنات في طريقة create . اسمحوا لي أن أذكرك بأن طريقة create المشاهد في Phaser لن يتم استدعاؤها إلا بعد تحميل جميع الموارد في طريقة preload وهذا مناسب تمامًا لنا.

 // StartScene.js //... public create(): void { this.add.text( this.cameras.main.centerX, this.cameras.main.centerY - 100, Texts.Title, {font: `52px ${Styles.Font}`, fill: Styles.Color}) .setOrigin(0.5); this.add.text( this.cameras.main.centerX, this.cameras.main.centerY + 100, Texts.Message, {font: `28px ${Styles.Font}`, fill: Styles.Color}) .setOrigin(0.5); } //... 

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

2.4 الانتقال إلى مستوى اللعبة


الشيء الأخير الذي سنفعله في هذا المشهد قبل الانتقال هو تتبع حدث النقر بالماوس لإطلاق اللاعب في اللعبة:

 // StartScene.js //... public create(): void { //... this.input.once('pointerdown', () => { this.scene.start('Game'); }); } //... 

2.5 مستوى المشهد


انطلاقًا من المعلمة "Game" تم تمريرها إلى طريقة this.scene.start لقد خمنت بالفعل أن الوقت قد حان لإنشاء مشهد ثانٍ ، والذي سيعالج منطق اللعبة الرئيسي. قم src/scripts/scenes/GameScene.ts :

 export class GameScene extends Phaser.Scene { constructor() { super('Game'); } public create(): void { } } 

في هذا المشهد ، لسنا بحاجة إلى طريقة preload ، لأن لقد قمنا بالفعل بتحميل جميع الموارد اللازمة في المشهد السابق.

2.6 ضبط المشاهد عند نقطة الدخول


الآن بعد إنشاء كلتا المشاهدتين ، أضفهما إلى نقطة الدخول الخاصة بنا
src/scripts/index.ts :

 //... import { StartScene } from "./scenes/StartScene"; import { GameScene } from "./scenes/GameScene"; //... new Phaser.Game({ // ... scene: [StartScene, GameScene] }); 

3. كائنات اللعبة


لذلك ، GameScene فئة GameScene المنطق على مستوى اللعبة. وماذا نتوقع من مستوى لعبة sapper؟ بصريا ، نتوقع أن نرى ملعب مع خلايا مغلقة. نحن نعلم أن الحقل عبارة عن جدول ، مما يعني أنه يحتوي على عدد معين من الصفوف والأعمدة ، يتم وضع القنابل في عدة منها بشكل مريح. وبالتالي ، لدينا معلومات كافية لإنشاء كيان منفصل يصف مجال اللعب.

3.1 لوحة اللعبة


قم src/scripts/models/Board.ts الذي نضع فيه فئة src/scripts/models/Board.ts :

 import { Field } from "./Field"; export class Board extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _rows: number = 0; private _cols: number = 0; private _bombs: number = 0; private _fields: Field[] = []; constructor(scene: Phaser.Scene, rows: number, cols: number, bombs: number) { super(); this._scene = scene; this._rows = rows; this._cols = cols; this._bombs = bombs; this._fields = []; } public get cols(): number { return this._cols; } public get rows(): number { return this._rows; } } 

لنجعل الفصل هو خليفة Phaser.Events.EventEmitter من أجل الوصول إلى واجهة تسجيل الأحداث والاتصال بها ، والتي سنحتاجها في المستقبل.

سيتم تخزين مجموعة من كائنات فئة Field في الخاصية الخاصة _fields . سنطبق هذا النموذج لاحقًا.

لقد قمنا بإعداد الخصائص الرقمية الخاصة _rows و _cols للإشارة إلى عدد الصفوف والأعمدة في الملعب. إنشاء مجموعات عامة لقراءة _rows و _cols .

يخبرنا مجال القنابل عن عدد القنابل التي يجب توليدها للمستوى. وفي المعلمة _scene نقوم بتمرير مرجع إلى كائن مشهد لعبة GameScene ، حيث سنقوم بإنشاء مثيل لفئة Board .

تجدر الإشارة إلى أننا ننقل كائن المشهد إلى النموذج فقط لمزيد من الإرسال إلى المشاهدات ، حيث سنستخدمه فقط لعرض العرض. والحقيقة هي أن phaser يستخدم كائن المشهد مباشرة لتقديم العفاريت ، وبالتالي يلزمنا بتوفير رابط إلى المشهد الحالي عند إنشاء أبنية جاهزة للعفاريت ، والتي سنطورها في المستقبل. وبالنسبة لأنفسنا ، سوف نقبل الاتفاقية التي تقضي بنقل الرابط إلى المشهد فقط لاستخدامه كمحرك عرض ونوافق على أننا لن ندعو مباشرة إلى الأساليب المخصصة للمشهد في النماذج ووجهات النظر.

بمجرد اتخاذ قرار بشأن واجهة إنشاء اللوحة ، أقترح التهيئة في مشهد المستوى ، مع وضع اللمسات الأخيرة على فئة GameScene :

  // GameScene.ts import { Board } from "../models/Board"; const Rows = 8; const Cols = 8; const Bombs = 8; export class GameScene extends Phaser.Scene { private _board: Board = null; //... public create(): void { this._board = new Board(this, Rows, Cols, Bombs); } } 

نأخذ معلمات اللوحة إلى الثوابت في بداية ملف المشهد وننقلها إلى مُنشئ اللوحة عند إنشاء مثيل لهذه الفئة.

3.2 نموذج الخلية


تتكون اللوحة من الخلايا التي تريد عرضها على الشاشة. يجب أن توضع كل خلية في الموضع المقابل ، يحددها الصف والعمود.

يتم تحديد الخلايا أيضًا ككيان منفصل. قم src/scripts/models/Field.ts الذي src/scripts/models/Field.ts فيه الفصل الذي يصف الخلية:

 import { Board } from "./Board"; export class Field extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _board: Board = null; private _row: number = 0; private _col: number = 0; constructor(scene: Phaser.Scene, board: Board, row: number, col: number) { super(); this._init(scene, board, row, col); } public get col(): number { return this._col; } public get row(): number { return this._row; } public get board(): Board { return this._board; } private _init(scene: Phaser.Scene, board: Board, row: number, col: number): void { this._scene = scene; this._board = board; this._row = row; this._col = col; } } 

يجب أن تحتوي كل خلية على مقاييس للصف والعمود التي توجد فيها. قمنا بإعداد المعلمات _board و _scene لإعداد روابط لكائنات اللوحة والمشهد. نحن _row _col _board و _board و _board .

3.3 عرض الخلية


يتم إنشاء الخلية المجردة ونريد الآن تصورها. لعرض خلية على الشاشة ، تحتاج إلى إنشاء طريقة العرض الخاصة بها. قم src/scripts/views/FieldView.ts ووضع فئة العرض فيه:

 import { Field } from "../models/Field"; export class FieldView extends Phaser.GameObjects.Sprite { private _model: Field = null; constructor(scene: Phaser.Scene, model: Field) { super(scene, 0, 0, 'spritesheet', 'closed'); this._model = model; this._init(); this._create(); } private _init(): void { } private _create(): void { } } 

يرجى ملاحظة أننا جعلنا هذه الفئة سليل Phaser.GameObjects.Sprite . من حيث المرحلتين ، أصبحت هذه الفئة من المباني الجاهزة ذات العفريت. هذا هو ، حصلت على وظيفة كائن اللعبة من sprite ، والتي سوف نقوم بتوسيعها بطرقنا الخاصة.

لنلقِ نظرة على مُنشئ هذه الفئة. هنا ، أولاً وقبل كل شيء ، يجب أن نسمي مُنشئ الفئة الأصل بمجموعات المعلمات التالية:

  • ارتباط بكائن المشهد (كما حذرت في القسم 3.1: يتطلب phaser الارتباط بالمشهد الحالي من أجل تقديم العفاريت)
  • إحداثيات x و y على قماش
  • مفتاح السلسلة الذي يتوفر للأطلس ، والذي قمنا بتحميله في طريقة preload لمشهد البداية
  • مفتاح سلسلة الإطار في هذا الأطلس الذي تريد تحديده لعرض العفريت

قم بتعيين مرجع للنموذج (أي ، مثيل لفئة Field ) في خاصية _model الخاصة.

لقد بدأنا أيضًا بحكمة 2 _init و _init فارغة حاليًا ، والتي _create لاحقًا.

3.4 إنشاء العفريت في فئة العرض


لذلك ، تم إنشاء العرض ، لكنها لا تزال لا تعرف كيفية رسم العفريت. لوضع العفريت مع الإطار الذي نحتاجه على القماش ، ستحتاج إلى تعديل طريقة _create الخاصة:

 // FieldView.js //... private _create(): void { this.scene.add.existing(this); //      this.setOrigin(0.5); //  pivot point    } //... 

3.5 العفريت لتحديد المواقع


في الوقت الحالي ، سيتم وضع جميع العفاريت التي تم إنشاؤها في إحداثيات (0 ، 0) من اللوحة القماشية. نحتاج أيضًا إلى وضع كل خلية في موضعها المقابل على السبورة. وهذا هو ، إلى المكان الذي يتوافق مع صف وعمود هذه الخلية. للقيام بذلك ، نحتاج إلى كتابة رمز لحساب إحداثيات كل مثيل لفئة FieldView .

أضف خاصية _position إلى الفصل ، وهو المسؤول عن الإحداثيات النهائية للخلية في الملعب:

 // FieldView.ts //... interface Vec2 {x: number, y: number}; export class FieldView extends Phaser.GameObjects.Sprite { private _position: Vec2 = {x: 0, y: 0}; //... 

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

 // FieldView.ts //... private get _offset(): Vec2 { return { x: (this.scene.cameras.main.width - this._model.board.cols * this.width) / 2, y: (this.scene.cameras.main.height - this._model.board.rows * this.height) / 2 }; } //... 

وبالتالي ، نحن:

  1. حصلت على إجمالي عرض الشاشة في this._scene.cameras.main.width .
  2. حصلنا على العرض الكلي this._board.cols * this.width عن طريق ضرب عدد الخلايا بعرض خلية واحدة: this._board.cols * this.width .
  3. إذا أخذنا عرض اللوحة من عرض الشاشة ، فقد حصلنا على مكان على الشاشة ، ولم تشغله اللوحة.
  4. بقسمة الرقم الناتج على 2 ، حصلنا على قيمة المسافة البادئة إلى اليسار واليمين من اللوحة.
  5. عن طريق تحويل كل خلية بقيمة هذه المسافة البادئة ، فإننا نضمن محاذاة اللوحة بأكملها على طول المحور x .

نقوم بتنفيذ إجراءات مماثلة تماما للحصول على النزوح العمودي.

يبقى لإضافة التعليمات البرمجية الضرورية في طريقة _init :

 // FieldView.ts // ... private _init(): void { const offset = this._offset; this.x = this._position.x = offset.x + this.width * this._model.col + this.width / 2; this.y = this._position.y = offset.y + this.height * this._model.row + this.height / 2; } // ... 

الخصائص this.y و this.x و this.width و this.height هنا هي الخصائص الموروثة للفئة الأصل Phaser.GameObjects.Sprite . يؤدي تغيير خصائص this.y و this.x إلى تحديد موضع العفريت الصحيح على اللوحة القماشية.

3.6 إنشاء مثيل لـ FieldView


إنشاء طريقة عرض في فئة Field :

 // Field.ts // ... private _view: FieldView = null; public get view(): FieldView { return this._view; } private _init(scene: Phaser.Scene, board: Board, row: number, col: number): void { //... this._view = new FieldView(this._scene, this); } // ... 

3.7 حقول لوحة العرض.


دعنا نعود إلى فئة اللوحة ، والتي هي في الأساس مجموعة من كائنات Field وستنشئ خلايا.

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

 // Board.ts constructor(scene: Phaser.Scene, rows: number, cols: number, bombs: number) { // ... this._create(); } private _create(): void { this._createFields(); } private _createFields(): void { } 

في هذه الطريقة سنقوم بإنشاء العدد المطلوب من الخلايا في حلقة متداخلة:

 // Board.ts // ... private _createFields(): void { for (let row = 0; row < this._rows; row++) { for (let col = 0; col < this._cols; col++) { this._fields.push(new Field(this._scene, this, row, col)); } } } //... 

لقد حان الوقت لأول مرة لتشغيل التجميع لتصحيح الأخطاء باستخدام الأمر

 npm start 

تأكد من أنه في وسط الشاشة ، من المتوقع أن نرى 64 خلية في 8 صفوف.

3.8 صنع القنابل


لقد ذكرت في وقت سابق أنه في طريقة إنشاء فئة Board ، لن نقوم فقط بإنشاء الحقول. ماذا بعد؟ سيكون هناك أيضًا إنشاء قنابل ، ووضع الخلايا التي تم إنشاؤها على عدد القنابل المجاورة. لنبدأ بالقنابل بأنفسهم.

نحن بحاجة إلى وضع قنابل N على السبورة في خلايا عشوائية. وصفنا عملية إنشاء القنابل باستخدام خوارزمية تقريبية:

                          

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

لإنشاء رقم عشوائي ، نستخدم الأسلوب الثابت Phaser.Math.Between .

 // Board.ts //... private _createBombs(): void { let count = this._bombs; //      while (count > 0) { //       let field = this._fields[Phaser.Math.Between(0, this._fields.length - 1)]; //    if (field.empty) { //     field.setBomb(); //     --count; //    } } } 

لا تنسَ أن تكتب المكالمة على هذا this._createBombs(); في ملف Board.ts this._createBombs(); في نهاية طريقة _create

كما لاحظت من قبل ، لكي تعمل هذه الشفرة بشكل صحيح ، تحتاج إلى تحسين فئة Field عن طريق إضافة getter empty وطريقة setBomb .

إضافة حقل _value خاص إلى _value الحقل ، والتي ستنظم محتويات الخلية. نحن نقبل الاتفاقيات التالية.
_value === 0الخلية فارغة ولا توجد فيها ألغام أو قيم
_value === -1يوجد منجم في الزنزانة
_value > 0في الخلية هو عدد الألغام الموجودة بجانب الخلية الحالية

باتباع هذه القواعد ، سنقوم بتطوير أساليب في فئة Field تعمل مع خاصية _value :

 // Field.ts // ... private _value: number = 0; // ... public get value(): number { return this._value; } public set value(value) { this._value = value; } public get empty(): boolean { return this._value === 0; } public get mined(): boolean { return this._value === -1; } public get filled(): boolean { return this._value > 0; } public setBomb(): void { this._value = -1; } // ... 

3.9 ضبط القيم


يتم ترتيب القنابل والآن لدينا جميع البيانات من أجل تعيين القيم العددية في جميع الخلايا التي تتطلب ذلك.

واسمحوا لي أن أذكرك أنه وفقًا لقواعد sapper ، يجب أن يكون للخلية رقم يتوافق مع عدد القنابل الموجودة بجانب هذه الخلية. بناءً على هذه القاعدة ، نكتب الكود الكاذب المقابل.

                    

في فئة اللوحة ، قم بإنشاء طريقة جديدة وترجمة الكود الكاذب المحدد إلى رمز حقيقي:

 // Board.ts //... private _createValues() { //      this._fields.forEach(field => { //      if (field.mined) { //     field.getClosestFields().forEach(item => { //      if (item.value >= 0) { ++item.value; } }); } }); } //... 

لنرى أي من الواجهات التي نستخدمها لم يتم تنفيذها. تحتاج إلى إضافة طريقة getClosestFields للحصول على الخلايا المجاورة.

كيفية التعرف على الخلايا المجاورة؟

على سبيل المثال ، ضع في اعتبارك أي خلية في اللوحة ليست على الحافة ، أي في الصفوف القصوى وليس في العمود أقصى. تحتوي هذه الخلايا على أقصى عدد من الجيران: 1 في الأعلى ، وواحد في الأسفل ، و 3 في اليسار ، و 3 على اليمين (بما في ذلك الخلايا في القطر).

وبالتالي ، في كل من الخلايا المجاورة ، لا تختلف المؤشرات _row و _col بأكثر من 1. وهذا يعني أنه يمكننا تحديد الفرق بين المعلمتين _row و _col مع الحقل الحالي. أضف ثابتًا في بداية الملف إلى وصف الفصل:

 // Field.ts const Positions = [ {row : 0, col : 1}, //  {row : 0, col : -1}, //  {row : 1, col : 0}, //  {row : 1, col : 1}, //   {row : 1, col : -1}, //   {row : -1, col : 0}, //  {row : -1, col : 1}, //   {row : -1, col : -1} //   ]; //... 

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

 // Field.ts //... public getClosestFields(): Field[] { let results = []; //      Positions.forEach(position => { //      let field = this._board.getField(this._row + position.row, this._col + position.col); //       if (field) { //     results.push(field); } }); return results; }; //... 

لا تنس التحقق من متغير field في كل تكرار ، حيث لا تحتوي جميع الخلايا الموجودة على اللوحة على 8 جيران. على سبيل المثال ، لن يكون للخلية اليسرى العليا جيران على يسارها ، وهكذا.

يبقى تنفيذ طريقة getField وإضافة جميع المكالمات الضرورية إلى طريقة _create في فئة _create

 // Board.ts //... public getField(row: number, col: number): Field { return this._fields.find(field => field.row === row && field.col === col); } //... private _create(): void { this._createFields(); this._createBombs(); this._createValues(); } //... 

4. التعامل مع أحداث الإدخال


4.1 تتبع أحداث النقر بالماوس


في الوقت الحالي ، تمت تهيئة اللوحة بالكامل ، ولديها قنابل وهناك خلايا بها أرقام ، لكن جميعها مغلقة حاليًا ولا توجد طريقة لفتحها. سنقوم بتصحيح هذا وننفذ فتح الخلايا عن طريق النقر على زر الماوس الأيسر.

أولاً ، نحن بحاجة إلى تتبع هذه النقرة ذاتها. في الفصل الدراسي ، FieldViewأضف _createالكود التالي إلى نهاية الطريقة :

 // FielView.ts //... private _create(): void { // ... this.setInteractive(); } //... 

في phaser ، يمكنك الاشتراك في كائنات من مساحة الاسم للأحداث المختلفة Phaser.GameObjects. على وجه الخصوص ، سوف نشترك في حدث النقر ( pointerdown) للأبنية الجاهزة للعفريت نفسها ، أي كائن من فئة FieldViewموروثة منها Phaser.GameObjects.Sprite.

ولكن قبل القيام بذلك ، يجب أن نشير بوضوح إلى أن العفريت تفاعليًا ، أي أنك تحتاج عمومًا إلى الاستماع إلى إدخال المستخدم عليه. تحتاج إلى القيام بذلك عن طريق استدعاء الأسلوب setInteractiveدون معلمات على sprite نفسه ، وهو ما قمنا به في المثال أعلاه.

والآن بعد أن أصبح العفريت لدينا تفاعليًا ، دعنا نعود إلى الفصل Boardفي المكان الذي يتم فيه إنشاء كائنات نموذج جديدة Field، أي إلى الطريقة _createFieldsوتسجيل رد الاتصال لأحداث الإدخال للعرض:

 // Board.ts //... private _createFields(): void { for (let row = 0; row < this._rows; row++) { for (let col = 0; col < this._cols; col++) { const field = new Field(this._scene, this, row, col) field.view.on('pointerdown', this._onFieldClick.bind(this, field)); this._fields.push(field); } } } //... 

بمجرد أن نثبت أنه من خلال النقر على العفريت نريد تشغيل الطريقة _onFieldClick، نحتاج إلى تنفيذها. لكننا سنزيل منطق معالجة النقرة من الفصل Board. يُعتقد أنه من الأفضل معالجة النموذج اعتمادًا على الإدخال وتغيير بياناته وفقًا لذلك في وحدة تحكم منفصلة ، تشابهها هو فئة مشهد اللعبة GameScene. لذلك ، نحتاج إلى إعادة توجيه حدث النقر إلى أبعد من ذلك ، من الفصل Boardإلى المشهد نفسه. لذلك سنفعل:

 // Board.ts //... private _onFieldClick(field: Field, pointer: Phaser.Input.Pointer): void { if (pointer.leftButtonDown()) { this.emit(`left-click`, field); } else if (pointer.rightButtonDown()) { this.emit(`right-click`, field); } } //... 

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

حسنًا ، دعنا نعود الآن إلى فئة مشهد اللعبة GameSceneوأضف _createرمزًا في نهاية الطريقة التي تتتبع أحداث النقر فوق الخلايا:

 // Board.ts //... import { Field } from "../models/Field"; //... public create(): void { this._board = new Board(this, Rows, Cols, Bombs); this._board.on('left-click', this._onFieldClickLeft, this); this._board.on('right-click', this._onFieldClickRight, this); } private _onFieldClickLeft(field: Field): void { } private _onFieldClickRight(field: Field): void { } //... 

4.2. انقر فوق معالجة اليسار


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

  1. عند النقر فوق خلية مغلقة ، يجب فتحها
  2. إذا كان هناك لغم في خلية مفتوحة - تضيع اللعبة
  3. إذا لم تكن هناك مناجم أو قيم في الخلية المفتوحة ، فلن تكون min في الخلايا المجاورة ، في هذه الحالة تحتاج إلى فتح جميع الخلايا المجاورة والاستمرار في القيام بذلك حتى تظهر القيمة في الخلية المفتوحة
  4. عند النقر فوق خلية مفتوحة ، يجب عليك التحقق مما إذا كانت جميع الأعلام قد تم تعيينها بشكل صحيح وإذا كان الأمر كذلك ، فأنهي اللعبة بالفوز

والآن ، لتبسيط فهم الوظيفة المطلوبة ، نترجم المنطق أعلاه إلى كود زائف:

                           

الآن لدينا فهم لما يحتاج إلى برمجة. نطبق الطريقة _onFieldClickLeft:

 // GameScene.ts //... private _onFieldClickLeft(field: Field): void { if (field.closed) { //    field.open(); //   if (field.mined) { //    field.exploded = true; this._onGameOver(false); //   } else if (field.empty) { //    this._board.openClosestFields(field); //   } } else if (field.opened) { //    if (this._board.completed) { //       this._onGameOver(true); //   } } } //... 

وبعد ذلك، كما هو الحال دائما، وسوف نقوم بتعديل الطبقات Fieldو Boardمن تنفيذها في الطرق التي نسميها معالج.

نحن نشير إلى 3 حالات محتملة للخلية في التعداد States، ونضيف حقلًا ونقوم _stateبتطبيق كل حالة ممكنة:

 // Field.ts enum States { Closed = 'closed', Opened = 'opened', Marked = 'flag' }; export class Field extends Phaser.Events.EventEmitter { private _state: string = States.Closed; //... public get marked(): boolean { return this._state === States.Marked; } public get closed(): boolean { return this._state === States.Closed; } public get opened(): boolean { return this._state === States.Opened; } //... 

الآن بما أن لدينا حالات تشير إلى ما إذا كانت الخلية مغلقة أم لا ، فيمكننا إضافة طريقة openلتغيير الحالة:

 // Field.ts //... public open(): void { this._setState(States.Opened); } private _setState(state: string): void { if (this._state !== state) { this._state = state; this.emit('change'); } } //... 

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

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

 // Field.ts private _exploded: boolean = false; //... public set exploded(exploded: boolean) { this._exploded = exploded; this.emit('change'); } public get exploded(): boolean { return this._exploded; } //... 

الآن افتح الفصل Boardوقم بتنفيذ الطريقة فيه openClosestFields. هذه الطريقة متكررة وستكون مهمتها هي فتح جميع الحقول المجاورة الفارغة بالنسبة للخلية المقبولة في المعلمة.
ستكون الخوارزمية كما يلي:

  :                 

وهذه المرة لدينا بالفعل جميع الواجهات اللازمة للتنفيذ الكامل لهذه الطريقة:

 // Board.ts //... public openClosestFields(field: Field): void { field.getClosestFields().forEach(item => {//     if (item.closed) {//    item.open();//   if (item.empty) {//    this.openClosestFields(item);//     } } }); } //... 

أضف جامعًا completedإلى الفصل Boardللإشارة إلى الموضع الصحيح للأعلام على السبورة. كيف يمكننا تحديد ما إذا كان تم مسح لوحة بنجاح؟ يجب أن يساوي عدد الحقول المحددة بشكل صحيح العدد الإجمالي للقنابل الموجودة على السبورة.

 // Board.ts //... public get completed(): boolean { return this._fields.filter(field => field.completed).length === this._bombs; } //... 

تقوم هذه الطريقة بتصفية الصفيف _fieldsبواسطة getter completed، والتي يجب أن تشير إلى صحة علامة الحقل. إذا كان طول المصفاة المصفاة (التي تسقط فيها الحقول التي تم تمييزها بشكل صحيح فقط ، والتي يتحمل مسؤوليتها completedبالفعل عن الفصل Field) مساوياً لقيمة الحقل _bombs(أي ، عدد القنابل على اللوحة) ، فإننا نرجع true، بمعنى آخر ، نحن نعتبر اللعبة منتصرة.
كما أننا لا نمانع بفرصة فتح اللوحة بالكامل بمكالمة واحدة ، ما يتعين علينا القيام به في نهاية المستوى. سنضيف أيضًا هذه الميزة إلى الفصل Board:

 // Board.ts //... public open(): void { this._fields.forEach(field => field.open()); } //... 

يبقى لإضافة جامع completedإلى الفصل نفسه Field. في هذه الحالة ، سيتم النظر في تطهير الحقل بنجاح؟ إذا كان الملغومة وعلمها. كلتا الحالتين الضروريتين موجودتان بالفعل ويمكننا إضافة هذه الطريقة:

 // Field.ts //... public get completed(): boolean { return this.marked && this.mined; } //... 

لإكمال معالجة النقر بالماوس الأيسر ، سننشئ طريقة _onGameOverلتعطيل تتبع أحداث اللوحة وإظهار اللاعب بأكمله. بعد ذلك ، سنضيف إليها أيضًا رمز تقديم لتقرير إتمام الحالة استنادًا إلى المعلمة status.

 // GameScene.ts //... private _onGameOver(status: boolean) { this._board.off('left-click', this._onFieldClickLeft, this); this._board.off('right-click', this._onFieldClickRight, this); this._board.open(); } //... 

4.3 حقل العرض


قبل البدء في معالجة النقر بزر الماوس الأيمن ، سنتعلم كيفية إعادة رسم الخلايا التي تم فتحها حديثًا.

في وقت سابق من الفصل ، Fieldقمنا بتطوير طريقة _setStateتطلق حدثًا changeعندما تتغير حالة النموذج. سنستخدم هذا وفي الفصل FieldViewسنتتبع هذا الحدث:

 // FieldView.ts //... private _init(): void { //... this._model.on('change', this._onStateChange, this); } private _onStateChange(): void { this._render(); } private _render(): void { this.setFrame(this._frameName); } //... 

لقد جعلنا على وجه التحديد الطريقة الوسيطة _onStateChangeردًا لحدث تغيير النموذج. في المستقبل ، سنحتاج إلى التحقق من كيفية تغيير النموذج لفهم ما إذا كان من الضروري القيام به _render.

لإظهار العفريت الحالي لخلية في حالة جديدة ، تحتاج إلى تغيير إطارها. نظرًا لأننا قمنا بتحميل الأطلس كأصول ، يمكننا استدعاء الأسلوب من setFrameأجل تغيير الإطار الحالي إلى إطار جديد.

للحصول على الإطار في سطر واحد ، استخدمنا الماكرة على نحو ماكر _frameName، والتي تحتاج الآن إلى تنفيذها. أولاً ، نحن نصف جميع القيم المحتملة التي يمكن أن يتخذها إطار الخلية.
إطارحالة
closedالحقل مغلق
flagحقل علم
emptyالحقل مفتوح ، وليس ملغومًا أو ممتلئًا بقيمة
exploded
الحقل مفتوح ، ملغوم ومتفجر
mined
الحقل مفتوح ، ملغوم ، لكن غير مهجور
1...9
الحقل مفتوح ويعرض قيمة من 1 إلى 9 ، مما يشير إلى عدد القنابل المجاورة لهذا الحقل

لقد حصلنا على وصف لجميع الحالات ولدينا بالفعل جميع أساليب النموذج ، بفضل هذه الحالات يمكن الحصول عليها. دعنا نحصل على تهيئة صغيرة في بداية الملف:

 // FieldView.ts const States = { 'closed': field => field.closed, 'flag': field => field.marked, 'empty': field => field.opened && !field.mined && !field.filled, 'exploded': field => field.opened && field.mined && field.exploded, 'mined': field => field.opened && field.mined && !field.exploded } //... 

ستكون المفاتيح في هذا الكائن هي قيم الإطارات ، وقيم هذه المفاتيح هي عمليات الاسترجاعات التي تُرجع نتيجة منطقية. استنادًا إلى هذا التكوين ، يمكننا تطوير طريقة للحصول على الإطار المطلوب (أي ، المفتاح من التكوين):

 // FieldView.ts //... private get _frameName(): string { for (let key in States) { if (States[key](this._model)) { return key; } } return this._model.value.toString(); } 

وبالتالي ، من خلال التعداد البسيط في حلقة ، نذهب من خلال جميع مفاتيح كائن التكوين ونستدعي كل رد اتصال بدوره. تشير الوظيفة التي تُرجعنا أولاً trueإلى أن المفتاح keyفي التكرار الحالي هو الإطار الصحيح للحالة الحالية للنموذج.

إذا لم يكن أي مفتاح مناسبًا ، فبالنسبة للحالة الافتراضية ، سنفكر في حقل مفتوح ذي قيمة _value، Statesلأننا لم نقم بتعيين هذه الحالة في التكوين .

الآن يمكننا اختبار النقر الأيسر بالكامل على حقول اللوحة والتحقق من كيفية فتح الخلايا وما يتم عرضه بعد فتحها.

4.4 انقر بزر الفأرة الأيمن


كما في حالة إنشاء معالج النقر بزر الماوس الأيسر ، فإننا نحدد أولاً الوظيفة المتوقعة بوضوح. بالنقر بزر الماوس الأيمن ، يجب أن نضع علامة على الخلية المحددة بعلامة. ولكن هناك بعض الشروط.

  1. لا يمكن وضع علامة إلا على الحقل المغلق غير المميز حاليًا
  2. إذا تم تحديد الحقل ، فيجب أن يؤدي النقر بزر الماوس الأيمن مرة أخرى إلى إزالة العلم من الحقل
  3. عند ضبط / إزالة علامة ، من الضروري تحديث عدد العلامات المتاحة على المستوى وعرض النص بالرقم الحالي

عند ترجمة هذه الشروط إلى كود زائف ، نحصل على أسطر التعليقات التالية:

                                

يمكننا الآن ترجمة هذه الخوارزمية إلى استدعاءات للطرق التي نحتاجها ، حتى لو لم يتم تطويرها بعد:

 // GameScene.ts private _flags: number = 0; //... private _onFieldClickRight(field: Field): void { if (field.closed && this._flags > 0) { //        field.addFlag(); //     } else if (field.marked) { //     field.removeFlag(); //   } this._flags = Bombs - this._board.countMarked; } //... public create(): void { this._flags = Bombs; //... } //... 

هنا بدأنا أيضًا مجالًا جديدًا _flags، وهو في بداية مستوى اللعبة يساوي عدد القنابل على اللوحة ، لأنه في بداية اللعبة لم يتم تعيين علم واحد. يجب تحديث هذا الحقل بكل نقرة صحيحة ، لأنه في هذه الحالة يتم إضافة العلم أو إزالته من اللوحة. إضافة Boardجامع إلى الفصل countMarked:

 // Board.ts //... public get countMarked(): number { return this._fields.filter(field => field.marked).length; } //... 

يعد إعداد العلامة وإزالتها تغييرًا في حالة النموذج Field، لذلك نقوم بتنفيذ هذه الطرق في الفصل المقابل بشكل مشابه للطريقة open:

 // Field.ts //... public addFlag(): void { this._setState(States.Marked); } public removeFlag(): void { this._setState(States.Closed); } //... 

دعني أذكرك بأنه _setStateسيؤدي إلى حدث changeيتم تتبعه في العرض ، وبالتالي ، سيتم إعادة رسم العفريت تلقائيًا هذه المرة عندما يتغير النموذج.

عند اختبار الوظيفة المطورة ، ستجد بالتأكيد أنه في كل مرة تنقر فوق زر الماوس الأيمن ، تفتح قائمة السياق. أضف التعليمات البرمجية التي تعطل هذا السلوك إلى مُنشئ مشهد اللعبة:

 // GameScene.ts //... constructor() { super('Game'); //        document.querySelector("canvas").oncontextmenu = e => e.preventDefault(); } //... 

4.5 GameSceneView كائن


لعرض واجهة المستخدم على مشهد اللعبة ، سنقوم بإنشاء فصل GameSceneViewووضعه فيه src/scripts/views/GameSceneView.ts.

في هذه الحالة ، سوف نتصرف بطريقة مختلفة عن الخلق FieldViewولن نجعل هذه الفئة من المباني الجاهزة ووريثًا GameObjects.
في هذه الحالة ، نحتاج إلى إخراج العناصر التالية من عرض المشهد:

  • النص في عدد الأعلام
  • زر الخروج
  • رسالة حالة إكمال اللعبة (الفوز / الخسارة)

لنجعل كل عنصر واجهة مستخدم حقل منفصل في الفصل GameSceneView.
سوف نستعد كعب.

 enum Styles { Color = '#008080', Font = 'Arial' } enum Texts { Flags = 'FLAGS: ', Exit = 'EXIT', Success = 'YOU WIN!', Failure = 'YOU LOOSE' }; export class GameSceneView { private _scene: Phaser.Scene = null; private _style: {font: string, fill: string}; constructor(scene: Phaser.Scene) { this._scene = scene; this._style = {font: `28px ${Styles.Font}`, fill: Styles.Color}; this._create(); } private _create(): void { } public render() { } } 

إضافة نص مع عدد الأعلام.

 // GameSceneView.ts //... private _txtFlags: Phaser.GameObjects.Text = null; //... private _createTxtFlags(): void { this._txtFlags = this._scene.add.text( 50, 50, Texts.Flags, this._style ).setOrigin(0, 1); } //... 

هذا الكود سوف يضع النص الذي نحتاجه في موضع بمسافة بادئة 50px من الجوانب العلوية واليسرى وتعيينه على النمط المحدد. بالإضافة إلى ذلك ، تقوم الطريقة setOriginبتعيين النقطة المحورية للنص على الإحداثيات (0 ، 1). هذا يعني أن النص سوف يتماشى مع حده الأيسر.

إضافة رسالة الحالة.

 // GameSceneView.ts //... private _txtStatus: Phaser.GameObjects.Text = null; //... private _createTxtStatus(): void { this._txtStatus = this._scene.add.text( this._scene.cameras.main.centerX, 50, Texts.Success, this._style ).setOrigin(0.5, 1); this._txtStatus.visible = false; } //... 

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

إنشاء زر الخروج ، والذي في جوهره هو أيضا كائن نصي.

 // GameSceneView.ts //... private _btnExit: Phaser.GameObjects.Text = null; //... private _createBtnExit(): void { this._btnExit = this._scene.add.text( this._scene.cameras.main.width - 50, 50, Texts.Exit, this._style ).setOrigin(1); this._btnExit.setInteractive(); this._btnExit.once('pointerdown', () => { this._scene.scene.start('Start'); }); } //... 

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

يبقى تطوير طريقة renderلتحديث جميع عناصر واجهة المستخدم بشكل صحيح وإضافة المكالمات إلى جميع الأساليب التي تم إنشاؤها في _create.

 // GameSceneView.ts //... private _create(): void { this._createTxtFlags(); this._createTxtStatus(); this._createBtnExit(); } public render(data: {flags?: number, status?: boolean}) { if (typeof data.flags !== 'undefined') { this._txtFlags.text = Texts.Flags + data.flags.toString(); } if (typeof data.status !== 'undefined') { this._txtStatus.text = data.status ? Texts.Success : Texts.Failure; this._txtStatus.visible = true; } } //... 

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

 // GameScene.ts //... import { GameSceneView } from "../views/GameSceneView"; //... export class GameScene extends Phaser.Scene { private _view: GameSceneView = null; //... private _onGameOver(status: boolean) { //... this._view.render({status}); } //... private _onFieldClickRight(field: Field): void { //... this._flags = Bombs - this._board.countMarked; this._view.render({flags: this._flags}); } //... public create(): void { //... this._view = new GameSceneView(this); this._view.render({flags: this._flags}); } //... } 

5. الرسوم المتحركة


أي نوع من المعجبين من خلق لعبة ، حتى بسيطة مثل لعبتنا ، إذا لم تكن هناك رسوم متحركة؟ علاوة على ذلك ، منذ أن بدأنا في دراسة phaser ، دعونا نتعرف على أهم ميزات الرسوم المتحركة ونفكر في وظيفة التوائم. يتم تنفيذ التوائم في الإطار نفسه ولا توجد مكتبات خارجية مطلوبة.

أضف اثنين من الرسوم المتحركة إلى اللعبة: املأ اللوحة بالخلايا في البداية وقلب الخلية عند الفتح. لنبدأ مع أول هذه.

5.1 مجلس ملء الرسوم المتحركة




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

في الفصل ، FiledViewأضف _createالدعوة إلى نهاية الطرق _animateShow:

 // FieldView.ts //... private _create(): void { //... this._animateShow(); } //... 

ب ننفذ الطريقة الجديدة التي نحتاجها. في ذلك ، كما اتفقنا أعلاه ، من الضروري القيام بأمرين:

  1. حرك الخلية خلف الزاوية اليسرى العليا حتى لا تظهر على الشاشة
  2. بدء حركة التوأم إلى الإحداثيات المطلوبة مع التأخير الصحيح

 // FieldView.ts //... private _animateShow(): Promise<void> { this.x = -this.width; this.y = -this.height; const delay = this._model.row * 50 + this._model.col * 10; return this._moveTo(this._position, delay); } //... 

نظرًا لأن الزاوية العليا اليسرى من اللوحة القماشية بها إحداثيات (0 ، 0) ، فإذا قمنا بتعيين الخلية على الإحداثيات مساوية لقيمها السالبة للعرض والارتفاع ، فسوف يضع هذا الخلية خلف الزاوية اليسرى العليا وتخفيها من الشاشة. وبالتالي ، أكملنا مهمتنا الأولى.

سوف تحقق الهدف الثاني عن طريق استدعاء الأسلوب _moveTo.

 // FieldView.ts //... private _moveTo(position: Vec2, delay: number): Promise<void> { return new Promise(resolve => { this.scene.tweens.add({ targets: this, x: position.x, y: position.y, duration: 600, ease: 'Elastic', easeParams: [1, 1], delay, onComplete: () => { resolve(); } }); }); } //... 

لإنشاء رسم متحرك ، نستخدم خاصية المشهد tweens. في طريقته ، addنقوم بتمرير كائن التكوين بالإعدادات:

  • targetsيجب أن تحتوي الخاصية هنا على كائنات اللعبة التي تريد تطبيق تأثيرات الحركة عليها كقيمة. في حالتنا ، يعد هذا رابطًا thisللكائن الحالي ، نظرًا لأنه جاهز للعفريت.
  • المعلمات الثانية والثالثة نمر إحداثيات الوجهة.
  • الخاصية durationهي المسؤولة عن مدة الرسوم المتحركة ، في حالتنا - 600ms.
  • المعلمات easeو easeParamsتعيين وظيفة تخفيف.
  • في حقل التأخير ، نستبدل القيمة من الوسيطة الثانية ، التي يتم إنشاؤها لكل خلية على حدة ، مع الأخذ في الاعتبار موقعها على السبورة. يتم ذلك بحيث لا تطير الخلايا في نفس الوقت. بدلاً من ذلك ، ستظهر كل خلية مع تأخير بسيط بالنسبة للخلية السابقة.
  • أخيرًا ، onCompleteنضع رد اتصال في العقار ، والذي سيتم استدعاؤه في نهاية إجراء tween.

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

5.2 خلية فليب الرسوم المتحركة




سيكون أمرا رائعا إذا ، عندما تم فتح الخلية ، تم استنساخ تأثير انعكاسها. كيف يمكننا تحقيق هذا؟

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

 // FieldView.ts //... private _onStateChange(): void { if (this._model.opened) { this._animateFlip(); } else { this._render(); } } //... 

للحصول على التأثير المطلوب ، سوف نستخدم تحويل العفريت من خلال الخاصية scale. إذا قمنا بقياس العفريت على المحور xإلى صفر بمرور الوقت ، فسوف يتقلص في النهاية ، ويربط الجانبين الأيسر والأيمن. والعكس صحيح ، إذا قمت بقياس العفريت على طول المحور xمن الصفر إلى عرضه الكامل ، فسنمده إلى حجمه الكامل. نحن ننفذ هذا المنطق في الطريقة _animateFlip.

 // FieldView.ts //... private _animateFlip(): void { this._scaleXTo(0).then(() => { this._render(); this._scaleXTo(1); }) } //... 

عن طريق القياس مع الطريقة ، نقوم _moveToبتنفيذ _scaleTo:

 // FieldView.ts //... private _scaleXTo(scaleX: number): Promise<void> { return new Promise(resolve => { this.scene.tweens.add({ targets: this, scaleX, ease: 'Elastic.easeInOut', easeParams: [1, 1], duration: 150, onComplete: () => { resolve() } }); }); } //... 

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

الآن سنبدأ المشروع للاختبار وبعد تصحيح الأخطاء سننظر في لعبتنا مكتملة ، ومهمة الاختبار مكتملة! :)

أتوجه بخالص الشكر للجميع على الوصول إلى هذه اللحظة معي!

استنتاج


الزملاء ، سأكون سعيدًا جدًا إذا كانت المادة المقدمة في المقالة مفيدة لك ويمكنك استخدام هذه الأساليب أو تلك الموصوفة في مشاريعك الخاصة. يمكنك دائمًا الرجوع إليّ بأي سؤال ، سواء في هذه المقالة أو في البرمجة المرحلية أو العمل في gamedev بشكل عام. أرحب بالتواصل وسأكون سعيدًا بتكوين معارف جديدة وتبادل الخبرات!

ولدي سؤال لك الآن. بما أنني أقوم بإنشاء دروس فيديو حول تطوير اللعبة ، فقد جمعت بشكل طبيعي عشرات من هذه الألعاب الصغيرة. كل لعبة تفتح الإطار بطريقتها الخاصة. على سبيل المثال ، في هذه اللعبة ، تطرقنا إلى موضوع التوائم ، ولكن هناك العديد من الميزات الأخرى ، مثل الفيزياء ، tilemap ، العمود الفقري ، إلخ.
في هذا الصدد ، السؤال هو ، هل أعجبك هذا المقال ، وإذا كان الأمر كذلك ، فهل ستكون مهتمًا بمتابعة قراءة مقالات مثل هذه ، ولكن عن الألعاب الكلاسيكية الصغيرة الأخرى؟ إذا كانت الإجابة "نعم" ، فسأقوم بكل سرور بترجمة مواد دروس الفيديو الخاصة بي إلى تنسيق نصي ومواصلة نشر أدلة جديدة مع مرور الوقت ، ولكن بالنسبة للألعاب الأخرى. أحضر المسح المقابلة.

شكرا لكم جميعا على اهتمامكم! سوف أكون سعيدًا بردود الفعل ونراكم قريبًا!

Source: https://habr.com/ru/post/ar476682/


All Articles