Angular ve RxJS Kullanarak Kart Eşleştirme Oyunu Nasıl Oluşturulur
Yayınlanan: 2022-03-10Bugün, kullanıcı arayüzündeki tıklama olaylarından kaynaklanan veri akışlarına odaklanmak istiyorum. Bu tür tıklama akışlarının işlenmesi, birçok olayın işlenmesi gereken yoğun kullanıcı etkileşimi olan uygulamalar için özellikle yararlıdır. Ayrıca sizi RxJS ile biraz daha tanıştırmak isterim; olay işleme rutinlerini kompakt ve özlü bir şekilde reaktif bir tarzda ifade etmek için kullanılabilen bir JavaScript kitaplığıdır.
Ne İnşa Ediyoruz?
Öğrenme oyunları ve bilgi sınavları hem genç hem de yaşlı kullanıcılar için popülerdir. Bir örnek, kullanıcının bir resim ve/veya metin parçacığı karışımında ilgili çiftleri bulması gereken “çift eşleştirme” oyunudur.
Aşağıdaki animasyon oyunun basit bir versiyonunu göstermektedir: Kullanıcı, oyun alanının sol ve sağ tarafındaki iki öğeyi birbiri ardına ve herhangi bir sırayla seçer. Doğru eşleşen çiftler oyun alanının ayrı bir alanına taşınırken, yanlış atamalar hemen çözülür, böylece kullanıcının yeni bir seçim yapması gerekir.
Bu eğitimde, adım adım böyle bir öğrenme oyunu oluşturacağız. İlk bölümde, sadece oyunun oyun alanını gösteren bir Angular bileşeni oluşturacağız. Amacımız, bileşenin bir hayvan testinden bir dil öğrenme uygulamasındaki kelime eğiticisine kadar farklı kullanım durumları ve hedef gruplar için yapılandırılabilmesidir. Bu amaçla Angular, yararlanacağımız özelleştirilebilir şablonlarla içerik projeksiyonu konseptini sunar. İlkeyi açıklamak için, oyunun iki versiyonunu (“oyun1” ve “oyun2”) farklı düzenler ile oluşturacağım.
Eğitimin ikinci bölümünde, reaktif programlamaya odaklanacağız. Bir çift eşleştirildiğinde, kullanıcının uygulamadan bir tür geri bildirim alması gerekir; RxJS kütüphanesinin yardımıyla gerçekleştirilen bu olay işlemedir.
- Gereksinimler
Bu öğreticiyi takip etmek için Angular CLI yüklenmelidir. - Kaynak kodu
Bu öğreticinin kaynak kodu burada bulunabilir (14KB).
1. Öğrenme Oyunu İçin Açısal Bir Bileşen Oluşturma
Temel Çerçeve Nasıl Oluşturulur
Öncelikle “learning-app” adında yeni bir proje oluşturalım. Angular CLI ile bunu ng new learning-app
komutuyla yapabilirsiniz. app.component.html dosyasında önceden oluşturulmuş kaynak kodunu aşağıdaki gibi değiştiriyorum:
<div> <h1>Learning is fun!</h1> </div>
Bir sonraki adımda, öğrenme oyununun bileşeni oluşturulur. Buna “eşleştirme oyunu” adını verdim ve ng generate component matching-game
komutunu kullandım. Bu, gerekli HTML, CSS ve TypeScript dosyalarını içeren oyun bileşeni için ayrı bir alt klasör oluşturacaktır.
Daha önce de belirtildiği gibi, eğitici oyun farklı amaçlar için yapılandırılabilir olmalıdır. Bunu göstermek için aynı komutu kullanarak iki ek bileşen ( game1
ve game2
) oluşturuyorum. game1.component.html veya game2.component.html dosyasında önceden oluşturulmuş kodu aşağıdaki etiketle değiştirerek oyun bileşenini alt bileşen olarak ekliyorum:
<app-matching-game></app-matching-game>
İlk başta sadece game1
bileşenini kullanıyorum. Uygulamayı başlattıktan hemen sonra oyun 1'in görüntülendiğinden emin olmak için bu etiketi app.component.html dosyasına ekliyorum:
<app-game1></app-game1>
ng serve --open
ile uygulamayı başlatırken, tarayıcı “eşleştirme oyunu çalışıyor” mesajını görüntüler. (Şu anda eşleşen-game.component.html'nin tek içeriği budur.)
Şimdi verileri test etmemiz gerekiyor. /app
klasöründe, Pair
sınıfını tanımladığım pair.ts adında bir dosya oluşturuyorum:
export class Pair { leftpart: string; rightpart: string; id: number; }
Bir çift nesnesi, ilgili iki metinden (sol kısım ve rightpart
leftpart
.
İlk oyunun, türlerin (örn. dog
) uygun hayvan sınıfına (örn. mammal
) atanması gereken bir tür testi olması gerekiyordu.
hayvanlar.ts dosyasında, test verileriyle bir dizi tanımlarım:
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
bileşeninin test verilerimize erişmesi gerekiyor. animals
mülkünde saklanırlar. game1.component.ts dosyası artık aşağıdaki içeriğe sahiptir:
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() { } }
Oyun Bileşeninin İlk Versiyonu
Bir sonraki hedefimiz: Oyun bileşeni matching-game
, ana bileşenden (örn. game1
) gelen oyun verilerini girdi olarak kabul etmelidir. Giriş, bir dizi "çift" nesnedir. Uygulama başlatılırken oyunun kullanıcı arayüzü, geçirilen nesnelerle başlatılmalıdır.
Bunun için şu şekilde hareket etmemiz gerekiyor:
-
@Input
dekoratörünü kullanarak özellikpairs
oyun bileşenine ekleyin. - ÇözülmüşPairs ve
solvedPairs
dizilerini bileşenin ek özel özellikleri olarakunsolvedPairs
. (Zaten “çözülmüş” ve “henüz çözülmemiş” çiftleri ayırt etmek gerekir.) - Uygulama başlatıldığında (
ngOnInit
işlevine bakın) tüm çiftler hala “unsolvedPairs
” ve bu nedenle unsolvedPairs dizisine taşınır.
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]); } } }
Ayrıca, matching-game
bileşeninin HTML şablonunu tanımlıyorum. Çözülmemiş ve çözülmüş çiftler için kaplar vardır. ngIf
yönergesi, ilgili kapsayıcının yalnızca en az bir çözülmemiş veya çözülmüş çift varsa görüntülenmesini sağlar.
Çözülmemiş çiftler için kapta (sınıf container unsolved
), önce tüm left
(yukarıdaki GIF'deki sol çerçeveye bakın) ve ardından tüm right
(GIF'deki sağ çerçeveye bakın) çiftlerin bileşenleri listelenir. (Çiftleri listelemek için ngFor
yönergesini kullanıyorum.) Şu anda şablon olarak basit bir düğme yeterli.
{{{pair.leftpart}}
ve { {{pair.rightpart}}}
şablon ifadeleriyle, pair
dizisi yinelenirken tek tek çift nesnelerinin leftpart
ve rightpart
özelliklerinin değerleri sorgulanır. Oluşturulan düğmeler için etiketler olarak kullanılırlar.
Atanan çiftler ikinci kapsayıcıda listelenir (sınıf container solved
). Yeşil çubuk (sınıf connector
) bunların birbirine ait olduğunu gösterir.
Match-game.component.css dosyasının ilgili CSS kodu, makalenin başındaki kaynak kodunda bulunabilir.
<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
bileşeninde, animals
dizisi şimdi bileşen matching-game
pairs
özelliğine bağlıdır (tek yönlü veri bağlama).
<app-matching-game [pairs]="animals"></app-matching-game>
Sonuç aşağıdaki resimde gösterilmiştir.
Açıkçası eşleştirme oyunumuz henüz çok zor değil çünkü çiftlerin sol ve sağ tarafları tam karşıda. Eşleştirmenin çok önemsiz olmaması için doğru parçalar karıştırılmalıdır. Sorunu, sağ taraftaki unsolvedPairs
dizisine uyguladığım, kendinden tanımlı bir boru shuffle
ile çözüyorum (boruyu güncellemeye zorlamak için daha sonra parametre test
gerekiyor):
... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...
Borunun kaynak kodu, uygulama klasöründeki shuffle.pipe.ts dosyasında saklanır (makalenin başındaki kaynak koduna bakın). Ayrıca, borunun içe aktarılması ve modül bildirimlerinde listelenmesi gereken app.module.ts dosyasına da dikkat edin. Artık istenen görünüm tarayıcıda belirir.
Genişletilmiş Sürüm: Oyunun Bireysel Tasarımına İzin Vermek İçin Özelleştirilebilir Şablonları Kullanma
Oyunu özelleştirmek için bir düğme yerine isteğe bağlı şablon parçacıkları belirlemek mümkün olmalıdır. Matching-game.component.html dosyasında, oyunun sol ve sağ tarafı için düğme şablonunu bir ng-template
etiketiyle değiştiriyorum. Daha sonra ngTemplateOutlet
özelliğine bir şablon başvurusunun adını atadım. Bu bana, görünümü oluştururken ilgili şablon referansının içeriğiyle değiştirilen iki yer tutucu verir.
Burada içerik projeksiyonu kavramıyla ilgileniyoruz: bileşen şablonunun belirli bölümleri dışarıdan verilir ve işaretli konumlarda şablona "yansıtılır".
Görünümü oluştururken, Angular oyun verilerini şablona eklemelidir. ngTemplateOutletContext parametresiyle ngTemplateOutletContext
, şablon içinde ngFor
yönergesinden pair
değişkeninin geçerli değerine atanması gereken bir contextPair
değişkeninin kullanıldığını söylüyorum.
Aşağıdaki liste, unsolved
kapsayıcının değiştirilmesini gösterir. solved
kapta, düğmelerin ng-template
etiketleriyle de değiştirilmesi gerekir.
<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 dosyasında, her iki şablon referansının ( leftpart_temp
ve rightpart_temp
) değişkenleri bildirilmelidir. Dekoratör @ContentChild
bunun bir içerik projeksiyonu olduğunu belirtir, yani Angular şimdi iki şablon parçacığının ilgili seçiciyle ( leftpart
veya rightpart
) ana bileşende <app-matching-game></app-matching-game>
app- etiketleri arasında sağlanmasını bekler. <app-matching-game></app-matching-game>
ana bilgisayar öğesinin (bkz. @ViewChild
).
@ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;
Unutmayın: ContentChild
ve TemplateRef
türleri çekirdek paketten içe aktarılmalıdır.
game1
ana bileşeninde, leftpart
ve rightpart
seçicileri ile birlikte gerekli iki şablon parçacığı artık eklenmiştir.
Basitlik adına, buradaki düğmeleri tekrar kullanacağım:
<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"
özniteliği, contextPair
bağlam değişkeninin, hayvan animalPair
adıyla şablon parçacığında kullanıldığını belirtmek için kullanılır.
Şablon parçacıkları artık kendi zevkinize göre değiştirilebilir. Bunu göstermek için game2
bileşenini kullanıyorum. game2.component.ts dosyası, game1.component.ts ile aynı içeriği alır . game2.component.html'de düğme yerine ayrı ayrı tasarlanmış bir div
öğesi kullanıyorum. CSS sınıfları game2.component.css dosyasında saklanır.
<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.component.html ana sayfasına <app-game2></app-game2>
game2 > etiketlerini ekledikten sonra, uygulamayı başlattığımda oyunun ikinci sürümü beliriyor:
Tasarım olanakları artık neredeyse sınırsız. Örneğin, ek özellikler içeren bir Pair
alt sınıfını tanımlamak mümkün olacaktır. Örneğin, resim adresleri sol ve/veya sağ kısımlar için saklanabilir. Görüntüler şablonda metinle birlikte veya metin yerine görüntülenebilir.
2. RxJS ile Kullanıcı Etkileşiminin Kontrolü
RxJS ile Reaktif Programlamanın Avantajları
Uygulamayı interaktif bir oyuna dönüştürmek için, kullanıcı arayüzünde tetiklenen olaylar (örn. fare tıklama olayları) işlenmelidir. Reaktif programlamada, "akışlar" olarak adlandırılan sürekli olay dizileri dikkate alınır. Bir akış gözlemlenebilir ("gözlenebilir"dir), yani akışa abone olan bir veya daha fazla "gözlemci" veya "abone" olabilir. Akıştaki her yeni değer hakkında (genellikle eşzamansız olarak) bilgilendirilirler ve buna belirli bir şekilde tepki verebilirler.
Bu yaklaşımla, bir uygulamanın parçaları arasında düşük düzeyde bir bağlantı sağlanabilir. Mevcut gözlemciler ve gözlenebilirler birbirinden bağımsızdır ve bunların eşleşmesi çalışma zamanında değiştirilebilir.
JavaScript kitaplığı RxJS, Observer tasarım modelinin olgun bir uygulamasını sağlar. Ayrıca, RxJS, akışları dönüştürmek (örneğin filtre, harita) veya bunları yeni akışlar halinde birleştirmek (örneğin birleştirme, concat) için çok sayıda operatör içerir. Operatörler, fonksiyonel programlama anlamında “saf fonksiyonlardır”: Yan etki üretmezler ve fonksiyonun dışındaki durumdan bağımsızdırlar. Yalnızca saf işlevlere yapılan çağrılardan oluşan bir program mantığı, ara durumları depolamak için global veya yerel yardımcı değişkenlere ihtiyaç duymaz. Bu da durumsuz ve gevşek bağlı kod bloklarının oluşturulmasını teşvik eder. Bu nedenle, olay işlemenin büyük bir bölümünün akış operatörlerinin akıllı bir kombinasyonu ile gerçekleştirilmesi arzu edilir. Bunun örnekleri, eşleştirme oyunumuza dayalı olarak sonraki bölümde verilmiştir.
Açısal Bileşenin Olay İşlemesine RxJS'yi Entegre Etme
Angular çerçevesi, RxJS kitaplığının sınıflarıyla çalışır. Bu nedenle, Angular kurulduğunda RxJS otomatik olarak kurulur.
Aşağıdaki resim, düşüncelerimizde rol oynayan ana sınıfları ve işlevleri göstermektedir:
Sınıf adı | İşlev |
---|---|
Gözlenebilir (RxJS) | Bir akışı temsil eden temel sınıf; başka bir deyişle, sürekli bir veri dizisi. Bir gözlemlenebilir abone olunabilir. pipe işlevi, gözlemlenebilir örneğe bir veya daha fazla operatör işlevi uygulamak için kullanılır. |
Konu (RxJS) | gözlemlenebilir alt sınıfı, akışta yeni verileri yayınlamak için bir sonraki işlevi sağlar. |
EventEmitter (Açısal) | Bu, bir bileşen çıktısını tanımlamak için genellikle yalnızca @Output dekoratörüyle birlikte kullanılan açıya özgü bir alt sınıftır. Bir sonraki fonksiyon gibi, emit fonksiyonu da abonelere veri göndermek için kullanılır. |
Abonelik (RxJS) | Bir gözlemlenebilirin subscribe olma işlevi, bir abonelik örneği döndürür. Bileşeni kullandıktan sonra aboneliği iptal etmek gerekir. |
Bu sınıflar yardımıyla kullanıcı etkileşimini oyunumuzda uygulamak istiyoruz. İlk adım, kullanıcı tarafından sol veya sağ tarafta seçilen bir öğenin görsel olarak vurgulandığından emin olmaktır.
Öğelerin görsel temsili, üst bileşendeki iki şablon parçacığı tarafından kontrol edilir. Bu nedenle, seçilen durumda nasıl görüntülenecekleri kararı da ana bileşene bırakılmalıdır. Sol veya sağ tarafta bir seçim yapılır yapılmaz veya bir seçim geri alınacağı anda uygun sinyalleri almalıdır.
Bu amaçla, match -game.component.ts dosyasında EventEmitter
tipinde dört çıktı değeri tanımlıyorum. Output
ve EventEmitter
türleri çekirdek paketten içe aktarılmalıdır.
@Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();
Eşleşen-game.component.html şablonunda, sol ve sağ taraftaki mousedown
olayına tepki veriyorum ve ardından seçilen öğenin kimliğini tüm alıcılara gönderiyorum.
<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)">
Bizim durumumuzda alıcılar game1
ve game2
bileşenleridir. Burada artık leftpartSelected
, rightpartSelected
, leftpartUnselected
ve rightpartUnselected
olayları için olay işlemeyi tanımlayabilirsiniz. $event
değişkeni, bizim durumumuzda ID olan yayılan çıktı değerini temsil eder. Aşağıda game1.component.html listesini görebilirsiniz, game2.component.html için aynı değişiklikler geçerlidir.
<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 dosyasında (ve benzer şekilde game2.component.ts dosyasında ), event
işleyici işlevleri artık uygulanmaktadır. Seçilen öğelerin kimliklerini saklarım. HTML şablonunda (yukarıya bakın), bu öğelere selected
sınıf atanır. CSS dosyası game1.component.css , bu sınıfın hangi görsel değişiklikleri (örneğin renk veya yazı tipi değişiklikleri) getireceğini tanımlar. Seçimi sıfırlama (seçimi kaldırma), çift nesnelerinin her zaman pozitif kimliklere sahip olduğu varsayımına dayanır.
onLeftpartSelected(id:number):void{ this.leftpartSelectedId = id; } onRightpartSelected(id:number):void{ this.rightpartSelectedId = id; } onLeftpartUnselected():void{ this.leftpartSelectedId = -1; } onRightpartUnselected():void{ this.rightpartSelectedId = -1; }
Bir sonraki adımda, eşleşen oyun bileşeninde olay işleme gereklidir. Bir atamanın doğru olup olmadığı, yani soldan seçilen öğenin sağdan seçilen öğeyle eşleşip eşleşmediği belirlenmelidir. Bu durumda, atanan çift, çözümlenen çiftler için kapsayıcıya taşınabilir.
Değerlendirme mantığını RxJS operatörlerini kullanarak formüle etmek istiyorum (bir sonraki bölüme bakın). Hazırlık için, match-game.component.ts assignmentStream
bir atamaStream konusu oluşturuyorum. Kullanıcı tarafından seçilen öğeleri sol veya sağ tarafta yaymalıdır. Amaç, akışı iki yeni akış elde edeceğim şekilde değiştirmek ve bölmek için solvedStream
operatörlerini kullanmaktır: doğru atanmış çiftleri sağlayan bir akış çözüldü Akış ve yanlış atamalar sağlayan ikinci akış failedStream
Akış. Her durumda uygun olay işlemeyi yapabilmek için bu iki akışa abone ol ile subscribe
olmak istiyorum.
Ayrıca, oyundan çıkarken "aboneliği iptal et" ile abonelikleri iptal edebilmem için oluşturulan abonelik nesnelerine bir referansa ihtiyacım var (bkz. ngOnDestroy
). Subject
ve Subscription
sınıfları “rxjs” paketinden içe aktarılmalıdır.
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(); }
Atama doğruysa, aşağıdaki adımlar yapılır:
- Atanan çift, çözülen çiftler için konteynere taşınır.
-
leftpartUnselected
verightpartUnselected
olayları üst bileşene gönderilir.
Atama yanlışsa hiçbir çift taşınmaz. Eğer yanlış atama soldan sağa yapılmışsa ( side1
left
değerine sahiptir), sol taraftaki eleman için seçim geri alınmalıdır (makalenin başındaki GIF'e bakınız). Sağdan sola atama yapılırsa, sağ taraftaki eleman için seçim geri alınır. Bu, tıklanan son öğenin seçili durumda kaldığı anlamına gelir.
Her iki durumda da, handleSolvedAssignment
ve handleFailedAssignment
karşılık gelen işleyici işlevlerini hazırlıyorum (işlevi kaldır: bu makalenin sonundaki kaynak koduna bakın):
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(); } }
Artık veriye abone olan tüketiciden veriyi üreten üreticiye bakış açısını değiştirmemiz gerekiyor. Matching-game.component.html dosyasında, bir öğeye tıklandığında, ilişkili çift nesnesinin akış assignmentStream
akışına itildiğinden emin oluyorum. Sol ve sağ taraf için ortak bir stream kullanmak mantıklı çünkü ödevin sırası bizim için önemli değil.
<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 Operatörleri ile Oyun Etkileşiminin Tasarımı
Geriye kalan tek şey akış assignmentStream
solvedStream
ve failedStream
Stream akışlarına dönüştürmektir. Aşağıdaki operatörleri sırayla uygularım:
pairwise
Bir ödevde her zaman iki çift vardır. pairwise
operatör, verileri akıştan çiftler halinde alır. Mevcut değer ve önceki değer bir çift halinde birleştirilir.
Aşağıdaki akıştan…
„{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“
…bu yeni akışın sonucu:
„({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“
Örneğin, kullanıcı sol tarafta dog
(id=1) ve sağ tarafta insect
(id=3) ({pair1, left}, {pair3, right})
ANIMALS
elde ederiz (bkz. makalenin başı). Bunlar ve diğer kombinasyonlar, yukarıdaki GIF'de gösterilen oyun dizisinden kaynaklanmaktadır.
filter
Oyun alanının aynı tarafında ({pair1, left}, {pair1, left})
veya ({pair1, left}, {pair4, left})
gibi tüm kombinasyonları akıştan kaldırmanız gerekir.
Kombine comb
için filtre koşulu bu nedenle comb[0].side != comb[1].side
.
partition
Bu operatör bir akış ve bir koşul alır ve bundan iki akış oluşturur. İlk akış, koşulu karşılayan verileri içerir ve ikinci akış, kalan verileri içerir. Bizim durumumuzda, akışlar doğru veya yanlış atamalar içermelidir. Dolayısıyla, bir kombinasyon comb
koşulu comb[0].pair===comb[1].pair
.
Örnek, "doğru" bir akışla sonuçlanır:
({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})
ve "yanlış" bir akış
({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})
map
pair2
gibi doğru bir atamanın daha fazla işlenmesi için yalnızca tek tek çift nesnesi gereklidir. Harita operatörü, kombinasyon comb
comb[0].pair
ile eşlenmesi gerektiğini ifade etmek için kullanılabilir. Atama yanlışsa, seçimin yan tarafından belirtilen side
sıfırlanması gerektiğinden, kombinasyon comb
comb[0].side
dizesiyle eşlenir.
pipe
işlevi, yukarıdaki operatörleri birleştirmek için kullanılır. Operatörler pairwise
, filter
, partition
, map
rxjs/operators
paketinden içe aktarılmalıdır.
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)); }
Şimdi oyun zaten çalışıyor!
Operatörler kullanılarak oyun mantığı bildirimsel olarak tanımlanabilir. Yalnızca iki hedef akışımızın (çiftler halinde birleştirilmiş, filtrelenmiş, bölümlenmiş, yeniden eşlenmiş) özelliklerini tanımladık ve bu işlemlerin uygulanması konusunda endişelenmemize gerek kalmadı. Bunları kendimiz uygulamış olsaydık, bileşende ara durumları da depolamak zorunda kalırdık (örneğin, sol ve sağ taraftaki son tıklanan öğelere referanslar). Bunun yerine, RxJS operatörleri, uygulama mantığını ve gerekli durumları bizim için kapsüller ve böylece programlamayı daha yüksek bir soyutlama düzeyine yükseltir.
Çözüm
Örnek olarak basit bir öğrenme oyunu kullanarak, bir Angular bileşeninde RxJS kullanımını test ettik. Reaktif yaklaşım, kullanıcı arayüzünde meydana gelen olayları işlemek için çok uygundur. RxJS ile olay işleme için gereken veriler akışlar olarak uygun şekilde düzenlenebilir. Akışları dönüştürmek için filter
, map
veya partition
gibi çok sayıda operatör mevcuttur. Ortaya çıkan akışlar, son haliyle hazırlanan ve doğrudan abone olunabilen verileri içerir. İlgili durum için uygun operatörleri seçmek ve bunları verimli bir şekilde bağlamak biraz beceri ve deneyim gerektirir. Bu makale buna bir giriş sağlamalıdır.
Diğer Kaynaklar
- Andre Staltz tarafından yazılan “Kayıp Olduğunuz Reaktif Programlamaya Giriş”
SmashingMag ile İlgili Okuma :
- Angular ile Görüntü Kesme Noktalarını Yönetme
- Bootstrap ile Angular Bir Uygulama Şekillendirme
- Açısal Malzeme Uygulaması Nasıl Oluşturulur ve Dağıtılır