技术分享:RxJS实战练习-经典游戏Breakout
效果图

数据流分析
1.ticker$ 数据流 interval配合scheduler/animationFrame 作为游戏随时间变化的控制数据流
- ticker$ = interval(this.TICKER_INTERVAL, animationFrame).pipe(
- map(() => ({
- time: Date.now(),
- deltaTime: null
- })),
- scan((previous, current) => ({
- time: current.time,
- deltaTime: (current.time - previous.time) / 1000
- }))
- ); // Observable单播 每次订阅都是启动一个数据流
2.key$ 数据流检测keydown/keyup 玩家控制的部分(整个状态中的一个副作用),改变底部船桨的位置
- PADDLE_CONTROLS = {
- ArrowLeft: -1,
- ArrowRight: 1
- };
- key$ = merge(
- fromEvent(document, 'keydown').pipe(
- map(event => this.PADDLE_CONTROLS[event['key']] || 0)
- ),
- fromEvent(document, 'keyup').pipe(map(event => 0))
- ).pipe(distinctUntilChanged()); // 提供船桨移动的方位的数据源
实现逻辑:按下‘<’直到 keyup 输出 -1 / 按下‘>’直到 keyup 输出 1 / keyup 输出 0 3.paddle$ 数据流使用操作符withLatestFrom合并了ticker$和key$ 持续流出船桨的位置
- createPaddle$(ticker$: Observable<{ time: number; deltaTime: any }>) {
- return ticker$.pipe(
- withLatestFrom(this.key$), // withLatestFrom操作符 作为游戏开始的触发条件,只有这个数据流产生数据才会往下游流动
- scan<[{ deltaTime: number; time: number }, number], number>(
- (position: number, [ticker, direction]) => {
- const nextPosition =
- position + direction * ticker.deltaTime * this.PADDLE_SPEED;
- return Math.max(
- Math.min(
- nextPosition,
- this.breakoutCanvasService.stage.width - config.PADDLE_WIDTH / 2
- ),
- config.PADDLE_WIDTH / 2
- );
- },
- this.breakoutCanvasService.stage.width / 2
- ),
- distinctUntilChanged()
- );
- }
3.createState$ 数据流使用withLatestFrom合并ticker$和paddle$ 最终输出界面需要的全部状态数据
- createState$(ticker$, paddle$) {
- return ticker$.pipe(
- withLatestFrom(paddle$),
- scan<
- [{ deltaTime: number; time: number }, number],
- { ball: Ball; bricks: Brick[]; score: number }
- >(({ ball, bricks, score }, [ticker, paddle]) => {
- const remainingBricks = [];
- const collisions = {
- paddle: false, // 球撞船桨
- floor: false, //
- wall: false, // 撞墙
- ceiling: false, // 撞顶
- brick: false // 球撞砖块
- };
- ball.position.x =
- ball.position.x +
- ball.direction.x * ticker.deltaTime * this.BALL_SPEED;
- ball.position.y =
- ball.position.y +
- ball.direction.y * ticker.deltaTime * this.BALL_SPEED;
- bricks.forEach(brick => {
- if (!this.isCollision(brick, ball)) {
- remainingBricks.push(brick);
- } else {
- collisions.brick = true;
- score = score + 10;
- }
- });
- collisions.paddle = this.isHit(paddle, ball);
- if (
- ball.position.x < config.BALL_RADIUS ||
- ball.position.x >
- this.breakoutCanvasService.stage.width - config.BALL_RADIUS
- ) {
- ball.direction.x = -ball.direction.x;
- collisions.wall = true;
- }
- collisions.ceiling = ball.position.y < config.BALL_RADIUS;
- if (collisions.brick || collisions.paddle || collisions.ceiling) {
- if (collisions.paddle) {
- ball.direction.y = -Math.abs(ball.direction.y);
- } else {
- ball.direction.y = -ball.direction.y;
- }
- }
- return {
- ball: ball,
- bricks: remainingBricks,
- collisions: collisions,
- score: score
- };
- }, this.initState())
- );
- }
- 用到ticker$流控制球的移动位置
- 根据当前状态控制下一步的状态,包括计分、球的运动方向、砖块数量
4.game$ 数据流最终的游戏控状态输出流(包括这状态数据、船桨位置数据)
- game$ = Observable.create(observer => {
- this.breakoutCanvasService.drawIntro();
- this.restart = new Subject();
- const paddle$ = this.createPaddle$(this.ticker$); // 数据源吐出船桨的位置
- const state$ = this.createState$(this.ticker$, paddle$);
- this.ticker$
- .pipe(
- withLatestFrom(paddle$, state$),
- OperatorMerge(this.restart)
- )
- .subscribe(observer); // 这个this.ticker$ 也可以不使用,直接通过merge合并后面两个数据流
- });
merge数据流restart$后 可以通过error方法终止流从而控制游戏结束
状态
两个结果状态:砖块数量、分数
两个影响状态的副作用:时间、游戏者的行为
状态交叉点
球接触砖块 -> 砖块消失
球接触船桨/墙 -> 球自然改变运动方向
整个过程用rxjs实现不需要额外保存中间数据,在管道中实现数据的缓存、状态处理 。
两个字形容 “优秀”
文章来自博客RxJS实战练习-经典游戏Breakout欢迎访问交流技术问题。
演示地址:http://tiny.pubuzhixing.com/
github:https://github.com/pubuzhixing8/tiny-game
出处:《深入浅出RxJS》十四章实例,使用TS+Angular重新包装,修改了一个小缺陷,据说这个游戏最初是由乔布斯和他的一个朋友设计