蚊章汇 - Kyrylo Subbotin
type: channel
link: https://linuxcn.bwsl.wang/author/401/
description: Kyrylo Subbotin
items: 使用 Python 和 Asyncio 编写在线多人游戏（三）
items: 使用 Python 和 Asyncio 编写在线多用人游戏（二）
items: 使用 Python 和 Asyncio 编写在线多人游戏（一）

使用 Python 和 Asyncio 编写在线多人游戏（三）
type: item
date: 2016-09-20T23:25:00+08:00
description: 在这个系列中，我们基于多人游戏 贪吃蛇 来制作一个异步的 Python 程序。上一篇文章聚焦于编写游戏循环上，而本系列第 1 部分则涵盖了如何异步化。代码戳这里4、制作一个完整的游戏4.1 工程概览在此部分，我们将回顾一个完整在线游戏的设计。这是一个经典的贪吃蛇游戏，增加了多玩家支持。你可以自己在 （http://snakepit-game.com） 亲自试玩。源码在 GitHub 的这个仓库。游戏包括下列文件：server.py - 处理主游戏循环和连接。game.py - 主要的 Game 类。实现游戏的逻辑和游戏的大部分通信协议。player.py - Player 类，包括每一个独立玩家的数据和蛇的展现。这个类负责获取玩家的输入并相应地移动蛇。datatypes.py - 基本数据结构。settings.py - 游戏设置，在注释中有相关的说明。index.html - 客户端所有的 html 和 javascript代码都放在一个文件中。4.2 游戏循环内窥多人的贪吃蛇游戏是个用于学习十分好的例子，因为它简单。所有的蛇在每个帧中移动到一个位置，而且帧以非常低的频率进行变化，这样就可以让你就观察到游戏引擎到底是如何工作的。因为速度慢，对于玩家的按键不会立马响应。按键先是记录下来，然后在一个游戏循环迭代的最后计算下一帧时使用。现代的动作游戏帧频率更高，而且通常服务端和客户端的帧频率是不相等的。客户端的帧频率通常依赖于客户端的硬件性能，而服务端的帧频率则是固定的。一个客户端可能根据一个游戏“嘀嗒”的数据渲染多个帧。这样就可以创建平滑的动画，这个受限于客户端的性能。在这个例子中，服务端不仅传输物体的当前位置，也要传输它们的移动方向、速度和加速度。客户端的帧频率称之为 FPS（ 每秒帧数 （ frames per second ） ），服务端的帧频率称之为 TPS（ 每秒滴答数 （ ticks per second ） ）。在这个贪吃蛇游戏的例子中，二者的值是相等的，在客户端显示的一帧是在服务端的一个“嘀嗒”内计算出来的。我们使用类似文本模式的游戏区域，事实上是 html 表格中的一个字符宽的小格。游戏中的所有对象都是通过表格中的不同颜色字符来表示。大部分时候，客户端将按键的码发送至服务端，然后每个“滴答”更新游戏区域。服务端一次更新包括需要更新字符的坐标和颜色。所以我们将所有游戏逻辑放置在服务端，只将需要渲染的数据发送给客户端。此外，我们通过替换通过网络发送的数据来减少游戏被破解的概率。4.3 它是如何运行的？这个游戏中的服务端出于简化的目的，它和例子 3.2 类似。但是我们用一个所有服务端都可访问的 Game 对象来代替之前保存了所有已连接 websocket 的全局列表。一个 Game 实例包括一个表示连接到此游戏的玩家的 Player 对象的列表（在 self._players 属性里面），以及他们的个人数据和 websocket 对象。将所有游戏相关的数据存储在一个 Game 对象中，会方便我们增加多个游戏房间这个功能——如果我们要增加这个功能的话。这样，我们维护多个 Game 对象，每个游戏开始时创建一个。客户端和服务端的所有交互都是通过编码成 json 的消息来完成。来自客户端的消息仅包含玩家所按下键码对应的编号。其它来自客户端消息使用如下格式：[command, arg1, arg2, ... argN ] 来自服务端的消息以列表的形式发送，因为通常一次要发送多个消息 （大多数情况下是渲染的数据）：[[command, arg1, arg2, ... argN ], ... ] 在每次游戏循环迭代的最后会计算下一帧，并且将数据发送给所有的客户端。当然，每次不是发送完整的帧，而是发送两帧之间的变化列表。注意玩家连接上服务端后不是立马加入游戏。连接开始时是 观望者 （ spectator ） 模式，玩家可以观察其它玩家如何玩游戏。如果游戏已经开始或者上一个游戏会话已经在屏幕上显示 “game over” （游戏结束），用户此时可以按下 “Join”（参与），来加入一个已经存在的游戏，或者如果游戏没有运行（没有其它玩家）则创建一个新的游戏。后一种情况下，游戏区域在开始前会被先清空。游戏区域存储在 Game._field 这个属性中，它是由嵌套列表组成的二维数组，用于内部存储游戏区域的状态。数组中的每一个元素表示区域中的一个小格，最终小格会被渲染成 html 表格的格子。它有一个 Char 的类型，是一个 namedtuple ，包括一个字符和颜色。在所有连接的客户端之间保证游戏区域的同步很重要，所以所有游戏区域的更新都必须依据发送到客户端的相应的信息。这是通过 Game.apply_render() 来实现的。它接受…

使用 Python 和 Asyncio 编写在线多用人游戏（二）
type: item
date: 2016-09-18T19:47:00+08:00
description: 你在 Python 中用过异步编程吗？本文中我会告诉你怎样做，而且用一个能工作的例子来展示它：这是一个流行的贪吃蛇游戏，而且是为多人游戏而设计的。介绍和理论部分参见“第一部分 异步化”。游戏入口在此，点此体验。3、编写游戏循环主体游戏循环是每一个游戏的核心。它持续地运行以读取玩家的输入、更新游戏的状态，并且在屏幕上渲染游戏结果。在在线游戏中，游戏循环分为客户端和服务端两部分，所以一般有两个循环通过网络通信。通常客户端的角色是获取玩家输入，比如按键或者鼠标移动，将数据传输给服务端，然后接收需要渲染的数据。服务端处理来自玩家的所有数据，更新游戏的状态，执行渲染下一帧的必要计算，然后将结果传回客户端，例如游戏中对象的新位置。如果没有可靠的理由，不混淆客户端和服务端的角色是一件很重要的事。如果你在客户端执行游戏逻辑的计算，很容易就会和其它客户端失去同步，其实你的游戏也可以通过简单地传递客户端的数据来创建。游戏循环的一次迭代称为一个 嘀嗒 （ tick ） 。嘀嗒是一个事件，表示当前游戏循环的迭代已经结束，下一帧（或者多帧）的数据已经就绪。在后面的例子中，我们使用相同的客户端，它使用 WebSocket 从一个网页上连接到服务端。它执行一个简单的循环，将按键码发送给服务端，并显示来自服务端的所有信息。客户端代码戳这里。例子 3.1：基本游戏循环例子 3.1 源码。我们使用 aiohttp 库来创建游戏服务器。它可以通过 asyncio 创建网页服务器和客户端。这个库的一个优势是它同时支持普通 http 请求和 websocket。所以我们不用其他网页服务器来渲染游戏的 html 页面。下面是启动服务器的方法：app = web.Application() app["sockets"] = [] asyncio.ensure_future(game_loop(app)) app.router.add_route('GET', '/connect', wshandler) app.router.add_route('GET', '/', handle) web.run_app(app) web.run_app 是创建服务主任务的快捷方法，通过它的 run_forever() 方法来执行 asyncio 事件循环。建议你查看这个方法的源码，弄清楚服务器到底是如何创建和结束的。app 变量就是一个类似于字典的对象，它用于在所连接的客户端之间共享数据。我们使用它来存储连接的套接字的列表。随后会用这个列表来给所有连接的客户端发送消息。asyncio.ensure_future() 调用会启动主游戏循环的任务，每隔2 秒向客户端发送嘀嗒消息。这个任务会在同样的 asyncio 事件循环中和网页服务器并行执行。有两个网页请求处理器：handle 是提供 html 页面的处理器；wshandler 是主要的 websocket 服务器任务，处理和客户端之间的交互。在事件循环中，每一个连接的客户端都会创建一个新的 wshandler 任务。这个任务会添加客户端的套接字到列表中，以便 game_loop 任务可以给所有的客户端发送消息。然后它将随同消息回显客户端的每个击键。在启动的任务中，我们在 asyncio 的主事件循环中启动 worker 循环。任务之间的切换发生在它们之间任何一个使用 await语句来等待某个协程结束时。例如 asyncio.sleep 仅仅是将程序执行权交给调度器一段指定的时间；ws.receive 等待 websocket 的消息，此时调度器可能切换到其它任务。在浏览器中打开主页，连接上服务器后，试试随便按下键。它们的键值会从服务端返回，每隔 2 秒这个数字会被游戏循环中发给所有客户端的嘀嗒消息所覆盖。我们刚刚创建了一个处理客户端按键的服务器，主游戏循环在后台做一些处理，周期性地同时更新所有的客户端。例子 3.2: 根据请求启动游戏例子 3.2 的源码在前一个例子中，在服务器的生命周期内，游戏循环一直运行着。但是现实中，如果没有一个人连接服务器，空运行游戏循环通常是不合理的。而且，同一个服务器上可能有不同的“游戏房间”。在这种假设下，每一个玩家“创建”一个游戏会话（比如说，多人游戏中的一个比赛或者大型多人游戏中的副本），这样其他用户可以加入其中。当游戏会话开始时，游戏循环才开始执行。在这个例子中，我们使用一个全局标记来检测游戏循环是否在执行。当第一个用户发起连接时，启动它。最开始，游戏循环没有执行，标记设置为 False。游戏循环是通过客户端的处理方法启动的。 if app["game_is_running"] == False: asyncio.ensure_future(game_loop(app)) 当 game_loop(…

使用 Python 和 Asyncio 编写在线多人游戏（一）
type: item
date: 2016-09-14T14:35:00+08:00
description: 你在 Python 中用过异步编程吗？本文中我会告诉你怎样做，而且用一个能工作的例子来展示它：这是一个流行的贪吃蛇游戏，而且是为多人游戏而设计的。游戏入口在此，点此体验。1、简介在技术和文化领域，大规模多人在线游戏（MMO）毋庸置疑是我们当今世界的潮流之一。很长时间以来，为一个 MMO 游戏写一个服务器这件事总是会涉及到大量的预算与复杂的底层编程技术，不过在最近这几年，事情迅速发生了变化。基于动态语言的现代框架允许在中档的硬件上面处理大量并发的用户连接。同时，HTML5 和 WebSockets 标准使得创建基于实时图形的游戏的直接运行至浏览器上的客户端成为可能，而不需要任何的扩展。对于创建可扩展的非堵塞性的服务器来说，Python 可能不是最受欢迎的工具，尤其是和在这个领域里最受欢迎的 Node.js 相比而言。但是最近版本的 Python 正在改变这种现状。asyncio 的引入和一个特别的 async/await 语法使得异步代码看起来像常规的阻塞代码一样，这使得 Python 成为了一个值得信赖的异步编程语言，所以我将尝试利用这些新特点来创建一个多人在线游戏。2、异步一个游戏服务器应该可以接受尽可能多的用户并发连接，并实时处理这些连接。一个典型的解决方案是创建线程，然而在这种情况下并不能解决这个问题。运行上千的线程需要 CPU 在它们之间不停的切换（这叫做上下文切换），这将导致开销非常大，效率很低下。更糟糕的是使用进程来实现，因为，不但如此，它们还会占用大量的内存。在 Python 中，甚至还有一个问题，Python 的解释器（CPython）并不是针对多线程设计的，相反它主要针对于单线程应用实现最大的性能。这就是为什么它使用 GIL（global interpreter lock），这是一个不允许同时运行多线程 Python 代码的架构，以防止同一个共享对象出现使用不可控。正常情况下，在当前线程正在等待的时候，解释器会转换到另一个线程，通常是等待一个 I/O 的响应（举例说，比如等待 Web 服务器的响应）。这就允许在你的应用中实现非阻塞 I/O 操作，因为每一个操作仅仅阻塞一个线程而不是阻塞整个服务器。然而，这也使得通常的多线程方案变得几近无用，因为它不允许你并发执行 Python 代码，即使是在多核心的 CPU 上也是这样。而与此同时，在一个单一线程中拥有非阻塞 I/O 是完全有可能的，因而消除了经常切换上下文的需要。实际上，你可以用纯 Python 代码来实现一个单线程的非阻塞 I/O。你所需要的只是标准的 select 模块，这个模块可以让你写一个事件循环来等待未阻塞的 socket 的 I/O。然而，这个方法需要你在一个地方定义所有 app 的逻辑，用不了多久，你的 app 就会变成非常复杂的状态机。有一些框架可以简化这个任务，比较流行的是 tornade 和 twisted。它们被用来使用回调方法实现复杂的协议（这和 Node.js 比较相似）。这种框架运行在它自己的事件循环中，按照定义的事件调用你的回调函数。并且，这或许是一些情况的解决方案，但是它仍然需要使用回调的方式编程，这使你的代码变得碎片化。与写同步代码并且并发地执行多个副本相比，这就像我们在普通的线程上做的一样。在单个线程上这为什么是不可能的呢？这就是为什么出现微线程（microthread）概念的原因。这个想法是为了在一个线程上并发执行任务。当你在一个任务中调用阻塞的方法时，有一个叫做“manager” (或者“scheduler”)的东西在执行事件循环。当有一些事件准备处理的时候，一个 manager 会转移执行权给一个任务，并等着它执行完毕。任务将一直执行，直到它遇到一个阻塞调用，然后它就会将执行权返还给 manager。微线程也称为轻量级线程（lightweight threads）或绿色线程（green threads）（来自于 Java 中的一个术语）。在伪线程中并发执行的任务叫做 tasklets、greenlets 或者协程（coroutines）。Python 中的微线程最早的实现之一是 Stackless Python。它之所以这么知名是因为它被用在了一个叫 EVE online 的非常有名的在线游戏中。这个 MMO 游戏自称说在一个持久的“宇宙”中，有上千个玩家在做不同的活动，这些都是实时发生的。Stackless 是一个独立的 Python 解释器，它代替了标准的函数栈调用，并且直接控制程序运行流程来减少上下文切换的开销。尽管这非常有效，这个解决方案不如在标准解释器中使用“软”库更流行，像 eventlet 和 gevent 的软件包配备了修补过的标准 I/O 库，I/O 函数会将执行权传递到内部事件循环。这使得将正常的…