The Station Manager game with Realtime Tube data
I’ve been learning a lot recently about using realtime data streams and how and why one might use them in an app. In order to better understand the differences between realtime streaming data and REST APIs (which I’ve had more experience with), I decided to build a game, the mechanic of which uses realtime train arrival data. As trains arrive into the station in real life, effects are triggered in the game which the user has to manage.
我最近在学习有关使用实时数据流以及如何以及为什么可以在应用程序中使用它们的很多知识。 为了更好地理解实时流数据和REST API(我有更多经验)之间的差异,我决定构建一个游戏,其机制使用实时火车到达数据。 当火车进入现实生活中的车站时,会在用户必须管理的游戏中触发效果。
自己动手! (Make it your own!)
All of the code for the game is on Glitch. This means that you can see the code, ‘remix’ it and make it your own. There is a thorough readme file in the repo and I’ll also go over some of the methods used in this blog post.
游戏的所有代码都在Glitch上 。 这意味着您可以看到代码,对其进行“重新混合”,然后自己制作。 回购中有一个完整的自述文件,我还将介绍此博客文章中使用的一些方法。
获取到达数据 (Getting Arrivals Data)
Ably has a Hub of realtime data streams for developers to try out and build apps with. I used the London Tube Schedule stream, which provides a stream of various data from TFL; including arrivals at a given station. For the tamagotchi game, I wanted to find out the arrivals at a busy station, so that I would have lots of trains arriving in close succession. I chose King’s Cross station for this reason. The data stream uses the station NAPTAN code rather than it’s name to get the correct data, so I had to look up the correct code for King’s Cross (you can look up station codes here), which is 940GZZLUKSX
. The stream I’ll therefore be using is:
Ably具有实时数据流中心,供开发人员试用和构建应用程序。 我使用了London Tube Schedule Schedule流 ,该流提供了来自TFL的各种数据流。 包括到达指定车站。 在他妈哥池游戏中,我想找出一个繁忙车站的到站时间,这样我就会有很多火车紧接到达。 因此,我选择了国王十字车站。 数据流使用站NAPTAN代码而不是名称来获取正确的数据,因此我必须为Kings Cross查找正确的代码(您可以在此处查找站代码 ),即940GZZLUKSX
。 因此,我将使用的流是:
[product:ably-tfl/tube]tube:940GZZLUKSX:arrivals
Every time the data from TFL is updated, this channel will publish a message with the new arrival times of trains into Kings Cross. The joy of the data being a realtime stream means that I don’t have to poll for the data, as I would with a REST API, instead, I establish a connection once and data is published to the channel as and when updates happen.
每次更新TFL的数据时,此频道都会发布一条消息,告知列车进入Kings Cross的新到达时间。 数据是实时流的乐趣意味着我不必像使用REST API那样轮询数据,而是只需建立一次连接,并在更新发生时将数据发布到通道。
连接到数据流 (Connecting to a Data Stream)
In order to connect to the data stream I used the Ably Javascript SDK. To do so, I need an Ably API key which comes with a free account. To keep my API key safe, I also used Token Authentication which makes a Token Request on the server side which is handed to the Ably realtime client to authenticate. There is a brilliant walkthrough of how to implement Token Authentication here:
为了连接到数据流,我使用了Ably Javascript SDK 。 为此,我需要一个带有免费帐户的Ably API密钥 。 为了确保我的API密钥安全,我还使用了令牌身份验证 ,该令牌身份验证在服务器端发出令牌请求 ,该请求被移交给Ably实时客户端进行身份验证。 这里是如何实现令牌认证的精彩演练:
TFL数据 (TFL Data)
The data published by the stream looks a little like this ↓
流发布的数据看起来像这样↓
It is a large array of train objects each with a lot of information. For the sake of this game, the only information I’m really interested in is the TimeToStation
value. I can use these numbers to calculate when to cause a train to arrive into the station in the game.
它是各种各样的火车对象,每个对象都有很多信息。 为了这个游戏,我真正感兴趣的唯一信息是TimeToStation
值。 我可以使用这些数字来计算何时使火车到达游戏中的车站。
I could have created all kinds of interesting extensions to the game, with multiple platforms and colour coded trains for their lines, even maybe an arrivals board with train destinations, but let’s not get too carried away…
我本可以为游戏创建各种有趣的扩展程序,为其线路配备多个平台和颜色编码的火车,甚至可能有到达目的地的到达板,但也不要太着迷……
游戏机制 (Game mechanics)
Congratulations! You’re the newest TamagoTrain Station Controller! Now you’ve got to keep your station in fine fettle.Don’t let your station get too hot, don’t let your platform fill up with passengers, litter or mice!
恭喜你! 您是最新的TamagoTrain Station控制器! 现在您需要保持车站的舒适度,不要让车站过热,也不要让平台上挤满乘客,垃圾或老鼠!
- Trains raise the temperature of your station, as do passengers 火车和旅客都会提高车站的温度
- If it gets too hot, passengers will start to faint! 如果太热,乘客会晕倒!
- Unconscious passengers can’t leave the platform 昏迷的乘客不能离开平台
- Passengers sometimes drop litter 乘客有时会掉落垃圾
- Too much litter attracts mice! 太多的垃圾会吸引老鼠!
- Trash and mice all take up space on the platform making it difficult for your passengers to exit 垃圾和老鼠都占用了平台上的空间,使您的乘客难以下车
- If your platform gets too full, too hot or too cold your station will have to shut and your game will end 如果平台太满,太热或太冷,您的工作站都将关闭,游戏将结束
怎么玩 (How to play)
- Clean the platform to clear away litter 清洁平台以清除垃圾
- Vent cold air through the station to keep everyone cool (but don’t get carried away!) 向车站排放冷空气以保持所有人的凉爽(但不要被带走!)
- Passengers departing through the exit will cool the platforms down a little 通过出口离开的乘客会使平台降温一点
- Departing trains also cool the platform slightly 出发的火车也稍微冷却了平台
- You can charm mice with songs! They’ll find their way off the platform if musically enticed 您可以用歌曲来吸引老鼠! 如果音乐动听,他们将离开平台
- Music will also wake up fainted passengers 音乐也会唤醒晕倒的乘客
游戏代码 (Game Code)
The game is an expressJS app. It is split into two parts — the simulation/game loop, which runs in ‘ticks’ and the ui/render loop which runs at 30 frames per second. This separation prevents us from tying the game logic to the frame rate, which will reduce the chance of the frame rate dropping if the game logic gets complicated. (If you’re interested, this is a great intro to game loops.)
该游戏是一个expressJS应用程序。 它分为两部分-模拟/游戏循环(以“滴答”运行)和ui / render循环,以每秒30帧的速度运行。 这种分离使我们无法将游戏逻辑与帧速率联系在一起,如果游戏逻辑变得复杂,这将减少帧速率下降的机会。 (如果您有兴趣,这是游戏循环的不错的介绍 。)
Game.js (Game.js)
The Game.js file is the main control loop for the game - in it, we define a JavaScript class called Game
. When games are created, we create a new instance of this class to hold the game state. This is also where we set up the tick()
function, which is called once per second. This ‘tick’ steps the simulation forward by iterating the game loop. It ‘ticks’ the game entities (the platform, passengers and trains), applies any problems (adding litter and mice) and applies any buffs (cleaning, venting or music).
Game.js文件是游戏的主要控制循环-在其中,我们定义了一个名为Game
JavaScript类。 创建游戏后,我们将创建此类的新实例以保持游戏状态。 这也是我们设置tick()
函数的地方,每秒调用一次。 这个“滴答”通过迭代游戏循环使仿真向前迈进。 它“打勾”游戏实体(平台,乘客和火车),应用任何问题(添加垃圾和鼠标)并应用任何增益(清洁,通风或音乐)。
The only input the user can supply is applying a Buff
— either clean
, vent
or music
, which are triggered by the buttons in the UI. When pressed, these buttons add a Buff
object to an array in the Game
instance, that we use as a queue of actions. Buffs can only be added to the queue a maximum of 3 times, after that clicking the buttons in the UI will just return until the Buff
has been taken off the queue.
用户可以提供的唯一输入就是应用Buff
clean
, vent
或music
,由用户界面中的按钮触发。 按下这些按钮时,会将Buff
对象添加到Game
实例中的数组,我们将其用作动作队列。 Buff最多只能添加到队列3次,然后单击UI中的按钮将一直返回,直到Buff
从队列中移出为止。
The Game
instance is responsible for three core things
Game
实例负责三件事
- Handling train arrival/departure messages, and routing them to the platform 处理火车的到达/离开消息,并将其路由到平台
Creating instances of
Buffs
创建
Buffs
实例Checking for game over
检查游戏结束
All the rest of the game logic happens in the tick()
functions found on the Entities
, Problems
and Buffs
.
游戏其余所有逻辑都发生在Entities
, Problems
和Buffs
的tick()
函数中。
GameUI.js (GameUI.js)
The GameUi.js file is where the rendering of the game happens. It uses an observer pattern to keep track of the game state.
GameUi.js文件是渲染游戏的地方。 它使用观察者模式来跟踪游戏状态。
30 times a second the GameUI.draw()
function is called and passed a snapshot of the game state. GameUI
instance keeps track of the last state it was called with so that it can avoid re-drawing things that have not changed.
每秒30次调用GameUI.draw()
函数,并传递游戏状态的快照。 GameUI
实例会跟踪调用它的最后一个状态,以便避免重新绘制未更改的内容。
The GameUi class has a collection called _renderingFunctions
— a list of functions it calls in order, each being passed the current game state. If any rendering function returns a value of -1, we use this as a signal to stop drawing to the screen and to display the Game Over screen. The rendering code places absolutely positioned divs on the page which are styled in the css. The divs contain animated gifs of the entities, buffs and problems. The appearance of the divs is changed by adding css classes and data-attributes dependant on the problems or buffs that have been applied in the game state.
GameUi类具有一个称为_renderingFunctions
的集合,它是按顺序调用的函数列表,每个函数都传递给当前游戏状态。 如果任何渲染函数返回值-1,我们将其用作停止在屏幕上绘制并显示“ 游戏结束”屏幕的信号。 呈现代码将绝对定位的div放置在页面中,并在CSS中设置样式。 div包含实体,buff和问题的GIF动画。 通过添加css类和数据属性来更改div的外观,具体取决于在游戏状态中应用的问题或增益。
实体,增益和问题 (Entities, Buffs and Problems)
By default, when an instance of Game
is created, a Platform
entity is created. This platform has some basic state (an age measured in ticks
, a width
, a height
) along with the three core stats the game is ranked on - hygiene
, temperature
and capacity
. The game is won or lost based on the state of these variables, which the game evaluates each tick. As the game ticks, the Game
instance will process any objects in its queue first in first out, creating an instance of the requested Buff
and applying it to the Platform
.
默认情况下,创建Game
实例时,将创建一个Platform
实体。 该平台具有一些基本状态(以ticks
为单位的年龄, width
, height
),以及游戏在hygiene
, temperature
和capacity
的三项核心统计数据。 根据这些变量的状态,游戏获胜或失败,游戏将评估每个刻度。 随着比赛的蜱中, Game
实例将处理其队列中的任何对象先进先出 ,创建要求的一个实例Buff
,并把它应用到Platform
。
When the Platform
ticks, the following things happen -
当Platform
打勾时,会发生以下情况-
- Any unprocessed messages are read, FIFO. FIFO读取所有未处理的消息。
- If a message for a train arrival or departure is found a train is created on the platform or removed from it. 如果找到有关火车到达或离开的消息,则会在平台上创建火车或从平台上删除火车。
All
tickables
aretick
ed.所有可
tick
tickables
均已tick
。Any completed contents or buffs are removed — an item is deemed complete if a property
completed
is present, and set to true on the object.删除所有已完成的内容或增益-如果存在已完成的属性,则该项目被视为已
completed
,并且在对象上设置为true。
The tickables
that the platform stores are:
平台存储的tickables
为:
- Any present train 任何目前的火车
- All of the contents of the platform 平台的所有内容
- All of the buffs applied to the platform 所有的buff都应用于平台
On each tick, the item that is being ticked
gets handed the current instance of the platform, and based on the logic in that item’s class, it can mutate the properties of the platform. For instance - every tick, a Mouse
could reduce the hygiene
property of the platform.
每次打勾时,被ticked
的项目都将获得平台的当前实例,并且基于该项目的类中的逻辑,它可以使平台的属性发生变化。 例如,每次打勾, Mouse
都可能降低平台的hygiene
性能。
The rest of the entities, Buffs and Problems are all JavaScript classes that can mutate the state of the Platform
instance in their tick
method.
其余实体(Buffs和Problems)都是JavaScript类,可以在其tick
方法中tick
Platform
实例的状态。
Both
Entities
andProblems
havex
andy
coordinates that are used to move them around the user interface.Entities
和Problems
都具有x
和y
坐标,用于在用户界面上移动它们。Problems
all inherit from aBase Class
calledProblem
which creates these properties by default for them.所有
Problems
均继承自称为Problem
的Base Class
,该Base Class
默认为它们创建这些属性。
A problem looks like this:
问题看起来像这样:
Entities and problems hold state which will cause effects during the lifetime of a game. For example:
实体和问题会保持状态,这会在游戏的生命周期内造成影响。 例如:
- Travellers walk towards the exit by moving 10 pixels closer to the exit each tick 旅行者通过在每个刻度线上向出口移近10个像素来走向出口
- Travellers have a chance of dropping litter 旅客有可能掉落垃圾
- Litter has a chance of adding mice to the platform 垃圾有机会将老鼠添加到平台上
- Trains add an extra Traveller to the platform every tick 火车每一刻都会在平台上增加一个旅行者
All of this logic exists in the tick
function of each kind of entity or problem.
所有这些逻辑都存在于每种实体或问题的tick
功能中。
开始游戏 (Starting the Game)
The game uses webpack to bundle the client side JavaScript. The script.js file is the webpack entry point, webpack bundles together all of the JavaScript files before they are sent to the browser. This is great because it means we only need to reference script.js to start the game.
该游戏使用webpack捆绑客户端JavaScript。 script.js文件是webpack的入口点,在将所有JavaScript文件发送到浏览器之前,webpack会将它们捆绑在一起。 这很棒,因为这意味着我们只需要引用script.js即可开始游戏。
The script.js file is referenced in the index.html file and it takes care of starting new games. It contains a startGame()
function which does all the work:
在index.html文件中引用了script.js文件,它负责启动新游戏。 它包含一个完成所有工作的startGame()
函数:
This function:
该功能:
Creates a
game
instance创建一个
game
实例Creates an instance of the
GameUI
class, passing it a reference to the newgame
instance创建
GameUI
类的实例,并将其传递给新game
实例的引用Calls
game.start()
passing a configuration object of two actions - one to execute on start, one on end.调用
game.start()
并传递两个动作的配置对象-一个动作在开始时执行,一个动作在结束时执行。- the onGameStart action listens for events on the dataSource
-onGameStart操作监听dataSource上的事件
- the onGameEnd action disconnects the dataSource to stop the game from using up messages that we don't need.
-onGameEnd操作会断开数据源的连接,以阻止游戏耗尽我们不需要的消息。
The
ui.startRendering()
function is called which will set up the render loopui.startRendering()
函数,它将设置渲染循环- Finally the game is returned so that the UI buttons will work in the browser. 最后,返回游戏,以便UI按钮将在浏览器中起作用。
游戏结束 (Game Over)
Game failure states are managed in the Game.js file, in the function isGameOver()
. This contains a collection of objects with functions for different failure conditions. At the start of each tick, each of those functions are run and if any of them return true
then the game is over.
游戏失败状态在Game.js文件中的isGameOver()
函数中进行管理。 它包含具有针对不同故障情况的功能的对象的集合。 在每个刻度线开始时,将运行这些功能中的每个功能,如果其中任何一个返回true
则游戏结束。
玩得开心! (Have fun!)
I hope you’ve enjoyed playing the game and will enjoy making your own versions, or even adding some extensions to mine. If you’ve any questions about the game or about realtime data streaming you can drop me a message in the comments or get me @thisisjofrank on twitter. I’d also love to see any remixes you make!
我希望您喜欢玩游戏,并喜欢制作自己的版本,甚至为我添加一些扩展。 如果您对游戏或实时数据流有任何疑问,可以在评论中给我留言,或在Twitter上给我@thisisjofrank 。 我也很乐意看到您制作的任何混音!
翻译自: https://medium.com/swlh/building-my-first-game-with-realtime-data-tamago-train-dce9674c5efa