使用Bacon.js构建吃豆子游戏
JavaScript包含异步编程。 这可能是带来“回调地狱”概念的祝福和诅咒。 有一些实用程序库可以处理组织异步代码,例如Async.js ,但是仍然很难有效地遵循控制流程和有关异步代码的原因。
在本文中,我将向您介绍反应性编程的概念,该反应性通过使用称为Bacon.js的库来帮助处理JavaScript的异步特性。
让我们活跃起来
反应式编程是关于异步数据流的。 它将Iterator Pattern替换为Observable Pattern。 这与命令式编程不同,在命令式编程中,您主动遍历数据以处理内容。 在反应式编程中,您订阅数据并异步响应事件。
Bart De Smet在这次演讲中解释了这一转变。 AndréStaltz在本文中深入介绍了反应式编程。
一旦您变得反应灵敏,一切都会变成异步数据流:服务器上的数据库,鼠标事件,承诺和服务器请求。 这样可以避免所谓的“回调地狱”,并为您提供更好的错误处理。 这种方法的另一个强大功能是能够将流组合在一起,从而为您提供了强大的控制能力和灵活性。 Jafar Husain在这次演讲中解释了这些概念。
Bacon.js是一个反应式编程库,它是RxJS的替代品 。 在下一节中,我们将使用Bacon.js来构建著名游戏“吃豆人”的版本。
设置项目
要安装Bacon.js,可以通过在CLI上运行以下命令来使用Bower :
$ bower install bacon
安装库之后,就可以开始进行反应了。
PacmanGame API和UnicodeTiles.js
在外观上,我将使用基于文本的系统,这样我就不必处理资产和精灵。 为了避免自己创建自己,我将使用一个很棒的库UnicodeTiles.js 。
首先,我建立了一个名为PacmanGame
的类,该类处理游戏逻辑。 以下是其提供的方法:
-
PacmanGame(parent)
:创建一个Pacman游戏对象 -
start()
:开始游戏 -
tick()
:更新游戏逻辑,渲染游戏 -
spawnGhost(color)
:产生一个新的幽灵 -
updateGhosts()
:更新游戏中的每个幽灵 -
movePacman(p1V)
:沿指定方向移动吃豆人
此外,它还公开了以下回调:
-
onPacmanMove(moveV)
:如果存在,则当用户通过按键要求Pacman移动时调用
因此,要使用此API,我们将start
游戏,定期调用spawnGhost
产生spawnGhost
,侦听onPacmanMove
回调,并在发生这种情况时调用movePacman
实际移动Pacman。 我们还定期调用updateGhosts
来更新幻影运动。 最后,我们定期调用tick
来更新更改。 重要的是,我们将使用Bacon.js帮助我们处理事件。
在开始之前,让我们创建游戏对象:
var game = new PacmanGame(parentDiv);
我们创建一个新的PacmanGame
并传递一个父DOM对象parentDiv
并将游戏渲染到该对象中。 现在我们准备好构建我们的游戏了。
EventStreams或Observables
事件流是可观察的,您可以订阅该事件流以异步观察事件。 使用这三种方法可以观察到三种类型的事件:
-
observable.onValue(f)
:侦听值事件,这是处理事件的最简单方法。 -
observable.onError(f)
:侦听错误事件,对于处理流中的错误很有用。 -
observable.onEnd(f)
:侦听流已结束并且没有移动值的事件。
创建流
既然我们已经了解了事件流的基本用法,让我们看看如何创建事件流。 Bacon.js提供了几种方法 ,可用于从jQuery事件,Ajax承诺,DOM EventTarget,简单回调甚至数组创建事件流。
关于事件流的另一个有用的概念是时间的概念。 也就是说,事件可能会在将来的某个时间发生。 例如,这些方法创建事件流,这些事件流在某个时间间隔传递事件:
-
Bacon.interval(interval, value)
:以给定间隔无限重复该value
。 -
Bacon.repeatedly(interval, values)
:无限期地重复给定间隔的values
。 -
Bacon.later(delay, value)
:在给定的delay
之后产生value
。
为了获得更多控制,您可以使用Bacon.fromBinder()
滚动自己的事件流。 我们将通过创建一个moveStream
变量在游戏中展示这一点,该变量为我们的Pacman动作生成事件。
var moveStream = Bacon.fromBinder(function(sink) {game.onPacmanMove = function(moveV) {sink(moveV);};
});
我们可以使用将发送事件的值调用接收sink
,观察者可以监听该值。 sink
的调用在我们的onPacmanMove
回调中–也就是说,只要用户按下某个键来请求Pacman移动,便会发生。 因此,我们创建了一个可观察对象,该事件发出有关Pacman移动请求的事件。
注意,我们用一个简单的值moveV
调用了接收sink
。 这将推入带有moveV
值的移动事件。 我们还可以推送诸如Bacon.Error
或Bacon.End
类的事件。
让我们创建另一个事件流。 这次,我们要发出通知以生成幻影的事件。 我们将spawnStream
创建一个spawnStream
变量:
var spawnStream = Bacon.sequentially(800, [PacmanGame.GhostColors.ORANGE,PacmanGame.GhostColors.BLUE,PacmanGame.GhostColors.GREEN,PacmanGame.GhostColors.PURPLE,PacmanGame.GhostColors.WHITE,
]).delay(2500);
Bacon.sequentially()
创建一个以给定间隔传递values
的流。 在我们的案例中,它将每800毫秒产生一次幻影颜色。 我们还调用了delay()
方法。 它延迟了流,因此事件将在2.5秒的延迟后开始发出。
事件流和大理石图上的方法
在本节中,我将列出一些可用于事件流的其他有用方法:
-
observable.map(f)
:映射值并返回新的事件流。 -
observable.filter(f)
:过滤具有给定谓词的值。 -
observable.takeWhile(f)
:在给定谓词为true时进行。 -
observable.skip(n)
:从流中跳过前n
元素。 -
observable.throttle(delay)
:将流限制一些delay
。 -
observable.debounce(delay)
:通过某种delay
流。 -
observable.scan(seed, f)
使用给定的种子值和累加器功能扫描流。 这将流减少到单个值。
有关事件流的更多方法,请参见官方文档页面 。 throttle
和debounce
之间的区别可以通过大理石图看出:
// `source` is an event stream.
//
var throttled = source.throttle(2);// source: asdf----asdf----
// throttled: --s--f----s--f--var debounced = source.debounce(2);// source: asdf----asdf----
// source.debounce(2): -----f-------f--
如您所见, throttle
像往常一样节流事件,而debounce
仅在给定的“安静时间”之后才发出事件。
这些实用程序方法简单但功能强大,能够概念化并控制流,从而控制其中的数据。 我建议观看有关Netflix如何利用这些简单方法创建自动填充框的演讲 。
观察事件流
到目前为止,我们已经创建并处理了事件流,现在我们将通过订阅事件流来观察事件。
回忆一下我们之前创建的moveStream
和spawnStream
。 现在让我们同时订阅它们:
moveStream.onValue(function(moveV) {game.movePacman(moveV);
});spawnStream.onValue(function(ghost) {game.spawnGhost(ghost);
});
尽管可以使用stream.subscribe()来订阅流,但是也可以使用stream.onValue() 。 不同的是, subscribe
将同时发射三种类型,我们以前见过的事件,而onValue
只会发出那些类型的事件Bacon.Next
。 那就是它将省略Bacon.Error
和Bacon.End
事件。
当事件到达spawnStream
(每800毫秒发生一次),其值将是幻影颜色之一,我们使用该颜色生成幻影。 当事件到达moveStream
,请记住,这是在用户按下键以移动Pacman时发生的。 我们将game.movePacman
的方向moveV
: moveV
,因此Pacman会移动。
结合事件流和Bacon.Bus
您可以组合事件流以创建其他流。 组合事件流的方法有很多,以下是其中几种:
-
BaconbineAsArray(streams)
:BaconbineAsArray(streams)
事件流,因此结果流将具有值数组。 -
Bacon.zipAsArray(streams)
:将流Bacon.zipAsArray(streams)
为新流。 每个流中的事件成对组合。 -
BaconbineTemplate(template)
:使用模板对象组合事件流。
我们来看一个BaconbineTemplate
的示例:
var password, username, firstname, lastname; // <- event streams
var loginInfo = BaconbineTemplate({magicNumber: 3,userid: username,passwd: password,name: { first: firstname, last: lastname }
});
如您所见,我们使用模板将事件流(即password
, username
, firstname
和lastname
组合为名为loginInfo
的组合事件流。 每当事件流获得事件时, loginInfo
流将发出事件,将所有其他模板组合为一个模板对象。
Bacon.js还有另一种组合流的方式,即Bacon.Bus()
。 Bacon.Bus()
是事件流,允许您将值推送到流中。 它还允许将其他流插入总线。 我们将使用它来构建游戏的最后一部分:
var ghostStream = Bacon.interval(1000, 0);ghostStream.subscribe(function() {game.updateGhosts();
});var combinedTickStream = new Bacon.Bus();combinedTickStream.plug(moveStream);
combinedTickStream.plug(ghostStream);combinedTickStream.subscribe(function() {game.tick();
});
现在我们创建另一个流-在ghostStream
,使用Bacon.interval
。 该流将每1秒发射0。 这次我们subscribe
它并调用game.updateGhosts
来移动幽灵。 这是每1秒移动一次幽灵。 注意注释掉game.tick
,并记住其他game.tick
我们moveStream
? 这两个流都更新了游戏,最后调用game.tick
来呈现更改,因此,我们可以在每个流中生成第三流(这两个流的组合),而不是在每个流中调用game.tick
,并在合并的流中调用game.tick
流。
为了合并流,我们可以使用Bacon.Bus
。 那就是我们游戏中的最终事件流,我们称其为combinedTickStream
。 然后我们plug
moveStream
和ghostStream
插入其中,最后subscribe
它并在其中调用game.tick
。
就是这样,我们完成了。 剩下要做的就是用game.start();
开始游戏game.start();
。
Bacon.Property
及更多示例
Bacon.Property是一种反应性。 考虑一个反应性属性,它是一个数组的总和。 当我们向数组添加元素时,反应性属性将做出反应并进行自我更新。 要使用Bacon.Property
,您可以订阅它并侦听更改,也可以使用property.assign(obj,method)方法,该method
在属性更改时调用给定object
的method
。 这是一个如何使用Bacon.Property
:
var source = Bacon.sequentially(1000, [1, 2, 3, 4]);var reactiveValue = source.scan(0, function(a, b) {return a + b;
});// 0 + 1 = 1
// 1 + 2 = 3
// 3 + 3 = 6
// 6 + 4 = 10
首先,我们创建一个事件流,该事件流以1秒的间隔产生给定数组(1、2、3和4)的值,然后创建一个响应性属性,该属性是scan
的结果。 这将分配为1,3,6,和10个值reactiveValue
。
了解更多和现场演示
在本文中,我们通过构建Pacman游戏介绍了Bacon.js的反应式编程。 它简化了我们的游戏设计,并通过事件流的概念为我们提供了更多的控制和灵活性。 完整的源代码可在GitHub上找到 ,并在此处提供实时演示。
以下是一些更有用的链接:
- Bacon.js API参考
- Bacon.js的视频介绍
- RxJS网站
- Highland.js高级流库
- Bodil Stokke为敏锐的Hispter设计的反应式游戏程序
From: /
使用Bacon.js构建吃豆子游戏
JavaScript包含异步编程。 这可能是带来“回调地狱”概念的祝福和诅咒。 有一些实用程序库可以处理组织异步代码,例如Async.js ,但是仍然很难有效地遵循控制流程和有关异步代码的原因。
在本文中,我将向您介绍反应性编程的概念,该反应性通过使用称为Bacon.js的库来帮助处理JavaScript的异步特性。
让我们活跃起来
反应式编程是关于异步数据流的。 它将Iterator Pattern替换为Observable Pattern。 这与命令式编程不同,在命令式编程中,您主动遍历数据以处理内容。 在反应式编程中,您订阅数据并异步响应事件。
Bart De Smet在这次演讲中解释了这一转变。 AndréStaltz在本文中深入介绍了反应式编程。
一旦您变得反应灵敏,一切都会变成异步数据流:服务器上的数据库,鼠标事件,承诺和服务器请求。 这样可以避免所谓的“回调地狱”,并为您提供更好的错误处理。 这种方法的另一个强大功能是能够将流组合在一起,从而为您提供了强大的控制能力和灵活性。 Jafar Husain在这次演讲中解释了这些概念。
Bacon.js是一个反应式编程库,它是RxJS的替代品 。 在下一节中,我们将使用Bacon.js来构建著名游戏“吃豆人”的版本。
设置项目
要安装Bacon.js,可以通过在CLI上运行以下命令来使用Bower :
$ bower install bacon
安装库之后,就可以开始进行反应了。
PacmanGame API和UnicodeTiles.js
在外观上,我将使用基于文本的系统,这样我就不必处理资产和精灵。 为了避免自己创建自己,我将使用一个很棒的库UnicodeTiles.js 。
首先,我建立了一个名为PacmanGame
的类,该类处理游戏逻辑。 以下是其提供的方法:
-
PacmanGame(parent)
:创建一个Pacman游戏对象 -
start()
:开始游戏 -
tick()
:更新游戏逻辑,渲染游戏 -
spawnGhost(color)
:产生一个新的幽灵 -
updateGhosts()
:更新游戏中的每个幽灵 -
movePacman(p1V)
:沿指定方向移动吃豆人
此外,它还公开了以下回调:
-
onPacmanMove(moveV)
:如果存在,则当用户通过按键要求Pacman移动时调用
因此,要使用此API,我们将start
游戏,定期调用spawnGhost
产生spawnGhost
,侦听onPacmanMove
回调,并在发生这种情况时调用movePacman
实际移动Pacman。 我们还定期调用updateGhosts
来更新幻影运动。 最后,我们定期调用tick
来更新更改。 重要的是,我们将使用Bacon.js帮助我们处理事件。
在开始之前,让我们创建游戏对象:
var game = new PacmanGame(parentDiv);
我们创建一个新的PacmanGame
并传递一个父DOM对象parentDiv
并将游戏渲染到该对象中。 现在我们准备好构建我们的游戏了。
EventStreams或Observables
事件流是可观察的,您可以订阅该事件流以异步观察事件。 使用这三种方法可以观察到三种类型的事件:
-
observable.onValue(f)
:侦听值事件,这是处理事件的最简单方法。 -
observable.onError(f)
:侦听错误事件,对于处理流中的错误很有用。 -
observable.onEnd(f)
:侦听流已结束并且没有移动值的事件。
创建流
既然我们已经了解了事件流的基本用法,让我们看看如何创建事件流。 Bacon.js提供了几种方法 ,可用于从jQuery事件,Ajax承诺,DOM EventTarget,简单回调甚至数组创建事件流。
关于事件流的另一个有用的概念是时间的概念。 也就是说,事件可能会在将来的某个时间发生。 例如,这些方法创建事件流,这些事件流在某个时间间隔传递事件:
-
Bacon.interval(interval, value)
:以给定间隔无限重复该value
。 -
Bacon.repeatedly(interval, values)
:无限期地重复给定间隔的values
。 -
Bacon.later(delay, value)
:在给定的delay
之后产生value
。
为了获得更多控制,您可以使用Bacon.fromBinder()
滚动自己的事件流。 我们将通过创建一个moveStream
变量在游戏中展示这一点,该变量为我们的Pacman动作生成事件。
var moveStream = Bacon.fromBinder(function(sink) {game.onPacmanMove = function(moveV) {sink(moveV);};
});
我们可以使用将发送事件的值调用接收sink
,观察者可以监听该值。 sink
的调用在我们的onPacmanMove
回调中–也就是说,只要用户按下某个键来请求Pacman移动,便会发生。 因此,我们创建了一个可观察对象,该事件发出有关Pacman移动请求的事件。
注意,我们用一个简单的值moveV
调用了接收sink
。 这将推入带有moveV
值的移动事件。 我们还可以推送诸如Bacon.Error
或Bacon.End
类的事件。
让我们创建另一个事件流。 这次,我们要发出通知以生成幻影的事件。 我们将spawnStream
创建一个spawnStream
变量:
var spawnStream = Bacon.sequentially(800, [PacmanGame.GhostColors.ORANGE,PacmanGame.GhostColors.BLUE,PacmanGame.GhostColors.GREEN,PacmanGame.GhostColors.PURPLE,PacmanGame.GhostColors.WHITE,
]).delay(2500);
Bacon.sequentially()
创建一个以给定间隔传递values
的流。 在我们的案例中,它将每800毫秒产生一次幻影颜色。 我们还调用了delay()
方法。 它延迟了流,因此事件将在2.5秒的延迟后开始发出。
事件流和大理石图上的方法
在本节中,我将列出一些可用于事件流的其他有用方法:
-
observable.map(f)
:映射值并返回新的事件流。 -
observable.filter(f)
:过滤具有给定谓词的值。 -
observable.takeWhile(f)
:在给定谓词为true时进行。 -
observable.skip(n)
:从流中跳过前n
元素。 -
observable.throttle(delay)
:将流限制一些delay
。 -
observable.debounce(delay)
:通过某种delay
流。 -
observable.scan(seed, f)
使用给定的种子值和累加器功能扫描流。 这将流减少到单个值。
有关事件流的更多方法,请参见官方文档页面 。 throttle
和debounce
之间的区别可以通过大理石图看出:
// `source` is an event stream.
//
var throttled = source.throttle(2);// source: asdf----asdf----
// throttled: --s--f----s--f--var debounced = source.debounce(2);// source: asdf----asdf----
// source.debounce(2): -----f-------f--
如您所见, throttle
像往常一样节流事件,而debounce
仅在给定的“安静时间”之后才发出事件。
这些实用程序方法简单但功能强大,能够概念化并控制流,从而控制其中的数据。 我建议观看有关Netflix如何利用这些简单方法创建自动填充框的演讲 。
观察事件流
到目前为止,我们已经创建并处理了事件流,现在我们将通过订阅事件流来观察事件。
回忆一下我们之前创建的moveStream
和spawnStream
。 现在让我们同时订阅它们:
moveStream.onValue(function(moveV) {game.movePacman(moveV);
});spawnStream.onValue(function(ghost) {game.spawnGhost(ghost);
});
尽管可以使用stream.subscribe()来订阅流,但是也可以使用stream.onValue() 。 不同的是, subscribe
将同时发射三种类型,我们以前见过的事件,而onValue
只会发出那些类型的事件Bacon.Next
。 那就是它将省略Bacon.Error
和Bacon.End
事件。
当事件到达spawnStream
(每800毫秒发生一次),其值将是幻影颜色之一,我们使用该颜色生成幻影。 当事件到达moveStream
,请记住,这是在用户按下键以移动Pacman时发生的。 我们将game.movePacman
的方向moveV
: moveV
,因此Pacman会移动。
结合事件流和Bacon.Bus
您可以组合事件流以创建其他流。 组合事件流的方法有很多,以下是其中几种:
-
BaconbineAsArray(streams)
:BaconbineAsArray(streams)
事件流,因此结果流将具有值数组。 -
Bacon.zipAsArray(streams)
:将流Bacon.zipAsArray(streams)
为新流。 每个流中的事件成对组合。 -
BaconbineTemplate(template)
:使用模板对象组合事件流。
我们来看一个BaconbineTemplate
的示例:
var password, username, firstname, lastname; // <- event streams
var loginInfo = BaconbineTemplate({magicNumber: 3,userid: username,passwd: password,name: { first: firstname, last: lastname }
});
如您所见,我们使用模板将事件流(即password
, username
, firstname
和lastname
组合为名为loginInfo
的组合事件流。 每当事件流获得事件时, loginInfo
流将发出事件,将所有其他模板组合为一个模板对象。
Bacon.js还有另一种组合流的方式,即Bacon.Bus()
。 Bacon.Bus()
是事件流,允许您将值推送到流中。 它还允许将其他流插入总线。 我们将使用它来构建游戏的最后一部分:
var ghostStream = Bacon.interval(1000, 0);ghostStream.subscribe(function() {game.updateGhosts();
});var combinedTickStream = new Bacon.Bus();combinedTickStream.plug(moveStream);
combinedTickStream.plug(ghostStream);combinedTickStream.subscribe(function() {game.tick();
});
现在我们创建另一个流-在ghostStream
,使用Bacon.interval
。 该流将每1秒发射0。 这次我们subscribe
它并调用game.updateGhosts
来移动幽灵。 这是每1秒移动一次幽灵。 注意注释掉game.tick
,并记住其他game.tick
我们moveStream
? 这两个流都更新了游戏,最后调用game.tick
来呈现更改,因此,我们可以在每个流中生成第三流(这两个流的组合),而不是在每个流中调用game.tick
,并在合并的流中调用game.tick
流。
为了合并流,我们可以使用Bacon.Bus
。 那就是我们游戏中的最终事件流,我们称其为combinedTickStream
。 然后我们plug
moveStream
和ghostStream
插入其中,最后subscribe
它并在其中调用game.tick
。
就是这样,我们完成了。 剩下要做的就是用game.start();
开始游戏game.start();
。
Bacon.Property
及更多示例
Bacon.Property是一种反应性。 考虑一个反应性属性,它是一个数组的总和。 当我们向数组添加元素时,反应性属性将做出反应并进行自我更新。 要使用Bacon.Property
,您可以订阅它并侦听更改,也可以使用property.assign(obj,method)方法,该method
在属性更改时调用给定object
的method
。 这是一个如何使用Bacon.Property
:
var source = Bacon.sequentially(1000, [1, 2, 3, 4]);var reactiveValue = source.scan(0, function(a, b) {return a + b;
});// 0 + 1 = 1
// 1 + 2 = 3
// 3 + 3 = 6
// 6 + 4 = 10
首先,我们创建一个事件流,该事件流以1秒的间隔产生给定数组(1、2、3和4)的值,然后创建一个响应性属性,该属性是scan
的结果。 这将分配为1,3,6,和10个值reactiveValue
。
了解更多和现场演示
在本文中,我们通过构建Pacman游戏介绍了Bacon.js的反应式编程。 它简化了我们的游戏设计,并通过事件流的概念为我们提供了更多的控制和灵活性。 完整的源代码可在GitHub上找到 ,并在此处提供实时演示。
以下是一些更有用的链接:
- Bacon.js API参考
- Bacon.js的视频介绍
- RxJS网站
- Highland.js高级流库
- Bodil Stokke为敏锐的Hispter设计的反应式游戏程序
From: /
发布评论