【响应式编程的思维艺术】 (4)从打飞机游戏理解并发与流的融合 (2)

笔者自己在生成敌机的时候,第一次写出这样一段代码:

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说明

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); }); }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wspdxg.html