. - , Angular React, , JavaScript. . , ( API), ( ) .
, , , ?
, , :
, , . , ! , , , : , . Observables () , . - Observable, , , .
.
HTML5 JavaScript RxJS, , .
Github,
-. , . ,
Twitter.
, , 1970- . . .
, . , , , . . , , . ! , . , . ?
, :
, . , ? , . , , , . , , , .
,
canvas (), API- JavaScript. , , , , . ,
canvas.
,
egghead Keith Peters.
index.html , JavaScript.
(script), (body), . ,
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;
}
,
canvas body :
let canvas = createCanvasElement();
let ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
,
CanvasRenderingContext2D,
getContext ('2d') canvas. 2D- , , , , , .
! .
, :
, . , , , , , , . , ,
.
. , , , , , .
, .
, . . ,
keydown$, , .
. , . , , , ,
1.
snakeLength$.
,
, , , , . . . .
.
, .
. : , , .
? , , ,
,
, ,
5 200 ms. ,
, ,
ticks$. .
, : . . . , , , . , . , .
, , . , :
- keydown$: (KeyboardEvent)
- snakeLength$: (Number)
- ticks$: , (Number)
, , , , .
, .
. , . , , (
observable) .
fromEvent():
let keydown$ = Observable.fromEvent(document, 'keydown');
,
KeyboardEvent , . ,
keydown . , , , . , :
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, , . .
(direction$)
,
keydown, , , ,
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)
, . , . , - . ?
, , , . . , , , ?
. , , . ,
next (
) :
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 (
) , - . ! , ?
, Observables. RxJS ,
scan().
scan() Array.reduce(), , , .
scan() . , , .
direction$ :
let direction$ = keydown$
.map((event: KeyboardEvent) => DIRECTIONS[event.keyCode])
.filter(direction => !!direction)
.scan(nextDirection)
.startWith(INITIAL_DIRECTION)
.distinctUntilChanged();
,
startWith(), , Observable (
keydown$). Observable , .
, , . ,
. ,
distinctUntilChanged(). . ,
distinctUntilChanged() , .
direction$ , . , , , , Observable, ,
, .
, . ? , . , , , , , . , . , , .
.
snake$, , , , .
snake$, . ,
ticks$, . , snake$ ,
ticks$,
x . , ,
snake$ - . , , .
, . ,
apples$ snake$. , , , , , - .
apples$ , , .
BehaviorSubject
,
,
BehaviorSubject. RxJS Subjects (
) . ,
Subject Subjects. , Subject , Observer (
) Observable (
). Observables , Observers Observables .
BehaviorSubject Subject, , . , Observer
BehaviorSubject, , . ,
, Observers .
BehaviorSubject SNAKE_LENGTH:
// SNAKE_LENGTH specifies the initial length of our snake
let length$ = new BehaviorSubject(SNAKE_LENGTH);
snakeLength$:
let snakeLength$ = length$
.scan((step, snakeLength) => snakeLength + step)
.share();
,
snakeLength$ length$,
BehaviorSubject. , , Subject,
next(),
snakeLength$. ,
scan() . , ,
share(), ?
,
snakeLength$ snake$, .
Observable. ,
length$ cold Observable (
).
hot and cold Observables (
),
Cold vs Hot Observables.
,
share(), Observable, . Subject . , Subject Observable . Subject, , Observable.
() .
! , , ,
score$.
(score$)
, .
snakeLength$,
score$,
scan():
let score$ = snakeLength$
.startWith(0)
.scan((score, _) => score + POINTS_PER_APPLE);
snakeLength$ , ,
length$, , , ,
POINTS_PER_APPLE, . ,
startWith(0) scan(), .
, :
, ,
BehaviorSubject snakeLength$ score$. ,
share() , , , .
. , . ?
(snake$)
snake$. , -
, . ,
interval(x),
x .
.
let ticks$ = Observable.interval(SPEED);
snake$ . , , , , .
scan() . , , , , .
direction$ snakeLength$?
. ,
snake$, . , .
, 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, 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 .