RxJS入门(6)----编写并发程序

接上面的(5)
- Avoid Drinking from the Firehose
- 这儿存在一个问题,就是接受数据太快了。大部分时间我们需要我们能得到的速度,但是依赖于那些Observable流值非常频繁的,我们可能想丢掉一些我们收到的值。现在我们就是这种场景。我们屏幕重绘的速度和Observable的速度应该是成比例的。但结果是我们最快的Observable对我们来说太快了,我们需要在游戏中创建一个持续更新的速度。
- sample是一个Observable的实例方法,它需要一个单位毫秒的time参数,并返回Observable,这个Observable发射最后一个由父Observable在每个time参数内interval的值。
RxJS入门(6)----编写并发程序_第1张图片
- 注意sample是如何丢掉在interval的最后一个值之前的所有值的。这是很重要的当你考虑是否需要这种行为时。实际上我们不关心丢掉的那些值,因为我们仅仅是想重绘每个40毫秒中每个元素现在的状态。如果所所有的值都对你很重要,需要考虑buffer操作符。

Rx.Observable.combineLatest(
StarStream, SpaceShip, Enemies,
function(stars, spaceship, enemies) {
return {
stars: stars,
spaceship: spaceship,
enemies: enemies
};
})
➤ .sample(SPEED)
.subscribe(renderScene);
  • 在combineLatest之后调用sample我们确保combineLatest在某一值之后不会稍后的40毫秒内产生任何值。(我们的常量SPEED是40)。
  • Shooting
  • 看到一大波敌人朝我们过来是很可怕的;我们所能做的就是移动并希望他们考到我们。我们怎样赋予我们的英雄射击敌人飞船的能力。
  • 我们希望我们的飞船可以射击无论何时我们点击鼠标或者按空格键。我们将给每个事件创建一个Observable并把它们合并到一个单个的叫做playerShots的Observable上。注意到我们过滤keydown Observable通过空格键的数值,32:
var playerFiring = Rx.Observable
.merge(
Rx.Observable.fromEvent(canvas, 'click'),
Rx.Observable.fromEvent(canvas, 'keydown')
.filter(function(evt) { return evt.keycode === 32; })
)
  • 现在我们已经知道了sample,我们可以使用它给游戏添加点佐料并限制我们飞船设计的频率。然而,游戏者可以很容易地高速射击并摧毁所有的敌人。我们使得游戏者最多每200毫秒才能进行设计。
var playerFiring = Rx.Observable
.merge(
Rx.Observable.fromEvent(canvas, 'click'),
Rx.Observable.fromEvent(canvas, 'keydown')
.filter(function(evt) { return evt.keycode === 32; }) ) .sample(200) .timestamp();
  • 我们可以看到增加了一个timestamp操作符,它设置我们Observable发射的每个值的确切时间的时间戳属性。之后,我们将用到它。
  • 最后,从我们飞船射击出来的子弹,我们需要知道在开火瞬间飞船的x坐标。这样我们就能在正确的x坐标渲染那个射击了。设置一个外部变量来保存飞船Observable最后一次发射值包含的x坐标是一种尝试,但是这破坏了我们没有变化的外部状态的口头协议。
  • 实际上,通过再次使用我们的好朋友combineLatest,我们将完成这些:
var HeroShots = Rx.Observable
.combineLatest(
playerFiring,
SpaceShip,
function(shotEvents, spaceShip) {
return { x: spaceShip.x };
})
.scan(function(shotArray, shot) {
shotArray.push({x: shot.x, y: HERO_Y});
return shotArray;
}, []);
  • 由于从SpaceShip和playerFiring我们获取更新值,所以我们可以得到我们想要的x坐标。就像在Enemy Observable中一样,我们使用scan,创建一个我们每次射击的的当前坐标的数组。这样,我么就要准备开始在屏幕上绘制我们的射击了。我们使用一个帮助函数来绘制数组中的每一次射击:
var SHOOTING_SPEED = 15;
function paintHeroShots(heroShots) {
heroShots.forEach(function(shot) {
shot.y -= SHOOTING_SPEED;
drawTriangle(shot.x, shot.y, 5, '#ffff00', 'up');
});
}
  • 之后在我们主要的combineLatest操作符中调用paintHeroShots。
Rx.Observable.combineLatest(
StarStream, SpaceShip, Enemies, HeroShots,
function(stars, spaceship, enemies, heroShots) {
return {
stars: stars,
spaceship: spaceship,
enemies: enemies,
➤ heroShots: heroShots
};
})
.sample(SPEED)
.subscribe(renderScene);
  • 在renderScene内部,我们调用paintHeroShots:
function renderScene(actors) {
paintStars(actors.stars);
paintSpaceShip(actors.spaceship.x, actors.spaceship.y);
paintEnemies(actors.enemies);
➤ paintHeroShots(actors.heroShots);
}
  • 现在,当你运行这个游戏的时候,每次移动鼠标你会注意到,我们的飞船发射子弹了。一个还不错的结果,但是这还不是我们想要的。让我们重新看下HeroShots Observable。在其中,我们使用combineLatest来获取来自playerFiring和SpaceShip的值。这看起来和我们之前的问题有点相似。每次鼠标移动HeroShots中的combineLatest就会发射值,这导致射击也被触发了。限制在这种情况下不会有帮助,我们想让使用者在他想射击的时候发射,限制会限制射击的次数并漏掉一些。
  • 当一个Observable发射了一个新的值,combineLatest发射每个Observable发射的最后一个值。我们可以利用这个优势。每当鼠标移动,combineLatest发射最新的SpaceShip位置和最后playerFiring发设置,这将不会改变除非我们新开火了。仅仅当射击提交的值与之前的值不同时我们才发射一个值。distinctUntilChanged操作符将会我们做这些复杂的活。
  • distinct和distinctUntilChanged允许我们过滤出一个Observable已发射值中我们想要的结果。distinct过滤出之前已经发射的任何结果,distinctUntilChanged完全一样的除非一个不同的值发射了。我们仅仅需要确认,新的射击和之前的不一样,因此distinctUntilChanged对我们来说足够了。(它也给我们节省了使用distinct的高内存;distinct需要在内存中保存之前所有的值)
  • 我们调整heroShots到它基于他们的时间戳仅能发射新值。
var HeroShots = Rx.Observable
.combineLatest(
playerFiring,
SpaceShip,
function(shotEvents, spaceShip) {
return {
timestamp: shotEvents.timestamp,
x: spaceShip.x
};
})
.distinctUntilChanged(function(shot) { return shot.timestamp; })
.scan(function(shotArray, shot) {
shotArray.push({ x:shot.x, y: HERO_Y });
return shotArray;
}, []);
  • 如果一切正常,我们将能在我们的飞船上射击敌人!
  • Enemy Shots
  • 我们因该允许敌人也能射击,不然这就是一个极不公平的宇宙了。这很麻烦!对于敌人的射击,我们要如下设计:
  • 1:每个敌人都要维护一个它们自己射击的更新数组。
  • 2:每个敌人都需要在给定的频率下射击。
  • 对于这些,我们将使用interval操作符来存储敌人新射击的值。我们也来介绍一个新的帮助函数,isVisible,它帮助过滤出每一个坐标在可见屏幕之外的元素。现在这就是Enemy Observable看起来:
function isVisible(obj) {
return obj.x > -40 && obj.x < canvas.width + 40 &&
obj.y > -40 && obj.y < canvas.height + 40;
}
var ENEMY_FREQ = 1500;
var ENEMY_SHOOTING_FREQ = 750;
var Enemies = Rx.Observable.interval(ENEMY_FREQ)
.scan(function(enemyArray) {
var enemy = {
x: parseInt(Math.random() * canvas.width),
y: -30,
shots: []
};
Rx.Observable.interval(ENEMY_SHOOTING_FREQ).subscribe(function() {
enemy.shots.push({ x: enemy.x, y: enemy.y });
enemy.shots = enemy.shots.filter(isVisible);
});
enemyArray.push(enemy);
return enemyArray.filter(isVisible);
}, []);
  • 在上面的代码中,每次我们创建一个新的敌人时,我们就创建了一个interval。这个interval将会保持这个添加射击到敌人的射击数组中,之后它过滤掉屏幕之外的。就像我们在return状态里做的,我们使用isVisible过滤掉屏幕之外的敌人。
  • paintEnemies来更新,以便重绘敌人的射击和更新它们的坐标。之后我们的drawTriangle函数重绘那些射击。
function paintEnemies(enemies) {
enemies.forEach(function(enemy) {
enemy.y += 5;
enemy.x += getRandomInt(-15, 15);
drawTriangle(enemy.x, enemy.y, 20, '#00ff00', 'down');
➤ enemy.shots.forEach(function(shot) {
➤ shot.y += SHOOTING_SPEED;
➤ drawTriangle(shot.x, shot.y, 5, '#00ffff', 'down');
➤ });
});
}

现在,每个人都在射击,但是没人能被摧毁。它们简单的划过敌人和我们的飞船因为当射击碰到飞船时,我们并没有定义什么。

  • Managing Collisions
  • 当击中了一个敌人,我们想让射击和敌人都消失。定义一个帮助函数来侦查两个目标碰撞在一起:
function collision(target1, target2) {
return (target1.x > target2.x - 20 && target1.x < target2.x + 20) &&
(target1.y > target2.y - 20 && target1.y < target2.y + 20);
}
  • 现在让我们来修改paintHeroShots帮助函数,来检查是否每个射击击中一个敌人。当一个击中发生的情况下,我们把敌人的一个isDead设置为true来表示击中,并且把坐标设置到屏幕之外。由于被定义到屏幕之外,这个射击将会被过滤掉。
function paintEnemies(enemies) {
enemies.forEach(function(enemy) {
enemy.y += 5;
enemy.x += getRandomInt(-15, 15);
➤ if (!enemy.isDead) {
➤ drawTriangle(enemy.x, enemy.y, 20, '#00ff00', 'down');
➤ }
enemy.shots.forEach(function(shot) {
shot.y += SHOOTING_SPEED;
drawTriangle(shot.x, shot.y, 5, '#00ffff', 'down');
});
});
}
var SHOOTING_SPEED = 15;
function paintHeroShots(heroShots, enemies) {
heroShots.forEach(function(shot, i) {
for (var l=0; l<enemies.length; l++) {
var enemy = enemies[l];
➤ if (!enemy.isDead && collision(shot, enemy)) {
➤ enemy.isDead = true;
➤ shot.x = shot.y = -100;
➤ break;
➤ }
}
shot.y -= SHOOTING_SPEED;
drawTriangle(shot.x, shot.y, 5, '#ffff00', 'up');
});
}
  • 现在让我们去掉那些isDead属性为true的敌人。仅仅需要说明的是:我们需要等待这个特定的敌人的所有射击都消失;否则,当我们击中一个敌人并且它所有的射击消失,这会很怪异的。因此我们需要检查它射击的长度并过滤掉那些没有射击遗留的敌人。
var Enemies = Rx.Observable.interval(ENEMY_FREQ)
.scan(function(enemyArray) {
var enemy = {
x: parseInt(Math.random() * canvas.width),
y: -30,
shots: []
};
Rx.Observable.interval(ENEMY_SHOOTING_FREQ).subscribe(function() {
➤ if (!enemy.isDead) {
➤ enemy.shots.push({ x: enemy.x, y: enemy.y });
➤ }
enemy.shots = enemy.shots.filter(isVisible);
});
enemyArray.push(enemy);
return enemyArray
.filter(isVisible)
➤ .filter(function(enemy) {
➤ return !(enemy.isDead && enemy.shots.length === 0);
➤ });
}, []);
  • 检查是否游戏者的飞船被击中,我们创建一个gameOver函数:
function gameOver(ship, enemies) {
return enemies.some(function(enemy) {
if (collision(ship, enemy)) {
return true;
}
return enemy.shots.some(function(shot) {
return collision(ship, shot);
});
});
}
  • 如果冲敌人出来的射击击中了游戏者的飞船,这个函数返回true。
  • 在移动之前,让我们来认知一个非常有用的操作符:takeWhile。当在一个存在的Observable上调用takewhile时,这个Observable将会一直发射值直到作为takewhile参数的函数返回一个false。
  • 我们可以使用takeWhile来告诉我们主要的combineLatest Observable来保存传递值直到gameOver返回true:
Rx.Observable.combineLatest(
StarStream, SpaceShip, Enemies, HeroShots,
function(stars, spaceship, enemies, heroShots) {
return {
stars: stars,
spaceship: spaceship,
enemies: enemies,
heroShots: heroShots
};
})
.sample(SPEED)
➤ .takeWhile(function(actors) {
➤ return gameOver(actors.spaceship, actors.enemies) === false;
➤ })
.subscribe(renderScene);
  • 当gameOver返回true,combineLatest将会停止发射值,实际上也停止了这个游戏。
  • One Last Thing: Keeping Score
  • 如果我们不把结果告诉朋友,这种游戏将会咋样?我们需要保存我们玩的记录。我们需要一个分数。
  • 让我们用一个简单的帮助函数,在屏幕的左上角画出这个分数:
function paintScore(score) {
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 26px sans-serif';
ctx.fillText('Score: ' + score, 40, 43);
}

为了保存这个分数我们将使用一个Subject。我们可以简单地在我们基于combineLatest游戏的环中使用它,把它当做另外一个Observable,这样我们在我们需要时候保存值。

var ScoreSubject = new Rx.Subject();
var score = ScoreSubject.scan(function (prev, cur) {
return prev + cur;
}, 0).concat(Rx.Observable.return(0));
  • 在上面的代码中,我们使用scan 操作符来累加每一新值到最后的聚合结果中。当游戏开始的时候我们没有分数,所以连接一个返回0的Observable作为我们的开始。
  • 现在每当我们击中了某个敌人时我们就在我们的Subject中保存一个分数;paintHeroShots的改变如下:
var SCORE_INCREASE = 10;
function paintHeroShots(heroShots, enemies) {
heroShots.forEach(function(shot, i) {
for (var l=0; l<enemies.length; l++) {
var enemy = enemies[l];
if (!enemy.isDead && collision(shot, enemy)) {
➤ ScoreSubject.onNext(SCORE_INCREASE);
enemy.isDead = true;
shot.x = shot.y = -100;
break;
}
}
shot.y -= SHOOTING_SPEED;
drawTriangle(shot.x, shot.y, 5, '#ffff00', 'up');
});
}
  • 当然,我们添加一个paintScore到renderScene中以便在屏幕上出现这个分数:
function renderScene(actors) {
paintStars(actors.stars);
paintSpaceShip(actors.spaceship.x, actors.spaceship.y);
paintEnemies(actors.enemies);
paintHeroShots(actors.heroShots, actors.enemies);
➤ paintScore(actors.score);
}
  • 上面完全完成了我们的飞船交互游戏。在两百行代码中,我们成功的设计了一个浏览器的完整游戏,通过强有力的Observable pipeline避免了任何的外部的状态。
  • Ideas for Improvements
  • 我可以肯定你已经有了一些制作这个游戏的更有趣的想法,但是让我提一些可以让这个游戏好的的建议,并同时使你的RxJs技能更犀利:
  • 1:增加一个第二(或者第三)星域,它以一个不同的速度移动来创建视觉差的影响。这可以用击中不同的方式解决。尝试着使用已经存在的代码并使用它像你的格言一样。
  • 2:确保不可预测的敌人,它们开火在一个随机的intervals而不是固定的指定 ENEMY_SHOOTING_FREQ速度。而为指出,当游戏者得分越高,它们开火越快。
  • 3:允许游戏者获取跟多分,当其中在短期内击中若干敌人的时候。

你可能感兴趣的:(rxjs)