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

نظرًا لأنني أتدرب على برمجة الألعاب ، وكذلك من أجل الاستخدام الأكثر كفاءة للرمز الخاص بي ، فقد قررت أنه سيكون من المفيد كتابة مقالة تدريبية حول المشروع المكتمل. وبما أن الاختبار المكتمل تلقى تقييماً إيجابياً وأدى إلى دعوة لإجراء مقابلة ، فربما يكون لقراري الحق في الوجود ، وربما سيساعد شخص ما في المستقبل.
سوف تقدم هذه المقالة فكرة عن مقدار العمل الكافي لإكمال مهمة الاختبار المتوسطة لموضع HTML5 الخاص بالمطور بنجاح. قد تكون المادة أيضًا موضع اهتمام أي شخص يريد التعرف على إطار عمل Phaser. وإذا كنت تعمل بالفعل مع Phaser والكتابة في JS - تعرف على كيفية تطوير مشروع في TypeScript.
لذلك ، تحت القط هناك الكثير من كود TypeScript!
مقدمة
نعطي بيانا موجزا للمشكلة.
- سنقوم بتطوير لعبة HTML5 بسيطة - sapper الكلاسيكية.
- كأدوات رئيسية سنستخدم phaser 3 و typescript و webpack.
- سيتم تصميم اللعبة لسطح المكتب وتشغيلها في المتصفح.
نحن نقدم روابط للمشروع النهائي.
روابط إلى العرض التوضيحي والمصدر واسترجع ميكانيكا القاتل ، فجأة إذا نسي شخص ما قواعد اللعبة. ولكن نظرًا لأن هذه الحالة غير مرجحة ، يتم وضع القواعد تحت المفسد :)
قواعد سابريتكون الملعب من خلايا مرتبة في طاولة. افتراضيًا ، عندما تبدأ اللعبة ، يتم إغلاق جميع الخلايا. يتم وضع القنابل في بعض الخلايا.
عند النقر بالزر الأيسر على خلية مغلقة ، يتم فتحه. إذا كان هناك قنبلة في زنزانة مفتوحة ، فإن اللعبة تنتهي بالهزيمة.
إذا لم يكن هناك قنبلة في الخلية ، فسيتم عرض رقم بداخلها ، مما يشير إلى عدد القنابل الموجودة في الخلايا المجاورة بالنسبة للفتح الحالي. إذا لم تكن هناك قنابل في مكان قريب ، فإن الخلية تبدو فارغة.
يؤدي النقر بزر الماوس الأيمن فوق خلية مغلقة إلى تعيين علامة عليها. مهمة اللاعب هي ترتيب جميع الأعلام المتاحة له حتى يتم تمييز جميع الخلايا الملغومة. بعد وضع جميع الأعلام ، يضغط اللاعب على زر الماوس الأيسر على إحدى الخلايا المفتوحة للتحقق مما إذا كان قد فاز.
بعد ذلك ، نذهب مباشرة إلى الدليل نفسه. تنقسم جميع المواد إلى خطوات صغيرة ، يصف كل منها تنفيذ مهمة محددة في وقت قصير. لذلك ، بأداء أهداف صغيرة خطوة بخطوة ، في النهاية سنقوم بإنشاء لعبة كاملة. استخدم جدول المحتويات إذا قررت الانتقال بسرعة إلى خطوة محددة.
1. التحضير
1.1 قالب المشروع
قم بتنزيل
قالب مشروع phaser الافتراضي . هذا هو القالب الموصى به من مؤلف الإطار ويقدم لنا بنية الدليل التالية:
بالنسبة
index.js
، لسنا بحاجة إلى ملف
index.js
الحالي ، لذلك احذفه. ثم قم بإنشاء الدليل
/src/scripts/
ثم ضع ملف
index.ts
الفارغ فيه. سنضيف كل نصوصنا إلى هذا المجلد.
تجدر الإشارة أيضًا إلى أنه عند إنشاء مشروع للإنتاج ، سيتم إنشاء دليل
dist
في الجذر ، حيث سيتم وضع إصدار الإصدار.
1.2 بناء التكوين
سوف نستخدم webpack للتجميع. نظرًا لأن القالب قد تم إعداده في الأصل للعمل مع JavaScript ، ونكتب في TypeScript ، فنحن بحاجة إلى إجراء تغييرات صغيرة على تكوين أداة التجميع.
في
webpack/base.js
أضف مفتاح
entry
، الذي يشير إلى نقطة الدخول عند إنشاء مشروعنا ، وكذلك تكوين
ts-loader
الذي يصف قواعد إنشاء البرامج النصية TS:
سنحتاج أيضًا إلى إنشاء ملف 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
.
- إنشاء مشروع للتصحيح وفتح في مستعرض من خلال خادم محلي
npm start
- تشغيل الإنشاء للبيع ووضع الإصدار الإصدار في المجلد 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.
نظرًا لأن اللعبة التي لدينا مصممة لسطح المكتب وستملأ الشاشة بالكامل ، فإننا نستخدم بجرأة العرض والارتفاع الكامل للمتصفح لحقول
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
. لكي نتمكن من العمل مع الأطلس الذي تم إنشاؤه في المستقبل ، نحتاج إلى تنفيذ خطوتين:
- الحصول على كل من ملفات
png
و json
atlas باستخدام:
- قم
preload
طريقة preload
لمشهد البداية:
2.3 نصوص المشهد الانطلاق
هناك 2 الأشياء المتبقية للقيام في مشهد البداية:
- أخبر اللاعب كيف تبدأ اللعبة
- ابدأ اللعبة بمبادرة من اللاعب
لتحقيق النقطة الأولى ، نقوم أولاً بإنشاء تعدادين في بداية ملف المشهد لوصف النصوص وأنماطها:
ثم قم بإنشاء كلا النصين ككائنات في طريقة
create
. اسمحوا لي أن أذكرك بأن طريقة
create
المشاهد في
Phaser
لن يتم استدعاؤها إلا بعد تحميل جميع الموارد في طريقة
preload
وهذا مناسب تمامًا لنا.
في مشروع آخر أكبر ، يمكن أن نأخذ النصوص والأساليب إما في ملفات الإعدادات المحلية أو في إعدادات منفصلة ، ولكن بالنظر إلى أن لدينا الآن سطرين فقط ، فأنا أعتبر هذه الخطوة ضرورية ، وفي هذه الحالة أقترح عدم تعقيد حياتنا ، حصر أنفسنا في القوائم في بداية ملف المشهد.
2.4 الانتقال إلى مستوى اللعبة
الشيء الأخير الذي سنفعله في هذا المشهد قبل الانتقال هو تتبع حدث النقر بالماوس لإطلاق اللاعب في اللعبة:
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
:
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
:
نأخذ معلمات اللوحة إلى الثوابت في بداية ملف المشهد وننقلها إلى مُنشئ اللوحة عند إنشاء مثيل لهذه الفئة.
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
الخاصة:
3.5 العفريت لتحديد المواقع
في الوقت الحالي ، سيتم وضع جميع العفاريت التي تم إنشاؤها في إحداثيات (0 ، 0) من اللوحة القماشية. نحتاج أيضًا إلى وضع كل خلية في موضعها المقابل على السبورة. وهذا هو ، إلى المكان الذي يتوافق مع صف وعمود هذه الخلية. للقيام بذلك ، نحتاج إلى كتابة رمز لحساب إحداثيات كل مثيل لفئة
FieldView
.
أضف خاصية
_position
إلى الفصل ، وهو المسؤول عن الإحداثيات النهائية للخلية في الملعب:
نظرًا لأننا نريد محاذاة اللوحة ، وبالتالي الخلايا الموجودة فيها ، بالنسبة إلى مركز الشاشة ، نحتاج أيضًا إلى خاصية
_offset
، مع الإشارة إلى إزاحة هذه الخلية المحددة بالنسبة إلى الحواف اليسرى
_offset
من الشاشة. أضفه مع جامع خاص:
وبالتالي ، نحن:
- حصلت على إجمالي عرض الشاشة في
this._scene.cameras.main.width
. - حصلنا على العرض الكلي
this._board.cols * this.width
عن طريق ضرب عدد الخلايا بعرض خلية واحدة: this._board.cols * this.width
. - إذا أخذنا عرض اللوحة من عرض الشاشة ، فقد حصلنا على مكان على الشاشة ، ولم تشغله اللوحة.
- بقسمة الرقم الناتج على 2 ، حصلنا على قيمة المسافة البادئة إلى اليسار واليمين من اللوحة.
- عن طريق تحويل كل خلية بقيمة هذه المسافة البادئة ، فإننا نضمن محاذاة اللوحة بأكملها على طول المحور
x
.
نقوم بتنفيذ إجراءات مماثلة تماما للحصول على النزوح العمودي.
يبقى لإضافة التعليمات البرمجية الضرورية في طريقة
_init
:
الخصائص
this.y
و
this.x
و
this.width
و
this.height
هنا هي الخصائص الموروثة للفئة الأصل
Phaser.GameObjects.Sprite
. يؤدي تغيير خصائص
this.y
و
this.x
إلى تحديد موضع العفريت الصحيح على اللوحة القماشية.
3.6 إنشاء مثيل لـ FieldView
إنشاء طريقة عرض في فئة
Field
:
3.7 حقول لوحة العرض.
دعنا نعود إلى فئة اللوحة ، والتي هي في الأساس مجموعة من كائنات
Field
وستنشئ خلايا.
سنقوم بإخراج كود إنشاء اللوحة إلى طريقة
_create
منفصلة
_create
هذه الطريقة من المُنشئ. مع العلم أننا في طريقة
_create
لن نقوم فقط بإنشاء خلايا ، بل سنقوم بإخراج الكود لإنشاء خلايا في طريقة
_createFields
منفصلة.
في هذه الطريقة سنقوم بإنشاء العدد المطلوب من الخلايا في حلقة متداخلة:
لقد حان الوقت لأول مرة لتشغيل التجميع لتصحيح الأخطاء باستخدام الأمر
npm start
تأكد من أنه في وسط الشاشة ، من المتوقع أن نرى 64 خلية في 8 صفوف.
3.8 صنع القنابل
لقد ذكرت في وقت سابق أنه في طريقة إنشاء فئة
Board
، لن نقوم فقط بإنشاء الحقول. ماذا بعد؟ سيكون هناك أيضًا إنشاء قنابل ، ووضع الخلايا التي تم إنشاؤها على عدد القنابل المجاورة. لنبدأ بالقنابل بأنفسهم.
نحن بحاجة إلى وضع قنابل N على السبورة في خلايا عشوائية. وصفنا عملية إنشاء القنابل باستخدام خوارزمية تقريبية:
في كل تكرار من الحلقة ، سوف نحصل على خلية عشوائية من خاصية
this._fields
حتى نقوم بإنشاء أكبر عدد ممكن من القنابل كما هو محدد في هذا المجال. إذا كانت الخلية المستلمة فارغة ، فسوف نقوم بتثبيت قنبلة فيها وتحديث عداد القنابل اللازمة للجيل.
لإنشاء رقم عشوائي ، نستخدم الأسلوب الثابت
Phaser.Math.Between
.
لا تنسَ أن تكتب المكالمة على هذا
this._createBombs();
في ملف
Board.ts
this._createBombs();
في نهاية طريقة
_create
كما لاحظت من قبل ، لكي تعمل هذه الشفرة بشكل صحيح ، تحتاج إلى تحسين فئة
Field
عن طريق إضافة getter
empty
وطريقة
setBomb
.
إضافة حقل
_value
خاص إلى
_value
الحقل ، والتي ستنظم محتويات الخلية. نحن نقبل الاتفاقيات التالية.
باتباع هذه القواعد ، سنقوم بتطوير أساليب في فئة
Field
تعمل مع خاصية
_value
:
3.9 ضبط القيم
يتم ترتيب القنابل والآن لدينا جميع البيانات من أجل تعيين القيم العددية في جميع الخلايا التي تتطلب ذلك.
واسمحوا لي أن أذكرك أنه وفقًا لقواعد sapper ، يجب أن يكون للخلية رقم يتوافق مع عدد القنابل الموجودة بجانب هذه الخلية. بناءً على هذه القاعدة ، نكتب الكود الكاذب المقابل.
في فئة اللوحة ، قم بإنشاء طريقة جديدة وترجمة الكود الكاذب المحدد إلى رمز حقيقي:
لنرى أي من الواجهات التي نستخدمها لم يتم تنفيذها. تحتاج إلى إضافة طريقة
getClosestFields
للحصول على الخلايا المجاورة.
كيفية التعرف على الخلايا المجاورة؟
على سبيل المثال ، ضع في اعتبارك أي خلية في اللوحة ليست على الحافة ، أي في الصفوف القصوى وليس في العمود أقصى. تحتوي هذه الخلايا على أقصى عدد من الجيران: 1 في الأعلى ، وواحد في الأسفل ، و 3 في اليسار ، و 3 على اليمين (بما في ذلك الخلايا في القطر).
وبالتالي ، في كل من الخلايا المجاورة ، لا تختلف المؤشرات
_row
و
_col
بأكثر من 1. وهذا يعني أنه يمكننا تحديد الفرق بين المعلمتين
_row
و
_col
مع الحقل الحالي. أضف ثابتًا في بداية الملف إلى وصف الفصل:
والآن يمكننا إضافة الطريقة المفقودة ، والتي سنعمل على حلها عبر هذه المجموعة:
لا تنس التحقق من متغير
field
في كل تكرار ، حيث لا تحتوي جميع الخلايا الموجودة على اللوحة على 8 جيران. على سبيل المثال ، لن يكون للخلية اليسرى العليا جيران على يسارها ، وهكذا.
يبقى تنفيذ طريقة
getField
وإضافة جميع المكالمات الضرورية إلى طريقة
_create
في فئة
_create
4. التعامل مع أحداث الإدخال
4.1 تتبع أحداث النقر بالماوس
في الوقت الحالي ، تمت تهيئة اللوحة بالكامل ، ولديها قنابل وهناك خلايا بها أرقام ، لكن جميعها مغلقة حاليًا ولا توجد طريقة لفتحها. سنقوم بتصحيح هذا وننفذ فتح الخلايا عن طريق النقر على زر الماوس الأيسر.أولاً ، نحن بحاجة إلى تتبع هذه النقرة ذاتها. في الفصل الدراسي ، FieldView
أضف _create
الكود التالي إلى نهاية الطريقة :
في phaser ، يمكنك الاشتراك في كائنات من مساحة الاسم للأحداث المختلفة Phaser.GameObjects
. على وجه الخصوص ، سوف نشترك في حدث النقر ( pointerdown
) للأبنية الجاهزة للعفريت نفسها ، أي كائن من فئة FieldView
موروثة منها Phaser.GameObjects.Sprite
.ولكن قبل القيام بذلك ، يجب أن نشير بوضوح إلى أن العفريت تفاعليًا ، أي أنك تحتاج عمومًا إلى الاستماع إلى إدخال المستخدم عليه. تحتاج إلى القيام بذلك عن طريق استدعاء الأسلوب setInteractive
دون معلمات على sprite نفسه ، وهو ما قمنا به في المثال أعلاه.والآن بعد أن أصبح العفريت لدينا تفاعليًا ، دعنا نعود إلى الفصل Board
في المكان الذي يتم فيه إنشاء كائنات نموذج جديدة Field
، أي إلى الطريقة _createFields
وتسجيل رد الاتصال لأحداث الإدخال للعرض:
بمجرد أن نثبت أنه من خلال النقر على العفريت نريد تشغيل الطريقة _onFieldClick
، نحتاج إلى تنفيذها. لكننا سنزيل منطق معالجة النقرة من الفصل Board
. يُعتقد أنه من الأفضل معالجة النموذج اعتمادًا على الإدخال وتغيير بياناته وفقًا لذلك في وحدة تحكم منفصلة ، تشابهها هو فئة مشهد اللعبة GameScene
. لذلك ، نحتاج إلى إعادة توجيه حدث النقر إلى أبعد من ذلك ، من الفصل Board
إلى المشهد نفسه. لذلك سنفعل:
نحن هنا لا نرمي حدث النقر فقط كما كان ، ولكننا نحدد أيضًا أي نقرة. سيكون هذا مفيدًا في المستقبل ، عندما نقوم بمعالجة كل خيار في فصل المشهد في فصل دراسي. بالطبع ، سيكون من الممكن إرسال حدث النقر كما هو ، لكننا سنقوم بتبسيط رمز المشهد ، مع ترك بعض المنطق فيما يتعلق بالحدث نفسه في الفصل Field
.حسنًا ، دعنا نعود الآن إلى فئة مشهد اللعبة GameScene
وأضف _create
رمزًا في نهاية الطريقة التي تتتبع أحداث النقر فوق الخلايا:
4.2. انقر فوق معالجة اليسار
ننتقل إلى تنفيذ معالجة أحداث النقر بالماوس. وابدأ بفتح الخلايا. يجب فتح الخلايا عن طريق الضغط على الزر الأيسر. وقبل أن نبدأ البرمجة ، دعنا نعبر عن الظروف التي يجب الوفاء بها:- عند النقر فوق خلية مغلقة ، يجب فتحها
- إذا كان هناك لغم في خلية مفتوحة - تضيع اللعبة
- إذا لم تكن هناك مناجم أو قيم في الخلية المفتوحة ، فلن تكون min في الخلايا المجاورة ، في هذه الحالة تحتاج إلى فتح جميع الخلايا المجاورة والاستمرار في القيام بذلك حتى تظهر القيمة في الخلية المفتوحة
- عند النقر فوق خلية مفتوحة ، يجب عليك التحقق مما إذا كانت جميع الأعلام قد تم تعيينها بشكل صحيح وإذا كان الأمر كذلك ، فأنهي اللعبة بالفوز
والآن ، لتبسيط فهم الوظيفة المطلوبة ، نترجم المنطق أعلاه إلى كود زائف:
الآن لدينا فهم لما يحتاج إلى برمجة. نطبق الطريقة _onFieldClickLeft
:
وبعد ذلك، كما هو الحال دائما، وسوف نقوم بتعديل الطبقات Field
و Board
من تنفيذها في الطرق التي نسميها معالج.نحن نشير إلى 3 حالات محتملة للخلية في التعداد States
، ونضيف حقلًا ونقوم _state
بتطبيق كل حالة ممكنة:
الآن بما أن لدينا حالات تشير إلى ما إذا كانت الخلية مغلقة أم لا ، فيمكننا إضافة طريقة open
لتغيير الحالة:
يجب أن يؤدي كل تغيير في حالة النموذج إلى تشغيل حدث يبلغ عن ذلك. لذلك ، نقدم طريقة خاصة إضافية يتم _setState
فيها تنفيذ منطق التغيير بالكامل. سيتم استدعاء هذه الطريقة في جميع الطرق العامة للنموذج ، والتي يجب أن تغير حالتها.أضف علامة منطقية _exploded
للإشارة بوضوح إلى كائن الحقل الذي تم تفجيره:
الآن افتح الفصل Board
وقم بتنفيذ الطريقة فيه openClosestFields
. هذه الطريقة متكررة وستكون مهمتها هي فتح جميع الحقول المجاورة الفارغة بالنسبة للخلية المقبولة في المعلمة.ستكون الخوارزمية كما يلي: :
وهذه المرة لدينا بالفعل جميع الواجهات اللازمة للتنفيذ الكامل لهذه الطريقة:
أضف جامعًا completed
إلى الفصل Board
للإشارة إلى الموضع الصحيح للأعلام على السبورة. كيف يمكننا تحديد ما إذا كان تم مسح لوحة بنجاح؟ يجب أن يساوي عدد الحقول المحددة بشكل صحيح العدد الإجمالي للقنابل الموجودة على السبورة.
تقوم هذه الطريقة بتصفية الصفيف _fields
بواسطة getter completed
، والتي يجب أن تشير إلى صحة علامة الحقل. إذا كان طول المصفاة المصفاة (التي تسقط فيها الحقول التي تم تمييزها بشكل صحيح فقط ، والتي يتحمل مسؤوليتها completed
بالفعل عن الفصل Field
) مساوياً لقيمة الحقل _bombs
(أي ، عدد القنابل على اللوحة) ، فإننا نرجع true
، بمعنى آخر ، نحن نعتبر اللعبة منتصرة.كما أننا لا نمانع بفرصة فتح اللوحة بالكامل بمكالمة واحدة ، ما يتعين علينا القيام به في نهاية المستوى. سنضيف أيضًا هذه الميزة إلى الفصل Board
:
يبقى لإضافة جامع completed
إلى الفصل نفسه Field
. في هذه الحالة ، سيتم النظر في تطهير الحقل بنجاح؟ إذا كان الملغومة وعلمها. كلتا الحالتين الضروريتين موجودتان بالفعل ويمكننا إضافة هذه الطريقة:
لإكمال معالجة النقر بالماوس الأيسر ، سننشئ طريقة _onGameOver
لتعطيل تتبع أحداث اللوحة وإظهار اللاعب بأكمله. بعد ذلك ، سنضيف إليها أيضًا رمز تقديم لتقرير إتمام الحالة استنادًا إلى المعلمة status
.
4.3 حقل العرض
قبل البدء في معالجة النقر بزر الماوس الأيمن ، سنتعلم كيفية إعادة رسم الخلايا التي تم فتحها حديثًا.في وقت سابق من الفصل ، Field
قمنا بتطوير طريقة _setState
تطلق حدثًا change
عندما تتغير حالة النموذج. سنستخدم هذا وفي الفصل FieldView
سنتتبع هذا الحدث:
لقد جعلنا على وجه التحديد الطريقة الوسيطة _onStateChange
ردًا لحدث تغيير النموذج. في المستقبل ، سنحتاج إلى التحقق من كيفية تغيير النموذج لفهم ما إذا كان من الضروري القيام به _render
.لإظهار العفريت الحالي لخلية في حالة جديدة ، تحتاج إلى تغيير إطارها. نظرًا لأننا قمنا بتحميل الأطلس كأصول ، يمكننا استدعاء الأسلوب من setFrame
أجل تغيير الإطار الحالي إلى إطار جديد.للحصول على الإطار في سطر واحد ، استخدمنا الماكرة على نحو ماكر _frameName
، والتي تحتاج الآن إلى تنفيذها. أولاً ، نحن نصف جميع القيم المحتملة التي يمكن أن يتخذها إطار الخلية.لقد حصلنا على وصف لجميع الحالات ولدينا بالفعل جميع أساليب النموذج ، بفضل هذه الحالات يمكن الحصول عليها. دعنا نحصل على تهيئة صغيرة في بداية الملف:
ستكون المفاتيح في هذا الكائن هي قيم الإطارات ، وقيم هذه المفاتيح هي عمليات الاسترجاعات التي تُرجع نتيجة منطقية. استنادًا إلى هذا التكوين ، يمكننا تطوير طريقة للحصول على الإطار المطلوب (أي ، المفتاح من التكوين):
وبالتالي ، من خلال التعداد البسيط في حلقة ، نذهب من خلال جميع مفاتيح كائن التكوين ونستدعي كل رد اتصال بدوره. تشير الوظيفة التي تُرجعنا أولاً true
إلى أن المفتاح key
في التكرار الحالي هو الإطار الصحيح للحالة الحالية للنموذج.إذا لم يكن أي مفتاح مناسبًا ، فبالنسبة للحالة الافتراضية ، سنفكر في حقل مفتوح ذي قيمة _value
، States
لأننا لم نقم بتعيين هذه الحالة في التكوين .الآن يمكننا اختبار النقر الأيسر بالكامل على حقول اللوحة والتحقق من كيفية فتح الخلايا وما يتم عرضه بعد فتحها.4.4 انقر بزر الفأرة الأيمن
كما في حالة إنشاء معالج النقر بزر الماوس الأيسر ، فإننا نحدد أولاً الوظيفة المتوقعة بوضوح. بالنقر بزر الماوس الأيمن ، يجب أن نضع علامة على الخلية المحددة بعلامة. ولكن هناك بعض الشروط.- لا يمكن وضع علامة إلا على الحقل المغلق غير المميز حاليًا
- إذا تم تحديد الحقل ، فيجب أن يؤدي النقر بزر الماوس الأيمن مرة أخرى إلى إزالة العلم من الحقل
- عند ضبط / إزالة علامة ، من الضروري تحديث عدد العلامات المتاحة على المستوى وعرض النص بالرقم الحالي
عند ترجمة هذه الشروط إلى كود زائف ، نحصل على أسطر التعليقات التالية:
يمكننا الآن ترجمة هذه الخوارزمية إلى استدعاءات للطرق التي نحتاجها ، حتى لو لم يتم تطويرها بعد:
هنا بدأنا أيضًا مجالًا جديدًا _flags
، وهو في بداية مستوى اللعبة يساوي عدد القنابل على اللوحة ، لأنه في بداية اللعبة لم يتم تعيين علم واحد. يجب تحديث هذا الحقل بكل نقرة صحيحة ، لأنه في هذه الحالة يتم إضافة العلم أو إزالته من اللوحة. إضافة Board
جامع إلى الفصل countMarked
:
يعد إعداد العلامة وإزالتها تغييرًا في حالة النموذج Field
، لذلك نقوم بتنفيذ هذه الطرق في الفصل المقابل بشكل مشابه للطريقة open
:
دعني أذكرك بأنه _setState
سيؤدي إلى حدث change
يتم تتبعه في العرض ، وبالتالي ، سيتم إعادة رسم العفريت تلقائيًا هذه المرة عندما يتغير النموذج.عند اختبار الوظيفة المطورة ، ستجد بالتأكيد أنه في كل مرة تنقر فوق زر الماوس الأيمن ، تفتح قائمة السياق. أضف التعليمات البرمجية التي تعطل هذا السلوك إلى مُنشئ مشهد اللعبة:
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() { } }
إضافة نص مع عدد الأعلام.
هذا الكود سوف يضع النص الذي نحتاجه في موضع بمسافة بادئة 50px من الجوانب العلوية واليسرى وتعيينه على النمط المحدد. بالإضافة إلى ذلك ، تقوم الطريقة setOrigin
بتعيين النقطة المحورية للنص على الإحداثيات (0 ، 1). هذا يعني أن النص سوف يتماشى مع حده الأيسر.إضافة رسالة الحالة.
نضع نص الحالة في وسط الشاشة ومواءمته مع منتصف السطر عن طريق الاتصال setOrigin
مع المعلمة 0.5 للإحداثي س. بالإضافة إلى ذلك ، بشكل افتراضي ، يجب إخفاء هذا النص ، لأننا لن نعرضه إلا عند الانتهاء من اللعبة.إنشاء زر الخروج ، والذي في جوهره هو أيضا كائن نصي.
نضع الزر في الركن الأيمن العلوي من الشاشة ونستخدمه مرة أخرى setOrigin
لمحاذاة النص هذه المرة بحافته اليمنى. نجعل الزر تفاعليًا ونضيف رد اتصال إلى حدث النقر ، الذي يرسل المشغل إلى مكان البداية. وبالتالي ، فإننا نعطي اللاعب فرصة للخروج من المستوى في أي وقت.يبقى تطوير طريقة render
لتحديث جميع عناصر واجهة المستخدم بشكل صحيح وإضافة المكالمات إلى جميع الأساليب التي تم إنشاؤها في _create
.
اعتمادًا على الخاصية التي تم تمريرها في المعلمة ، نقوم بتحديث واجهة المستخدم ، مع عرض التغييرات اللازمة.قم بإنشاء تمثيل في مشهد اللعبة في فئة GameScene واكتب المكالمة إلى طريقة _render حيثما كان ذلك مطلوبًا بالمعنى:
5. الرسوم المتحركة
أي نوع من المعجبين من خلق لعبة ، حتى بسيطة مثل لعبتنا ، إذا لم تكن هناك رسوم متحركة؟ علاوة على ذلك ، منذ أن بدأنا في دراسة phaser ، دعونا نتعرف على أهم ميزات الرسوم المتحركة ونفكر في وظيفة التوائم. يتم تنفيذ التوائم في الإطار نفسه ولا توجد مكتبات خارجية مطلوبة.أضف اثنين من الرسوم المتحركة إلى اللعبة: املأ اللوحة بالخلايا في البداية وقلب الخلية عند الفتح. لنبدأ مع أول هذه.5.1 مجلس ملء الرسوم المتحركة
نتأكد من أن جميع خلايا اللوحة تطير في مكانها من الحافة اليسرى العليا من الشاشة. عند بدء مستوى اللعبة ، نحتاج إلى تحويل جميع الخلايا إلى الركن الأيسر العلوي من الشاشة ولكي تبدأ كل خلية الرسوم المتحركة للحركة إلى الإحداثيات المقابلة لها.في الفصل ، FiledView
أضف _create
الدعوة إلى نهاية الطرق _animateShow
:
ب ننفذ الطريقة الجديدة التي نحتاجها. في ذلك ، كما اتفقنا أعلاه ، من الضروري القيام بأمرين:- حرك الخلية خلف الزاوية اليسرى العليا حتى لا تظهر على الشاشة
- بدء حركة التوأم إلى الإحداثيات المطلوبة مع التأخير الصحيح
نظرًا لأن الزاوية العليا اليسرى من اللوحة القماشية بها إحداثيات (0 ، 0) ، فإذا قمنا بتعيين الخلية على الإحداثيات مساوية لقيمها السالبة للعرض والارتفاع ، فسوف يضع هذا الخلية خلف الزاوية اليسرى العليا وتخفيها من الشاشة. وبالتالي ، أكملنا مهمتنا الأولى.سوف تحقق الهدف الثاني عن طريق استدعاء الأسلوب _moveTo
.
لإنشاء رسم متحرك ، نستخدم خاصية المشهد tweens
. في طريقته ، add
نقوم بتمرير كائن التكوين بالإعدادات:targets
يجب أن تحتوي الخاصية هنا على كائنات اللعبة التي تريد تطبيق تأثيرات الحركة عليها كقيمة. في حالتنا ، يعد هذا رابطًا this
للكائن الحالي ، نظرًا لأنه جاهز للعفريت.- المعلمات الثانية والثالثة نمر إحداثيات الوجهة.
- الخاصية
duration
هي المسؤولة عن مدة الرسوم المتحركة ، في حالتنا - 600ms. - المعلمات
ease
و easeParams
تعيين وظيفة تخفيف. - في حقل التأخير ، نستبدل القيمة من الوسيطة الثانية ، التي يتم إنشاؤها لكل خلية على حدة ، مع الأخذ في الاعتبار موقعها على السبورة. يتم ذلك بحيث لا تطير الخلايا في نفس الوقت. بدلاً من ذلك ، ستظهر كل خلية مع تأخير بسيط بالنسبة للخلية السابقة.
- أخيرًا ،
onComplete
نضع رد اتصال في العقار ، والذي سيتم استدعاؤه في نهاية إجراء tween.
من المنطقي أن نلتف مع التوأم بحيث يكون في المستقبل قادرين على إرساء رسوم متحركة مختلفة بشكل جميل ، لذلك سنقوم بإجراء استدعاء دالة في رد الاتصال resolve
للإشارة إلى التنفيذ الناجح للرسوم المتحركة.5.2 خلية فليب الرسوم المتحركة
سيكون أمرا رائعا إذا ، عندما تم فتح الخلية ، تم استنساخ تأثير انعكاسها. كيف يمكننا تحقيق هذا؟يتم فتح الخلية حاليًا عن طريق تغيير الإطار عندما يتم استدعاء الطريقة _render
في العرض. إذا تحققنا من حالة النموذج في هذه الطريقة ، فسنرى ما إذا كانت الخلية مفتوحة أم لا. إذا كانت الخلية مفتوحة ، فقم بتشغيل الرسوم المتحركة بدلاً من عرض إطار انعكاس جديد على الفور.
للحصول على التأثير المطلوب ، سوف نستخدم تحويل العفريت من خلال الخاصية scale
. إذا قمنا بقياس العفريت على المحور x
إلى صفر بمرور الوقت ، فسوف يتقلص في النهاية ، ويربط الجانبين الأيسر والأيمن. والعكس صحيح ، إذا قمت بقياس العفريت على طول المحور x
من الصفر إلى عرضه الكامل ، فسنمده إلى حجمه الكامل. نحن ننفذ هذا المنطق في الطريقة _animateFlip
.
عن طريق القياس مع الطريقة ، نقوم _moveTo
بتنفيذ _scaleTo
:
في هذه الطريقة ، كمعلمة ، نأخذ قيمة المقياس ، والتي سنستخدمها لتغيير حجم العفريت في كلا الاتجاهين وتمريرها كمعلمة ثانية إلى كائن تكوين الرسوم المتحركة. جميع معلمات التكوين الأخرى مألوفة لنا بالفعل من الرسوم المتحركة السابقة.الآن سنبدأ المشروع للاختبار وبعد تصحيح الأخطاء سننظر في لعبتنا مكتملة ، ومهمة الاختبار مكتملة! :)
أتوجه بخالص الشكر للجميع على الوصول إلى هذه اللحظة معي!استنتاج
الزملاء ، سأكون سعيدًا جدًا إذا كانت المادة المقدمة في المقالة مفيدة لك ويمكنك استخدام هذه الأساليب أو تلك الموصوفة في مشاريعك الخاصة. يمكنك دائمًا الرجوع إليّ بأي سؤال ، سواء في هذه المقالة أو في البرمجة المرحلية أو العمل في gamedev بشكل عام. أرحب بالتواصل وسأكون سعيدًا بتكوين معارف جديدة وتبادل الخبرات!ولدي سؤال لك الآن. بما أنني أقوم بإنشاء دروس فيديو حول تطوير اللعبة ، فقد جمعت بشكل طبيعي عشرات من هذه الألعاب الصغيرة. كل لعبة تفتح الإطار بطريقتها الخاصة. على سبيل المثال ، في هذه اللعبة ، تطرقنا إلى موضوع التوائم ، ولكن هناك العديد من الميزات الأخرى ، مثل الفيزياء ، tilemap ، العمود الفقري ، إلخ.في هذا الصدد ، السؤال هو ، هل أعجبك هذا المقال ، وإذا كان الأمر كذلك ، فهل ستكون مهتمًا بمتابعة قراءة مقالات مثل هذه ، ولكن عن الألعاب الكلاسيكية الصغيرة الأخرى؟ إذا كانت الإجابة "نعم" ، فسأقوم بكل سرور بترجمة مواد دروس الفيديو الخاصة بي إلى تنسيق نصي ومواصلة نشر أدلة جديدة مع مرور الوقت ، ولكن بالنسبة للألعاب الأخرى. أحضر المسح المقابلة.شكرا لكم جميعا على اهتمامكم! سوف أكون سعيدًا بردود الفعل ونراكم قريبًا!