Angular ve RxJS Kullanarak Kart Eşleştirme Oyunu Nasıl Oluşturulur

Yayınlanan: 2022-03-10
Hızlı özet ↬ Bu makale, reaktif programlama kavramından yararlanmak isteyen Angular geliştiricilerine yöneliktir. Bu, basitçe söylemek gerekirse, asenkron veri akışlarının işlenmesiyle ilgilenen bir programlama stilidir.

Bugü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.

"Eşleşen çiftler" öğrenme oyununun ekran görüntüsü
Bugün oluşturacağımız oyuna bir bakış

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).
Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

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.

"Eşleşen çiftler" öğrenme oyununun ekran görüntüsü

Bunun için şu şekilde hareket etmemiz gerekiyor:

  1. @Input dekoratörünü kullanarak özellik pairs oyun bileşenine ekleyin.
  2. ÇözülmüşPairs ve solvedPairs dizilerini bileşenin ek özel özellikleri olarak unsolvedPairs . (Zaten “çözülmüş” ve “henüz çözülmemiş” çiftleri ayırt etmek gerekir.)
  3. 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.

Kullanıcı arayüzünün mevcut durumu
Kullanıcı arayüzünün mevcut durumu

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:

Game2 bileşenindeki oyunun alternatif bir görünümü
game2 bileşenindeki oyunun alternatif bir görünümü

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:

Angular/RxJS'de olay işleme için temel sınıfların bir modeli
Angular/RxJS'de olay işleme için temel sınıfların bir modeli
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 ve rightpartUnselected 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!

"Eşleşen çiftler" öğrenme oyununun ekran görüntüsü
Son sonuç

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