效果图

![2-3.jpg](https://wt-box.worktile.com/public/f39c064a-e096-4345-80c1-2fd702461d58)

数据流分析

1.ticker$ 数据流 interval配合scheduler/animationFrame 作为游戏随时间变化的控制数据流

  1. ticker$ = interval(this.TICKER_INTERVAL, animationFrame).pipe(
  2. map(() => ({
  3. time: Date.now(),
  4. deltaTime: null
  5. })),
  6. scan((previous, current) => ({
  7. time: current.time,
  8. deltaTime: (current.time - previous.time) / 1000
  9. }))
  10. ); // Observable单播 每次订阅都是启动一个数据流

2.key$ 数据流检测keydown/keyup 玩家控制的部分(整个状态中的一个副作用),改变底部船桨的位置

  1. PADDLE_CONTROLS = {
  2. ArrowLeft: -1,
  3. ArrowRight: 1
  4. };
  5. key$ = merge(
  6. fromEvent(document, 'keydown').pipe(
  7. map(event => this.PADDLE_CONTROLS[event['key']] || 0)
  8. ),
  9. fromEvent(document, 'keyup').pipe(map(event => 0))
  10. ).pipe(distinctUntilChanged()); // 提供船桨移动的方位的数据源

实现逻辑:按下‘<’直到 keyup 输出 -1 / 按下‘>’直到 keyup 输出 1 / keyup 输出 0 3.paddle$ 数据流使用操作符withLatestFrom合并了ticker$和key$ 持续流出船桨的位置

  1. createPaddle$(ticker$: Observable<{ time: number; deltaTime: any }>) {
  2. return ticker$.pipe(
  3. withLatestFrom(this.key$), // withLatestFrom操作符 作为游戏开始的触发条件,只有这个数据流产生数据才会往下游流动
  4. scan<[{ deltaTime: number; time: number }, number], number>(
  5. (position: number, [ticker, direction]) => {
  6. const nextPosition =
  7. position + direction * ticker.deltaTime * this.PADDLE_SPEED;
  8. return Math.max(
  9. Math.min(
  10. nextPosition,
  11. this.breakoutCanvasService.stage.width - config.PADDLE_WIDTH / 2
  12. ),
  13. config.PADDLE_WIDTH / 2
  14. );
  15. },
  16. this.breakoutCanvasService.stage.width / 2
  17. ),
  18. distinctUntilChanged()
  19. );
  20. }

3.createState$ 数据流使用withLatestFrom合并ticker$和paddle$ 最终输出界面需要的全部状态数据

  1. createState$(ticker$, paddle$) {
  2. return ticker$.pipe(
  3. withLatestFrom(paddle$),
  4. scan<
  5. [{ deltaTime: number; time: number }, number],
  6. { ball: Ball; bricks: Brick[]; score: number }
  7. >(({ ball, bricks, score }, [ticker, paddle]) => {
  8. const remainingBricks = [];
  9. const collisions = {
  10. paddle: false, // 球撞船桨
  11. floor: false, //
  12. wall: false, // 撞墙
  13. ceiling: false, // 撞顶
  14. brick: false // 球撞砖块
  15. };
  16. ball.position.x =
  17. ball.position.x +
  18. ball.direction.x * ticker.deltaTime * this.BALL_SPEED;
  19. ball.position.y =
  20. ball.position.y +
  21. ball.direction.y * ticker.deltaTime * this.BALL_SPEED;
  22. bricks.forEach(brick => {
  23. if (!this.isCollision(brick, ball)) {
  24. remainingBricks.push(brick);
  25. } else {
  26. collisions.brick = true;
  27. score = score + 10;
  28. }
  29. });
  30. collisions.paddle = this.isHit(paddle, ball);
  31. if (
  32. ball.position.x < config.BALL_RADIUS ||
  33. ball.position.x >
  34. this.breakoutCanvasService.stage.width - config.BALL_RADIUS
  35. ) {
  36. ball.direction.x = -ball.direction.x;
  37. collisions.wall = true;
  38. }
  39.  
  40. collisions.ceiling = ball.position.y < config.BALL_RADIUS;
  41. if (collisions.brick || collisions.paddle || collisions.ceiling) {
  42. if (collisions.paddle) {
  43. ball.direction.y = -Math.abs(ball.direction.y);
  44. } else {
  45. ball.direction.y = -ball.direction.y;
  46. }
  47. }
  48.  
  49. return {
  50. ball: ball,
  51. bricks: remainingBricks,
  52. collisions: collisions,
  53. score: score
  54. };
  55. }, this.initState())
  56. );
  57. }
  • 用到ticker$流控制球的移动位置
  • 根据当前状态控制下一步的状态,包括计分、球的运动方向、砖块数量

4.game$ 数据流最终的游戏控状态输出流(包括这状态数据、船桨位置数据)

  1. game$ = Observable.create(observer => {
  2. this.breakoutCanvasService.drawIntro();
  3. this.restart = new Subject();
  4. const paddle$ = this.createPaddle$(this.ticker$); // 数据源吐出船桨的位置
  5. const state$ = this.createState$(this.ticker$, paddle$);
  6. this.ticker$
  7. .pipe(
  8. withLatestFrom(paddle$, state$),
  9. OperatorMerge(this.restart)
  10. )
  11. .subscribe(observer); // 这个this.ticker$ 也可以不使用,直接通过merge合并后面两个数据流
  12. });

merge数据流restart$后 可以通过error方法终止流从而控制游戏结束

状态

两个结果状态:砖块数量、分数

两个影响状态的副作用:时间、游戏者的行为

状态交叉点

球接触砖块 -> 砖块消失

球接触船桨/墙 -> 球自然改变运动方向

整个过程用rxjs实现不需要额外保存中间数据,在管道中实现数据的缓存、状态处理 。

两个字形容 “优秀”

文章来自博客RxJS实战练习-经典游戏Breakout欢迎访问交流技术问题。

演示地址:http://tiny.pubuzhixing.com/

github:https://github.com/pubuzhixing8/tiny-game 

出处:《深入浅出RxJS》十四章实例,使用TS+Angular重新包装,修改了一个小缺陷,据说这个游戏最初是由乔布斯和他的一个朋友设计

版权声明:本文为worktile原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/worktile/p/10255454.html