.html
本文是Rxjs 响应式编程-第三章: 构建并发程序这篇文章的学习笔记。前端
示例代码托管在:http://www.github.com/dashnowords/blogsgit
更多博文:《大史住在大前端》原创博文目录github
尽可能避免外部状态编程
在基本的函数式编程中,纯函数能够保障构建出的数据管道获得确切的可预测的结果,响应式编程中有着一样的要求,博文中的示例能够很清楚地看到,当依赖于外部状态时,多个订阅者在观察同一个流时就容易互相影响而引起混乱。canvas
当不一样的流之间出现共享的外部依赖时,通常的实现思路有两种:segmentfault
Subject
来将其和其余逻辑流联系起来。管道的执行效率数组
在上一节中经过compose
运算符组合纯函数就能够看到,容器相关的方法几乎全都是高阶函数,这样的作法就使得管道在构建过程当中并不不会被启用,而是缓存组合在了一块儿(从上一篇的IO容器的示例中就能够看到延缓执行的形式),当它被订阅时才会真正启动。缓存
Subject类
Subject
同时具有Observable
和observer
的功能,可订阅消息,也可产生数据,通常做为流和观察者的代理来使用,能够用来实现流的解耦。为了实现更精细的订阅控制,Subject
还提供了如下几种方法。
AsyncSubject
AsyncSubject
观察的序列完成后它才会发出最后一个值,并永远缓存这个值,以后订阅这个AsyncSubject
的观察者都会马上获得这个值。
BehaviorSubject
Observer在订阅BehaviorSubject
时,它接收最后发出的值,而后接收后续发出的值,通常要求提供一个初始值,观察者接收到的消息就是距离订阅时间最近的那个数据以及流后续产生的数据。
ReplaySubject
ReplaySubject
会缓存它监听的流发出的值,而后将其发送给任何较晚的Observer,它能够经过在构造函数中传入参数来实现缓冲时间长度的设定。
原文中提供了一个很是详细的打飞机游戏的代码,但我仍然建议你在熟悉了其基本原理和思路后本身将它实现出来,而后去和原文中的代码做对比,好搞清楚哪些东西是真的理解了,哪些只是你觉得本身理解了,接着找一些很明显的优化点,继续使用响应式编程的思惟模式来试着实现它们,起初不知道从何下手是很是正常的(固然也多是笔者的自我安慰),但这对于培养响应式编程思惟习惯大有裨益。笔者在本身的实现中又加入了右键切换飞船类型的功能,必须得说开发游戏的确比写业务逻辑要有意思。
因为没有精确计算雪碧图的坐标,因此在碰撞检测时会有一些误差。
关于canvas的尺寸问题
建议经过如下方式来设置:
<!--推荐方式1--> <canvas height="300" width="400"></canvas>
//推荐方式2 canvas = document.getElementById('canvas'); canvas.height = 300; canvas.width = 300;
须要避免的几种方式(都是只改变画板尺寸,不改变画布尺寸,会形成绘图被拉伸):
//1.CSS设置 #mycanvas{ height:300px; width:300px; }
//2.DOM元素API设置 canvas = document.getElementById('canvas'); canvas.style.height = 300; canvas.style.width= 300; //3.Jquery设置 $('#mycanvas').width(300);
同时须要注意canvas的宽高不支持百分比设定。
Rx.Observable.combineLatest
之后总体的流不自动触发了
combineLatest
这个运算符须要等全部的流都emit一次数据之后才会开始emit数据,由于它须要为整合在一块儿的每个流保持一个最新值。因此自动启动的方法也很简单,为那些不容易触发首次数据的流添加一个初始值就能够了,就像笔者在上述实现右键来更换飞船外观时所实现的那样,使用startWith
运算符提供一个初始值后,在鼠标移动时combineLatest
生成的飞船流就会开始生产数据了。另一点须要注意的就是combineLatest
结合在一块儿后,其中任何一个流产生数据都会致使合成后的流产生数据,因为图例数据的坐标是在绘制函数中实现的,因此被动的触发可能会打乱原有流的预期频率,使得一些舞台元素的位置或形状变化更快,这种状况可使用sample( )
运算符对合并后的流进行取样操做来限制数据触发频率。
一段愈来愈快的流
笔者本身在生成敌机的时候,第一次写出这样一段代码:
let enemyShipStream = Rx.Observable.interval(1500) .scan((prev)=>{//敌机信息须要一个数组来记录,因此经过scan运算符将随机出现的敌机信息聚合 prev.push({ shape:[238,178,120,76], x:parseInt(Math.random() * canvas.width,10), y:50 }); return prev },[]) .flatMap((enemies)=>{ return Rx.Observable.interval(40).map(()=>{ enemies.forEach(function (enemy) { enemy.y = enemy.y + 2; }); return enemies; }) });
运行的时候发现敌机的速度变得愈来愈快,很诡异,若是你看不出问题在哪,建议画一下大理石图,看看flatMap
汇聚的总的数据流是如何构成的,就很容易看到随着时间推移,多个流都在操做最初的源数据,因此坐标自增的频率愈来愈快。
限制scan操做符聚合结果的大小
本身写代码时多处使用scan
操做符对产生的数据进行聚合,若是聚合的形式是集合形式的,其所占空间就会随着时间推移愈来愈大,解决的办法就是在scan
操做符接收的回调函数中利用数组的filter
方法对聚合结果进行过滤,生成新的数组并返回,以此来控制聚合结果的大小。
碰撞检测的实现思路
碰撞检测是即时生效的,因此每一帧都须要进行,最终汇总的流每次发射数据时均可以拿到全部待绘制元素的坐标信息,此时便是实现碰撞检测的时机,当检测到碰撞时,只须要在坐标数据中加个标记,而后在最初的scan
的聚合方法中将符合标记的数据清除掉就能够了,检测碰撞的逻辑和碰撞发生后的数据清除以及绘制判断是编写在不一样地方的,在笔者提供的示例中就能够看到。
demo中的
index.html
是学习原文时拷贝的代码,mygame中的代码是笔者写的,有须要的读者自行使用便可。
myspace.js
-星空背景流
/** * 背景 * 扩展思考:如何融入全屏resize事件来自动调整星空 */ //将全屏初始化为画布舞台 let canvas = document.getElementById('canvas'); canvas.height = window.innerHeight; canvas.width = window.innerWidth; canvas.style.backgroundColor = 'black'; let ctx = canvas.getContext('2d'); ctx.fillStyle = '#FFFFFF'; let spaceShipImg = new Image(); spaceShipImg.src = 'plane2.png'; //生成星空 //每一个数据点但愿获得的数据形式是[{x:1,y:1,size:1},{}] let starStream = Rx.Observable.range(1,250) .map(function(data){ return { x:Math.ceil(Math.random()*canvas.width), y:Math.ceil(Math.random()*canvas.height), size: Math.ceil((Math.random()*4)) } }) .toArray() .flatMap(function(stars){ /*此处是默写时的难点,静态生成的数组流须要一直保持 *后续的结果都是在此之上不断累加的 */ return Rx.Observable.interval(40).map(function () { stars.forEach(function (star) { star.y = (star.y+2) % canvas.height; }); return stars; }) }) //绘制星空 function paintStar(stars){ //暴力清屏,若是不清除则上次的星星不会被擦除 ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#FFFFFF'; //绘制星星 stars.forEach(function (star) { ctx.fillRect(star.x, star.y, star.size, star.size); }); }
myship.js
-我方飞船流
/** * 本身的飞船 * 扩展思考:如何实现右键点击时更换飞船类型? */ //鼠标移动流 let mouseMoveStream = Rx.Observable.fromEvent(window, 'mousemove') .distinct() //位置发生变化时触发 .map(function (data) { return { x:data.clientX, y:canvas.height - 100 } }); //飞船类型静态流 let shipTypeStream = Rx.Observable.from([ [0,0,130,90], [135,0,130,100], [265,0,126,100], [0,170,110,100] ]).toArray(); //鼠标右键流-实现类型切换,每次生成一个序号,而后从静态飞船流中拿出图形数据 let mouseRightStream = Rx.Observable.fromEvent(window, 'contextmenu') .map(function (event) { event.preventDefault();//禁止右键弹出菜单 }) .scan(count=>count+1,0)//记录点击次数 .map(count=>count % 4).startWith(0);//将次数转换为飞船类型序号 //鼠标左键流-实现子弹发射 let mouseClickStream = Rx.Observable.fromEvent(canvas, 'click') .sample(200) .scan((prev,cur)=>{ prev.push({ x:cur.clientX, y:canvas.height - 50, used:false //标记是否已经击中某个飞船 }); return prev.filter((bullet)=>{return bullet.y || !bullet.used}); },[]) .startWith([{x:0,y:0}]); //玩家飞船流 let myShipStream = Rx.Observable.combineLatest(mouseMoveStream, shipTypeStream, mouseRightStream, mouseClickStream, function(pos,typeArr,typeIndex,bullets){ return { x:pos.x, y:pos.y, shape:typeArr[typeIndex], bullets:bullets } }); //绘制飞船 function paintMyShip(ship) { //绘制飞船 ctx.drawImage(spaceShipImg,ship.shape[0],ship.shape[1],ship.shape[2],ship.shape[3], ship.x - 50, ship.y, ship.shape[2],ship.shape[3]); //绘制本身子弹 ship.bullets.forEach(function (bullet) { bullet.y = bullet.y - 10; ctx.drawImage(spaceShipImg, ship.shape[0],ship.shape[1],ship.shape[2],ship.shape[3], bullet.x , bullet.y, ship.shape[2] / 4 ,ship.shape[3] / 4); }); }
enemy.js
-敌机流
/** * 敌方飞船 */ //辅助函数-判断是否超出画布范围 function isVisible(obj) { return obj.x > -60 && obj.x < canvas.width + 60 && obj.y > -60 && obj.y < canvas.height + 60; } //每2秒在随机横向位置产生一个敌机 let enemyShipStream = Rx.Observable.interval(2000) .scan((prev)=>{//敌机信息须要一个数组来记录,因此经过scan运算符将随机出现的敌机信息聚合 let newEnemy = { shape:[238,178,120,76], x:parseInt(Math.random() * canvas.width,10), y:50, isDead:false,//标记敌机是否被击中 bullets:[] } //定时生成子弹 Rx.Observable.interval(1500).subscribe(()=>{ if (!newEnemy.isDead) {//被击中的敌人再也不产生子弹 newEnemy.bullets.push({ x: newEnemy.x, y: newEnemy.y }); } newEnemy.bullets = newEnemy.bullets.filter(isVisible); }); prev.push(newEnemy); return prev.filter(isVisible); },[]); //绘制飞船 function paintEnemy(enemies) { enemies.forEach(function (enemy) { //绘制时增量改变敌机坐标 enemy.y = enemy.y + 3; enemy.x = enemy.x + parseInt(Math.random()*8 - 4,10); //绘制时增量改变敌机子弹坐标 enemy.bullets.forEach(function(bullet){bullet.y = bullet.y + 16;}); //若是敌机没挂则绘制飞机 if (!enemy.isDead) { ctx.save(); ctx.translate(enemy.x, enemy.y); ctx.rotate(Math.PI); //绘制敌机 ctx.drawImage(spaceShipImg,enemy.shape[0],enemy.shape[1],enemy.shape[2],enemy.shape[3], 0, 0, enemy.shape[2] * 0.8 ,enemy.shape[3] * 0.8); ctx.restore(); } //绘制子弹 enemy.bullets.forEach(function (bullet) { ctx.save(); ctx.translate(bullet.x, bullet.y); ctx.rotate(Math.PI); ctx.drawImage(spaceShipImg,enemy.shape[0],enemy.shape[1],enemy.shape[2],enemy.shape[3], 0, 0, enemy.shape[2] / 4,enemy.shape[3] / 4); ctx.restore(); }); ctx.restore(); }); }
collision.js
-碰撞检测
// 辅助函数 function isCollision(target1, target2) { return (target1.x > target2.x - 50 && target1.x < target2.x + 50) && (target1.y > target2.y - 20 && target1.y < target2.y + 20); } //碰撞检测方法 function checkCollision(myship, enemies) { let gameOver = false; myship.bullets.forEach(function(bullet) { enemies.forEach(function (enemy) { //检查是否击中了敌机 if (isCollision(bullet, enemy)) { bullet.used = true; enemy.isDead = true; }; //检查是否被击中,被击中则游戏结束 enemy.bullets.forEach(function (enemyBullet) { if (isCollision(myship, enemyBullet)) { gameOver = true; } }) }) }); return gameOver; }
combineAll.js
-融合最终的游戏流
/** * 集合全部流 */ let gameStream = Rx.Observable.combineLatest(starStream, myShipStream, enemyShipStream, function (stars,myship,enemies) { return { stars,myship,enemies } }) .sample(40);//sample函数来规避鼠标移动事件过快触发致使坐标数据更新过快 //绘制全部元素 function paintAll(data) { let isGameOver; isGameOver = checkCollision(data.myship, data.enemies);//检查子弹是否击中敌人 if (!isGameOver) { paintStar(data.stars); paintMyShip(data.myship); paintEnemy(data.enemies); }else{ gameSubscription.dispose(); alert('被击中了'); } } //订阅全部汇总的流来启动游戏 let gameSubscription = gameStream.subscribe(paintAll);