วิธีสร้างเกมจับคู่การ์ดโดยใช้ Angular และ RxJS

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างรวดเร็ว ↬ บทความนี้มีไว้สำหรับนักพัฒนาเชิงมุมที่ต้องการควบคุมแนวคิดของการเขียนโปรแกรมเชิงโต้ตอบ นี่คือรูปแบบการเขียนโปรแกรมที่ — พูดง่ายๆ — เกี่ยวข้องกับการประมวลผลสตรีมข้อมูลแบบอะซิงโครนัส

วันนี้ ฉันต้องการเน้นที่สตรีมข้อมูลที่เกิดจากเหตุการณ์การคลิกบนอินเทอร์เฟซผู้ใช้ การประมวลผลการคลิกสตรีมดังกล่าวมีประโยชน์อย่างยิ่งสำหรับแอปพลิเคชันที่มีการโต้ตอบกับผู้ใช้อย่างเข้มข้น ซึ่งต้องประมวลผลหลายเหตุการณ์ ฉันยังอยากจะแนะนำให้คุณรู้จักกับ RxJS อีกสักหน่อย เป็นไลบรารี JavaScript ที่สามารถใช้เพื่อแสดงรูทีนการจัดการเหตุการณ์อย่างกระชับและรัดกุมในรูปแบบปฏิกิริยา

เรากำลังสร้างอะไร

เกมการเรียนรู้และแบบทดสอบความรู้เป็นที่นิยมทั้งสำหรับผู้ใช้ที่อายุน้อยกว่าและสูงอายุ ตัวอย่างคือเกม "การจับคู่คู่" ซึ่งผู้ใช้ต้องค้นหาคู่ที่เกี่ยวข้องในส่วนผสมของรูปภาพและ/หรือตัวอย่างข้อความ

แอนิเมชั่นด้านล่างแสดงรูปแบบเกมอย่างง่าย: ผู้ใช้เลือกสององค์ประกอบทางด้านซ้ายและด้านขวาของสนามเด็กเล่นทีละรายการ และในลำดับใดก็ได้ คู่ที่จับคู่อย่างถูกต้องจะถูกย้ายไปยังพื้นที่แยกต่างหากของสนามเด็กเล่น ในขณะที่การมอบหมายที่ไม่ถูกต้องจะถูกยกเลิกทันที เพื่อให้ผู้ใช้ต้องทำการเลือกใหม่

แคปหน้าจอเกมการเรียนรู้ "คู่ที่ตรงกัน"
ตัวอย่างเกมที่เราจะสร้างในวันนี้

ในบทช่วยสอนนี้ เราจะสร้างเกมการเรียนรู้ทีละขั้นตอน ในส่วนแรก เราจะสร้างองค์ประกอบเชิงมุมที่แสดงเพียงสนามเด็กเล่นของเกม เป้าหมายของเราคือสามารถกำหนดค่าส่วนประกอบสำหรับกรณีการใช้งานและกลุ่มเป้าหมายที่แตกต่างกัน ตั้งแต่แบบทดสอบสัตว์ไปจนถึงผู้ฝึกคำศัพท์ในแอปการเรียนรู้ภาษา เพื่อจุดประสงค์นี้ Angular นำเสนอแนวคิดของการฉายเนื้อหาด้วยเทมเพลตที่ปรับแต่งได้ ซึ่งเราจะใช้ประโยชน์ เพื่อแสดงหลักการ ฉันจะสร้างเกมสองเวอร์ชัน ("game1" และ "game2") ด้วยรูปแบบที่แตกต่างกัน

ในส่วนที่สองของบทช่วยสอน เราจะเน้นที่การเขียนโปรแกรมเชิงโต้ตอบ เมื่อใดก็ตามที่มีการจับคู่คู่กัน ผู้ใช้จำเป็นต้องได้รับคำติชมบางอย่างจากแอป มันคือการจัดการเหตุการณ์ที่เกิดขึ้นด้วยความช่วยเหลือของไลบรารี RxJS

  • ความต้องการ
    ต้องติดตั้ง Angular CLI เพื่อทำตามบทช่วยสอนนี้
  • รหัสแหล่งที่มา
    ซอร์สโค้ดของบทช่วยสอนนี้สามารถพบได้ที่นี่ (14KB)
เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

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 แสดงขึ้นทันทีหลังจากเริ่มแอปพลิเคชัน ฉันเพิ่มแท็กนี้ในไฟล์ app.component.html :

 <app-game1></app-game1>

เมื่อเริ่มต้นแอปพลิเคชันด้วย ng serve --open เบราว์เซอร์จะแสดงข้อความ "matching-game works" (ขณะนี้เป็นเพียงเนื้อหาเดียวของ match-game.component.html )

ตอนนี้ เราต้องทดสอบข้อมูล ในโฟลเดอร์ /app ฉันสร้างไฟล์ชื่อ pair.ts โดยที่ฉันกำหนดคลาส Pair :

 export class Pair { leftpart: string; rightpart: string; id: number; }

คู่วัตถุประกอบด้วยสองข้อความที่เกี่ยวข้อง ( leftpart และ rightpart ) และ ID

เกมแรกควรจะเป็นแบบทดสอบชนิดพันธุ์ซึ่งจะต้องกำหนดสายพันธุ์ (เช่น 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 ต้องการการเข้าถึงข้อมูลการทดสอบของเรา พวกเขาจะถูกเก็บไว้ใน 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 ) เป็นอินพุต อินพุตเป็นอาร์เรย์ของวัตถุ "คู่" อินเทอร์เฟซผู้ใช้ของเกมควรเริ่มต้นด้วยวัตถุที่ส่งผ่านเมื่อเริ่มแอปพลิเคชัน

แคปหน้าจอเกมการเรียนรู้ "คู่ที่ตรงกัน"

เพื่อจุดประสงค์นี้ เราต้องดำเนินการดังต่อไปนี้:

  1. เพิ่ม pairs คุณสมบัติให้กับองค์ประกอบเกมโดยใช้ @Input decorator
  2. เพิ่มอาร์เรย์ unsolvedPairs และ solvedPairs เป็นคุณสมบัติส่วนตัวเพิ่มเติมของส่วนประกอบ (จำเป็นต้องแยกความแตกต่างระหว่างคู่ที่ "แก้แล้ว" กับ "ยังไม่แก้")
  3. เมื่อแอปพลิเคชันเริ่มทำงาน (ดูฟังก์ชัน 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}} และ { {{pair.rightpart}}} ค่าของคุณสมบัติ leftpart และ rightpart ของวัตถุคู่แต่ละรายการจะถูกสอบถามเมื่อวนซ้ำอาร์เรย์ pair ใช้เป็นป้ายกำกับสำหรับปุ่มที่สร้างขึ้น

คู่ที่กำหนดจะแสดงอยู่ในคอนเทนเนอร์ที่สอง (คอนเทนเนอร์คลาส container solved แล้ว ) แถบสีเขียว ( connector คลาส ) แสดงว่าอยู่ร่วมกัน

โค้ด CSS ที่สอดคล้องกันของไฟล์ matching-game.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 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 ซึ่งจะต้องนำเข้าไปป์และแสดงรายการในการประกาศโมดูล ตอนนี้มุมมองที่ต้องการปรากฏในเบราว์เซอร์

เวอร์ชันเพิ่มเติม: การใช้เทมเพลตที่ปรับแต่งได้เพื่อให้การออกแบบเกมเป็นรายบุคคล

แทนที่จะใช้ปุ่ม คุณควรระบุตัวอย่างเทมเพลตที่กำหนดเองเพื่อปรับแต่งเกมได้ ในไฟล์ matching-game.component.html ฉันแทนที่เทมเพลตปุ่มสำหรับด้านซ้ายและด้านขวาของเกมด้วยแท็ก ng-template จากนั้นฉันจะกำหนดชื่อของเทมเพลตที่อ้างอิงถึงคุณสมบัติ ngTemplateOutlet สิ่งนี้ให้ตัวยึดตำแหน่งสองตัวแก่ฉัน ซึ่งแทนที่ด้วยเนื้อหาของการอ้างอิงเทมเพลตที่เกี่ยวข้องเมื่อแสดงมุมมอง

เรากำลังจัดการกับแนวคิดของ การฉายเนื้อหา : บางส่วนของเทมเพลตองค์ประกอบได้รับจากภายนอกและ "ฉาย" ลงในเทมเพลตในตำแหน่งที่ทำเครื่องหมายไว้

เมื่อสร้างมุมมอง Angular ต้องแทรกข้อมูลเกมลงในเทมเพลต ด้วยพารามิเตอร์ ngTemplateOutletContext ฉันบอก Angular ว่ามีการใช้ตัวแปร contextPair ภายในเทมเพลต ซึ่งควรกำหนดค่าปัจจุบันของตัวแปร pair จากคำสั่ง ngFor

รายการต่อไปนี้แสดงการเปลี่ยนคอนเทนเนอร์ที่ 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> ...

ในไฟล์ matching-game.component.ts จะต้องประกาศตัวแปรของการอ้างอิงเทมเพลต ( leftpart_temp และ rightpart_temp ) มัณฑนากร @ContentChild ระบุว่านี่คือการฉายเนื้อหา เช่น ตอนนี้ Angular คาดว่าตัวอย่างเทมเพลตสองตัวอย่างที่มีตัวเลือกที่เกี่ยวข้อง ( leftpart หรือ rightpart ) ถูกจัดเตรียมไว้ในองค์ประกอบหลักระหว่างแท็ก <app-matching-game></app-matching-game> match <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 ข้อมูลโค้ดเทมเพลตที่จำเป็นสองรายการพร้อมตัวเลือก leftpart และ rightpart จะถูกแทรก

เพื่อความเรียบง่าย ฉันจะใช้ปุ่มนี้ซ้ำอีกครั้ง:

 <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 เกมเวอร์ชันที่สองจะปรากฏขึ้นเมื่อฉันเริ่มแอปพลิเคชัน:

มุมมองทางเลือกของเกมในองค์ประกอบ game2
มุมมองทางเลือกของเกมในองค์ประกอบ game2

ความเป็นไปได้ในการออกแบบเกือบจะไร้ขีดจำกัดแล้ว อาจเป็นไปได้ ตัวอย่างเช่น เพื่อกำหนดคลาสย่อยของ Pair ที่มีคุณสมบัติเพิ่มเติม ตัวอย่างเช่น สามารถจัดเก็บที่อยู่รูปภาพสำหรับส่วนด้านซ้ายและ/หรือด้านขวา รูปภาพสามารถแสดงในเทมเพลตพร้อมกับข้อความหรือแทนข้อความได้

2. การควบคุมการโต้ตอบกับผู้ใช้ด้วย RxJS

ข้อดีของการเขียนโปรแกรมเชิงโต้ตอบด้วย RxJS

ในการเปลี่ยนแอปพลิเคชันให้กลายเป็นเกมแบบโต้ตอบ เหตุการณ์ (เช่น เหตุการณ์การคลิกเมาส์) ที่ถูกทริกเกอร์ที่ส่วนต่อประสานผู้ใช้จะต้องได้รับการประมวลผล ในการเขียนโปรแกรมเชิงโต้ตอบ จะพิจารณาลำดับเหตุการณ์ที่เรียกว่า "สตรีม" อย่างต่อเนื่อง สามารถสังเกตสตรีมได้ (เป็น "ที่สังเกตได้") กล่าวคือสามารถมี "ผู้สังเกตการณ์" หรือ "ผู้ติดตาม" ได้ตั้งแต่หนึ่งรายขึ้นไป พวกเขาจะได้รับแจ้ง (มักจะเป็นแบบอะซิงโครนัส) เกี่ยวกับค่าใหม่ทุกค่าในสตรีมและสามารถตอบสนองต่อค่าดังกล่าวได้ในบางวิธี

ด้วยวิธีนี้ การเชื่อมต่อระหว่างส่วนต่าง ๆ ของแอปพลิเคชันสามารถทำได้ในระดับต่ำ ผู้สังเกตการณ์และสิ่งที่สังเกตได้ที่มีอยู่นั้นเป็นอิสระจากกัน และการมีเพศสัมพันธ์สามารถเปลี่ยนแปลงได้ที่รันไทม์

ไลบรารี JavaScript RxJS มีการนำรูปแบบการออกแบบ Observer ไปใช้อย่างครบถ้วน นอกจากนี้ RxJS ยังมีโอเปอเรเตอร์จำนวนมากในการแปลงสตรีม (เช่น ตัวกรอง แผนที่) หรือรวมเข้ากับสตรีมใหม่ (เช่น ผสาน คอนแคต) ตัวดำเนินการคือ "ฟังก์ชันที่บริสุทธิ์" ในแง่ของการเขียนโปรแกรมเชิงฟังก์ชัน: ไม่ก่อให้เกิดผลข้างเคียงและไม่ขึ้นกับสถานะภายนอกฟังก์ชัน ลอจิกของโปรแกรมประกอบด้วยการเรียกฟังก์ชันบริสุทธิ์เท่านั้น ไม่ต้องการตัวแปรเสริมแบบโกลบอลหรือโลคัลเพื่อเก็บสถานะระดับกลาง ในทางกลับกัน ส่งเสริมการสร้างบล็อคโค้ดแบบไร้สัญชาติและแบบหลวมๆ ดังนั้นจึงควรตระหนักถึงส่วนใหญ่ของการจัดการเหตุการณ์โดยการผสมผสานที่ชาญฉลาดของตัวดำเนินการสตรีม ตัวอย่างนี้มีให้ในหัวข้อถัดไป โดยอิงจากเกมจับคู่ของเรา

การรวม RxJS เข้ากับการจัดการเหตุการณ์ของส่วนประกอบเชิงมุม

กรอบงานเชิงมุมทำงานร่วมกับคลาสของไลบรารี RxJS RxJS จะถูกติดตั้งโดยอัตโนมัติเมื่อติดตั้ง Angular

ภาพด้านล่างแสดงคลาสหลักและฟังก์ชันที่มีบทบาทในการพิจารณาของเรา:

แบบจำลองของคลาสที่จำเป็นสำหรับการจัดการเหตุการณ์ใน Angular/RxJS
แบบจำลองของคลาสที่จำเป็นสำหรับการจัดการเหตุการณ์ใน Angular/RxJS
ชื่อคลาส การทำงาน
สังเกตได้ (RxJS) คลาสฐานที่แสดงถึงกระแส; กล่าวอีกนัยหนึ่งคือลำดับของข้อมูลอย่างต่อเนื่อง สามารถสมัครรับข้อมูลการสังเกตได้ ฟังก์ชัน pipe ถูกใช้เพื่อใช้ฟังก์ชันตัวดำเนินการตั้งแต่หนึ่งฟังก์ชันขึ้นไปกับอินสแตนซ์ที่สังเกตได้
หัวเรื่อง (RxJS) คลาสย่อยของ observable จัดเตรียมฟังก์ชันถัดไปเพื่อเผยแพร่ข้อมูลใหม่ในสตรีม
EventEmitter (เชิงมุม) นี่คือคลาสย่อยเฉพาะเชิงมุมที่มักใช้ร่วมกับตัวตกแต่ง @Output เพื่อกำหนดเอาต์พุตคอมโพเนนต์เท่านั้น เช่นเดียวกับฟังก์ชันถัดไป ฟังก์ชันการ emit จะใช้เพื่อส่งข้อมูลไปยังสมาชิก
การสมัครสมาชิก (RxJS) ฟังก์ชัน subscribe ที่สังเกตได้จะคืนค่าอินสแตนซ์การสมัครรับข้อมูล จำเป็นต้องยกเลิกการสมัครหลังจากใช้ส่วนประกอบ

ด้วยความช่วยเหลือของคลาสเหล่านี้ เราต้องการใช้การโต้ตอบกับผู้ใช้ในเกมของเรา ขั้นตอนแรกคือต้องแน่ใจว่าองค์ประกอบที่ผู้ใช้เลือกทางด้านซ้ายหรือขวาถูกเน้นด้วยสายตา

การแสดงภาพขององค์ประกอบถูกควบคุมโดยตัวอย่างข้อมูลเทมเพลตสองรายการในองค์ประกอบหลัก การตัดสินใจว่าจะแสดงในสถานะที่เลือกอย่างไรจึงควรปล่อยให้เป็นองค์ประกอบหลักด้วย ควรรับสัญญาณที่เหมาะสมทันทีที่มีการเลือกทางด้านซ้ายหรือด้านขวา หรือทันทีที่มีการยกเลิกการเลือก

เพื่อจุดประสงค์นี้ ฉันกำหนดค่าเอาต์พุตของประเภท EventEmitter สี่ค่าในไฟล์ matching-game.component.ts ต้องนำเข้าประเภท Output และ EventEmitter จากแพ็คเกจหลัก

 @Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();

ในเทมเพลต matching-game.component.html ฉันตอบสนองต่อเหตุการณ์ mousedown ที่ด้านซ้ายและด้านขวา จากนั้นส่ง ID ของรายการที่เลือกไปยังผู้รับทั้งหมด

 <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 แสดงถึงค่าเอาต์พุตที่ปล่อยออกมา ในกรณีของเราคือ ID ต่อไปนี้ คุณสามารถดูรายการสำหรับ 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 กำหนดว่าการเปลี่ยนแปลงใดในคลาสนี้จะทำให้เกิด (เช่น การเปลี่ยนสีหรือฟอนต์) การรีเซ็ตการเลือก (ยกเลิกการเลือก) ขึ้นอยู่กับสมมติฐานที่ว่าออบเจ็กต์คู่มี ID ที่เป็นบวกเสมอ

 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 (ดูหัวข้อถัดไป) สำหรับการเตรียมตัว ฉันสร้างการ assignmentStream หัวเรื่องสตรีมใน matching-game.component.ts ควรปล่อยองค์ประกอบที่เลือกโดยผู้ใช้ทางด้านซ้ายหรือด้านขวา เป้าหมายคือการใช้ตัวดำเนินการ 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(); } }

ตอนนี้เราต้องเปลี่ยนมุมมองจากผู้บริโภคที่สมัครรับข้อมูลเป็นผู้ผลิตที่สร้างข้อมูล ในไฟล์ matching-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 ผสมคือ comb[0].pair===comb[1].pair

ตัวอย่างส่งผลให้สตรีม "ถูกต้อง" ด้วย

 ({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 พ์ใช้เพื่อเชื่อมโอเปอเรเตอร์ข้างต้น ต้องนำเข้าตัวดำเนินการ pairwise filter partition ชั่น map จากแพ็คเกจ 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 ในองค์ประกอบเชิงมุม วิธีการเชิงโต้ตอบนั้นเหมาะสมอย่างยิ่งกับการประมวลผลเหตุการณ์ที่เกิดขึ้นบนอินเทอร์เฟซผู้ใช้ ด้วย RxJS ข้อมูลที่จำเป็นสำหรับการจัดการเหตุการณ์สามารถจัดเป็นสตรีมได้อย่างสะดวก มีโอเปอเรเตอร์มากมาย เช่น filter map หรือ partition ชั่นสำหรับการแปลงสตรีม สตรีมที่ได้จะมีข้อมูลที่จัดเตรียมไว้ในรูปแบบสุดท้ายและสามารถสมัครรับข้อมูลได้โดยตรง ต้องใช้ทักษะและประสบการณ์เพียงเล็กน้อยในการเลือกโอเปอเรเตอร์ที่เหมาะสมสำหรับแต่ละกรณีและเชื่อมโยงอย่างมีประสิทธิภาพ บทความนี้ควรให้ข้อมูลเบื้องต้นเกี่ยวกับเรื่องนี้

แหล่งข้อมูลเพิ่มเติม

  • “บทนำสู่การเขียนโปรแกรมเชิงโต้ตอบที่คุณพลาดไป” เขียนโดย Andre Staltz

การอ่านที่เกี่ยวข้อง ใน SmashingMag:

  • การจัดการเบรกพอยต์ของรูปภาพด้วย Angular
  • การจัดรูปแบบแอปพลิเคชันเชิงมุมด้วย Bootstrap
  • วิธีสร้างและปรับใช้แอปพลิเคชันวัสดุเชิงมุม