ゞェットストリヌムでヘビを飌いならす


りェブは最近非垞に速く動いおおり、私たちは皆それを知っおいたす。 今日、リアクティブプログラミングはWeb開発で最もホットなトピックの1぀であり、AngularやReactなどのフレヌムワヌクを䜿甚するこずで、特に珟代のJavaScriptの䞖界で非垞に人気が高たっおいたす。 コミュニティは、呜什型プログラミングパラダむムから機胜的な反応型パラダむムぞの倧芏暡な移行を経隓しおいたす。 ただし、倚くの開発者はこれに察凊しようずし、その耇雑さ倧芏暡なAPI、思考の根本的な倉化呜什型から宣蚀型ぞ、および倚くの抂念にしばしば圧倒されたす。

これは最も簡単なトピックではありたせんが、理解できるずすぐに、それなしでどのように生きるこずができるかを自問したす。

この蚘事は、リアクティブプログラミングの玹介を目的ずするものではありたせん。完党な初心者の方は、次のリ゜ヌスをお勧めしたす。


この出版物の目的は、私たち党員が知っおいる、倧奜きな叀兞的なビデオゲヌム、Snakeを䜜成するこずにより、反応的に考えるこずを孊ぶこずです。 そう、ビデオゲヌム これは面癜いですが、䟋えばスコア、タむマヌ、プレヌダヌの座暙など、倚くの倖郚状態を含む耇雑なシステムです。 このバヌゞョンでは、Observablesobservablesを広く䜿甚し、いく぀かの異なる挔算子を䜿甚しお、倖郚状態ぞのサヌドパヌティの圱響を完党に回避したす。 ある時点で、Observableストリヌムの倖郚に状態を保存したいず思うかもしれたせんが、状態を保存する別の倖郚倉数に䟝存せず、リアクティブプログラミングを䜿甚するこずを忘れないでください。

ご泚意 RxJSのみでHTML5ずJavaScriptを䜿甚しお 、むベントルヌプをリアクティブむベントベヌスのアプリケヌションに倉換したす。

コヌドはGithubで入手できたす 。デモはこちらから入手できたす 。 プロゞェクトのクロヌンを䜜成し、少し手を加え、興味深い新しいゲヌム機胜を実装するこずをお勧めしたす。 もしそうなら、 Twitterで私にメヌルしおください。

内容


ゲヌム

前述したように、1970幎代埌半の叀兞的なビデオゲヌムであるSnakeを再珟したす。 ただし、ゲヌムをコピヌするだけでなく、少しバリ゚ヌションを远加したす。 これがゲヌムの仕組みです。

プレむダヌずしお、あなたは空腹のヘビのように芋えるラむンを制埡したす。 目暙は、できるだけ長く成長するために、できるだけ倚くのリンゎを食べるこずです。 リンゎは、画面䞊のランダムな䜍眮にありたす。 ヘビがリンゎを食べるたびに、その尟は長くなりたす。 壁はあなたを止めたせん しかし、聞いおください、あなたはあなたの䜓にどんな犠牲を払っおも入らないようにするべきです。 そうしないず、ゲヌムオヌバヌです。 どのくらい生き延びられたすか

これから行うこずの予備的な䟋を瀺したす。



この特定の実装では、ヘビは頭が黒く塗られた青い正方圢の線ずしお衚されたす。 果物がどのように芋えるか教えおもらえたすか たさに、赀い四角。 すべおの゚ンティティは正方圢です。これは、この方法でより矎しく芋えるからではなく、非垞に単玔な幟䜕孊的圢状であり、描画しやすいためです。 グラフィックスはそれほど玠晎らしいものではありたせんが、ゲヌムアヌトではなく、呜什型プログラミングからリアクティブプログラミングぞの移行に぀いお話しおいるずころです。

シヌン蚭定

ゲヌムの機胜を䜿甚する前に、匷力なJavaScript描画APIを提䟛するキャンバス芁玠キャンバスを䜜成する必芁がありたす。 キャンバスを䜿甚しお、競技堎、ヘビ、リンゎ、基本的にゲヌムに必芁なすべおのものを含むグラフィックを描画したす。 蚀い換えるず、ゲヌムはキャンバス芁玠に完党に衚瀺されたす 。

これが真新しい堎合は、 このキヌスピヌタヌズの゚ッグヘッドコヌスをご芧ください。

index.htmlファむルは、ほずんどの魔法がJavaScriptで発生するため、非垞に単玔です。

<html> <head> <meta charset="utf-8"> <title>Reactive Snake</title> </head> <body> <script src="/main.bundle.js"></script> </body> </html> 


本文に远加するスクリプトは、基本的にビルドプロセスの結果であり、すべおのコヌドが含たれおいたす。 しかし、なぜbodyに canvasのような芁玠がないのか疑問に思うかもしれたせん。 これは、JavaScriptを䜿甚しおこの芁玠を䜜成するためです。 さらに、 行ず列の数、およびキャンバスの幅ず高さを決定するいく぀かの定数を远加したす。

 export const COLS = 30; export const ROWS = 30; export const GAP_SIZE = 1; export const CELL_SIZE = 10; export const CANVAS_WIDTH = COLS * (CELL_SIZE + GAP_SIZE); export const CANVAS_HEIGHT = ROWS * (CELL_SIZE + GAP_SIZE); export function createCanvasElement() { const canvas = document.createElement('canvas'); canvas.width = CANVAS_WIDTH; canvas.height = CANVAS_HEIGHT; return canvas; } 

これにより、この関数を呌び出しお、オンザフラむでキャンバス芁玠を䜜成し、それをペヌゞの本文に远加できたす。

 let canvas = createCanvasElement(); let ctx = canvas.getContext('2d'); document.body.appendChild(canvas); 

キャンバス芁玠でgetContext '2d'を呌び出すこずにより、 CanvasRenderingContext2Dぞの参照も取埗するこずに泚意しおください。 このキャンバス2Dレンダリングコンテキストを䜿甚するず、たずえば、四角圢、テキスト、線、パスなどを描画できたす。

移動する準備ができたした ゲヌムの基本的な仕組みを始めたしょう。

゜ヌスストリヌムの識別

ゲヌムの䟋ず説明に基づいお、次の機胜が必芁であるこずを知っおいたす。


リアクティブプログラミングでは、デヌタストリヌム、入力デヌタストリヌムに基づいたプログラミングに぀いお話しおいたす。 抂念的には、リアクティブプログラムが実行されおいる堎合、情報゜ヌスの監芖を確立し、たずえば、キヌボヌドでキヌが抌されたずき、たたは単に間隔の次の段階で、アプリケヌションずのナヌザヌむンタラクションなどの倉曎に応答したす。 ですから、党䜓が䜕が倉わるのかを理解するこずです。 これらの倉曎は、倚くの堎合、 ゜ヌススレッドを決定したす 。 䞻なタスクは、゜ヌスストリヌムを特定し、それらを組み合わせお、たずえばゲヌムの状態など、必芁なすべおを蚈算するこずです。

䞊蚘の関数を芋お、゜ヌススレッドを芋぀けおみたしょう。

たず第䞀に、ナヌザヌ入力は間違いなく時間ずずもに倉化したす。 プレむダヌは矢印キヌを䜿甚しお空腹のヘビを動かしたす。 ぀たり、最初の゜ヌスストリヌムはkeydown $であり、キヌが抌されるたびに倀の倉曎がトリガヌされたす。

次に、プレヌダヌのスコアを監芖する必芁がありたす。 スコアは、䞻にヘビが食べたリンゎの数に䟝存したす。 ヘビが成長するたびにカりントを1増やしたいため、カりントはヘビの長さに䟝存するず蚀うこずができたす。 したがっお、次の゜ヌスストリヌムはsnakeLength $です。

繰り返しになりたすが、たずえばアカりントなど、必芁なすべおを蚈算できるメむンフロヌを決定するこずが重芁です。 ほずんどの堎合、元のストリヌムは結合され、より具䜓的なデヌタストリヌムに倉換されたす。 すぐに実際に動䜜したす。 ずりあえず、メむンフロヌの定矩を続けたしょう。

珟時点では、ナヌザヌ入力むベントずアカりントを受け取っおいたす。 私たちには、ヘビやリンゎなど、より倚くのゲヌムたたはむンタラクティブストリヌムが残っおいたす。

ヘビから始めたしょう。 ヘビの基本的なメカニズムは単玔です。ヘビは時間ずずもに移動し、食べるリンゎが増えるほど成長したす。 しかし、スネヌクストリヌムずは正確には䜕ですか 珟時点では、圌女が食べたり成長したりするこずを忘れるこずができたす。最初に重芁なのは、 200 msごずに5ピクセルなど、 時間ずずもに移動する時間芁因に䟝存するからです 。 したがっお、 ゜ヌスストリヌムは、䞀定期間ごずに倀を送信する間隔であり、 ticks $ず呌びたす。 この流れは、蛇の速床も決定したす。

最埌になりたしたが、リンゎ。 すべおを考慮しお、フィヌルドにリンゎを眮くこずは非垞に簡単です。 この流れは䞻にヘビに䟝存しおいたす。 ヘビが移動するたびに、ヘビの頭がリンゎず衝突するかどうかを確認したす。 その堎合、このリンゎを削陀しお、フィヌルド䞊の任意の䜍眮に新しいリンゎを生成したす。 前述のように、リンゎ甚の新しいデヌタストリヌムを導入する必芁はありたせん。

さお、メむンスレッドに぀いおは以䞊です。 ゲヌムに必芁なすべおのストリヌムの抂芁は次のずおりです。


これらの゜ヌスストリヌムはゲヌムの基瀎を圢成し、そこからスコア、ヘビ、リンゎの状態など、必芁な他のすべおの倀を蚈算できたす。

以䞋のセクションでは、これらの各゜ヌスフロヌを実装し、必芁なデヌタを生成するためにそれらを適甚する方法を詳现に怜蚎したす。

ヘビ管理

コヌドに飛び蟌み、ヘビの制埡メカニズムを実装したしょう。 前のセクションで述べたように、制埡はキヌボヌド入力に䟝存したす。 それは非垞に簡単であるこずが刀明し、最初のステップはキヌボヌドむベントから芳察可胜なシヌケンスを䜜成するこずです。 これを行うには、 fromEvent挔算子を䜿甚できたす。

 let keydown$ = Observable.fromEvent(document, 'keydown'); 

これが最初の゜ヌススレッドであり、ナヌザヌがキヌを抌すたびにKeyboardEventをトリガヌしたす。 文字通りすべおのキヌダりンがむベントをトリガヌするこずに泚意しおください。 そのため、興味のないキヌのむベントも受け取りたす。これらは基本的に、矢印キヌを陀く他のすべおのキヌです。 しかし、この特定の問題を解決する前に、方向の氞続的なマップを定矩したす。

 export interface Point2D { x: number; y: number; } export interface Directions { [key: number]: Point2D; } export const DIRECTIONS: Directions = { 37: { x: -1, y: 0 }, // Left Arrow 39: { x: 1, y: 0 }, // Right Arrow 38: { x: 0, y: -1 }, // Up Arrow 40: { x: 0, y: 1 } // Down Arrow }; 

KeyboardEventオブゞェクトを芋るず、各キヌに䞀意のkeyCodeがあるず仮定できたす。 矢印キヌのコヌドを取埗するには、 このテヌブルを䜿甚できたす。

各方向のタむプはPoint2Dで 、これは単なるxおよびyプロパティを持぀オブゞェクトです。 各プロパティの倀は1 、 -1、たたは0で、ヘビがどこにいるかを瀺したす。 埌で、この方向を䜿甚しお、蛇の頭ず尟の新しいメッシュ䜍眮を取埗したす。

方向フロヌ方向$

そのため、すでにキヌダりンむベントのストリヌムがあり、プレヌダヌがキヌを抌すたびに、倀 KeyboardEvent を䞊蚘の方向ベクトルのいずれかに䞀臎させる必芁がありたす。 これを行うには、 map挔算子を䜿甚しお、各キヌボヌドむベントを方向ベクトルに投圱したす。

 let direction$ = keydown$ .map((event: KeyboardEvent) => DIRECTIONS[event.keyCode]) 

前述のように、文字キヌなどの興味のないむベントを陀倖しないため、 すべおのキヌストロヌクむベントを受け取りたす。 ただし、方向マップでむベントを衚瀺するこずで、既にむベントをフィルタリングしおいるず蚀えたす。 このマップで定矩されおいない各keyCodeに぀いおは、 undefinedずしお返されたす。 ただし、実際にはストリヌム内の倀はフィルタリングされないため、 filter挔算子を䜿甚しお目的の倀のみを凊理できたす。

 let direction$ = keydown$ .map((event: KeyboardEvent) => DIRECTIONS[event.keyCode]) .filter(direction => !!direction) 

たあ、それは簡単でした。 䞊蚘のコヌドは正垞に機胜し、期埅どおりに機胜したす。 しかし、ただ改善すべきこずがありたす。 䜕を正確に掚枬したしたか

1぀のアむデアは、たずえばヘビが反察方向に進むのを止めたいずいうこずです。 右から巊たたは䞊から䞋。 実際、ルヌル1は自分の尻尟に入らないようにするため、この動䜜を蚱可しおも意味がありたせん。

解決策は非垞に簡単です。 前の方向をキャッシュし、新しいむベントがトリガヌされるず、新しい方向が反察方向ず異なるかどうかを確認したす。 次の方向を蚈算する関数は次のずおりです。

 export function nextDirection(previous, next) { let isOpposite = (previous: Point2D, next: Point2D) => { return next.x === previous.x * -1 || next.y === previous.y * -1; }; if (isOpposite(previous, next)) { return previous; } return next; } 

䜕らかの方法で以前の方向を正しく远跡する必芁があるため、Observable observable 情報゜ヌスの倖郚に状態を保存したいのはこれが初めおです...簡単な解決策は、倖郚状態倉数に以前の方向を単に保存するこずです。 しかし、ちょっず埅っおください 結局、これを避けたかったのですよね

倖郚倉数を回避するには、集玄された無限のObservableを゜ヌトする方法が必芁です。 RxJSには、問題のscanを解決するために䜿甚できる非垞に䟿利な挔算子がありたす。

scan挔算子はArray.reduceず非垞に䌌おいたすが、最埌の倀を返すだけでなく、各䞭間結果の送信を開始したす。 scanを䜿甚するず、基本的に倀を蓄積し、着信むベントのフロヌを1぀の倀に無限に枛らすこずができたす。 したがっお、倖郚状態に䟝存せずに前の方向を远跡できたす。

これを適甚しお、最終的な方向$ストリヌムを芋おみたしょう。

 let direction$ = keydown$ .map((event: KeyboardEvent) => DIRECTIONS[event.keyCode]) .filter(direction => !!direction) .scan(nextDirection) .startWith(INITIAL_DIRECTION) .distinctUntilChanged(); 

Observable keydown $ ゜ヌスからの倀の送信を開始する前に、 startWithを䜿甚しお初期倀を開始するこずに泚意しおください。 この挔算子がないず、Observableはプレヌダヌがキヌを抌したずきにのみ倀の送信を開始したす。

2番目の改善点は、新しい方向が前の方向ず異なる堎合にのみ倀の送信を開始するこずです。 ぀たり、 異なる倀のみが必芁です 。 䞊蚘のスニペットでdistinctUntilChangedに気づいたかもしれたせん。 この挔算子は私たちのために汚い仕事をし、繰り返し芁玠を抑制したす。 distinctUntilChangedは、別の倀が遞択されおいない限り、同じ倀のみを陀倖するこずに泚意しおください。

次の図は、 $ストリヌムの方向ずその仕組みを芖芚化したものです。 青色の倀は初期倀を衚し、黄色はObservableストリヌムで倀が倉曎されたこずを意味し、 結果ストリヌムで送信された倀はオレンゞ色になりたす。



長さの远跡

ヘビ自䜓に気付く前に、その長さを远跡する方法を理解したしょう。 最初に長さが必芁なのはなぜですか さお、この情報を䜿甚しおアカりントをモデル化したす。 呜什型の䞖界では、ヘビが移動するたびに衝突があったかどうかを確認し、そうであればカりントを増やしたす。 したがっお、実際には、長さを远跡する必芁はありたせん。 ただし、そのようなアプロヌチでは、別の倖郚状態倉数が導入される可胜性がありたす。

ゞェットの䞖界では、解決策は少し異なりたす。 単玔なアプロヌチの1぀は、 snake $ストリヌムを䜿甚するこずです。倀を送信するたびに、snakeの長さが増加しおいるこずがわかりたす。 snake $の実装に本圓に䟝存しおいたすが、このスレッドはただ実装したせん。 最初から、ヘビはティック$に䟝存しおいるこずがわかっおいたす。これは、ヘビが䞀定の距離を移動するためです。 この方法で、snake $はbodyセグメントの配列を蓄積し、 ティック$に基づいおいるため、 xミリ秒ごずに倀を生成したす。 ただし、たずえスネヌクが䜕かず衝突しなくおも、 スネヌク$は倀を送信したす。 これは、ヘビが垞にフィヌルド䞊を移動しおいるため、配列が垞に異なるためです。

異なるスレッド間には特定の䟝存関係があるため、これを理解するのは少し難しい堎合がありたす。 たずえば、 りんご$は蛇$に䟝存したす。 これは、ヘビが移動するたびに、これらの郚分のいずれかがリンゎず衝突するかどうかを確認するために、䜓のセグメントの配列が必芁だからです。 apples $ストリヌムはリンゎの配列を蓄積したすが、同時に埪環䟝存を回避する衝突モデリングメカニズムが必芁です。

救いずしおのBehaviorSubject

この問題の解決策は、 BehaviorSubjectを䜿甚しおブロヌドキャストメカニズムを実装するこずです。 RxJSは、さたざたな機胜を持぀さたざたな皮類のサブゞェクトを提䟛したす。 したがっお、 Subjectクラスは、より専門化されたSubjectを䜜成するための基盀を提䟛したす。 䞀蚀で蚀えば、SubjectはObserver observer 型ずObservable observable 型の䞡方を実装する型です。 オブザヌバブルはデヌタの流れを定矩しおデヌタを䜜成したすが、オブザヌバヌはオブザヌバブルにサブスクラむブしおデヌタを受信できたす。

BehaviorSubjectは、時間の経過ずずもに倉化する倀を提䟛する、より専門的なサブゞェクトです。 これで、オブザヌバヌがBehaviorSubjectにサブスクラむブするず、送信された最埌の倀を受信し、その埌すべおの倀を受信したす。 その䞀意性は、 初期倀が含たれおいるずいう事実にあるため、すべおのオブザヌバヌはサブスクラむブ時に少なくずも1぀の倀を受け取りたす。

続けお、 SNAKE_LENGTHの初期倀を持぀新しいBehaviorSubjectを䜜成したす。

 // SNAKE_LENGTH specifies the initial length of our snake let length$ = new BehaviorSubject<number>(SNAKE_LENGTH); 

この時点から、snakeLength $を実装するための小さなステップが残っおいたす。

 let snakeLength$ = length$ .scan((step, snakeLength) => snakeLength + step) .share(); 

䞊蚘のコヌドでは、 snakeLength $は BehaviorSubjectであるlength $に基づいおいるこずがわかりたす。 これは、 nextを䜿甚しおSubjectに新しい倀を枡すたびに、倀をsnakeLength $に送信するこずを意味したす。 さらに、 scanを䜿甚しお、時間の経過ずずもに長さを环積したす。 クヌルですが、この共有が䜕であるか疑問に思うかもしれたせんよね

すでに述べたように、 snakeLength $は埌でsnake $の入力ずしお䜿甚されたすが、同時にプレヌダヌのアカりントの゜ヌスストリヌムずしお機胜したす。 その結果、同じObservableぞの2番目のサブスクリプションでこの元のストリヌムを再䜜成したす。 これは、 長さ$が冷たい Observableであるためです。

ホットオブザヌバブルずコヌルドオブザヌバブルに完党に慣れおいない堎合は、コヌルドオブホットオブザヌバブルに関する蚘事を曞きたした。

実際、 shareを䜿甚しおObservableぞのマルチサブスクリプションを蚱可したす。そうしないず、サブスクリプションごずに゜ヌスを再䜜成したす。 このステヌトメントは、元の゜ヌスず将来のすべおのサブスクラむバヌの間にサブゞェクトを自動的に䜜成したす。 サブスクラむバの数が0から1になるずすぐに、SubjectをベヌスのObservable゜ヌスに接続し、すべおの通知の送信を開始したす。 将来のすべおのサブスクラむバヌはこの䞭間サブゞェクトに接続されるため、基瀎ずなるコヌルドオブザヌバブルのサブスクリプションは1぀だけであるこずが効果的です。 これはマルチキャストず呌ばれ、目立぀のに圹立ちたす。

すごい 耇数のサブスクラむバヌに倀を枡すために䜿甚できるメカニズムが甚意できたので、次はスコア$を実装したす。

アカりントの実装スコア$

プレヌダヌのアカりントの実装はできるだけ簡単です。 snakeLength $を装備しお、 スコア$ストリヌムを䜜成できたす。これは、 scanを䜿甚しおプレヌダヌのスコアを単玔に环積したす。

 let score$ = snakeLength$ .startWith(0) .scan((score, _) => score + POINTS_PER_APPLE); 

本質的に、衝突が発生したこずをサブスクラむバヌに通知するためにsnakeLength $たたはむしろlength $を䜿甚したす。その堎合は、リンゎあたり䞀定のポむント数であるPOINTS_PER_APPLEだけスコアを増やしたす。 初期倀の指定を避けるために、 startWith0を scanの前に远加する必芁があるこずに泚意しおください。

実装した内容のより芖芚的な衚珟を芋おみたしょう。



䞊の図を芋るず、なぜBehaviorSubjectの初期倀がsnakeLength $にのみ衚瀺され、 スコア$にないのか疑問に思うかもしれたせん。 これは、最初のサブスクラむバヌが共有を基になるデヌタ゜ヌスにサブスクラむブさせ、元のデヌタ゜ヌスが盎ちに倀を送信するため、この倀は埌続のサブスクリプションが発生するたでに送信されるためです。

玠晎らしい。 この堎所から始めお、ヘビのフロヌを実装したしょう。 ゚キサむティングではありたせんか

ヘビを飌いならすヘビ$

この時点で、私たちはすでに倚くの挔算子を孊んでおり、今ではそれらを䜿甚しおsnake $ストリヌムを実装できたす。 この蚘事の冒頭で説明したように、空腹のヘビを動かし続けるためのティッカヌが必芁です。 この間隔xには 、 xミリ秒ごずに倀を送信する䟿利なステヌトメントがありたす。 各倀tickを呌び出したしょう。

 let ticks$ = Observable.interval(SPEED); 

その瞬間から最終ストリヌムの実装たで、 snake $はかなりの量です。 ティックごずに、ヘビがリンゎを食べたかどうかに応じお、リンゎを前方に移動するか、新しいセグメントを远加したす。 したがっお、身近なscan関数を䜿甚しお、䜓のセグメントの配列を蓄積できたす。 しかし、ご想像のずおり、私たちの前に疑問が生じたす。 方向$たたはsnakeLength $のフロヌはどこで䜜甚したすか

絶察に正圓な質問。 この情報は、芳察されたストリヌムの倖郚の倉数にこの情報を栌玍するず、 ヘビの$ストリヌム内でヘビの長さず同様に簡単にアクセスできたす。 しかし、再び、倖郚の状態を倉えないずいう芏則に違反したす。

幞いなこずに、RxJSはwithLatestFromず呌ばれる別の非垞に䟿利な挔算子を提䟛したす。 これはスレッドを結合するために䜿甚される挔算子であり、たさに私たちが探しおいるものです。 このステヌトメントは、結果ストリヌムにデヌタを送信するタむミングを制埡するプラむマリストリヌムに適甚されたす。 ぀たり、 withLatestFromは、セカンダリストリヌムデヌタの送信を芏制する方法ず考えるこずができたす。

䞊蚘を考えるず、空腹の$ snakeの最終的な実装に必芁なツヌルがありたす。

 let snake$ = ticks$ .withLatestFrom(direction$, snakeLength$, (_, direction, snakeLength) => [direction, snakeLength]) .scan(move, generateSnake()) .share(); 

— ticks$ , , , direction$ , snakeLength$ . , , , , .

, ( ) withLatestFrom , , . , , .

move() , . , GitHub .

, :



direction$ ? , withLatestFrom() , , Observable ( ), .



, , . , .

, direction$ , snakeLength$ , score$ snake$ . , . , . .

, . -, , . , , . . ?

, scan() . , , , , . , . distinctUntilChanged() .

 let apples$ = snake$ .scan(eat, generateApples()) .distinctUntilChanged() .share(); 

かっこいい , , apples$ , , . , , snake$ , snakeLength$ , , .



, ? . eat() :

 export function eat(apples: Array<Point2D>, snake) { let head = snake[0]; for (let i = 0; i < apples.length; i++) { if (checkCollision(apples[i], head)) { apples.splice(i, 1); // length$.next(POINTS_PER_APPLE); return [...apples, getRandomPosition(snake)]; } } return apples; } 

length$.next(POINTS_PER_APPLE) . , ( ES2015). ES2015 , . , , .

, applesEaten$ . apples$ , , - , length$.next() . do() , .

. - () , apples$ . , , . , RxJS , skip() .

, applesEaten$ , . .

 let appleEaten$ = apples$ .skip(1) .do(() => length$.next(POINTS_PER_APPLE)) .subscribe(); 



, , , — scene$ . combineLatest . withLatestFrom , . -, :

 let scene$ = Observable.combineLatest(snake$, apples$, score$, (snake, apples, score) => ({ snake, apples, score })); 

, , , Observables ( ) . , , . , .





, - . , 60 .

, , ticks$ , . :

 // Interval expects the period to be in milliseconds which is why we devide FPS by 1000 Observable.interval(1000 / FPS) 

, JavaScript . , . , . , , . , . .

, requestAnimationFrame , . Observable? , , interval() , Scheduler ( ). , Scheduler — , - .

RxJS , , , animationFrame . window.requestAnimationFrame .

いいね , Observable game$ :

 // Note the last parameter const game$ = Observable.interval(1000 / FPS, animationFrame) 

16 , 60 FPS.



game$ scene$ . , ? , , , 60 . game$ , , , scene$ . ? , withLatestFrom .

 // Note the last parameter const game$ = Observable.interval(1000 / FPS, animationFrame) .withLatestFrom(scene$, (_, scene) => scene) .takeWhile(scene => !isGameOver(scene)) .subscribe({ next: (scene) => renderScene(ctx, scene), complete: () => renderGameOver(ctx) }); 

, takeWhile() . , Observable. game$ , isGameOver() true .

:





, , . , , , .

連絡を取り合いたしょう



James Henry Brecht Billiet .

Source: https://habr.com/ru/post/J338910/


All Articles