كيفية إنشاء لعبة مطابقة البطاقات باستخدام Angular و RxJS
نشرت: 2022-03-10اليوم ، أود التركيز على تدفقات البيانات الناتجة عن أحداث النقر على واجهة المستخدم. تعد معالجة تدفق النقرات هذه مفيدة بشكل خاص للتطبيقات ذات التفاعل المكثف للمستخدم حيث يجب معالجة العديد من الأحداث. أود أيضًا أن أقدم لكم RxJS أكثر قليلاً ؛ إنها مكتبة JavaScript يمكن استخدامها للتعبير عن إجراءات معالجة الأحداث بشكل مضغوط وموجز بأسلوب تفاعلي.
ماذا نبني؟
ألعاب التعلم واختبارات المعرفة شائعة للمستخدمين الصغار والكبار على حد سواء. مثال على ذلك هو لعبة "مطابقة الأزواج" ، حيث يتعين على المستخدم العثور على أزواج ذات صلة في مزيج من الصور و / أو المقتطفات النصية.
تُظهر الرسوم المتحركة أدناه إصدارًا بسيطًا من اللعبة: يختار المستخدم عنصرين على الجانب الأيسر والأيمن من الملعب واحدًا تلو الآخر ، وبأي ترتيب. يتم نقل الأزواج المتطابقة بشكل صحيح إلى منطقة منفصلة من الملعب ، بينما يتم حل أي مهام خاطئة على الفور بحيث يتعين على المستخدم تحديد اختيار جديد.
في هذا البرنامج التعليمي ، سنبني مثل هذه اللعبة التعليمية خطوة بخطوة. في الجزء الأول ، سنقوم ببناء مكون Angular يعرض فقط مجال اللعب في اللعبة. هدفنا هو أنه يمكن تكوين المكون لحالات استخدام مختلفة ومجموعات مستهدفة - من اختبار الحيوان إلى مدرب المفردات في تطبيق تعلم اللغة. لهذا الغرض ، يقدم Angular مفهوم عرض المحتوى باستخدام قوالب قابلة للتخصيص ، والتي سنستخدمها. لتوضيح المبدأ ، سأقوم ببناء نسختين من اللعبة ("game1" و "game2") بتصميمات مختلفة.
في الجزء الثاني من البرنامج التعليمي ، سنركز على البرمجة التفاعلية. عندما يتم مطابقة الزوج ، يحتاج المستخدم إلى الحصول على نوع من التعليقات من التطبيق ؛ يتم معالجة هذا الحدث بمساعدة مكتبة RxJS.
- متطلبات
لمتابعة هذا البرنامج التعليمي ، يجب تثبيت Angular CLI. - مصدر الرمز
يمكن العثور على الكود المصدري لهذا البرنامج التعليمي هنا (14 كيلوبايت).
1. بناء مكون زاوي للعبة التعلم
كيفية إنشاء الإطار الأساسي
أولاً ، لنقم بإنشاء مشروع جديد باسم "Learning-app". باستخدام Angular CLI ، يمكنك القيام بذلك باستخدام الأمر ng new learning-app
. في ملف app.component.html ، استبدل شفرة المصدر المُنشأة مسبقًا على النحو التالي:
<div> <h1>Learning is fun!</h1> </div>
في الخطوة التالية ، يتم إنشاء مكون لعبة التعلم. لقد أطلقت عليها اسم "لعبة المطابقة" واستخدمت الأمر ng generate component matching-game
. سيؤدي هذا إلى إنشاء مجلد فرعي منفصل لمكون اللعبة مع ملفات HTML و CSS و Typescript المطلوبة.
كما ذكرنا سابقًا ، يجب أن تكون اللعبة التعليمية قابلة للتكوين لأغراض مختلفة. لإثبات ذلك ، قمت بإنشاء مكونين إضافيين ( game1
و game2
) باستخدام نفس الأمر. أقوم بإضافة مكون اللعبة كمكون فرعي عن طريق استبدال الرمز الذي تم إنشاؤه مسبقًا في الملف game1.component.html أو game2.component.html بالعلامة التالية:
<app-matching-game></app-matching-game>
في البداية ، أستخدم فقط game1
المكونة 1. للتأكد من عرض اللعبة 1 فور بدء التطبيق ، أضفت هذه العلامة إلى ملف app.component.html :
<app-game1></app-game1>
عند بدء التطبيق مع ng serve --open
، سيعرض المتصفح الرسالة "مطابقة اللعبة تعمل". (هذا هو المحتوى الوحيد حاليًا لـ match-game.component.html .)
الآن ، نحن بحاجة إلى اختبار البيانات. في مجلد /app
، أقوم بإنشاء ملف باسم pair.ts حيث أحدد فئة Pair
:
export class Pair { leftpart: string; rightpart: string; id: number; }
يتألف كائن الزوج من نصين leftpart
(الجزء الأيسر والجزء rightpart
) ومعرف.
من المفترض أن تكون اللعبة الأولى عبارة عن اختبار حول الأنواع حيث يجب تخصيص الأنواع (مثل dog
) إلى فئة الحيوانات المناسبة (مثل mammal
).
في ملف Animals.ts ، أحدد مصفوفة ببيانات الاختبار:
import { Pair } from './pair'; export const ANIMALS: Pair[] = [ { id: 1, leftpart: 'dog', rightpart: 'mammal'}, { id: 2, leftpart: 'blickbird', rightpart: 'bird'}, { id: 3, leftpart: 'spider', rightpart: 'insect'}, { id: 4, leftpart: 'turtle', rightpart: 'reptile' }, { id: 5, leftpart: 'guppy', rightpart: 'fish'}, ];
تحتاج game1
المكونة 1 إلى الوصول إلى بيانات الاختبار الخاصة بنا. يتم تخزينها في animals
الملكية. يحتوي ملف game1.component.ts الآن على المحتوى التالي:
import { Component, OnInit } from '@angular/core'; import { ANIMALS } from '../animals'; @Component({ selector: 'app-game1', templateUrl: './game1.component.html', styleUrls: ['./game1.component.css'] }) export class Game1Component implements OnInit { animals = ANIMALS; constructor() { } ngOnInit() { } }
الإصدار الأول من مكون اللعبة
هدفنا التالي: يجب أن تقبل لعبة matching-game
مكون اللعبة بيانات اللعبة من المكون الرئيسي (مثل game1
) كمدخلات. الإدخال عبارة عن مصفوفة من كائنات "الزوج". يجب تهيئة واجهة مستخدم اللعبة باستخدام الكائنات التي تم تمريرها عند بدء التطبيق.
لهذا الغرض ، نحتاج إلى المضي قدمًا على النحو التالي:
- أضف
pairs
الخصائص إلى مكون اللعبة باستخدام@Input
decorator. - أضف المصفوفات التي تم حلها
solvedPairs
غيرunsolvedPairs
كخصائص خاصة إضافية للمكون. (من الضروري التمييز بين الأزواج "التي تم حلها" بالفعل والأزواج "التي لم يتم حلها بعد".) - عند بدء تشغيل التطبيق (انظر الوظيفة
ngOnInit
) ، تظل جميع الأزواج "بدون حل" وبالتالي يتم نقلها إلى المصفوفة التي لم يتمunsolvedPairs
.
import { Component, OnInit, Input } from '@angular/core'; import { Pair } from '../pair'; @Component({ selector: 'app-matching-game', templateUrl: './matching-game.component.html', styleUrls: ['./matching-game.component.css'] }) export class MatchingGameComponent implements OnInit { @Input() pairs: Pair[]; private solvedPairs: Pair[] = []; private unsolvedPairs: Pair[] = []; constructor() { } ngOnInit() { for(let i=0; i<this.pairs.length; i++){ this.unsolvedPairs.push(this.pairs[i]); } } }
علاوة على ذلك ، أحدد قالب HTML لمكون matching-game
. توجد حاويات للأزواج غير المحلولة والمحللة. يضمن التوجيه ngIf
عرض الحاوية المعنية فقط في حالة وجود زوج واحد على الأقل لم يتم حله أو حله.
في الحاوية الخاصة بالأزواج التي لم يتم حلها ( container unsolved
) ، يتم أولاً إدراج كل مكونات الأزواج الموجودة على left
(انظر الإطار الأيسر في GIF أعلاه) ثم جميع العناصر الموجودة على right
(انظر الإطار الأيمن في ملف GIF). (أستخدم التوجيه ngFor
لسرد الأزواج.) في الوقت الحالي ، يكفي زر بسيط كقالب.
باستخدام تعبير القالب {{{pair.leftpart}}
و {{{ leftpart
{{pair.rightpart}}}
، يتم الاستعلام عن قيم خصائص الجزء الأيسر والجزء rightpart
من كائنات الزوج الفردية عند تكرار مصفوفة pair
. يتم استخدامها كعناوين للأزرار التي تم إنشاؤها.
يتم سرد الأزواج المخصصة في الحاوية الثانية (تم container solved
الفئة). يشير الشريط الأخضر ( connector
الفئة) إلى أنهما ينتميان معًا.
يمكن العثور على كود CSS المقابل لملف match.component.css في شفرة المصدر في بداية المقالة.
<div> <div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.leftpart}} </button> </div> <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.rightpart}} </button> </div> </div> <div class="container solved" *ngIf="solvedPairs.length>0"> <div *ngFor="let pair of solvedPairs" class="pair"> <button>{{pair.leftpart}}</button> <div class="connector"></div> <button>{{pair.rightpart}}</button> </div> </div> </div>
في game1
المكون 1 ، أصبحت animals
المصفوفة مرتبطة الآن بخاصية pairs
الخاصة matching-game
المكونة (ربط البيانات أحادي الاتجاه).
<app-matching-game [pairs]="animals"></app-matching-game>
تظهر النتيجة في الصورة أدناه.
من الواضح أن لعبتنا المطابقة ليست صعبة للغاية بعد ، لأن الجزأين الأيمن والأيسر من الأزواج متقابلان مباشرة. حتى لا يكون الاقتران بسيطًا جدًا ، يجب خلط الأجزاء الصحيحة. لقد قمت بحل المشكلة shuffle
الأنابيب المحدد ذاتيًا ، والذي أطبقه على أزواج الصفيف التي لم يتم unsolvedPairs
على الجانب الأيمن (يلزم test
المعلمة لاحقًا لفرض تحديث الأنبوب):
... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...
يتم تخزين الكود المصدري للأنبوب في ملف shuffle.pipe.ts في مجلد التطبيق (انظر التعليمات البرمجية المصدر في بداية المقالة). لاحظ أيضًا ملف app.module.ts ، حيث يجب استيراد الأنبوب وإدراجه في إعلانات الوحدة النمطية. الآن يظهر العرض المطلوب في المتصفح.
الإصدار الموسع: استخدام قوالب قابلة للتخصيص للسماح بتصميم فردي للعبة
بدلاً من الزر ، يجب أن يكون من الممكن تحديد مقتطفات قالب عشوائية لتخصيص اللعبة. في ملف match-game.component.html ، استبدل قالب الزر للجانب الأيسر والأيمن من اللعبة بعلامة ng-template
. ثم أقوم بتعيين اسم مرجع قالب إلى الخاصية ngTemplateOutlet
. هذا يعطيني عنصرين نائبين ، يتم استبدالهما بمحتوى مرجع القالب المعني عند تقديم العرض.
نحن نتعامل هنا مع مفهوم عرض المحتوى : يتم تقديم أجزاء معينة من قالب المكون من الخارج ويتم "إسقاطها" في القالب في المواضع المحددة.
عند إنشاء العرض ، يجب على Angular إدراج بيانات اللعبة في القالب. باستخدام المعلمة ngTemplateOutletContext
، أخبر Angular أنه يتم استخدام سياق متغير ضمن القالب ، والذي يجب تعيين القيمة الحالية لمتغير pair
من ngFor
contextPair
القائمة التالية توضح استبدال الحاوية التي لم يتم unsolved
. في الحاوية التي تم solved
، يجب استبدال الأزرار ng-template
أيضًا.
<div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <div *ngFor="let pair of unsolvedPairs" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> <div class="pair_items right"> <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> </div> ...
في الملف match-game.component.ts ، يجب التصريح عن متغيرات كل من مراجع القالب ( leftpart_temp
و rightpart_temp
). يشير @ContentChild
إلى أن هذا هو إسقاط للمحتوى ، أي أن Angular تتوقع الآن أن يتم توفير مقتطفين من القالب مع المحدد المعني (الجزء الأيسر أو rightpart
) في المكون الرئيسي بين العلامات <app-matching-game></app-matching-game>
leftpart
<app-matching-game></app-matching-game>
لعنصر المضيف (انظر @ViewChild
).
@ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;
لا تنس: يجب استيراد الأنواع ContentChild
و TemplateRef
من الحزمة الأساسية.
في لعبة المكون game1
، يتم الآن إدراج مقتطفات القالب المطلوبة مع الجزء الأيسر rightpart
leftpart
من أجل البساطة ، سأعيد استخدام الأزرار هنا مرة أخرى:
<app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <button>{{animalPair.leftpart}}</button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button>{{animalPair.rightpart}}</button> </ng-template> </app-matching-game>
يتم استخدام السمة let-animalPair="contextPair"
لتحديد استخدام سياق متغير contextPair
في مقتطف القالب مع الاسم animalPair
.
يمكن الآن تغيير مقتطفات القالب حسب ذوقك الخاص. لإثبات ذلك أستخدم لعبة المكون game2
. يحصل الملف game2.component.ts على نفس محتوى game1.component.ts . في game2.component.html أستخدم عنصر div
مصممًا بشكل فردي بدلاً من زر. يتم تخزين فئات CSS في ملف game2.component.css .
<app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <div class="myAnimal left">{{animalPair.leftpart}}</div> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <div class="myAnimal right">{{animalPair.rightpart}}</div> </ng-template> </app-matching-game>
بعد إضافة العلامات <app-game2></app-game2>
على الصفحة الرئيسية app.component.html ، يظهر الإصدار الثاني من اللعبة عند بدء التطبيق:
إمكانيات التصميم تكاد تكون غير محدودة الآن. سيكون من الممكن ، على سبيل المثال ، تحديد فئة فرعية من Pair
تحتوي على خصائص إضافية. على سبيل المثال ، يمكن تخزين عناوين الصور للأجزاء اليسرى و / أو اليمنى. يمكن عرض الصور في النموذج مع النص أو بدلاً من النص.
2. التحكم في تفاعل المستخدم مع RxJS
مزايا البرمجة التفاعلية مع RxJS
لتحويل التطبيق إلى لعبة تفاعلية ، يجب معالجة الأحداث (مثل أحداث النقر بالماوس) التي يتم تشغيلها في واجهة المستخدم. في البرمجة التفاعلية ، تؤخذ في الاعتبار التسلسلات المستمرة للأحداث ، والتي تسمى "التدفقات". يمكن ملاحظة التدفق (يمكن ملاحظته) ، أي يمكن أن يكون هناك واحد أو أكثر من "المراقبين" أو "المشتركين" المشتركين في التدفق. يتم إخطارهم (عادةً بشكل غير متزامن) بكل قيمة جديدة في الدفق ويمكنهم التفاعل معها بطريقة معينة.
باستخدام هذا النهج ، يمكن تحقيق مستوى منخفض من الاقتران بين أجزاء التطبيق. المراقبون الحاليون والمراقبون مستقلون عن بعضهم البعض ويمكن أن يتنوع اقترانهم في وقت التشغيل.
توفر مكتبة JavaScript RxJS تطبيقًا ناضجًا لنمط تصميم Observer. علاوة على ذلك ، يحتوي RxJS على العديد من المشغلين لتحويل التدفقات (مثل مرشح ، خريطة) أو لدمجها في تدفقات جديدة (على سبيل المثال ، دمج ، concat). العوامل هي "وظائف خالصة" بمعنى البرمجة الوظيفية: فهي لا تنتج آثارًا جانبية وتكون مستقلة عن الحالة خارج الوظيفة. لا يحتاج منطق البرنامج الذي يتكون فقط من استدعاءات الوظائف البحتة إلى متغيرات مساعدة عامة أو محلية لتخزين الحالات الوسيطة. وهذا بدوره يشجع على إنشاء كتل تعليمات برمجية عديمة الحالة وغير محكمة الترابط. لذلك من المستحسن تحقيق جزء كبير من معالجة الحدث من خلال مجموعة ذكية من مشغلي التيار. يتم تقديم أمثلة على ذلك في القسم التالي ، بناءً على لعبتنا المطابقة.
دمج RxJS في معالجة الحدث لمكون زاوي
يعمل Angular framework مع فئات مكتبة RxJS. لذلك يتم تثبيت RxJS تلقائيًا عند تثبيت Angular.
توضح الصورة أدناه الفئات والوظائف الرئيسية التي تلعب دورًا في اعتباراتنا:
اسم الفصل | دور |
---|---|
يمكن ملاحظته (RxJS) | الفئة الأساسية التي تمثل دفقًا ؛ بمعنى آخر ، تسلسل مستمر للبيانات. يمكن ملاحظته يمكن الاشتراك في. تُستخدم وظيفة pipe لتطبيق وظيفة مشغل واحدة أو أكثر على الحالة التي يمكن ملاحظتها. |
الموضوع (RxJS) | توفر الفئة الفرعية التي يمكن ملاحظتها الوظيفة التالية لنشر بيانات جديدة في الدفق. |
EventEmitter (الزاوي) | هذه فئة فرعية خاصة بالزاوية تُستخدم عادةً فقط بالاقتران مع @Output decorator لتحديد ناتج مكون. مثل الوظيفة التالية ، يتم استخدام وظيفة emit لإرسال البيانات إلى المشتركين. |
الاشتراك (RxJS) | ترجع وظيفة subscribe في ما يمكن ملاحظته نسخة اشتراك. مطلوب إلغاء الاشتراك بعد استخدام المكون. |
بمساعدة هذه الفئات ، نريد تنفيذ تفاعل المستخدم في لعبتنا. تتمثل الخطوة الأولى في التأكد من تمييز العنصر الذي حدده المستخدم على الجانب الأيسر أو الأيمن بشكل مرئي.
يتم التحكم في التمثيل المرئي للعناصر بواسطة مقتطفين من القالب في المكون الرئيسي. لذلك يجب أيضًا ترك القرار المتعلق بكيفية عرضها في الحالة المحددة للمكوِّن الأصلي. يجب أن يتلقى الإشارات المناسبة بمجرد إجراء التحديد على الجانب الأيسر أو الأيمن أو بمجرد التراجع عن التحديد.
لهذا الغرض ، أحدد أربع قيم إخراج من النوع EventEmitter
في ملف match-game.component.ts . يجب استيراد النوعين Output
و EventEmitter
من الحزمة الأساسية.
@Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();
في نموذج match-game.component.html ، أتفاعل مع حدث mousedown
على الجانب الأيسر والأيمن ، ثم أرسل معرف العنصر المحدد إلى جميع المستلمين.
<div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)">
في حالتنا ، فإن أجهزة الاستقبال هي المكونان game1
و game2
. هناك يمكنك الآن تحديد معالجة الحدث للأحداث leftpartSelected
المحدد ، rightpartSelected
، الجزء الأيسر غير leftpartUnselected
والجزء الأيمن غير rightpartUnselected
. يمثل المتغير $event
قيمة الإخراج المنبعثة ، في حالتنا المعرف. في ما يلي يمكنك مشاهدة قائمة game1.component.html ، بالنسبة إلى game2.component.html تنطبق نفس التغييرات.
<app-matching-game [pairs]="animals" (leftpartSelected)="onLeftpartSelected($event)" (rightpartSelected)="onRightpartSelected($event)" (leftpartUnselected)="onLeftpartUnselected()" (rightpartUnselected)="onRightpartUnselected()"> <ng-template #leftpart let-animalPair="contextPair"> <button [class.selected]="leftpartSelectedId==animalPair.id"> {{animalPair.leftpart}} </button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button [class.selected]="rightpartSelectedId==animalPair.id"> {{animalPair.rightpart}} </button> </ng-template> </app-matching-game>
في game1.component.ts (وبالمثل في game2.component.ts ) ، يتم الآن تنفيذ وظائف معالج event
. أقوم بتخزين معرفات العناصر المحددة. في قالب HTML (انظر أعلاه) ، يتم تعيين الفئة selected
لهذه العناصر. يحدد ملف CSS game1.component.css التغييرات المرئية التي ستحدثها هذه الفئة (مثل تغييرات اللون أو الخط). تعتمد إعادة تعيين التحديد (إلغاء التحديد) على افتراض أن كائنات الزوج تحتوي دائمًا على معرفات موجبة.
onLeftpartSelected(id:number):void{ this.leftpartSelectedId = id; } onRightpartSelected(id:number):void{ this.rightpartSelectedId = id; } onLeftpartUnselected():void{ this.leftpartSelectedId = -1; } onRightpartUnselected():void{ this.rightpartSelectedId = -1; }
في الخطوة التالية ، يلزم معالجة الحدث في مكون اللعبة المطابق. يجب تحديد ما إذا كانت المهمة صحيحة ، أي إذا كان العنصر المحدد الأيسر يطابق العنصر المحدد الأيمن. في هذه الحالة ، يمكن نقل الزوج المعين إلى حاوية الأزواج التي تم حلها.
أرغب في صياغة منطق التقييم باستخدام عوامل تشغيل RxJS (انظر القسم التالي). للتحضير ، أقوم بإنشاء بث مباشر للموضوع في match-game.component.ts assignmentStream
يجب أن تنبعث منها العناصر المحددة من قبل المستخدم على الجانب الأيسر أو الأيمن. الهدف هو استخدام مشغلي RxJS لتعديل الدفق وتقسيمه بطريقة أحصل على solvedStream
: دفق واحد تم حله والذي يوفر الأزواج المعينة بشكل صحيح ودفق failedStream
في البث الذي يوفر التعيينات الخاطئة. أرغب في الاشتراك في هذين الدفقين مع subscribe
حتى أتمكن من إجراء معالجة مناسبة للحدث في كل حالة.
أحتاج أيضًا إلى مرجع إلى كائنات الاشتراك التي تم إنشاؤها ، حتى أتمكن من إلغاء الاشتراكات مع "إلغاء الاشتراك" عند مغادرة اللعبة (انظر ngOnDestroy
). يجب استيراد فئات Subject
Subscription
من الحزمة "rxjs".
private assignmentStream = new Subject<{pair:Pair, side:string}>(); private solvedStream = new Observable<Pair>(); private failedStream = new Observable<string>(); private s_Subscription: Subscription; private f_Subscription: Subscription; ngOnInit(){ ... //TODO: apply stream-operators on //assignmentStream this.s_Subscription = this.solvedStream.subscribe(pair => handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(() => handleFailedAssignment()); } ngOnDestroy() { this.s_Subscription.unsubscribe(); this.f_Subscription.unsubscribe(); }
إذا كان الواجب صحيحًا ، يتم تنفيذ الخطوات التالية:
- يتم نقل الزوج المعين إلى الحاوية للأزواج المحلولة.
- يتم إرسال الأحداث
leftpartUnselected
الأيسر غير المحدد والجزءrightpartUnselected
غير المحدد إلى المكون الرئيسي.
لا يتم نقل أي زوج إذا كانت المهمة غير صحيحة. إذا تم تنفيذ المهمة الخاطئة من اليسار إلى اليمين ( side1
لديه القيمة left
) ، يجب التراجع عن التحديد للعنصر الموجود على الجانب الأيسر (انظر GIF في بداية المقالة). إذا تم إجراء مهمة من اليمين إلى اليسار ، يتم التراجع عن التحديد للعنصر الموجود على الجانب الأيمن. هذا يعني أن العنصر الأخير الذي تم النقر عليه يظل في الحالة المحددة.
في كلتا الحالتين ، أقوم بإعداد وظائف المعالج المقابلة handleSolvedAssignment
و handleFailedAssignment
(وظيفة الإزالة: انظر التعليمات البرمجية المصدر في نهاية هذه المقالة):
private handleSolvedAssignment(pair: Pair):void{ this.solvedPairs.push(pair); this.remove(this.unsolvedPairs, pair); this.leftpartUnselected.emit(); this.rightpartUnselected.emit(); //workaround to force update of the shuffle pipe this.test = Math.random() * 10; } private handleFailedAssignment(side1: string):void{ if(side1=="left"){ this.leftpartUnselected.emit(); }else{ this.rightpartUnselected.emit(); } }
الآن يتعين علينا تغيير وجهة النظر من المستهلك الذي يشترك في البيانات إلى المنتج الذي يقوم بإنشاء البيانات. في ملف match-game.component.html ، أتأكد من أنه عند النقر فوق أحد العناصر ، يتم دفع كائن الزوج المرتبط إلى تيار assignmentStream
. من المنطقي استخدام دفق مشترك للجانب الأيسر والأيمن لأن ترتيب المهمة ليس مهمًا بالنسبة لنا.
<div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'left'})"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'right'})">
تصميم تفاعل اللعبة مع مشغلي RxJS
كل ما تبقى هو تحويل assignmentStream
الدفق إلى التدفقات solvedStream
، failedStream
. أطبق العوامل التالية بالتسلسل:
pairwise
يوجد دائمًا زوجان في الواجب. عامل التشغيل pairwise
يختار البيانات في أزواج من الدفق. يتم دمج القيمة الحالية والقيمة السابقة في زوج.
من التدفق التالي ...
„{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“
… ينتج هذا البث الجديد:
„({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“
على سبيل المثال ، نحصل على المجموعة ({pair1, left}, {pair3, right})
عندما يختار المستخدم dog
(id = 1) على الجانب الأيسر insect
(id = 3) على الجانب الأيمن (انظر مجموعة ANIMALS
في بداية المقال). تنتج هذه المجموعات والتركيبات الأخرى من تسلسل اللعبة الموضح في GIF أعلاه.
filter
يجب عليك إزالة جميع التركيبات من الدفق التي تم إجراؤها على نفس الجانب من ساحة اللعب مثل ({pair1, left}, {pair1, left})
أو ({pair1, left}, {pair4, left})
.
لذا فإن شرط التصفية comb
المركب هو comb[0].side != comb[1].side
.
partition
يأخذ هذا المشغل دفقًا وشرطًا وينشئ دفقين من هذا. يحتوي الدفق الأول على البيانات التي تفي بالشرط ويحتوي الدفق الثاني على البيانات المتبقية. في حالتنا ، يجب أن تحتوي التدفقات على تعيينات صحيحة أو غير صحيحة. لذا فإن الشرط الخاص comb[0].pair===comb[1].pair
comb
زوج.
ينتج عن المثال دفق "صحيح" مع
({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})
و تدفق "خاطئ" مع
({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})
map
مطلوب فقط كائن الزوج الفردي لمزيد من المعالجة لتعيين صحيح ، مثل pair2
. يمكن استخدام عامل تشغيل الخريطة للتعبير عن أنه يجب تعيين comb
المركب على comb[0].pair
. إذا كان التعيين غير صحيح ، يتم تعيين comb
المركب إلى comb[0].side
لأنه يجب إعادة تعيين التحديد على الجانب المحدد جنبًا إلى side
.
تُستخدم وظيفة pipe
لتوصيل المشغلين المذكورين أعلاه. يجب استيراد عوامل التشغيل المزدوجة ، filter
، partition
، map
من الحزمة pairwise
rxjs/operators
.
ngOnInit() { ... const stream = this.assignmentStream.pipe( pairwise(), filter(comb => comb[0].side != comb[1].side) ); //pipe notation leads to an error message (Angular 8.2.2, RxJS 6.4.0) const [stream1, stream2] = partition(comb => comb[0].pair === comb[1].pair)(stream); this.solvedStream = stream1.pipe( map(comb => comb[0].pair) ); this.failedStream = stream2.pipe( map(comb => comb[0].side) ); this.s_Subscription = this.solvedStream.subscribe(pair => this.handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(side => this.handleFailedAssignment(side)); }
الآن اللعبة تعمل بالفعل!
باستخدام المشغلين ، يمكن وصف منطق اللعبة بشكل إعلاني. لقد وصفنا فقط خصائص التدفقات المستهدفة لدينا (مجتمعة في أزواج ، مصفاة ، مقسمة ، مُعاد تعيينها) ولا داعي للقلق بشأن تنفيذ هذه العمليات. إذا قمنا بتنفيذها بأنفسنا ، فسيتعين علينا أيضًا تخزين الحالات الوسيطة في المكون (على سبيل المثال ، الإشارات إلى آخر العناصر التي تم النقر عليها على الجانب الأيسر والأيمن). بدلاً من ذلك ، يقوم مشغلو RxJS بتغليف منطق التنفيذ والحالات المطلوبة لنا وبالتالي رفع مستوى البرمجة إلى مستوى أعلى من التجريد.
خاتمة
باستخدام لعبة تعليمية بسيطة كمثال ، اختبرنا استخدام RxJS في مكون Angular. النهج التفاعلي مناسب تمامًا لمعالجة الأحداث التي تحدث على واجهة المستخدم. باستخدام RxJS ، يمكن ترتيب البيانات اللازمة لمعالجة الحدث بشكل ملائم كتدفقات. تتوفر العديد من العوامل ، مثل filter
أو map
أو partition
لتحويل التدفقات. تحتوي التدفقات الناتجة على بيانات معدة في شكلها النهائي ويمكن الاشتراك فيها مباشرة. يتطلب الأمر القليل من المهارة والخبرة لاختيار المشغلين المناسبين للحالة المعنية وربطهم بكفاءة. يجب أن توفر هذه المقالة مقدمة لهذا.
مزيد من الموارد
- "مقدمة إلى البرمجة التفاعلية كنت تفتقدها" ، كتبه أندريه ستالتز
القراءة ذات الصلة على SmashingMag:
- إدارة نقاط فصل الصورة مع الزاوي
- تصميم تطبيق زاوي باستخدام Bootstrap
- كيفية إنشاء ونشر تطبيق المواد الزاوي