博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
玩游戏,学RxJS
阅读量:7167 次
发布时间:2019-06-29

本文共 9392 字,大约阅读时间需要 31 分钟。

前一篇简单的介绍了RxJS并不过瘾,对于新同学来讲多多少少还是有点疑惑,没有案例的支持并不能很好的理解他。受codeopen.io的启发,借助上面的小游戏《打转块》来学习下RxJS相关api来加深理解,先看下最后的效果

jdfw

搭环境

RxJS可以在客户端用也可以在服务器端用,安装的方式有很多种(在此)。我这里习惯了工程化的方式,就以的方式来启动这个项目,源码链接放在了文章底部

webpack的配置大概是这样:

module.exports = {    entry: {        "app": "./src/index.js"    },    output: {        filename: '[name].boundle.js',        path: path.resolve(__dirname,'../dist'),        publicPath:'/'    },    module: {        rules: [            {                test: /\.scss$/,                use: [                    'css-loader',                    'sass-loader'                ]            },            {                test: /\.js$/,                exclude: /node_modules/,                use: [                    'babel-loader'                ]            }        ]    },    plugins: [        new CleanWebpackPlugin('../dist'),        new HtmlWebpackPlugin({            title:'A game intro to RxJS',            template: 'src/index.html'        })    ]}

然后运行:

npm start

image

在没有错误的情况下就说明环境是没问题的。打开浏览器输入http://localhost:8000

这个时候浏览器是一片空白
好了,环境搞定。现在正式开始

编码

Step1

先定义一个画布

然后导入Rx包和样式文件

import Rx from 'rxjs/Rx'import './style.scss'

Step2

现在我们定义画布上元素的相关属性

// 获取canvas对象const canvas = document.getElementById('stage');// 创建2D的运行环境const context = canvas.getContext('2d');// 给画布上色context.fillStyle = 'pink';// 桨const PADDLE_WIDTH = 100;const PADDLE_HEIGHT = 20;// 定义球的大小const BALL_RADIUS = 10;// 定义砖块const BRICK_ROWS = 5;const BRICK_COLUMNS = 7;const BRICK_HEIGHT = 20;const BRICK_GAP = 3;

接着把各种元素给画上去,如砖块,球等

function drawTitle() {    context.textAlign = 'center';    context.font = '24px Courier New';    context.fillText('rxjs breakout', canvas.width / 2, canvas.height / 2 - 24);}function drawControls() {    context.textAlign = 'center';    context.font = '16px Courier New';    context.fillText('press [<] and [>] to play', canvas.width / 2, canvas.height / 2);}function drawGameOver(text) {    context.clearRect(canvas.width / 4, canvas.height / 3, canvas.width / 2, canvas.height / 3);    context.textAlign = 'center';    context.font = '24px Courier New';    context.fillText(text, canvas.width / 2, canvas.height / 2);}function drawAuthor() {    context.textAlign = 'center';    context.font = '16px Courier New';    context.fillText('by Manuel Wieser', canvas.width / 2, canvas.height / 2 + 24);}function drawScore(score) {    context.textAlign = 'left';    context.font = '16px Courier New';    context.fillText(score, BRICK_GAP, 16);}function drawPaddle(position) {    context.beginPath();    context.rect(        position - PADDLE_WIDTH / 2,        context.canvas.height - PADDLE_HEIGHT,        PADDLE_WIDTH,        PADDLE_HEIGHT    );    context.fill();    context.closePath();}function drawBall(ball) {    context.beginPath();    context.arc(ball.position.x, ball.position.y, BALL_RADIUS, 0, Math.PI * 2);    context.fill();    context.closePath();}function drawBrick(brick) {    context.beginPath();    context.rect(        brick.x - brick.width / 2,        brick.y - brick.height / 2,        brick.width,        brick.height    );    context.fill();    context.closePath();}function drawBricks(bricks) {    bricks.forEach((brick) => drawBrick(brick));}

好,现在我们已经准备好了场景里面静态元素,接下来的动态的事情会让RxJS来替我们完成

Step3

这个弹方块多少都玩过,小球在发生碰撞的时候是有声音的。那么就需要创建一个音效的可观察对象Observable。声音的创建采用HTML5AudioContextAPI,Observable通过Subject来创建,Subject是一个特殊的Observable它继承于Observable,它和Observable最大的区别就是他可以多路传播共享一个可观察环境。小球会在多个地方发生碰撞,每次碰撞都需要发一次不同的声音表示碰撞的不同区域,那么我们需要完全手动控制next()方法去触发声音,这样的场景用Subject来创建可观察对象再合适不过了。

onst audio = new (window.AudioContext || window.webkitAudioContext)();const beeper = new Rx.Subject();beeper.subscribe((key) => {    let oscillator = audio.createOscillator();    oscillator.connect(audio.destination);    // 设置音频影调    oscillator.type = 'square';    // https://en.wikipedia.org/wiki/Piano_key_frequencies    // 设置音频频率    oscillator.frequency.value = Math.pow(2, (key - 49) / 12) * 440;    oscillator.start();    oscillator.stop(audio.currentTime + 0.100);});

这样在需要发声的地方执行beeper.next(value)(注:在老的版本onNext已经替换为next)碰撞的声音就有了

那么接下来就该创建动画,绑定键盘事件,做碰撞检测等等。最早我们创建逐帧动画是用setInterval后来有了requsetAnimation,在RxJS中做逐帧动画需要用到Scheduler(调度)。

调度器的作用就是可以让你规定 Observable 在什么样的执行上下文中发送通知给它的观察者。结合interval操作符就能实现平滑的帧动画

const TICKER_INTERVAL = 17;const ticker$ = Rx.Observable    .interval(TICKER_INTERVAL, Rx.Scheduler.requestAnimationFrame)    .map(() => ({        time: Date.now(),        deltaTime:     }))    .scan(        (previous, current) => ({            time: current.time,            deltaTime: (current.time - previous.time) / 1000        })    );

一般在变量后添加$表示Observable对象。然后绑定键盘事件

const PADDLE_SPEED = 240;const PADDLE_KEYS = {    left: 37,    right: 39};const input$ = Rx.Observable    .merge(        Rx.Observable.fromEvent(document, 'keydown', event => {            switch (event.keyCode) {                case PADDLE_KEYS.left:                    return -1;                case PADDLE_KEYS.right:                    return 1;                default:                    return 0;            }        }),        Rx.Observable.fromEvent(document, 'keyup', event => 0)    )    .distinctUntilChanged();const paddle$ = ticker$    .withLatestFrom(input$)    .scan((position, [ticker, direction]) => {        let next = position + direction * ticker.deltaTime * PADDLE_SPEED;        return Math.max(Math.min(next, canvas.width - PADDLE_WIDTH / 2), PADDLE_WIDTH / 2);    }, canvas.width / 2)    .distinctUntilChanged();

同时绑定了keydownkeyup事件,按左方向键返回-1,按右方向键返回1,松开则返回0,最后通过distinctUntilChanged把结果进行比对输出。相比传统addEventListener代码优雅了不少。withLatestFrom获取球拍的实时坐标,然后通过scan进行过度对坐标做累加或累减操作。

接下来就是做小球的动画,小球的动画要复杂一些,需要做球与球拍,球与砖块,球与墙体的碰撞检测。碰撞检测的原理也很简单主要是比对物体之间的坐标

const BALL_SPEED = 60;const INITIAL_OBJECTS = {    ball: {        position: {            x: canvas.width / 2,            y: canvas.height / 2        },        direction: {            x: 2,            y: 2        }    },    bricks: factory(),    score: 0};// 球与球拍的碰撞检测function hit(paddle, ball) {    return ball.position.x > paddle - PADDLE_WIDTH / 2        && ball.position.x < paddle + PADDLE_WIDTH / 2        && ball.position.y > canvas.height - PADDLE_HEIGHT - BALL_RADIUS / 2;}const objects$ = ticker$    .withLatestFrom(paddle$)    .scan(({ball, bricks, collisions, score}, [ticker, paddle]) => {        let survivors = [];        collisions = {            paddle: false,            floor: false,            wall: false,            ceiling: false,            brick: false        };        ball.position.x = ball.position.x + ball.direction.x * ticker.deltaTime * BALL_SPEED;        ball.position.y = ball.position.y + ball.direction.y * ticker.deltaTime * BALL_SPEED;        bricks.forEach((brick) => {            if (!collision(brick, ball)) {                survivors.push(brick);            } else {                collisions.brick = true;                score = score + 10;            }        });        collisions.paddle = hit(paddle, ball);        if (ball.position.x < BALL_RADIUS || ball.position.x > canvas.width - BALL_RADIUS) {            ball.direction.x = -ball.direction.x;            collisions.wall = true;        }        collisions.ceiling = ball.position.y < BALL_RADIUS;        if (collisions.brick || collisions.paddle || collisions.ceiling ) {            ball.direction.y = -ball.direction.y;        }        return {            ball: ball,            bricks: survivors,            collisions: collisions,            score: score        };    }, INITIAL_OBJECTS);

小球与砖块

function factory() {    let width = (canvas.width - BRICK_GAP - BRICK_GAP * BRICK_COLUMNS) / BRICK_COLUMNS;    let bricks = [];    for (let i = 0; i < BRICK_ROWS; i++) {        for (let j = 0; j < BRICK_COLUMNS; j++) {            bricks.push({                x: j * (width + BRICK_GAP) + width / 2 + BRICK_GAP,                y: i * (BRICK_HEIGHT + BRICK_GAP) + BRICK_HEIGHT / 2 + BRICK_GAP + 20,                width: width,                height: BRICK_HEIGHT            });        }    }    return bricks;}//小球与砖块的碰撞检测function collision(brick, ball) {    return ball.position.x + ball.direction.x > brick.x - brick.width / 2        && ball.position.x + ball.direction.x < brick.x + brick.width / 2        && ball.position.y + ball.direction.y > brick.y - brick.height / 2        && ball.position.y + ball.direction.y < brick.y + brick.height / 2;}

基本工作已经做完,剩下的就是绘制场景让游戏跑起来。

drawTitle();drawControls();drawAuthor();function update([ticker, paddle, objects]) {    context.clearRect(0, 0, canvas.width, canvas.height);    drawPaddle(paddle);    drawBall(objects.ball);    drawBricks(objects.bricks);    drawScore(objects.score);    if (objects.ball.position.y > canvas.height - BALL_RADIUS) {        beeper.next(28);        drawGameOver('GAME OVER');        game.unsubscribe();    }    if (!objects.bricks.length) {        beeper.next(52);        drawGameOver('CONGRATULATIONS');        game.unsubscribe();    }    if (objects.collisions.paddle) beeper.next(40);    if (objects.collisions.wall || objects.collisions.ceiling) beeper.next(45);    if (objects.collisions.brick) beeper.next(47 + Math.floor(objects.ball.position.y % 12));}const game = Rx.Observable    .combineLatest(ticker$, paddle$, objects$)    .subscribe(update);

通过combineLatest组合 ticker$,paddle$

objects$三个Observable,他们的输出是一个数组。通过.subscribe集中处理我们小球的运动逻辑。每次动画重绘canvas区域,碰撞不同的区域触发beeper.next()发出不同的声音。
github源码:

总结

RxJS完全避免了异步回掉问题代码的可读性变得更强,当然,RxJS是可异步可同步的

可以更好的实现模块化,代码的复用度变得更高
学习RxJS做动画是一条捷径

转载地址:http://jkhwm.baihongyu.com/

你可能感兴趣的文章
一段代码的时间复杂度计算
查看>>
类似系统alerview 的弹出框动画
查看>>
Xocde插件实效的解决办法
查看>>
Oracle Study--Oracle SQL执行计划查看(MindMap)
查看>>
谷歌翻译及谷歌加一工具
查看>>
LAMP架构介绍Mysql安装
查看>>
redmine支持的第三方SCM以及说明
查看>>
IDEA常用快捷键
查看>>
单元测试实施解惑(一)— 无缝整合
查看>>
Linux KVM总结
查看>>
百万级数据库记录下的Mysql快速分页优化实例
查看>>
Nginx主要功能
查看>>
DispatcherTimer 当前时间
查看>>
python 判断变量类型
查看>>
what is Edge Note of MapR
查看>>
领课教育—在线教育系统本地部署运行|windows&Eclipse
查看>>
vcenter6.7升级到vcenter6.7U1,Esxi6.7升级到Esxi6.7U1
查看>>
SpringMVC中的ContextLoaderListener设计困惑
查看>>
如何判断当前主机是物理机还是虚拟机?
查看>>
我的友情链接
查看>>