AngularとRxJSを使用してカードマッチングゲームを作成する方法
公開: 2022-03-10今日は、ユーザーインターフェイスのクリックイベントから生じるデータストリームに焦点を当てたいと思います。 このようなクリックストリームの処理は、多くのイベントを処理する必要があるユーザーとの対話が集中するアプリケーションで特に役立ちます。 また、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つの追加コンポーネント( game1
とgame2
)を作成します。 ファイルgame1.component.htmlまたはgame2.component.htmlで事前に生成されたコードを次のタグに置き換えることにより、ゲームコンポーネントを子コンポーネントとして追加します。
<app-matching-game></app-matching-game>
最初は、コンポーネントgame1
のみを使用します。 アプリケーションの起動直後にゲーム1が表示されるようにするために、次のタグをapp.component.htmlファイルに追加します。
<app-game1></app-game1>
ng serve --open
を使用してアプリケーションを起動すると、ブラウザに「matching-gameworks」というメッセージが表示されます。 (これは現在、 matching-game.component.htmlの唯一のコンテンツです。)
次に、データをテストする必要があります。 /app
フォルダーに、 pair.tsという名前のファイルを作成します。ここでクラスPair
を定義します。
export class Pair { leftpart: string; rightpart: string; id: number; }
ペアオブジェクトは、2つの関連するテキスト( leftpart
とrightpart
)とIDで構成されます。
最初のゲームは、種( dog
など)を適切な動物クラス( mammal
など)に割り当てる必要がある種クイズであると想定されています。
ファイルanimals.tsで、テストデータを使用して配列を定義します。
import { Pair } from './pair'; export const ANIMALS: Pair[] = [ { id: 1, leftpart: 'dog', rightpart: 'mammal'}, { id: 2, leftpart: 'blickbird', rightpart: 'bird'}, { id: 3, leftpart: 'spider', rightpart: 'insect'}, { id: 4, leftpart: 'turtle', rightpart: 'reptile' }, { id: 5, leftpart: 'guppy', rightpart: 'fish'}, ];
コンポーネントgame1
は、テストデータにアクセスする必要があります。 それらはプロパティanimals
に保存されます。 ファイルgame1.component.tsには、次のコンテンツが含まれています。
import { Component, OnInit } from '@angular/core'; import { ANIMALS } from '../animals'; @Component({ selector: 'app-game1', templateUrl: './game1.component.html', styleUrls: ['./game1.component.css'] }) export class Game1Component implements OnInit { animals = ANIMALS; constructor() { } ngOnInit() { } }
ゲームコンポーネントの最初のバージョン
次の目標:ゲームコンポーネントのmatching-game
は、親コンポーネント(例: game1
)からのゲームデータを入力として受け入れる必要があります。 入力は「ペア」オブジェクトの配列です。 ゲームのユーザーインターフェイスは、アプリケーションの起動時に渡されたオブジェクトで初期化する必要があります。
この目的のために、次のように進める必要があります。
-
@Input
デコレータを使用して、プロパティpairs
をゲームコンポーネントに追加します。 - コンポーネントの追加のプライベートプロパティとして、配列
solvedPairs
とunsolvedPairs
を追加します。 (すでに「解決済み」と「未解決」のペアを区別する必要があります。) - アプリケーションが開始されたとき(関数
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-game
のpairs
プロパティにバインドされています(一方向データバインディング)。
<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_temp
とrightpart_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
に、セレクターleftpart
とrightpart
を持つ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"
は、コンテキスト変数contextPair
がanimalPair
という名前のテンプレートスニペットで使用されることを指定するために使用されます。
テンプレートスニペットを自分の好みに変更できるようになりました。 これを示すために、コンポーネントgame2
を使用します。 ファイルgame2.component.tsは、 game1.component.tsと同じコンテンツを取得します。 game2.component.htmlでは、ボタンの代わりに個別に設計されたdiv
要素を使用しています。 CSSクラスは、 game2.component.cssファイルに保存されます。
<app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <div class="myAnimal left">{{animalPair.leftpart}}</div> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <div class="myAnimal right">{{animalPair.rightpart}}</div> </ng-template> </app-matching-game>
ホームページapp.component.htmlにタグ<app-game2></app-game2>
を追加した後、アプリケーションを起動すると、ゲームの2番目のバージョンが表示されます。
デザインの可能性は今やほぼ無制限です。 たとえば、追加のプロパティを含むPair
のサブクラスを定義することができます。 たとえば、画像アドレスを左側および/または右側の部分に保存できます。 画像は、テキストと一緒に、またはテキストの代わりにテンプレートに表示できます。
2.RxJSとのユーザーインタラクションの制御
RxJSを使用したリアクティブプログラミングの利点
アプリケーションをインタラクティブなゲームに変えるには、ユーザーインターフェイスでトリガーされるイベント(マウスクリックイベントなど)を処理する必要があります。 リアクティブプログラミングでは、イベントの連続シーケンス、いわゆる「ストリーム」が考慮されます。 ストリームを監視できます(「監視可能」です)。つまり、ストリームにサブスクライブしている1つ以上の「オブザーバー」または「サブスクライバー」が存在する可能性があります。 ストリーム内のすべての新しい値について(通常は非同期で)通知され、特定の方法でそれに反応することができます。
このアプローチでは、アプリケーションのパーツ間の低レベルの結合を実現できます。 既存のオブザーバブルとオブザーバブルは互いに独立しており、それらの結合は実行時に変更できます。
JavaScriptライブラリRxJSは、オブザーバーデザインパターンの成熟した実装を提供します。 さらに、RxJSには、ストリームを変換したり(フィルター、マップなど)、新しいストリームに結合したり(マージ、連結など)するための多数の演算子が含まれています。 演算子は、関数型プログラミングの意味での「純粋関数」です。これらは副作用を生成せず、関数外の状態に依存しません。 純粋関数の呼び出しのみで構成されるプログラムロジックは、中間状態を格納するためにグローバルまたはローカルの補助変数を必要としません。 これにより、ステートレスで疎結合のコードブロックの作成が促進されます。 したがって、ストリーム演算子の巧妙な組み合わせによってイベント処理の大部分を実現することが望ましい。 この例は、マッチングゲームに基づいて、次のセクションに示されています。
RxJSをAngularコンポーネントのイベント処理に統合する
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)">
この場合、レシーバーはコンポーネントgame1
とgame2
です。 ここで、イベントleftpartSelected
、 rightpartSelected
、 leftpartUnselected
、およびrightpartUnselected
のイベント処理を定義できます。 変数$event
は、出力された出力値(この場合はID)を表します。 以下では、 game1.component.htmlのリストを確認できます。game2.component.htmlの場合も同じ変更が適用されます。
<app-matching-game [pairs]="animals" (leftpartSelected)="onLeftpartSelected($event)" (rightpartSelected)="onRightpartSelected($event)" (leftpartUnselected)="onLeftpartUnselected()" (rightpartUnselected)="onRightpartUnselected()"> <ng-template #leftpart let-animalPair="contextPair"> <button [class.selected]="leftpartSelectedId==animalPair.id"> {{animalPair.leftpart}} </button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button [class.selected]="rightpartSelectedId==animalPair.id"> {{animalPair.rightpart}} </button> </ng-template> </app-matching-game>
game1.component.ts (および同様にgame2.component.ts )に、 event
ハンドラー関数が実装されました。 選択した要素の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を参照)。 右から左に割り当てると、右側の要素の選択が取り消されます。 これは、最後にクリックされた要素が選択された状態のままであることを意味します。
どちらの場合も、対応するハンドラー関数handleSolvedAssignment
とhandleFailedAssignment
を準備します(関数の削除:この記事の最後にあるソースコードを参照してください)。
private handleSolvedAssignment(pair: Pair):void{ this.solvedPairs.push(pair); this.remove(this.unsolvedPairs, pair); this.leftpartUnselected.emit(); this.rightpartUnselected.emit(); //workaround to force update of the shuffle pipe this.test = Math.random() * 10; } private handleFailedAssignment(side1: string):void{ if(side1=="left"){ this.leftpartUnselected.emit(); }else{ this.rightpartUnselected.emit(); } }
ここで、データをサブスクライブするコンシューマーからデータを生成するプロデューサーに視点を変更する必要があります。 ファイルmatching-game.component.htmlで、要素をクリックすると、関連付けられたペアオブジェクトがストリームassignmentStream
にプッシュされることを確認します。 割り当ての順序は重要ではないため、左側と右側に共通のストリームを使用することは理にかなっています。
<div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'left'})"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'right'})">
RxJSオペレーターとのゲームインタラクションのデザイン
残っているのは、ストリームassignmentStream
をストリームsolvedStream
とfailedStream
に変換することだけです。 次の演算子を順番に適用します。
pairwise
割り当てには常に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
などの正しい割り当てをさらに処理するには、個々のペアオブジェクトのみが必要です。 マップ演算子を使用して、組み合わせcomb
をcomb[0].pair
にマップする必要があることを表現できます。 割り当てが正しくない場合、sideで指定されたside
で選択をリセットする必要があるため、組み合わせcomb
は文字列comb[0].side
にマップされます。
pipe
関数は、上記の演算子を連結するために使用されます。 演算子pairwise
、 filter
、 partition
、 map
は、パッケージrxjs/operators
からインポートする必要があります。
ngOnInit() { ... const stream = this.assignmentStream.pipe( pairwise(), filter(comb => comb[0].side != comb[1].side) ); //pipe notation leads to an error message (Angular 8.2.2, RxJS 6.4.0) const [stream1, stream2] = partition(comb => comb[0].pair === comb[1].pair)(stream); this.solvedStream = stream1.pipe( map(comb => comb[0].pair) ); this.failedStream = stream2.pipe( map(comb => comb[0].side) ); this.s_Subscription = this.solvedStream.subscribe(pair => this.handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(side => this.handleFailedAssignment(side)); }
今、ゲームはすでに動作しています!
演算子を使用することで、ゲームロジックを宣言的に記述することができます。 2つのターゲットストリーム(ペアに結合、フィルタリング、パーティション化、再マップ)のプロパティについてのみ説明し、これらの操作の実装について心配する必要はありませんでした。 それらを自分で実装した場合は、コンポーネントに中間状態を格納する必要もあります(たとえば、左側と右側で最後にクリックされたアイテムへの参照)。 代わりに、RxJSオペレーターは、実装ロジックと必要な状態をカプセル化して、プログラミングをより高いレベルの抽象化に引き上げます。
結論
例として簡単な学習ゲームを使用して、AngularコンポーネントでのRxJSの使用をテストしました。 リアクティブアプローチは、ユーザーインターフェイスで発生するイベントを処理するのに適しています。 RxJSを使用すると、イベント処理に必要なデータをストリームとして便利に配置できます。 ストリームの変換には、 filter
、 map
、 partition
などの多数の演算子を使用できます。 結果のストリームには、最終的な形式で準備され、直接サブスクライブできるデータが含まれています。 それぞれのケースに適切な演算子を選択し、それらを効率的にリンクするには、少しのスキルと経験が必要です。 この記事では、これについて紹介します。
その他のリソース
- AndreStaltz著「あなたが見逃していたリアクティブプログラミング入門」
SmashingMagの関連資料:
- Angularによる画像ブレークポイントの管理
- ブートストラップを使用したAngularアプリケーションのスタイリング
- AngularMaterialアプリケーションを作成してデプロイする方法