วิธีสร้างเกมจับคู่การ์ดโดยใช้ 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
) เป็นอินพุต อินพุตเป็นอาร์เรย์ของวัตถุ "คู่" อินเทอร์เฟซผู้ใช้ของเกมควรเริ่มต้นด้วยวัตถุที่ส่งผ่านเมื่อเริ่มแอปพลิเคชัน
เพื่อจุดประสงค์นี้ เราต้องดำเนินการดังต่อไปนี้:
- เพิ่ม
pairs
คุณสมบัติให้กับองค์ประกอบเกมโดยใช้@Input
decorator - เพิ่มอาร์เรย์
unsolvedPairs
และsolvedPairs
เป็นคุณสมบัติส่วนตัวเพิ่มเติมของส่วนประกอบ (จำเป็นต้องแยกความแตกต่างระหว่างคู่ที่ "แก้แล้ว" กับ "ยังไม่แก้") - เมื่อแอปพลิเคชันเริ่มทำงาน (ดูฟังก์ชัน
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 เกมเวอร์ชันที่สองจะปรากฏขึ้นเมื่อฉันเริ่มแอปพลิเคชัน:
ความเป็นไปได้ในการออกแบบเกือบจะไร้ขีดจำกัดแล้ว อาจเป็นไปได้ ตัวอย่างเช่น เพื่อกำหนดคลาสย่อยของ Pair
ที่มีคุณสมบัติเพิ่มเติม ตัวอย่างเช่น สามารถจัดเก็บที่อยู่รูปภาพสำหรับส่วนด้านซ้ายและ/หรือด้านขวา รูปภาพสามารถแสดงในเทมเพลตพร้อมกับข้อความหรือแทนข้อความได้
2. การควบคุมการโต้ตอบกับผู้ใช้ด้วย RxJS
ข้อดีของการเขียนโปรแกรมเชิงโต้ตอบด้วย RxJS
ในการเปลี่ยนแอปพลิเคชันให้กลายเป็นเกมแบบโต้ตอบ เหตุการณ์ (เช่น เหตุการณ์การคลิกเมาส์) ที่ถูกทริกเกอร์ที่ส่วนต่อประสานผู้ใช้จะต้องได้รับการประมวลผล ในการเขียนโปรแกรมเชิงโต้ตอบ จะพิจารณาลำดับเหตุการณ์ที่เรียกว่า "สตรีม" อย่างต่อเนื่อง สามารถสังเกตสตรีมได้ (เป็น "ที่สังเกตได้") กล่าวคือสามารถมี "ผู้สังเกตการณ์" หรือ "ผู้ติดตาม" ได้ตั้งแต่หนึ่งรายขึ้นไป พวกเขาจะได้รับแจ้ง (มักจะเป็นแบบอะซิงโครนัส) เกี่ยวกับค่าใหม่ทุกค่าในสตรีมและสามารถตอบสนองต่อค่าดังกล่าวได้ในบางวิธี
ด้วยวิธีนี้ การเชื่อมต่อระหว่างส่วนต่าง ๆ ของแอปพลิเคชันสามารถทำได้ในระดับต่ำ ผู้สังเกตการณ์และสิ่งที่สังเกตได้ที่มีอยู่นั้นเป็นอิสระจากกัน และการมีเพศสัมพันธ์สามารถเปลี่ยนแปลงได้ที่รันไทม์
ไลบรารี JavaScript RxJS มีการนำรูปแบบการออกแบบ Observer ไปใช้อย่างครบถ้วน นอกจากนี้ RxJS ยังมีโอเปอเรเตอร์จำนวนมากในการแปลงสตรีม (เช่น ตัวกรอง แผนที่) หรือรวมเข้ากับสตรีมใหม่ (เช่น ผสาน คอนแคต) ตัวดำเนินการคือ "ฟังก์ชันที่บริสุทธิ์" ในแง่ของการเขียนโปรแกรมเชิงฟังก์ชัน: ไม่ก่อให้เกิดผลข้างเคียงและไม่ขึ้นกับสถานะภายนอกฟังก์ชัน ลอจิกของโปรแกรมประกอบด้วยการเรียกฟังก์ชันบริสุทธิ์เท่านั้น ไม่ต้องการตัวแปรเสริมแบบโกลบอลหรือโลคัลเพื่อเก็บสถานะระดับกลาง ในทางกลับกัน ส่งเสริมการสร้างบล็อคโค้ดแบบไร้สัญชาติและแบบหลวมๆ ดังนั้นจึงควรตระหนักถึงส่วนใหญ่ของการจัดการเหตุการณ์โดยการผสมผสานที่ชาญฉลาดของตัวดำเนินการสตรีม ตัวอย่างนี้มีให้ในหัวข้อถัดไป โดยอิงจากเกมจับคู่ของเรา
การรวม RxJS เข้ากับการจัดการเหตุการณ์ของส่วนประกอบเชิงมุม
กรอบงานเชิงมุมทำงานร่วมกับคลาสของไลบรารี RxJS RxJS จะถูกติดตั้งโดยอัตโนมัติเมื่อติดตั้ง Angular
ภาพด้านล่างแสดงคลาสหลักและฟังก์ชันที่มีบทบาทในการพิจารณาของเรา:
ชื่อคลาส | การทำงาน |
---|---|
สังเกตได้ (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
- วิธีสร้างและปรับใช้แอปพลิเคชันวัสดุเชิงมุม