使用Obelisk.js和Spike引擎的持久客户端-服务器生活游戏(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/persistent-client-server-game-of-life-with-obelisk-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 8 分钟阅读 - 3927 个词 阅读量 0使用Obelisk.js和Spike引擎的持久客户端-服务器生活游戏(译文)
原文地址:https://www.codeproject.com/Articles/787606/Persistent-Client-Server-Game-of-Life-with-Obelisk
原文作者:Kel_
译文由本站 robot-v1.0 翻译
前言
A game of life “MMO”, with a persistent simulation running on the server.
生活类" MMO"游戏,在服务器上运行持久模拟.
介绍(Introduction)
生活游戏(Game of Life) 是由约翰霍顿
康威(John Horton Conway)于1970年发明的一种蜂窝自动化系统.它是零玩家进化的游戏.如果我们拿起这款游戏并将其放置在服务器上怎么办,从而允许两个人观察同一块游戏板,即使他们在不同的大陆上也是如此.这就是本文的想法:创建一个(is a cellular automation invented by John Horton Conway in 1970. It’s a zero player game of evolution. What if we took this game and let it live on a server, continuosly thus allowing two people observe the same game board, even if they’re on different continents. That’s the idea behind this article: create a)在服务器上进行持久的几乎类似于" MMO"的模拟(persistent, almost “MMO”-like simulation on the server)并允许客户远程观察和渲染此模拟.(and allow clients to observe and render this simulation remotely.)
[现场演示](*[Live Demo]*)
背景(Background)
让我们从简短的摘要开始,以解释本文的完成及其主要亮点:(Let’s start with a short summary explaining that this article accomplishes, and its main highlights:)
- 生活游戏(The game of life)模拟在服务器上连续运行(simulation runs continuously on the server).然后,网格广播给正在"观察"游戏的每个客户端.(. The grid then broadcasted to every client who is “observing” the game.)
- 仿真介绍(The simulation introduces)随机突变(random mutations)和(and)董事会随机化(board randomizations)因此它可以连续运行而不会变得很无聊.(so it can run continusly without getting too boring.)
- 渲染是使用javascript构建的(The rendering is built using javascript) 方尖碑(Obelisk.js) 图书馆(library).(.)
- 它用(It uses)网络套接字(websockets)内部,但由Spike Engine提取.(internally, but abstracted by Spike Engine.)
- 应用服务器是(The application server is a)自托管可执行文件(self-hosted executable)而客户只是一个(and the client is just a)纯HTML文件(plain html file).(.)
由于模拟是在服务器上运行并由客户端渲染的,因此我们需要拆分角色以及每个节点将执行的操作.在我们的情况下:(Since the simulation runs on the server and rendered by the clients, we need to split the roles and what each node will do. In our case:)
- 服务器(Server)负责整个(is responsible for entire)模拟执行(simulation execution),从一代到另一代.(, from one generation to another.)
- 服务器(Server)还负责(is also rseponsible for)管理观察员名单(managing a list of observers)游戏世界和定期(of the game world and for periodically)感动国家(senging the state)对观察者的游戏世界.(of the game world to the observers.)
- 客户群(Clients)(或观察员)负责加入/离开服务器,并且((or observers) are responsible for joining/leaving the server and)渲染游戏世界(rendering the game world)他们收到.(they receive.) 下图说明了该过程:(The following figure illustrates the process:)
服务器端实施(Server-Side Implementation)
让我们开始研究代表客户端-服务器通信过程的定义.我们有3个操作:(Let’s begin by examining the definition that represents the process of client-server communication. We have 3 operations:)
JoinGameOfLife
:被观察者召唤加入游戏.这告诉服务器开始向该特定客户端发送更新.(: called by the observer to join the game. This tells the server to start sending updates to that particular client.)LeaveGameOfLife
:被观察者叫离开游戏.这告诉服务器停止发送更新.(: called by the observer to leave the game. This tells the server to stop sending updates.)NewGeneration
:由服务器启动,因此(: initiated by the server, hence)Direction="Push"
,只需将单元格网格发送给客户端.这个网格是(, simply send the grid of cells to the client. This grid is)填充零和/或一(filled with zeros and/or ones)(二进制矩阵),代表地图上的活动或空白单元格.((binary matrix) and represents live or empty cells of the map.)
<?xml version="1.0" encoding="UTF-8"?>
<Protocol Name="MyGameOfLifeProtocol" xmlns="http://www.spike-engine.com/2011/spml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Operations>
<!-- Joins the game of life and allows clients to observe the board -->
<Operation Name="JoinGameOfLife" Direction="Pull" SuppressSecurity="true"></Operation>
<!-- Leaves the game of life -->
<Operation Name="LeaveGameOfLife" Direction="Pull" SuppressSecurity="true"></Operation>
<!-- Sends a new generation to the observers -->
<Operation Name="NewGeneration"
Direction="Push"
SuppressSecurity="true">
<Outgoing>
<Member Name="Grid" Type="ListOfInt16" />
</Outgoing>
</Operation>
</Operations>
</Protocol>
我们将不涉及生活游戏的实际实现,因为它只是众多游戏之一,而且非常简单.但是,我们添加了一些有趣的修改以加快仿真速度,并添加了一些不错的性能技巧以加快处理速度.如果您看下面的代码片段,该函数(We are not going to go through the actual implementation of the game of life, as it’s just one of many and it’s pretty straightforward. However, we added a couple of interesting modifications to spice up the simulation and a couple of nice performance tricks to speed up things. If you look at the snippet of code below, the function)更新单元(UpdateCell)是负责更新该字段的单个单元格的人.如果更改了至少一个单元格,我们会将整个世代标记为(is the one responsible for updating a single cell of the field. If at least one cell is changed, we mark the entire generation as)变了(changed).(.)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateCell(int i, int j)
{
var oldState = GetAt(this.FieldOld, i, j);
var neighbors = CountNeighbours(this.FieldOld, i, j);
// Update the cell
this.Field[i * FieldSize + j] =
(short) (neighbors == 2 ? oldState : (neighbors == 3 ? 1 : 0));
// Mark as dirty if new is not the same as old
if (this.Field[i * FieldSize + j] != oldState)
this.FieldChange = true;
}
在更新期间,我们还做两件事:(During the update, we also do two additional things:)
- 如果领域没有改变,这在生活中经常发生,那么我们将再次随机分配董事会,(If the field wasn’t changed, which can happen quite often in the game of life, we randomize the board again and)重新初始化(reinitialize it).例如,这使我们可以永久运行仿真,并在没有活细胞的情况下自动重新启动仿真.(. This allows us to run the simulation forever and automatically restart the simulation if there’s no more living cells, for example.)
- 每一代人(Each generation got)5%的机会产生一些随机突变(5% of chance to create some random mutations).一旦发生单个突变,我们就有95%的概率在同一代中具有更多的突变.这使我们能够"打破"生活游戏中的稳定结构,从而使事情变得更加有趣.(. Once a single mutation have occured, we have 95% probability to have more mutation within the same generation. This allows us to “break” stable structures in the game of life, spicing things up.)
private void Mutate()
{
if(!this.FieldChange)
this.Randomize();
var probability = 0.05;
while (Dice.NextDouble() < probability)
{
var x = Dice.Next(0, this.FieldSize);
var y = Dice.Next(0, this.FieldSize);
probability = 0.95;
this.Field[x * FieldSize + y] =
(short)(this.Field[x * FieldSize + y] == (short)1 ? 0 : 1);
}
}
现在我们已经实现了仿真,我们如何实际将仿真连接到我们的网络后端?加入和离开操作非常简单,只需放入/删除(Now that we have the simulation implemented, how do we actually connect the simulation to our networking backend? Join and leave operations are pretty straightforward and they simply put/remove an)IClient(IClient)实例往返于(instance to/from an)IList (IList).我们还使用以下命令开始游戏循环(. We also start a game loop using)秒杀计时器(Spike.Timer)为我们处理所有线程.重要的是要注意,如果您有多个计时器,它们将共享同一线程,从而避免了性能问题,例如超额预订.可以在这里调整游戏循环本身的速度,在下面的代码段中,我们每隔50毫秒调用一次,在我们的实时演示中将其设置为200毫秒.(which handles all the threading for us. It is important to notice that if you have several timers, they will all share the same thread, avoiding performance problems such as oversubscription. The speed of game loop itself can be adjusted here, on the piece of code below we call it every 50 milliseconds and in our live demo it’s set to 200 milliseconds.)
[InvokeAt(InvokeAtType.Initialize)]
public static void Initialize()
{
// Hook the events
MyGameOfLifeProtocol.JoinGameOfLife += OnJoinGame;
MyGameOfLifeProtocol.LeaveGameOfLife += OnLeaveGame;
// Start the game loop,
Timer.PeriodicCall(TimeSpan.FromMilliseconds(50), OnTick);
}
游戏循环几乎可以达到您的期望.它更新生活游戏,执行模拟,然后将网格(32 x 32二进制矩阵)发送给每个客户端.我们在协议和协议中都定义了(The game loop does pretty much that you would expect. It updates the game of life, performing the simulation and then sends the grid (32 by 32 binary matrix) to every client. We defined in both, our protocol and our)游戏(Game)将相同矩阵分类为(class the same matrix to be a)IList (IList).因此,我们只需将该列表传递给send方法,而无需进行任何转换.(. So we simply pass that list to the send method, without doing any conversion at all.)
private static void OnTick()
{
World.Update();
// Make sure we don't add new observers while preparing to send
lock (Observers)
{
// Send the grid to every observer
foreach (var observer in Observers)
observer.SendNewGenerationInform(World.World);
}
}
客户端实施(Client-Side Implementation) 现在让我们检查客户端.客户端需要连接到服务器并加入游戏,我们还需要挂钩(Let’s examine now the client side. The client needs to connect to the server and join the game, we also need to hook)newGenerationInform(newGenerationInform)事件,每次我们从服务器收到新的网格时都会调用该事件.收到网格后,我们将从(event which will be invoked every time we receive a new grid from the server. Once we receive a grid, we copy it from an)数组(Array)到(to an)Int8Array(Int8Array)画出来(and draw it.)
// When the document is ready, we connect
$(document).ready(function () {
var server = new spike.ServerChannel("127.0.0.1:8002");
// When the browser is connected to the server
server.on('connect', function () {
// Join the game
server.joinGameOfLife();
// Receive the updates
server.on('newGenerationInform', function (p) {
var field = new Int8Array(gridSize * gridSize);
for (var i = 0; i < gridSize * gridSize; ++i)
field[i] = p.grid[i];
render(field);
});
});
});
我们使用了一个名为(We have used a rendering engine called*) 方尖碑(Obelisk.js) 渲染我们的等距图块,并受到(to render our isometric blocks and inspired by the work of*) @Safx)@萨克斯( ,实施(, implementing the) javascript中的生活游戏(game of life in javascript) .但是,我们的客户没有任何与生活相关的逻辑游戏.我们只是有一个render函数来绘制一个(. However, we do not have any game of life-related logic in our client. We simply have a render function that draws a)Int8Array(Int8Array)从服务器收到的网格.由于服务器会推送数据,因此我们甚至不必具有渲染循环,而只需在每个对应的接收上重新绘制画布的所有元素.(grid we receive from the server. Since the server pushes the data, we do not even have to have a render loop and simply redraw all the elements of our canvas on every corresponding receive.)
function render(field) {
// Clear the screen
pixelView.clear();
// Draw the board
var boardColor = new obelisk.CubeColor().getByHorizontalColor(obelisk.ColorPattern.GRAY);
var p = new obelisk.Point3D(cubeSide / 2, cubeSide / 2, 0);
var cube = new obelisk.Cube(boardDimension, boardColor, false);
pixelView.renderObject(cube, p);
// Draw cells
for (var i = 0; i < gridSize; ++i) {
for (var j = 0; j < gridSize; ++j) {
var z = field[i * gridSize + j];
if (z == 0) continue;
var color = new obelisk.CubeColor().getByHorizontalColor((i * 8) << 16 | (j * 8) << 8 | 0x80);
var p = new obelisk.Point3D(cubeSide * i, cubeSide * j, 0);
var cube = new obelisk.Cube(dimension, color, false);
pixelView.renderObject(cube, p);
}
}
}
希望您喜欢本文,请查看我们编写的其他Spike Engine文章,并随时为我们贡献力量!(I hope you liked this article, please check out other Spike Engine articles we’ve written and feel free to contribute!)
历史(History)
- 23/06/2015-源代码和文章已更新为Spike v3(23/06/2015 - Source code & article updated to Spike v3)
- 2014年6月19日-初始版本(19/06/2014 - Initial Version)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
Javascript HTML HTML5 C# VS2013 Canvas WebSockets game 新闻 翻译