Cara Membuat Game Mencocokkan Kartu Menggunakan Angular Dan RxJS
Diterbitkan: 2022-03-10Hari ini, saya ingin fokus pada aliran data yang dihasilkan dari peristiwa klik pada antarmuka pengguna. Pemrosesan aliran klik tersebut sangat berguna untuk aplikasi dengan interaksi pengguna yang intensif di mana banyak peristiwa harus diproses. Saya juga ingin memperkenalkan Anda ke RxJS sedikit lebih banyak; ini adalah pustaka JavaScript yang dapat digunakan untuk mengekspresikan rutinitas penanganan acara secara ringkas dan ringkas dalam gaya reaktif.
Apa yang Kami Bangun?
Game pembelajaran dan kuis pengetahuan populer baik untuk pengguna yang lebih muda maupun yang lebih tua. Contohnya adalah game “pair matching”, di mana pengguna harus menemukan pasangan terkait dalam campuran gambar dan/atau potongan teks.
Animasi di bawah ini menunjukkan versi permainan yang sederhana: Pengguna memilih dua elemen di sisi kiri dan kanan lapangan bermain satu demi satu, dan dalam urutan apa pun. Pasangan yang cocok dengan benar dipindahkan ke area terpisah dari lapangan permainan, sementara tugas yang salah segera dibubarkan sehingga pengguna harus membuat pilihan baru.
Dalam tutorial ini, kita akan membangun sebuah game pembelajaran selangkah demi selangkah. Pada bagian pertama, kita akan membangun komponen Angular yang hanya menunjukkan lapangan permainan. Tujuan kami adalah agar komponen dapat dikonfigurasi untuk kasus penggunaan dan kelompok sasaran yang berbeda — dari kuis hewan hingga pelatih kosa kata dalam aplikasi pembelajaran bahasa. Untuk tujuan ini, Angular menawarkan konsep proyeksi konten dengan templat yang dapat disesuaikan, yang akan kami gunakan. Untuk mengilustrasikan prinsipnya, saya akan membuat dua versi game (“game1” dan “game2”) dengan tata letak yang berbeda.
Di bagian kedua tutorial, kita akan fokus pada pemrograman reaktif. Setiap kali pasangan cocok, pengguna perlu mendapatkan semacam umpan balik dari aplikasi; penanganan acara inilah yang diwujudkan dengan bantuan perpustakaan RxJS.
- Persyaratan
Untuk mengikuti tutorial ini, Angular CLI harus diinstal. - Kode sumber
Kode sumber dari tutorial ini dapat ditemukan di sini (14KB).
1. Membangun Komponen Sudut Untuk Game Pembelajaran
Cara Membuat Kerangka Dasar
Pertama, mari kita buat proyek baru bernama “learning-app”. Dengan Angular CLI, Anda dapat melakukannya dengan perintah ng new learning-app
. Di file app.component.html , saya mengganti kode sumber yang sudah dibuat sebelumnya sebagai berikut:
<div> <h1>Learning is fun!</h1> </div>
Pada langkah selanjutnya, komponen untuk game pembelajaran dibuat. Saya menamakannya “matching-game” dan menggunakan perintah ng generate component matching-game
. Ini akan membuat subfolder terpisah untuk komponen game dengan file HTML, CSS, dan TypeScript yang diperlukan.
Seperti yang telah disebutkan, game edukasi harus dapat dikonfigurasi untuk tujuan yang berbeda. Untuk mendemonstrasikan ini, saya membuat dua komponen tambahan ( game1
dan game2
) dengan menggunakan perintah yang sama. Saya menambahkan komponen game sebagai komponen anak dengan mengganti kode yang dibuat sebelumnya di file game1.component.html atau game2.component.html dengan tag berikut:
<app-matching-game></app-matching-game>
Pada awalnya, saya hanya menggunakan komponen game1
. Untuk memastikan bahwa game 1 segera ditampilkan setelah memulai aplikasi, saya menambahkan tag ini ke file app.component.html :
<app-game1></app-game1>
Saat memulai aplikasi dengan ng serve --open
, browser akan menampilkan pesan “matching-game works”. (Ini adalah satu-satunya konten dari matching-game.component.html .)
Sekarang, kita perlu menguji data. Di folder /app
, saya membuat file bernama pair.ts di mana saya mendefinisikan kelas Pair
:
export class Pair { leftpart: string; rightpart: string; id: number; }
Objek pasangan terdiri dari dua teks terkait ( leftpart
dan rightpart
) dan sebuah ID.
Permainan pertama seharusnya menjadi kuis spesies di mana spesies (misalnya dog
) harus ditugaskan ke kelas hewan yang sesuai (yaitu mammal
).
Dalam file animal.ts , saya mendefinisikan sebuah array dengan data uji:
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'}, ];
Komponen game1
membutuhkan akses ke data pengujian kami. Mereka disimpan di properti animals
. File game1.component.ts sekarang memiliki konten berikut:
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() { } }
Versi Pertama Komponen Game
Tujuan kami berikutnya: Game matching-game
harus menerima data game dari komponen induk (misalnya game1
) sebagai masukan. Inputnya adalah larik objek "pasangan". Antarmuka pengguna game harus diinisialisasi dengan objek yang diteruskan saat memulai aplikasi.
Untuk tujuan ini, kita perlu melanjutkan sebagai berikut:
- Tambahkan
pairs
properti ke komponen game menggunakan dekorator@Input
. - Tambahkan
solvedPairs
danunsolvedPairs
sebagai properti pribadi tambahan dari komponen. (Perlu untuk membedakan antara pasangan yang sudah "terpecahkan" dan "belum terpecahkan".) - Saat aplikasi dijalankan (lihat fungsi
ngOnInit
) semua pasangan masih "tidak terpecahkan" dan oleh karena itu dipindahkan ke arrayunsolvedPairs
.
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]); } } }
Selanjutnya, saya mendefinisikan template HTML dari komponen matching-game
. Ada wadah untuk pasangan yang belum terpecahkan dan yang terpecahkan. Arahan ngIf
memastikan bahwa wadah masing-masing hanya ditampilkan jika setidaknya ada satu pasangan yang belum terpecahkan atau terpecahkan.
Dalam wadah untuk pasangan yang belum terpecahkan ( container unsolved
), pertama-tama semua left
(lihat bingkai kiri di GIF di atas) dan kemudian baik-baik right
(lihat bingkai kanan di GIF) komponen pasangan terdaftar. (Saya menggunakan direktif ngFor
untuk membuat daftar pasangan.) Saat ini, tombol sederhana sudah cukup sebagai template.
Dengan ekspresi template {{{pair.leftpart}}
dan { {{pair.rightpart}}}
, nilai properti leftpart
dan rightpart
dari objek pasangan individu ditanyakan saat iterasi larik pair
. Mereka digunakan sebagai label untuk tombol yang dihasilkan.
Pasangan yang ditugaskan terdaftar di wadah kedua (wadah kelas container solved
). Bilah hijau ( connector
kelas ) menunjukkan bahwa mereka milik bersama.
Kode CSS yang sesuai dari file matching-game.component.css dapat ditemukan di kode sumber di awal artikel.
<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>
Dalam component game1
, array animals
sekarang terikat ke properti pairs
dari component matching-game
(pengikatan data satu arah).
<app-matching-game [pairs]="animals"></app-matching-game>
Hasilnya ditunjukkan pada gambar di bawah ini.
Jelas, permainan menjodohkan kita belum terlalu sulit, karena bagian kiri dan kanan dari pasangan tersebut berhadapan langsung. Agar pasangan tidak terlalu sepele, bagian yang tepat harus dicampur. Saya memecahkan masalah dengan shuffle
pipa yang ditentukan sendiri, yang saya terapkan ke array unsolvedPairs
di sisi kanan ( test
parameter diperlukan nanti untuk memaksa pipa diperbarui):
... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...
Kode sumber pipa disimpan dalam file shuffle.pipe.ts di folder aplikasi (lihat kode sumber di awal artikel). Perhatikan juga file app.module.ts , di mana pipa harus diimpor dan dicantumkan dalam deklarasi modul. Sekarang tampilan yang diinginkan muncul di browser.
Versi Diperpanjang: Menggunakan Template yang Dapat Disesuaikan Untuk Memungkinkan Desain Individual Game
Alih-alih tombol, seharusnya dimungkinkan untuk menentukan cuplikan template arbitrer untuk menyesuaikan game. Pada file matching-game.component.html saya mengganti template tombol untuk sisi kiri dan kanan game dengan tag ng-template
. Saya kemudian menetapkan nama referensi template ke properti ngTemplateOutlet
. Ini memberi saya dua placeholder, yang digantikan oleh konten referensi template masing-masing saat merender tampilan.
Kami di sini berurusan dengan konsep proyeksi konten : bagian-bagian tertentu dari templat komponen diberikan dari luar dan "diproyeksikan" ke dalam templat pada posisi yang ditandai.
Saat membuat tampilan, Angular harus memasukkan data game ke dalam template. Dengan parameter ngTemplateOutletContext
saya memberi tahu Angular bahwa variabel contextPair
digunakan di dalam template, yang harus diberi nilai saat ini dari variabel pair
dari direktif ngFor
.
Daftar berikut menunjukkan penggantian wadah yang unsolved
. Dalam wadah yang solved
, tombol-tombolnya juga harus diganti dengan tag 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> ...
Dalam file matching-game.component.ts , variabel dari kedua referensi template ( leftpart_temp
dan rightpart_temp
) harus dideklarasikan. Dekorator @ContentChild
menunjukkan bahwa ini adalah proyeksi konten, yaitu Angular sekarang mengharapkan bahwa dua cuplikan template dengan pemilih masing-masing ( leftpart
atau rightpart
) disediakan di komponen induk di antara tag <app-matching-game></app-matching-game>
dari elemen host (lihat @ViewChild
).
@ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;
Jangan lupa: Jenis ContentChild
dan TemplateRef
harus diimpor dari paket inti.
Dalam komponen induk game1
, dua cuplikan template yang diperlukan dengan pemilih bagian leftpart
dan rightpart
sekarang dimasukkan.
Demi kesederhanaan, saya akan menggunakan kembali tombol di sini lagi:
<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>
Atribut let-animalPair="contextPair"
digunakan untuk menentukan bahwa variabel konteks contextPair
digunakan dalam cuplikan template dengan nama animalPair
.
Cuplikan template sekarang dapat diubah sesuai selera Anda. Untuk mendemonstrasikan ini saya menggunakan komponen game2
. File game2.component.ts mendapatkan konten yang sama dengan game1.component.ts . Di game2.component.html saya menggunakan elemen div
yang dirancang secara individual alih-alih tombol. Kelas CSS disimpan dalam file 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>
Setelah menambahkan tag <app-game2></app-game2>
di beranda app.component.html , versi kedua dari game tersebut muncul saat saya memulai aplikasi:
Kemungkinan desain sekarang hampir tidak terbatas. Mungkin saja, misalnya, untuk mendefinisikan subkelas Pair
yang berisi properti tambahan. Misalnya, alamat gambar dapat disimpan untuk bagian kiri dan/atau kanan. Gambar dapat ditampilkan dalam template bersama dengan teks atau sebagai pengganti teks.
2. Kontrol Interaksi Pengguna Dengan RxJS
Keuntungan Pemrograman Reaktif Dengan RxJS
Untuk mengubah aplikasi menjadi permainan interaktif, peristiwa (misalnya peristiwa klik mouse) yang dipicu pada antarmuka pengguna harus diproses. Dalam pemrograman reaktif, urutan kejadian yang berkelanjutan, yang disebut "aliran", dipertimbangkan. Aliran dapat diamati (ini adalah "yang dapat diamati"), yaitu dapat ada satu atau lebih "pengamat" atau "pelanggan" yang berlangganan aliran tersebut. Mereka diberi tahu (biasanya secara asinkron) tentang setiap nilai baru dalam aliran dan dapat bereaksi dengan cara tertentu.
Dengan pendekatan ini, tingkat kopling yang rendah antara bagian-bagian aplikasi dapat dicapai. Pengamat yang ada dan yang dapat diamati adalah independen satu sama lain dan sambungannya dapat bervariasi pada saat runtime.
Pustaka JavaScript RxJS menyediakan implementasi matang dari pola desain Pengamat. Selanjutnya, RxJS berisi banyak operator untuk mengubah aliran (misalnya filter, peta) atau menggabungkannya menjadi aliran baru (misalnya menggabungkan, menggabungkan). Operator adalah "fungsi murni" dalam arti pemrograman fungsional: Mereka tidak menghasilkan efek samping dan tidak tergantung pada keadaan di luar fungsi. Logika program yang hanya terdiri dari panggilan ke fungsi murni tidak memerlukan variabel bantu global atau lokal untuk menyimpan status perantara. Ini, pada gilirannya, mendorong pembuatan blok kode stateless dan digabungkan secara longgar. Oleh karena itu diinginkan untuk mewujudkan sebagian besar penanganan acara dengan kombinasi yang cerdas dari operator aliran. Contoh ini diberikan di bagian setelah berikutnya, berdasarkan permainan kami yang cocok.
Mengintegrasikan RxJS Ke Dalam Penanganan Acara Komponen Sudut
Kerangka kerja Angular bekerja dengan kelas-kelas perpustakaan RxJS. Oleh karena itu RxJS diinstal secara otomatis ketika Angular diinstal.
Gambar di bawah menunjukkan kelas dan fungsi utama yang berperan dalam pertimbangan kami:
Nama kelas | Fungsi |
---|---|
Dapat diamati (RxJS) | Kelas dasar yang mewakili aliran; dengan kata lain, urutan data yang berkesinambungan. Observable dapat menjadi langganan. Fungsi pipe digunakan untuk menerapkan satu atau lebih fungsi operator ke instance yang dapat diamati. |
Subjek (RxJS) | Subclass dari observable menyediakan fungsi berikutnya untuk memublikasikan data baru dalam aliran. |
EventEmitter (Sudut) | Ini adalah subkelas khusus sudut yang biasanya hanya digunakan bersama dengan dekorator @Output untuk menentukan output komponen. Seperti fungsi selanjutnya, fungsi emit digunakan untuk mengirim data ke pelanggan. |
Berlangganan (RxJS) | Fungsi subscribe dari yang dapat diamati mengembalikan instance berlangganan. Diperlukan untuk membatalkan langganan setelah menggunakan komponen. |
Dengan bantuan kelas-kelas ini, kami ingin mengimplementasikan interaksi pengguna dalam game kami. Langkah pertama adalah memastikan bahwa elemen yang dipilih oleh pengguna di sisi kiri atau kanan disorot secara visual.
Representasi visual elemen dikendalikan oleh dua cuplikan template di komponen induk. Keputusan bagaimana mereka ditampilkan dalam keadaan yang dipilih karena itu juga harus diserahkan kepada komponen induk. Itu harus menerima sinyal yang sesuai segera setelah pemilihan dibuat di sisi kiri atau kanan atau segera setelah pemilihan dibatalkan.
Untuk tujuan ini, saya mendefinisikan empat nilai keluaran dari tipe EventEmitter
dalam file matching-game.component.ts . Jenis Output
dan EventEmitter
harus diimpor dari paket inti.
@Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();
Dalam template matching-game.component.html , saya bereaksi terhadap peristiwa mousedown
di sisi kiri dan kanan, dan kemudian mengirim ID item yang dipilih ke semua penerima.
<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)">
Dalam kasus kami, penerima adalah komponen game1
dan game2
. Di sana Anda sekarang dapat menentukan penanganan acara untuk acara leftpartSelected
, rightpartSelected
, leftpartUnselected
dan rightpartUnselected
. Variabel $event
mewakili nilai keluaran yang dipancarkan, dalam kasus kami ID. Berikut ini Anda dapat melihat daftar untuk game1.component.html , untuk game2.component.html perubahan yang sama berlaku.
<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>
Di game1.component.ts (dan juga di game2.component.ts ), fungsi event
handler sekarang diimplementasikan. Saya menyimpan ID dari elemen yang dipilih. Dalam template HTML (lihat di atas), elemen-elemen ini ditetapkan ke kelas yang selected
. File CSS game1.component.css mendefinisikan perubahan visual mana yang akan dibawa oleh kelas ini (misalnya perubahan warna atau font). Menyetel ulang seleksi (unselect) didasarkan pada asumsi bahwa objek pasangan selalu memiliki ID positif.
onLeftpartSelected(id:number):void{ this.leftpartSelectedId = id; } onRightpartSelected(id:number):void{ this.rightpartSelectedId = id; } onLeftpartUnselected():void{ this.leftpartSelectedId = -1; } onRightpartUnselected():void{ this.rightpartSelectedId = -1; }
Pada langkah selanjutnya, penanganan event diperlukan dalam komponen game yang cocok. Itu harus ditentukan apakah penugasan benar, yaitu, jika elemen yang dipilih kiri cocok dengan elemen yang dipilih kanan. Dalam hal ini, pasangan yang ditugaskan dapat dipindahkan ke wadah untuk pasangan yang diselesaikan.
Saya ingin merumuskan logika evaluasi menggunakan operator RxJS (lihat bagian selanjutnya). Untuk persiapan, saya membuat aliran assignmentStream
mata pelajaran di matching-game.component.ts . Itu harus memancarkan elemen yang dipilih oleh pengguna di sisi kiri atau kanan. Tujuannya adalah menggunakan operator RxJS untuk memodifikasi dan membagi aliran sedemikian rupa sehingga saya mendapatkan dua aliran baru: satu solvedStream
yang menyediakan pasangan yang ditetapkan dengan benar dan aliran kedua failedStream
yang memberikan penugasan yang salah. Saya ingin berlangganan dua aliran ini dengan subscribe
agar dapat melakukan penanganan acara yang sesuai dalam setiap kasus.
Saya juga memerlukan referensi ke objek langganan yang dibuat, sehingga saya dapat membatalkan langganan dengan "berhenti berlangganan" saat meninggalkan game (lihat ngOnDestroy
). Kelas Subject
dan Subscription
harus diimpor dari paket "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(); }
Jika penugasan sudah benar, langkah-langkah berikut dilakukan:
- Pasangan yang ditugaskan dipindahkan ke wadah untuk pasangan yang diselesaikan.
- Peristiwa
leftpartUnselected
danrightpartUnselected
dikirim ke komponen induk.
Tidak ada pasangan yang dipindahkan jika penugasannya salah. Jika penugasan yang salah dijalankan dari kiri ke kanan ( side1
memiliki nilai left
), pemilihan harus dibatalkan untuk elemen di sisi kiri (lihat GIF di awal artikel). Jika penugasan dibuat dari kanan ke kiri, pemilihan dibatalkan untuk elemen di sisi kanan. Ini berarti bahwa elemen terakhir yang diklik tetap dalam status yang dipilih.
Untuk kedua kasus, saya menyiapkan fungsi handler yang sesuai handleSolvedAssignment
dan handleFailedAssignment
(hapus fungsi: lihat kode sumber di akhir artikel ini):
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(); } }
Sekarang kita harus mengubah sudut pandang dari konsumen yang berlangganan data menjadi produsen yang menghasilkan data. Dalam file matching-game.component.html , saya memastikan bahwa ketika mengklik elemen, objek pasangan terkait didorong ke dalam stream assignmentStream
. Masuk akal untuk menggunakan aliran yang sama untuk sisi kiri dan kanan karena urutan tugas tidak penting bagi kami.
<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'})">
Desain Interaksi Game Dengan Operator RxJS
Yang tersisa hanyalah mengubah stream assignmentStream
menjadi solvedStream
dan failedStream
. Saya menerapkan operator berikut secara berurutan:
pairwise
Selalu ada dua pasangan dalam sebuah tugas. Operator pairwise
mengambil data berpasangan dari aliran. Nilai saat ini dan nilai sebelumnya digabungkan menjadi pasangan.
Dari aliran berikut…
„{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“
…menghasilkan aliran baru ini:
„({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“
Misalnya, kita mendapatkan kombinasi ({pair1, left}, {pair3, right})
ketika pengguna memilih dog
(id=1) di sisi kiri dan insect
(id=3) di sisi kanan (lihat larik ANIMALS
di awal artikel). Ini dan kombinasi lainnya dihasilkan dari urutan permainan yang ditunjukkan pada GIF di atas.
filter
Anda harus menghapus semua kombinasi dari aliran yang dibuat di sisi yang sama dari lapangan permainan seperti ({pair1, left}, {pair1, left})
atau ({pair1, left}, {pair4, left})
.
Oleh karena itu, kondisi filter untuk comb
kombinasi adalah comb[0].side != comb[1].side
.
partition
Operator ini mengambil aliran dan kondisi dan membuat dua aliran dari ini. Aliran pertama berisi data yang memenuhi syarat dan aliran kedua berisi data yang tersisa. Dalam kasus kami, aliran harus berisi tugas yang benar atau salah. Jadi syarat comb
kombinasi adalah comb[0].pair===comb[1].pair
.
Contoh menghasilkan aliran yang "benar" dengan
({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})
dan aliran "salah" dengan
({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})
map
Hanya objek pasangan individu yang diperlukan untuk pemrosesan lebih lanjut dari penetapan yang benar, seperti pair2
. Operator peta dapat digunakan untuk menyatakan bahwa kombinasi comb
harus dipetakan ke comb[0].pair
. Jika penetapannya salah, kombinasi comb
dipetakan ke sisi comb[0].side
karena pemilihan harus diatur ulang pada sisi yang ditentukan oleh side
.
Fungsi pipe
digunakan untuk menggabungkan operator di atas. Operator pairwise
, filter
, partition
, map
harus diimpor dari paket 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)); }
Sekarang permainan sudah bekerja!
Dengan menggunakan operator, logika permainan dapat digambarkan secara deklaratif. Kami hanya menjelaskan properti dari dua aliran target kami (digabungkan menjadi pasangan, difilter, dipartisi, dipetakan ulang) dan tidak perlu khawatir tentang implementasi operasi ini. Jika kami telah mengimplementasikannya sendiri, kami juga harus menyimpan status perantara dalam komponen (misalnya referensi ke item yang terakhir diklik di sisi kiri dan kanan). Sebagai gantinya, operator RxJS merangkum logika implementasi dan status yang diperlukan untuk kami dan dengan demikian meningkatkan pemrograman ke tingkat abstraksi yang lebih tinggi.
Kesimpulan
Menggunakan permainan pembelajaran sederhana sebagai contoh, kami menguji penggunaan RxJS dalam komponen Angular. Pendekatan reaktif sangat cocok untuk memproses peristiwa yang terjadi pada antarmuka pengguna. Dengan RxJS, data yang diperlukan untuk penanganan acara dapat dengan mudah diatur sebagai aliran. Banyak operator, seperti filter
, map
atau partition
tersedia untuk mengubah aliran. Aliran yang dihasilkan berisi data yang disiapkan dalam bentuk akhirnya dan dapat dilanggankan secara langsung. Dibutuhkan sedikit keterampilan dan pengalaman untuk memilih operator yang sesuai untuk kasus masing-masing dan menghubungkannya secara efisien. Artikel ini harus memberikan pengantar untuk ini.
Sumber Daya Lebih Lanjut
- “Pengantar Pemrograman Reaktif yang Telah Anda Lewati,” ditulis oleh Andre Staltz
Bacaan Terkait di SmashingMag:
- Mengelola Breakpoint Gambar Dengan Angular
- Menata Aplikasi Sudut Dengan Bootstrap
- Cara Membuat Dan Menyebarkan Aplikasi Material Sudut