AngularとRxJSを使用してカードマッチングゲームを作成する方法

公開: 2022-03-10
クイックサマリー↬この記事は、リアクティブプログラミングの概念を活用したいAngular開発者を対象としています。 これは、簡単に言えば、非同期データストリームの処理を処理するプログラミングスタイルです。

今日は、ユーザーインターフェイスのクリックイベントから生じるデータストリームに焦点を当てたいと思います。 このようなクリックストリームの処理は、多くのイベントを処理する必要があるユーザーとの対話が集中するアプリケーションで特に役立ちます。 また、RxJSについてもう少し紹介したいと思います。 これは、イベント処理ルーチンをリアクティブなスタイルでコンパクトかつ簡潔に表現するために使用できるJavaScriptライブラリです。

私たちは何を構築していますか?

学習ゲームと知識クイズは、若いユーザーと年配のユーザーの両方に人気があります。 例として、ゲームの「ペアマッチング」があります。この場合、ユーザーは画像やテキストスニペットの組み合わせから関連するペアを見つける必要があります。

以下のアニメーションは、ゲームの簡単なバージョンを示しています。ユーザーは、競技場の左側と右側にある2つの要素を次々に、任意の順序で選択します。 正しく一致したペアは競技場の別のエリアに移動されますが、間違った割り当てはすぐに解消されるため、ユーザーは新しい選択を行う必要があります。

学習ゲームの「マッチングペア」のスクリーンキャプチャ
今日作成するゲームの概要

このチュートリアルでは、このような学習ゲームを段階的に作成します。 最初の部分では、ゲームのプレイフィールドを表示するだけのAngularコンポーネントを作成します。 私たちの目的は、動物のクイズから語学学習アプリの語彙トレーナーまで、さまざまなユースケースとターゲットグループに合わせてコンポーネントを構成できるようにすることです。 この目的のために、Angularはカスタマイズ可能なテンプレートを使用したコンテンツプロジェクションの概念を提供します。これを利用します。 原理を説明するために、レイアウトの異なる2つのバージョンのゲーム(「game1」と「game2」)を作成します。

チュートリアルの第2部では、リアクティブプログラミングに焦点を当てます。 ペアが一致するたびに、ユーザーはアプリから何らかのフィードバックを取得する必要があります。 ライブラリRxJSの助けを借りて実現されるのは、このイベント処理です。

  • 要件
    このチュートリアルに従うには、AngularCLIをインストールする必要があります。
  • ソースコード
    このチュートリアルのソースコードはここにあります(14KB)。
ジャンプした後もっと! 以下を読み続けてください↓

1.学習ゲーム用のAngularコンポーネントの構築

基本的なフレームワークを作成する方法

まず、「learning-app」という名前の新しいプロジェクトを作成しましょう。 Angular CLIを使用すると、コマンドng new learning-appを使用してこれを実行できます。 app.component.htmlファイルで、事前に生成されたソースコードを次のように置き換えます。

 <div> <h1>Learning is fun!</h1> </div>

次のステップでは、学習ゲームのコンポーネントが作成されます。 私はそれを「matching-game」と名付け、コマンドng generate component matching-gameを使用しました。 これにより、必要なHTML、CSS、およびTypescriptファイルを含むゲームコンポーネント用の個別のサブフォルダーが作成されます。

すでに述べたように、教育ゲームはさまざまな目的のために構成可能でなければなりません。 これを示すために、同じコマンドを使用して2つの追加コンポーネント( game1game2 )を作成します。 ファイル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-gameworks」というメッセージが表示されます。 (これは現在、 matching-game.component.htmlの唯一のコンテンツです。)

次に、データをテストする必要があります。 /appフォルダーに、 pair.tsという名前のファイルを作成します。ここでクラスPairを定義します。

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

ペアオブジェクトは、2つの関連するテキスト( leftpartrightpart )とIDで構成されます。

最初のゲームは、種( dogなど)を適切な動物クラス( mammalなど)に割り当てる必要がある種クイズであると想定されています。

ファイルanimals.tsで、テストデータを使用して配列を定義します。

 import { Pair } from './pair'; export const ANIMALS: Pair[] = [ { id: 1, leftpart: 'dog', rightpart: 'mammal'}, { id: 2, leftpart: 'blickbird', rightpart: 'bird'}, { id: 3, leftpart: 'spider', rightpart: 'insect'}, { id: 4, leftpart: 'turtle', rightpart: 'reptile' }, { id: 5, leftpart: 'guppy', rightpart: 'fish'}, ];

コンポーネントgame1は、テストデータにアクセスする必要があります。 それらはプロパティanimalsに保存されます。 ファイルgame1.component.tsには、次のコンテンツが含まれています。

 import { Component, OnInit } from '@angular/core'; import { ANIMALS } from '../animals'; @Component({ selector: 'app-game1', templateUrl: './game1.component.html', styleUrls: ['./game1.component.css'] }) export class Game1Component implements OnInit { animals = ANIMALS; constructor() { } ngOnInit() { } }

ゲームコンポーネントの最初のバージョン

次の目標:ゲームコンポーネントのmatching-gameは、親コンポーネント(例: game1 )からのゲームデータを入力として受け入れる必要があります。 入力は「ペア」オブジェクトの配列です。 ゲームのユーザーインターフェイスは、アプリケーションの起動時に渡されたオブジェクトで初期化する必要があります。

学習ゲームの「マッチングペア」のスクリーンキャプチャ

この目的のために、次のように進める必要があります。

  1. @Inputデコレータを使用して、プロパティpairsをゲームコンポーネントに追加します。
  2. コンポーネントの追加のプライベートプロパティとして、配列solvedPairsunsolvedPairsを追加します。 (すでに「解決済み」と「未解決」のペアを区別する必要があります。)
  3. アプリケーションが開始されたとき(関数ngOnInitを参照)、すべてのペアはまだ「未解決」であるため、配列unsolvedPairsに移動されます。
 import { Component, OnInit, Input } from '@angular/core'; import { Pair } from '../pair'; @Component({ selector: 'app-matching-game', templateUrl: './matching-game.component.html', styleUrls: ['./matching-game.component.css'] }) export class MatchingGameComponent implements OnInit { @Input() pairs: Pair[]; private solvedPairs: Pair[] = []; private unsolvedPairs: Pair[] = []; constructor() { } ngOnInit() { for(let i=0; i<this.pairs.length; i++){ this.unsolvedPairs.push(this.pairs[i]); } } }

さらに、 matching-gameコンポーネントのHTMLテンプレートを定義します。 未解決のペアと解決済みのペアのコンテナがあります。 ngIfディレクティブは、少なくとも1つの未解決または解決済みのペアが存在する場合にのみ、それぞれのコンテナーが表示されるようにします。

未解決のペアのコンテナ(クラスcontainer unsolved )には、最初にすべてleft (上記のGIFの左フレームを参照)、次にすべてright (GIFの右フレームを参照)のコンポーネントが一覧表示されます。 (ペアを一覧表示するためにngForディレクティブを使用します。)現時点では、テンプレートとして単純なボタンで十分です。

テンプレート式{{{pair.leftpart}}および{ {{pair.rightpart}}}を使用すると、 pair配列を反復処理するときに、個々のペアオブジェクトのプロパティleftpartおよびrightpartの値が照会されます。 これらは、生成されたボタンのラベルとして使用されます。

割り当てられたペアは、2番目のコンテナーにリストされます(クラスcontainer solvedました)。 緑のバー(クラスconnector )は、それらが一緒に属していることを示します。

ファイルmatching-game.component.cssの対応する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がコンポーネントmatching-gamepairsプロパティにバインドされています(一方向データバインディング)。

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

パイプのソースコードは、appフォルダーのshuffle.pipe.tsファイルに保存されています(記事の冒頭にあるソースコードを参照してください)。 また、ファイルapp.module.tsにも注意してください。このファイルでは、パイプをインポートしてモジュール宣言にリストする必要があります。 これで、目的のビューがブラウザに表示されます。

拡張バージョン:カスタマイズ可能なテンプレートを使用して、ゲームの個別のデザインを可能にする

ボタンの代わりに、ゲームをカスタマイズするために任意のテンプレートスニペットを指定できる必要があります。 ファイルmatching-game.component.htmlで、ゲームの左側と右側のボタンテンプレートをng-templateタグに置き換えます。 次に、テンプレート参照の名前をプロパティngTemplateOutletに割り当てます。 これにより、2つのプレースホルダーが得られ、ビューをレンダリングするときにそれぞれのテンプレート参照のコンテンツに置き換えられます。

ここでは、コンテンツプロジェクションの概念を扱っています。コンポーネントテンプレートの特定の部分は外部から提供され、マークされた位置でテンプレートに「プロジェクション」されます。

ビューを生成するとき、Angularはゲームデータをテンプレートに挿入する必要があります。 パラメーターngTemplateOutletContextを使用して、変数contextPairがテンプレート内で使用されていることをAngularに通知します。これには、 ngForディレクティブからpair変数の現在の値を割り当てる必要があります。

次のリストは、 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_temprightpart_temp )を宣言する必要があります。 デコレータ@ContentChildは、これがコンテンツプロジェクションであることを示します。つまり、Angularは、それぞれのセレクタ( leftpartまたはrightpart )を持つ2つのテンプレートスニペットがタグ<app-matching-game></app-matching-game>間の親コンポーネントで提供されることを期待しています。ホスト要素の<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に、セレクターleftpartrightpartを持つ2つの必須テンプレートスニペットが挿入されます。

簡単にするために、ここでボタンを再利用します。

 <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"は、コンテキスト変数contextPairanimalPairという名前のテンプレートスニペットで使用されることを指定するために使用されます。

テンプレートスニペットを自分の好みに変更できるようになりました。 これを示すために、コンポーネント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.component.htmlにタグ<app-game2></app-game2>を追加した後、アプリケーションを起動すると、ゲームの2番目のバージョンが表示されます。

コンポーネントgame2のゲームの別のビュー
コンポーネントgame2のゲームの別のビュー

デザインの可能性は今やほぼ無制限です。 たとえば、追加のプロパティを含むPairのサブクラスを定義することができます。 たとえば、画像アドレスを左側および/または右側の部分に保存できます。 画像は、テキストと一緒に、またはテキストの代わりにテンプレートに表示できます。

2.RxJSとのユーザーインタラクションの制御

RxJSを使用したリアクティブプログラミングの利点

アプリケーションをインタラクティブなゲームに変えるには、ユーザーインターフェイスでトリガーされるイベント(マウスクリックイベントなど)を処理する必要があります。 リアクティブプログラミングでは、イベントの連続シーケンス、いわゆる「ストリーム」が考慮されます。 ストリームを監視できます(「監視可能」です)。つまり、ストリームにサブスクライブしている1つ以上の「オブザーバー」または「サブスクライバー」が存在する可能性があります。 ストリーム内のすべての新しい値について(通常は非同期で)通知され、特定の方法でそれに反応することができます。

このアプローチでは、アプリケーションのパーツ間の低レベルの結合を実現できます。 既存のオブザーバブルとオブザーバブルは互いに独立しており、それらの結合は実行時に変更できます。

JavaScriptライブラリRxJSは、オブザーバーデザインパターンの成熟した実装を提供します。 さらに、RxJSには、ストリームを変換したり(フィルター、マップなど)、新しいストリームに結合したり(マージ、連結など)するための多数の演算子が含まれています。 演算子は、関数型プログラミングの意味での「純粋関数」です。これらは副作用を生成せず、関数外の状態に依存しません。 純粋関数の呼び出しのみで構成されるプログラムロジックは、中間状態を格納するためにグローバルまたはローカルの補助変数を必要としません。 これにより、ステートレスで疎結合のコードブロックの作成が促進されます。 したがって、ストリーム演算子の巧妙な組み合わせによってイベント処理の大部分を実現することが望ましい。 この例は、マッチングゲームに基づいて、次のセクションに示されています。

RxJSをAngularコンポーネントのイベント処理に統合する

AngularフレームワークはRxJSライブラリのクラスで動作します。 したがって、Angularのインストール時にRxJSが自動的にインストールされます。

以下の画像は、考慮事項で役割を果たす主なクラスと機能を示しています。

Angular / RxJSでのイベント処理に不可欠なクラスのモデル
Angular / RxJSでのイベント処理に不可欠なクラスのモデル
クラス名関数
オブザーバブル(RxJS) ストリームを表す基本クラス。 言い換えれば、データの連続シーケンスです。 オブザーバブルをサブスクライブできます。 pipe関数は、1つ以上の演算子関数を監視可能なインスタンスに適用するために使用されます。
件名(RxJS) observableのサブクラスは、ストリームに新しいデータを公開するための次の関数を提供します。
EventEmitter(Angular) これは角度固有のサブクラスであり、通常、コンポーネント出力を定義するために@Outputデコレータと組み合わせてのみ使用されます。 次の関数と同様に、 emit関数はサブスクライバーにデータを送信するために使用されます。
サブスクリプション(RxJS) observableのsubscribe関数はサブスクリプションインスタンスを返します。 コンポーネントを使用した後、サブスクリプションをキャンセルする必要があります。

これらのクラスの助けを借りて、ゲームにユーザーインタラクションを実装したいと思います。 最初のステップは、左側または右側でユーザーが選択した要素が視覚的に強調表示されていることを確認することです。

要素の視覚的表現は、親コンポーネントの2つのテンプレートスニペットによって制御されます。 したがって、選択した状態でそれらをどのように表示するかについての決定も、親コンポーネントに任せる必要があります。 左側または右側で選択が行われるとすぐに、または選択が取り消されるとすぐに、適切な信号を受信する必要があります。

この目的のために、 matching-game.component.tsファイルでEventEmitterタイプの4つの出力値を定義します。 タイプ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)">

この場合、レシーバーはコンポーネントgame1game2です。 ここで、イベントleftpartSelectedrightpartSelectedleftpartUnselected 、および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ハンドラー関数が実装されました。 選択した要素のIDを保存します。 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演算子を使用して評価ロジックを作成したいと思います(次のセクションを参照)。 準備のために、 matching-game.component.tsにサブジェクトassignmentStreamを作成します。 左側または右側でユーザーが選択した要素を放出する必要があります。 目標は、RxJSオペレーターを使用して、2つの新しいストリームを取得するようにストリームを変更および分割することです。1つは正しく割り当てられたペアを提供するsolvedStreamで、もう1つは間違った割り当てを提供するfailedStreamです。 いずれの場合も適切なイベント処理を実行できるように、これら2つのストリームをsubscribeでサブスクライブしたいと思います。

ゲームを終了するときに「unsubscribe」でサブスクリプションをキャンセルできるように、作成されたサブスクリプションオブジェクトへの参照も必要です( 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を参照)。 右から左に割り当てると、右側の要素の選択が取り消されます。 これは、最後にクリックされた要素が選択された状態のままであることを意味します。

どちらの場合も、対応するハンドラー関数handleSolvedAssignmenthandleFailedAssignmentを準備します(関数の削除:この記事の最後にあるソースコードを参照してください)。

 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をストリームsolvedStreamfailedStreamに変換することだけです。 次の演算子を順番に適用します。

pairwise

割り当てには常に2つのペアがあります。 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})“

たとえば、ユーザーが左側でdog (id = 1)を選択し、右側でinsect (id = 3)を選択すると、組み合わせ({pair1, left}, {pair3, right})が得られます(配列ANIMALSを参照)。記事の冒頭)。 これらと他の組み合わせは、上のGIFに示されているゲームシーケンスから生じます。

filter

({pair1, left}, {pair1, left})または({pair1, left}, {pair4, left})のように、競技場の同じ側で作成されたすべての組み合わせをストリームから削除する必要があります。

したがって、コンビネーションcombのフィルター条件はcomb[0].side != comb[1].sideです。

partition

この演算子は、ストリームと条件を取得し、これから2つのストリームを作成します。 最初のストリームには条件を満たすデータが含まれ、2番目のストリームには残りのデータが含まれます。 この場合、ストリームには正しいまたは誤った割り当てが含まれている必要があります。 したがって、コンビネーション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などの正しい割り当てをさらに処理するには、個々のペアオブジェクトのみが必要です。 マップ演算子を使用して、組み合わせcombcomb[0].pairにマップする必要があることを表現できます。 割り当てが正しくない場合、sideで指定されたsideで選択をリセットする必要があるため、組み合わせcombは文字列comb[0].sideにマップされます。

pipe関数は、上記の演算子を連結するために使用されます。 演算子pairwisefilterpartitionmapは、パッケージ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)); }

今、ゲームはすでに動作しています!

学習ゲームの「マッチングペア」のスクリーンキャプチャ
最終結果

演算子を使用することで、ゲームロジックを宣言的に記述することができます。 2つのターゲットストリーム(ペアに結合、フィルタリング、パーティション化、再マップ)のプロパティについてのみ説明し、これらの操作の実装について心配する必要はありませんでした。 それらを自分で実装した場合は、コンポーネントに中間状態を格納する必要もあります(たとえば、左側と右側で最後にクリックされたアイテムへの参照)。 代わりに、RxJSオペレーターは、実装ロジックと必要な状態をカプセル化して、プログラミングをより高いレベルの抽象化に引き上げます。

結論

例として簡単な学習ゲームを使用して、AngularコンポーネントでのRxJSの使用をテストしました。 リアクティブアプローチは、ユーザーインターフェイスで発生するイベントを処理するのに適しています。 RxJSを使用すると、イベント処理に必要なデータをストリームとして便利に配置できます。 ストリームの変換には、 filtermappartitionなどの多数の演算子を使用できます。 結果のストリームには、最終的な形式で準備され、直接サブスクライブできるデータが含まれています。 それぞれのケースに適切な演算子を選択し、それらを効率的にリンクするには、少しのスキルと経験が必要です。 この記事では、これについて紹介します。

その他のリソース

  • AndreStaltz著「あなたが見逃していたリアクティブプログラミング入門」

SmashingMagの関連資料:

  • Angularによる画像ブレークポイントの管理
  • ブートストラップを使用したAngularアプリケーションのスタイリング
  • AngularMaterialアプリケーションを作成してデプロイする方法