该帖子最初于2017年5月发布在 http://blog.u2i.com/we-made-a-multiplayer-browser-game-in-go-for-fun/
在过去的几年中,Go赢得了极大的欢迎。 许多人喜欢它提供的简单而纯净的方法,以及一些很酷的功能,例如并发 , 结构 , 隐式满意的接口 。 我决定与u2i的同事一起试一试,看看它能提供什么。 我们没有任何可用于它的商业项目,因此我们以阅读大多数教程的方式开始了你们大多数人可能会做的事情。 但是仅仅做一个教程还不足以学习如何使用技术。 因此,我们决定自己创建一个东西-一个叫做Superstellar的多人浏览器游戏。 在本文中,我想向您简要介绍我们遇到的问题以及如何使用Go解决这些问题。
- 安杰洛和恶魔的艺术风格
- 职位:Unity&游戏开发主管
- Pygame教程#5:简洁的事件编辑器。 为游戏增加可点击性。
- 自插入式开发人员草案2 | 2018–05–21
- 从触摸事件到新框架。 ARTris-第3部分
如果您想知道我们如何学习新技术,请参阅我的另一篇有关在公司中组建自学小组的文章。
我只想提到,在游戏开发方面,我们不是专家。 我们每天都在开发Web应用程序,但是对我们大多数人来说,这是有史以来的第一个浏览器游戏。 无论您在这篇博客文章中读到的内容都是我们自己解决遇到的问题的方式-不一定是最好的方式。 我希望您会发现它有用!
Superstellar是一款多人浏览器太空游戏。 它的灵感来自名为Asteroids的旧街机太空射击游戏(每个人都玩过!)。 我们选择了它,以便我们可以只专注于实现而不是设计游戏本身。 老实说,我们只是喜欢互相射击。
规则很简单: 销毁移动物体 , 不要被其他玩家和小行星杀死 。 您有两种资源-健康点和能量点。 您遭受的每一次打击以及与小行星的每次接触都会失去健康。 拍摄和使用升压驱动器时会消耗能量点。 您杀死的物体越多,健康栏就会越大。
下图显示了飞船的状态和用户输入结构的简化版本。 用户可以随时发送消息,因此可以修改用户输入结构。 仿真步骤每20毫秒唤醒一次,并执行两个操作。 首先,它接受用户输入并更新状态(例如,如果用户启用推力,则增加加速度)。 然后,它获取状态(在t时刻)并将其转换为下一个时刻( t + 1 )。 整个过程重复进行。
由于具有并行功能,因此在Go中实现这种并行逻辑非常容易。 每个逻辑都在其自己的goroutine中运行,并侦听某些通道,以便从客户端获取数据或同步到自动报价机,以定义模拟步骤的步伐或将更新发送回客户端。 我们也不必担心并行性-Go会自动利用所有可用的CPU内核。 goroutine和通道的概念很简单,但是功能强大。 如果您不熟悉它们,请阅读本文。
服务器通过websocket与客户端通信。 借助Gorilla网络工具包,在Golang中使用websocket既简单又可靠。 还有一个本地websocket库,但是其官方文档说它目前缺乏某些功能,建议使用Gorilla。
为了使websocket运行,我们必须编写一个处理程序函数以获取初始客户端请求,建立websocket连接并创建客户端结构:
一旦这样做,我们的网络流量就会大大下降。 这样,我们还可以减轻网络延迟的影响。 如果消息停留在Internet上的某个地方,则每个客户端都可以继续进行自己的模拟,最终,当数据到达时,赶上并相应地更新模拟状态。
设计应用程序的代码结构也被证明是一个有趣的案例。 在第一种方法中,我们创建了一个Go包,并将所有逻辑放入其中。 如果大多数人不得不以一种新的编程语言创建一个爱好项目,那可能就是这样做的。 但是,随着代码库的扩大,我们意识到它不再是一个好主意。 因此,我们将代码分为几个软件包,而没有花太多时间思考如何正确执行此操作。 它回来很快就咬我们:
$去建立
不允许导入周期
事实证明,Go不允许软件包循环依赖。 实际上,这是一件好事,因为它迫使程序员仔细考虑应用程序的结构。 因此,在没有其他选择的情况下,我们坐在白板前,写下每个部分,并提出了引入一个模块的想法,该模块将在系统的其他部分之间传递消息。 我们称其为事件分配器(您也可以将其称为事件总线)。
事件分配器是一个概念,它使我们可以将发生在服务器上的所有事件包装在所谓的事件中。 例如:客户端加入,离开,发送输入消息,或者该运行模拟步骤了。 在这种情况下,我们使用调度程序创建并触发相应的事件。 另一方面,每个结构都可以将自己注册为侦听器,并在发生有趣的事情时进行学习。 这样可以使有问题的程序包仅依赖于事件程序包而不相互依赖,从而解决了我们的循环依赖问题。
这是一个有关如何使用事件分发程序传播模拟更新时间滴答的示例。 首先,我们需要创建一个能够监听事件的结构:
然后,我们需要实例化它并向事件分配器注册它:
现在我们需要一些代码来运行代码并触发事件:
这样,我们可以定义任何事件并注册所需的尽可能多的侦听器。 事件分配器是循环运行的,因此我们需要记住不要将长时间运行的任务放在处理函数中。 相反,我们可以创建一个新的goroutine并在那里进行大量的计算。
不幸的是,Go不支持泛型(未来可能会改变),因此为了实现许多不同的事件类型,我们使用了该语言的另一功能-代码生成。 事实证明,这是解决此问题的非常有效的方法,至少在我们这样规模的项目中。
从长远来看,我们意识到实现事件调度程序是一件有价值的事情。 而且由于Go迫使我们避免循环依赖,因此在开发的早期阶段就提出了。 否则,这是我们可能不会做的。
实施多人浏览器游戏非常有趣,也是学习Go的一种很好的方法。 我们有可能使用其最佳功能,例如并发工具,简单性和高性能。 因为它的语法类似于动态类型的语言,所以我们可以快速编写代码,但又不牺牲静态类型的安全性。 这非常有用,尤其是在像我们这样编写低级应用服务器时。
我们还了解了在创建实时多人游戏时必须面对的问题。 客户端和服务器之间的通信量可能非常大,必须付出很多努力来降低它。 您也不能忘记不可避免会出现的滞后和网络问题。
最后值得一提的是,创建一个简单的在线游戏也需要大量的工作,无论是在内部实现方面还是在您想要使其有趣和可玩时。 我们花了无尽的时间讨论要在游戏中放入哪种武器,资源或其他功能,只是意识到要实际实现需要多少工作。 但是,当您尝试做一些对您来说完全陌生的事情时,即使您设法构建的最小的东西也能给您带来很大的满足感。