45-从0开始学游戏开发

开篇词 | 跟我学，你也可以开发一款游戏！

你好，我是蔡能。从 96 年编写第一行代码开始，我在 IT 行业从业已经有 22 年的时间。

从程序员到管理者，我参与过各式各样的项目，也主导过很多项目。当然，我经历的大部分项目都是游戏。20 多年里，我目睹了从最初的像素游戏，到如今高清贴图的 3D 游戏的发展过程。游戏行业，乃至整个 IT 行业都发生了翻天覆地的变化。

当然，我本人也非常喜欢玩游戏。我喜欢自由自在的游戏和需要脑力的游戏。比如，“GTA 系列”“老滚系列”和“三国志系列”。在玩游戏的过程中，我体会到各种各样的乐趣，同时，作为开发者，我也会从专业的角度出发，思考这款游戏究竟是如何制作出来的。

我发现，喜欢玩游戏的人，都很有兴趣自己做一款游戏。哪怕自己不懂编程，但是自己做做策划，懂美术的话自己做做图，能让自己的游戏跑起来，就是一件幸福的事情。

还有一部分人，可能对游戏并不感兴趣，但是对赚钱感兴趣，那么我的专栏也可以让你知道游戏开发的基础流程和知识，避免走弯路，不花冤枉钱，不掉技术坑。

我看过网上各式各样的“傻瓜式”游戏开发工具，比如 RPG MAKER。任何不懂编程的人，只需要导入规定的模板，就能方便地开发出一款游戏。然而，这种方法只能开发某一类非常简单的游戏。如果想要开发出一款真正意义上的游戏，还是要借助编程基础和系统的开发知识。

我举几个简单的例子，你就明白了。

在《守望先锋》里，我们看到的子弹射击，对方被击伤，这些都是经过怎样不断地网络优化实现的呢？还有各种网络预判模型，你是否都了解呢？

我们玩的《星际争霸》，究竟是每个线程控制一个兵种，还是一个线程刷新全部兵种画面？

《魔兽世界》如此庞大，它是如何将海量逻辑写在游戏客户端里？将逻辑编写成硬代码显然不理性，写成配置文件又缺乏机动性，那它是怎么做到的呢？

另外，我发现，很多公司在开发游戏的时候，选择引擎也是一个尴尬事。是自己研发一款引擎还是购买别家成熟的引擎呢？引擎的好坏，对开发的游戏来讲有什么影响？很多人也是不知道的。

这些问题，我都会在专栏文章中一一解答。但前提是你最好具备一定的编程知识和基础。

我将会尽量选择简洁的 Python 语言来编写。我会从最上层的脚本语言 Python 的使用开始，剖析为什么游戏开发要从这里开始。在用到 C/C++ 代码的地方，我将会尽可能地使用简单易懂的语法来示范，所以，其实你只需要懂点儿英文就可以了（笑）。

我会尽量选择简单的开发工具，而不是动辄几个 G 的开发工具。比如“VS 系列”，比如各种复杂的 IDE。简单的开发工具足够简单明了地让你了解开发的具体流程，快速进入状态，而不是在设置工具上浪费大量的时间和精力。

我会尽量避免特别复杂的底层知识，但是也不会一带而过。我会帮你一步步梳理开发的流程，并仔细剖析，在 C/C++ 中如何控制计算机的各种设备，操作计算机的各种硬件。或许你还会在编译中遇到问题，我也会一步一步，由浅入深，教你如何解决编译问题。

我将用制作“打飞机”游戏 demo 的过程，来完成游戏开发内容的分解步骤讲解。之所以选择“打飞机”作为游戏 demo，是因为它的开发过程几乎涵盖了我要讲述的所有开发流程，包括操作、画面的前后遮挡、图像的碰撞和切换、资源包、脚本语言等等。

对于专栏的具体内容，我将分以下几个方面来阐述。

我会先带你熟悉游戏开发的基础概念。包括开发游戏需要涉及的基础知识和需要准备的工具：游戏引擎、底层图形接口和地图编辑器。

然后从 0 开始搭建一个游戏窗体，往窗体里添加图形、界面和操作。

我还会讲解如何嵌入脚本语言。毕竟使用硬代码编写游戏逻辑并不是一个很好的选择，而脚本语言的嵌入，可以让我们很方便地完成游戏的逻辑开发。我们只需要将底层图形图像的显示、函数等捆绑好，接下来交给脚本语言就好了。

除此之外，我还会阐述各种平台上的游戏开发和实现，带你学习一些最近热点的游戏开发知识。比如 HTML5 游戏和手机移动平台游戏。我们已经有了游戏开发的基本知识，也掌握了游戏开发的流程，再来看这些游戏的开发，就会变得很轻松了。

最后，我会与你谈谈，我对游戏行业未来发展的看法。游戏将会如何发展，往哪个方向走？开开脑洞，我们大胆畅想游戏领域的未来。

我希望，学习完这个专栏，能让你对游戏开发有个实质性的掌握，让你对游戏的开发和理念有一个完整并且系统的了解，并且能很快地着手进行游戏的开发。万变不离其宗，希望我的分享不会辜负你为此投入的金钱和时间。

让我们一起踏上游戏开发的旅程吧！

第1讲 | 游戏开发需要了解哪些背景知识？

这一节开始，我将带你进入游戏开发的世界。

从最初的小霸王，到充满混混和烟味的街机厅，到 PS、Xbox、DC、N64 等次世代游戏主机，再到电脑游戏，再到如今，在手机上就能玩到各种各样的游戏。

我会依次为你揭密，这些令人痴迷、沉浸的游戏，究竟是怎么发展的，常见的游戏种类有哪些，这些游戏在开发流程和细节上有什么不同，以及游戏开发究竟要从哪里开始学起。

浅述游戏的发展

我们现在公认的第一款电子游戏（也就是主机游戏），是 1952 年面世的。游戏玩的是井字棋，6 年后才出现了简陋的网球游戏。

下面这幅图就是井字棋游戏，尽管它与 1952 年的游戏画面还是有所出入，但是游戏内容是一样的。

此后，我们就进入了“雅达利”时期。随后的《太空侵略者》，是最经典的一款游戏。在这之后，我们耳熟能详的任天堂红白机，发布了《吃豆人》《大金刚》等游戏。再之后，就进入了各种游戏主机竞相绽放的阶段。

在电脑游戏方面，第一款电脑游戏是 1962 年面世的《太空战争》。这是一个学生编写的。到 1980 年之后，随着电脑技术的日趋成熟，电脑游戏的画面才开始有了逐渐赶超电子游戏的态势。到 2005 年之后，随着电脑图形硬件的发展，电脑游戏的发展进入了加速期。

电子游戏的优势是，它集合了当时高精尖的图形显卡技术，纯粹为游戏所设计。比如索尼的 PS 系列。据闻，PS 使用的显卡技术来自军用科技（传闻的真实性有待考证，但每一代 PS 的图形显卡的确都是业内顶尖水平）。而电脑游戏的优势是，它能更换显卡和硬件。当显卡升级换代的时候，理论上，只要主板支持，你就可以更新；而电子游戏主机是固化的硬件，只能靠购买新的版本。

游戏有哪些种类？

当前全世界的游戏琳琅满目，从硬件来讲，大致可以分为四大类。

第一类是我们前面提到的主机游戏，业界称为Console Game。比如微软的 Xbox 系列、索尼的 PS 系列、任天堂的 Switch、N64、Wii 等。这类游戏的硬件可以插在电视机或者显示屏上。以前，游戏的载体一般是光盘、卡带，现在也可以直接从网上下载。

这些公司还相应推出了掌上游戏机，比如任天堂的 3DS、NDS、GBA，索尼的 PSP、PSV 等。掌上游戏机的推出，让玩家随时随地可以玩上游戏。由于是专业的游戏机，比之手机上的游戏，有更好的操作感，同时也更能体验到游戏的乐趣。

第二类是电脑游戏。电脑游戏品类繁多，有 PC 客户端游戏，比如《梦幻西游》《魔兽世界》《星际争霸》《GTA》等；还有 HTML5 网页游戏、Flash 游戏等等。

另外一类是移动游戏。包括 Pad 游戏、手机游戏。手机游戏目前品类繁多，包括 App 游戏、HTML5 游戏，以及微信发布的小游戏。

最后一类是目前并不是特别发达的电视机游戏。随着安卓系统的兴起，电视盒子的出现，出现了一种不需要游戏主机却能在电视屏幕上直接玩的游戏，这就是电视机游戏。但这类游戏一般需要用电视遥控器玩，而且由于电视盒子的机能限制，画面甚至可能比不上手机游戏，所以只适合闲暇的时候偶尔玩一下。

游戏开发使用什么编程语言？

在最早期的游戏开发中，比如任天堂的《FamiCom》（我们俗称《红白机 FC》）、《Super FamiCom》（我们俗称《超任 SFC》），都是使用汇编语言进行开发的。早期由于 8 位机能限制，程序员们为了优化游戏速度、提升游戏画面，无所不用其极，甚至到了奇技淫巧的地步。到了后期的游戏（比如《无赖战士》《足球小将》等），已经将机能挖掘到了极限。到了 16 位机后（任天堂《SFC》、世嘉的《MD》），才逐步使用C 语言进行编程。到了 32 位机之后，基本都是使用C/C++ 语言进行编程。

游戏开发从哪里开始学起？

手游这么火，我为什么不直接讲手游呢？原因很简单，想要开发手机游戏，对技术人员要求很高。手游有两种，一种是传统意义上的原生开发手游，一种是微信小游戏和网页小游戏。后面这个就不用说了，我主要说原生手游。

我在后面会讲到，游戏引擎可以购买也可以自己开发。原生手游的话，用购买的成熟引擎做的话，调试起来就会比较困难。毕竟要在手机跑，出问题再改，再跑，加上手机内存和硬件限制，不如电脑端可以随时调试。其次，手游的开发需要考量更多资源的分配和占比，发布的包就是一个考验，谁也不会下载一个三四百兆的安装包。

我会以电脑游戏为主，进行开发流程的讲解。首先，电脑游戏开发便捷。事实上，只要开发游戏，就一定离不开电脑，不管是主机游戏，还是掌上游戏、移动游戏，开发一定是在电脑上（或者专用开发电脑）；其次，搞明白了电脑游戏的开发流程和知识之后，其他游戏的开发也一通百通。

我会针对 2D 游戏（特指客户端游戏）进行剖析和讲解。为何专门选择 2D 游戏进行讲解呢？

主要有两方面的原因：

2D 游戏涉及最基本的图形、图像知识。从画面方面考虑，开发 2D 游戏只需要你知道 X 和 Y 的位置即可，其他深层次的知识，引擎都可以替你完成。想要制作 3D 画面，你需要掌握更多的知识，特别是几何知识，这是毋庸置疑的。对于初学者来说，我们首先需要掌握核心的开发流程，至于过于细节的知识，可以在有了一定基础之后再进行拓展学习。

2D 游戏涉及最核心的网络呈现过程。网络数据在客户端和服务器端之间传输，经过服务器端计算的数据返回给客户端，客户端进行呈现。比如，从“一颗炸弹消灭几个敌人”，就能知道网络是否有延迟，服务器计算是否正确等等。

2D 游戏的类型

1. 横版游戏

经典游戏《超级玛丽》就是一款横版过关游戏。横版游戏，是 2D 游戏可以实现的最基本的内容。横版游戏最主要的特点是它的画面，这种 2D 平面的画面只有左右两侧画面可以控制，玩家体验通常比较顺畅。还有前几年大火的《水管鸟》、耳熟能详的《魂斗罗》，以及 Steam 上的《返校》等等，都是横版 2D 游戏。

2. 俯视视角游戏

其次就是俯视视角的游戏。比如 FC 上的《勇者斗恶龙》《大航海时代》等等。这类的游戏提供一个空中俯视的视角给玩家，能很清晰地看到游戏中呈现的所有地图和区域。在区域中，主角可以给玩家呈现四个、六个或者八个方向的旋转和移动。在横版游戏中，玩家只能左右或者上下进行移动，而俯视角的游戏则可以移动四个、六个或八个方向。比如，在《GTA2》中，可以朝各个方向移动，并且通过缩放实现了主角的上下跳跃的视觉呈现。

3. 斜 45 度角的视角游戏（“伪 3D”游戏）

另外，还有斜 45 度角的视角游戏。我们看到大量的网页游戏、Flash 游戏、一些早期所谓的 2.5D MMO 游戏，都是这种类型的视角。这种视角下的游戏，也被俗称为“伪 3D”游戏。我们可以通过八个或者六个方向进行操作。地图和建筑物都是斜 45 度角的，做得好的游戏可以进行缩放，所以看起来就像是 3D 的画面。

事实上，到了 2D 斜 45 度角的后期，我们都使用 3D 建模，然后把它转成 2D 图形来进行制作。因为在 2D 角度下，有一个很细节的问题，那就是左右手脚的交换。也就是说，如果角色右手拿着武器，往左边走，当它在往右侧走的时候，为了节省资源和编程的方便，会对人物进行镜像绘制，这就变成了左手拿着武器了。

所以，到了后期的 2D MMO 游戏中，我们会通过 3D 建模，制作大量的非镜像图片来契合左右手的限制。比如主角骑的坐骑中，左右脚被坐骑遮挡，我们可以通过 3D 建模将各种模型的脚和坐骑制作在一起，以解决左右脚或者装备被遮挡的问题。

不管是开发 2D 游戏还是 3D 游戏，你首先需要理解什么是游戏引擎，这一点我会在第三节的时候着重介绍。以目前的技术手段，开发一款完整的游戏已经不是特别难的事情，只要你有一款强大的引擎，然后将焦点 focus 在你所要实现的逻辑上，只需要配置一些图片、关卡、音乐，就能很快地实现一款游戏。

但是，如果你不明白游戏的总体开发逻辑和实现细节，在开发过程中出现了问题将会变得很棘手。如果你了解和明白了游戏开发的本质、网络传输的基础知识，那么对于其他游戏的开发，比如移动端，比如 HTML5 端，比如小游戏的开发，则会变得更容易。因为，知道了游戏开发的底层知识和开发逻辑之后，几乎所有的游戏都是以这样的结构和逻辑进行开发的。

小结

以上是我今天分享的全部内容，你只需要记住一个理念即可：

不管何种类型、何种平台的游戏，其开发的顺序和手段几乎是一样的。而对于初学游戏开发的人来说，首先需要掌握的是开发流程，在有了一定基础之后，再学习细节的知识，就会如虎添翼。

最后，给你留一个思考题吧。在游戏开发中，很多游戏公司都会先出一个游戏 Demo，请问游戏 Demo 在游戏的开发流程中扮演一个怎样的角色？

欢迎留言说出你的看法，我在下一节的挑战中等你！

第2讲 | 2D游戏和3D游戏有什么区别？

我们知道，游戏的体现形式最主要是 2D 和 3D。最近比较流行的 AR/VR 等，都是属于 3D 类的体现形式。关于 VR 游戏，我会在之后的“热点剖析”模块详细介绍，这里就不多说了。

最初的游戏，2D 是绝对的主流。虽然现如今 3D 游戏大行其道，但是 2D 游戏还是拥有一席之地。比如我们上一节提到的《返校》，日本光荣公司前几年仍然在推出 2D 版的《三国志》，以及最近暴雪又复刻的《星际争霸 1》高清版。

2D 游戏和 3D 游戏的区别

我在前面已经讲过我为何选择 2D 游戏作为讲解的对象，也阐述过手机游戏开发的难点。至于 3D 游戏和 2D 游戏具体的区别，我还需要再和你讲一讲。

呈现画面

3D 和 2D 游戏最大的区别就是，游戏呈现画面的方式有了质的改变。3D 较之 2D 画面有更强大的冲击力和震撼力，就好像 VR 较之普通 3D，对人们视觉的冲击力和震撼力也是质的飞升。

所以，要做出一款好的 2D 游戏，要比 3D 游戏下更多的功夫，特别是在游戏的内涵、创意、音乐和细节上面。因为 2D 游戏先天只能呈现 2D 画面，所以玩家在入手游戏后，会很快对游戏有一个总体的评价，因此，开发者会想方设法留住用户。而一些 3D 游戏创意和游戏内容其实都不是非常优秀，只是由于画面突出，效果卓绝，也能吸引不少玩家战斗到最后一关。

文件体积

2D 游戏的体积基本控制在 1G 以内。因为 2D 游戏本身的图片量并不是很大，图片体积也就不会很大。而 3D 游戏的体积基本都是 2~4G，现在甚至会有 30G、40G，甚至 60、100 多个 G。那是因为 3D 模型、骨骼、动作、贴图等资源占用了大量的空间，高清贴图动辄几十甚至上百兆都是正常的体积，所以 3D 游戏比 2D 游戏体积大这是很重要的一个方面。

“伪 3D”游戏

我们熟知的 3D 游戏，有广义、狭义之分。狭义的 3D 游戏是指 Polygon 游戏，也就是多边形游戏，需要靠纯 3D 计算技术实现操作。而广义的 3D 游戏，则包括了“伪 3D”游戏。所谓伪 3D，其实就是 2D+3D，也有人称之为 2.5D。其实就是将 2D 和 3D 技术结合起来，实现 3D 游戏的体验。

类似《暴力摩托》这样的早期 3D 游戏，其实就是“伪 3D”，它基本上是通过模拟计算和光线跟踪（Raycasting）算法做出来的。因此，2D 游戏引擎无法制作狭义的 3D 游戏，而 3D 游戏引擎则可以做 2D 游戏。

比如，《梦幻西游》可以称作“伪 3D”，而《暴力摩托》、早期的赛车游戏、《DOOM》等，都可以归类为广义的 3D 游戏。

我们继续拿《暴力摩托》，或者更早期的赛车游戏来举例。

这类游戏的内存中，存在两种甚至多种主角赛车的形态图像，比如近景摩托车、远景摩托车。在判断速度大小之后，我们可以对图像进行替换，以此让玩家感觉到远近；背景和地面则是按照线条来处理，也就是将一副背景图片分成 N 条线，而第 N+1 条线会比第 N 条放大一倍，以此达到给赛道设置不同远近的效果；根据玩家赛车的前后位置，来判断绘制每一行的“放大”。而玩家控制的赛车则是一个活动块。每个活动块都存放有不同远近大小的图像形态，来控制前后图像大小。

国外有不少专门研究这类“伪 3D”技术的网站，我在这里推荐几个，你有兴趣的话可以去看看。 http://www.extentofthejam.com/pseudo/ http://lodev.org/cgtutor/raycasting.html https://codeincomplete.com/posts/javascript-racer-v1-straight/

那么，用 3D 游戏引擎制作 2D 游戏究竟是怎么回事呢？就是将 3D 游戏的视角和控制锁定在只有 2D 能呈现的范围内，让玩家误以为是 2D 游戏，但是这类游戏可以借助 3D 引擎来优化游戏的声光和绚丽的特效，让 2D 游戏更大放异彩。

再者，在移动端的表现上，2D 游戏其实和 3D 游戏不相上下。

首先，利用小屏幕观看 3D 画面非常累。由于手机计算能力有限，所以目前移动端的 3D 游戏大部分都还是以低模为主。浅显地讲，高模就是相当精细的模型和贴图，由无数个面（看需求）组成的模型，而低模一般会为了计算效率考虑控制其 3D 面数，贴图也相对简单。

其次，在移动设备上，因为移动平台的硬件限制，3D 引擎的表现一定不如电脑或者主机游戏来得好。这样一来，2D 游戏其实有非常大的施展空间。

2D 游戏和 3D 游戏在电子竞技中的区别

我们再来看看电子竞技这一块。

我们都知道《星际争霸》在前几年一直是韩国的“国技”。《星际争霸 1》的选手水平占据着全球巅峰位置。2015 年之后，由于圈内丑闻加上《星际争霸 2》的出现，《星际争霸 1》的三大联赛几乎销声匿迹。然而《星际争霸 2》却没有想象中那么火爆。虽然有《DOTA》《LOL》的泰山压顶之势，但总体原因除了《星际争霸 2》的平衡性外，还有一个原因是，《星际 2》的 3D 画面并不完全适合电视转播。

我们拿《星际争霸 1》的 2D 画面和《星际争霸 2》的 3D 画面进行比较。这里，第一幅图是《星际 1》的画面，第二幅图是《星际 2》的画面。

你可以看到，3D 画面几乎要在高清画质的情况下才能看清楚单位兵种、战斗场面，而且很多时候都是一团黑漆漆、绿乎乎的 3D 画面堆在一起。《LOL》看似没有这种情况，是因为大家都熟悉每个英雄的形象，加上地图比较明亮，兵种没有《星际争霸》那么多。

我们再来看《星际争霸 1》的画面，层次分明，清晰可辨，哪怕在低画质的情况下仍然可以一眼看出究竟是什么兵种，有的时候，甚至连地雷都能看清楚。当然，这并不是说 3D 比不过 2D，而是在某些特殊情况和场景下，3D 图形会有一些缺陷。

由于表现方式不同，3D 游戏最典型的就是比 2D 游戏的坐标多出一个 Z 值。所谓的 Z 值，就是除了 X、Y 的长宽之外，还多了一个高度值。这个值我们也可以用在 2D 游戏中。

在 PS 时代，有不少 2D 和 3D 结合在一起的游戏。比如，《生化危机》系列的 1、2、3 部，都属于 2D 静态场景、3D 模型。再比如，《最终幻想》系列的 7、8。玩家在世界地图行动的时候，使用的是 3D 画面，但是在游戏主场景里面，使用的仍然是《生化危机》那种 3D 建模。2D 渲染场景使用伪 3D 技术，一般是因为机能限制或者 3D 技术不成熟，但实际的实现难度不会比纯 3D 技术更简单。

3D 游戏和 2D 游戏在美术上的区别

从美术方面来说，3D 游戏和 2D 游戏的区别也相当大。

在 3D 建模和骨骼方面，一个熟练的美工，只需要在原有模型上添加或者修改某些物件。比如给某个生物加一个角，给它增加一个小小的动作。这样模型的修改，对于美工来说，是比 2D 美术要简单的。因为 2D 美术一般都是手绘，所以如果需求改动特别明显，那一系列跟该角色相关的图案都需要手工调整，这是非常大的工作量。

准确地讲，2D 游戏是 2D 图像图形进行线性的一些变换将之渲染至屏幕而成。而 3D 的游戏是将 3D 的图形图像进行 3D 线性变换然后投影（Projection）至显示器（显示器是 2D 的）而成。显示器在视觉上形成远近大小的效果，让我们看起来是立体的。事实上，我们眼睛的视网膜也是这么工作的。

游戏的本质

说了这么多 2D 游戏和 3D 游戏的区别，最后，我想跟你谈一点轻松的内容。游戏的本质究竟是什么？不管是作为开发者，还是策划、运营人员，或者是打游戏的玩家，我想请你思考一下这个问题。

有人说，游戏的本质是人类的层次需求，并且还进行了细致的分析。其实不需要这么复杂，要我说，游戏的本质就是供人娱乐，给人各种开心的、刺激的、恐怖的感官刺激。

有些人喜欢复杂策略类的游戏，那么繁琐复杂的《太空帝国》就能满足他们。有些人喜欢快节奏的游戏，那么赛车或者打一局就走的《FPS》可以满足你的胃口；像我就喜欢悠哉悠哉地不受任务限制，那么“GTA 系列”和“老滚系列”就是我的选择。

那如何定义一款成功的游戏呢？如何做出一个爆款游戏呢？我想说的是，一款游戏的成功离不开美术、音乐等等这些硬指标，然而这些指标却不能决定这款游戏是否能深得人心。每一个人心中的判断并不一样，大量的人玩《王者荣耀》，但这并不代表它是优秀的游戏，只能说是成功的游戏。毕竟，青菜萝卜各有所爱。

小结

这一节内容差不多了，总结一下，你需要记住这几点：

3D 和 2D 游戏的区别主要体现在呈现画面和文件体积上；

借助 3D 引擎可以提升 2D 游戏的声光和特效效果；

成功的游戏不一定是优秀的游戏。

最后，给你留个思考题吧。你觉得一款成功的游戏需要具备什么样的特点呢？

欢迎留言说出你的看法，我在下一节的挑战中等你！

第3讲 | 游戏的发动机：游戏引擎

估计长期玩游戏的玩家，都能脱口而出几款游戏引擎的名字，比如“虚幻”“寒霜”“狂怒”等等。那你能说说，游戏引擎究竟是什么吗？它在游戏开发中究竟起着什么样的作用呢？

游戏引擎是什么？

汽车没有发动机就无法跑起来，人类没有心脏就会死亡。而引擎就是发动机，就是心脏。

我们先来看一下维基百科对于游戏引擎的定义：

游戏引擎是指一些已编写好的可编辑电脑游戏系统或者一些交互式实时图像应用程序的核心组件。这些系统为游戏设计者提供各种编写游戏所需的各种工具，其目的在于让游戏设计者能容易和快速地做出游戏程序而不用由零开始。大部分都支持多种操作系统平台，如 Linux、Mac OS X、Windows。大多数游戏引擎包含以下系统：渲染引擎、物理引擎、碰撞检测系统、音效、脚本引擎、电脑动画、人工智能、网络引擎以及场景管理。

这个概念是不是看起来还是不太好理解？我来具体解释一下，游戏引擎到底是什么，以及它究竟能干什么。

游戏引擎就是图形引擎。准确地说，游戏引擎约等于图形引擎。玩家口中所说的“虚幻”“寒霜”等，这每一款引擎对于图形渲染的处理方式都不同，所以用某个引擎编写出来的游戏，具体的表现画面也会不同。比如，有的引擎编写出的光影效果特别绚丽，有的则粒子效果特别真实。

游戏引擎是一整套游戏解决方案。其实，游戏引擎并不仅仅等同于图形引擎。图形引擎只是游戏引擎中一个占比极大的组成部分。一款好的游戏引擎，不仅要看它对于图形图像的处理能力，也要看它对于其他部分的处理能力，比如对音频、音效的播放、键盘鼠标的处理，以及 UI 界面的编辑和各种处理工具的提供。这里的处理工具包括地图编辑器、关卡编辑器、人物编辑器、资源编辑器、物理碰撞、碰撞检测等。所以，从专业的角度讲，游戏引擎是完成开发一套游戏的解决方案，而不仅涉及图形部分。

游戏引擎是一整套编程接口。要写游戏就必须写程序，所以有游戏引擎就一定需要编程。不管你是直接面对引擎编写代码，还是在引擎提供的编辑器（比如关卡编辑器）基础上编写简单的逻辑代码，写代码都是必不可少的。在引擎提供的编程接口上，你能很容易地调用各种接口完成游戏的开发。

游戏引擎是怎么工作的？

说完了游戏引擎是什么，我们来看游戏引擎具体是怎么工作的。

我先从代码层面来说。这里是一段伪代码：

int DrawLine(const Surface& s, int sx, int sy, int fx, int fy, int fit, const Color& color);

这段伪代码提供了一个在屏幕上画一个线条的函数。该函数提供了 7 个传入参数，分别是：

目标图层

线（在游戏窗口内）的起始 x 点

线（在游戏窗口内）的起始 y 点

线（在游戏窗口内）的终止 x 点

线（在游戏窗口内）的终止 y 点

线条粗细

线条颜色

在调用了这个函数之后，你就能很容易在屏幕上画出一个线条。同时，在这个函数背后，引擎做了一系列这样的动作：

判断传入的 Surface 图层对象是否正确、是否存在；

判断传入的起始点、终止点是不是存在负数；

判断颜色是不是正确；

拆分 Color 变量为 RGB 值，填入到 SDL 绘图接口，调用 SDL 绘图接口绘制一条线。

关于绘图接口，我会在下一节的内容中详细讲解。这里，你只需要知道，画线的接口函数在背后分解、组合、计算，然后会将绘制工作交给底层绘图接口就可以了。

之所以要拿这个画线函数举例，是因为它展示了引擎中函数的使用方式。如果我们用一般的画线函数画圆，那首先要知道圆的直径和计算方式；如果画矩形，那就要知道哪里是起始点和终结点。这一系列动作等于将引擎所做的工作包含在了画线函数里，你只需要关心画线的这一系列参数如何使用就可以了。

说完了代码层面，我们来说说非代码层面的东西。

游戏引擎其实也包括游戏开发的一系列工具，也就是诸如地图编辑器、关卡编辑器、人物编辑器、资源编辑器等。美术、策划、制作人等，这些开发流程中的责任人，可以往工具内填入需要的东西，制作出需要的内容。

我们拿人物编辑器来举例。如果我们使用通用编辑工具（比如 3DMAX、Maya 等）编辑出来内容，需要经过格式的转换才能应用到游戏中，但是这样不仅耗费转换时间，也耗费调试时间。因为通用工具可能并不适合引擎本身所定义的格式，需要不停修正和更改。如果引擎本身就提供了这样的制作工具，那么制作出的内容直接就能在游戏中使用，不需要转换，所见即所得。

所以，游戏引擎背后的工作方式是：

在代码层面，游戏引擎是对绘图接口、操作系统、音频等接口进行的代码层面的封装；

在工具层面，游戏引擎是一整套游戏内容的制作工具，方便你制作针对这个引擎的游戏内容。

自己开发引擎还是直接购买？

了解了游戏引擎的概念和工作方式，那么开发者究竟是自己编写引擎还是购买商业引擎比较好呢？

从一般意义上讲，如果有实力，当然是自己开发引擎更贴合公司的情况。然而，这样付出的代价就是，花费大量的人力和财力，而且，以一般小公司或者起步阶段的开发者的开发水平，编写出的引擎，很可能只是一个半成品或者问题多多的残次品。自研引擎并不是一个不可能完成的任务，但是要看公司的财力和程序员的实力。

购买商业引擎是现在大部分公司都会走的一条路。购买商业引擎只需要花费一笔钱就能拿到成熟的游戏引擎，直接可以开发游戏，然而付出的代价就是，你可能需要从头开始学习这套引擎的工作原理、工具套装。

如果你非常熟悉 3DMAX 等通用工具，那要从头学习开发工具，就会产生许多问题，比如：

引擎中包含的开发工具基本没有通用性可言。就算吃透了工具，对你今后的能力提升和职业规划也没有明显的帮助；

如果引擎升级或者更换引擎，就需要从头再学一次工具，会耗费大量时间和精力。当然会有一些优秀的商业引擎支持通用工具制作的内容导出和转换，直接在引擎中可以使用，当然这种引擎的购买费用也会更高；

另外，商业引擎本身也会存在一些隐藏得很深的 bug。在游戏发布后，这些 bug 可能会影响到游戏本身的质量和口碑。引擎出问题，游戏一定出问题，这也是购买商业引擎需要考量的一个风险。

但是，在实际的开发过程中，考虑到公司的经济实力和程序员的开发能力，一般来说，大多数开发者都会选择购买商业引擎，毕竟这在一定程度上，是个非常省时省力的事情。

游戏引擎是用什么编写的？

尽管如此，你还是需要掌握更多游戏引擎相关的知识。因为，不管你是购买游戏引擎还是自己开发游戏引擎，了解游戏引擎，会让你对游戏编程的总体脉络有一个了解。一旦在开发过程中出现问题，比如你发现屏幕贴图出问题了，那究竟是自己的代码出现问题，还是引擎本身出现问题呢？熟知游戏引擎的开发逻辑，你就能很快定位问题所在。

事实上，游戏引擎并没有一种固定的开发语言，就看你所制作游戏的目标平台是什么。

1.C/C++

如果你在 Windows 或者 Linux 下开发游戏，游戏引擎 99% 都是使用 C/C++ 或者汇编语言编写。由于 C/C++ 和汇编运行效率高，所以在 Windows 下的执行效率也非常高。你看到的 Python 游戏引擎、Ruby 游戏引擎等脚本语言引擎，都是在 C/C++ 的基础上进行封装的。这样可以方便程序员将专注力放在游戏逻辑上，而不是在处理底层问题上。

2.JavaScript/TypeScript

如果是 HTML5 游戏，游戏引擎的编程接口 99% 是使用 JavaScript 完成的。比如耳熟能详的 Cocos2d 引擎、白鹭引擎等等。至于其他配套的开发工具，可以使用任意软件开发语言进行编写。

3..NET

经过微软的努力和版本迭代，在 Windows 下，.NET 的运行效率和开发效率已经提高了好几个等级。由于和 Windows 紧密结合，现在的.NET 的运行效率只比 C/C++ 编写的代码低一点。在电脑配置比较高的情况下，用户基本不会有太多的感知。而.NET 对于 Windows 底层的调用和控制，比 C/C++ 更方便，编程也更容易，所以现在已经出现了一些引擎是使用.NET 编写的。这对于.NET 开发者来说是一件好事。

4.Java

和.NET 一样，使用 Java 编写的游戏引擎并不多。虽然 Java 的运行效率已经有了质的提升，但是对于编写大型游戏来说还是有相当大的瓶颈。随着电脑硬件配置的提升，使用 Java 编写游戏也不再是一件不可能的事情。比如大火的《我的世界》，就是使用 Java 编写的。

小结

好了，这一节内容差不多了，总结一下，我主要给你分享了四个内容，分别是游戏引擎的定义、游戏引擎的工作方式、游戏引擎的选择和编写。

你只需要记住以下这些内容即可：

游戏引擎是一整套的游戏开发程序接口和开发组件，可以让你更方便、更专注于游戏的开发；

游戏引擎控制着游戏的几乎所有内容，游戏的表现质量和游戏引擎直接相关；

针对不同的平台，游戏引擎所使用的编程语言也不一样。而了解了游戏引擎的编写，在之后的开发过程中，会有非常多的便利。

最后，给你留一个思考题：

什么情况下，我们可以跳过游戏引擎，直接编写一款游戏呢？

欢迎留言说出你的看法，我在下一节的挑战中等你！

第4讲 | 底层绘图接口的妙用

上一节，我给你介绍了游戏引擎的概念及其在游戏开发中所起的作用。同时，我也提到了游戏引擎背后的工作方式。在代码层面，游戏引擎是一套对底层绘图、音频、操作系统接口的封装实现。

在此基础上，我还举了一个在游戏屏幕上画线条的例子。在这个例子中，画线的接口函数在背后分解、组合、计算，并将绘制工作交给底层绘图接口。这个绘图接口，就是今天要讲的内容。

几种常见的绘图接口

前面我已经说过，我会针对 2D 游戏来讲解游戏开发的流程和细节，所以，这里我先介绍几种 2D绘图接口（即API，全称Application Programming Interface）。我选择了 5 种 Windows 下最流行的绘图接口，分别讲解。

1.OpenGL

OpenGL 是老牌的图形图像接口。GL 是 Graphics Library 的缩写。所以，顾名思义，OpenGL 就是开放图形接口的意思。和接下来要讲的 DirectX 一样，OpenGL 也可以创建和渲染 2D、3D 图形。但是，和 DirectX 不同的是，它可以在多种平台下运行，比如 Windows、Linux、macOS 和部分 UNIX，而 DirectX 只能在 Windows 生态下运行。

OpenGL 本身只提供图形渲染接口，如果你需要别的功能，比如音频、鼠标、键盘的操作，甚至是创建一个窗体，都需要别的扩展库支持。

2.DirectX

说起 DirectX，这个名字已经如雷贯耳。DirectX 的开发初衷，是为了让游戏开发者能像在 DOS 平台编写游戏一样，在当时新的 Windows 95 平台上，也能一样高效、快速地操纵各种硬件设备。

其实，在 DirectX 发布之前，微软已经将 OpenGL 包含在 Windows 系统里面。随着时间的推移，OpenGL 逐渐成为了行业标准，而 DirectX 自然免不了与其展开竞争。

这里，我主要介绍一下 DirectX 中的两个核心组件。这两个核心组件的功能与 2D 游戏编程息息相关，你一定得了解一下。

第一个是DirectDraw。它是早期 DirectX 中掌管 2D 部分的组件。DirectDraw 类似我之后要说的 GDI，支持显存位图，而不是只能将位图存放在内存里，所以 DirectDraw 更贴近硬件。但是在 DirectX 7 版本之后，DirectDraw 被合并到 Direct Graphics 组件中。虽然目前仍有很多人在使用 DirectDraw 的老版本开发包，然而 DirectDraw 已经被微软逐渐淘汰。

第二个是Direct2D。它是微软推出的最新 2D 组件，它的出现是为了取代 Windows 下的 GDI、GDI+ 和 DirectDraw。Direct2D 能通过硬件加速来绘制 2D 图形，也支持高质量 2D 图形渲染，比如支持 ClearType 呈现的方式、除锯齿、几何位图的绘制和填充等等。

3.SDL

SDL 全称Simple DirectMedia Layer，直译就是简单的直接媒体层。从严格意义上来讲，SDL 并不算是“独立的”图形渲染接口，因为它将各类操作系统的图形图像渲染接口进行了封装，包装成统一的函数，以此来方便调用。比如，在 Windows 下，它封装了 DirectX 和 GDI+；在 Linux 下，它封装了 Xlib 等等。同时，它也提供了 OpenGL 的调用函数。

SDL 不仅仅可以对现有图形图像接口进行封装，它也提供 SDL 官方自己发布的编程接口。比如，SDL_image、图像接口、SDL_net、网络接口等等。后续我将介绍到的 Pygame，其背后就是调用 SDL 编写的。

Pygame 是使用 Python 封装的游戏库，你可以很方便地利用 Pygame 进行 2D 游戏的编写，它的背后，调用的就是 SDL 的接口。所以我们将利用 Pygame 来对 2D 游戏开发流程做一个完整的梳理。虽然网上关于 Pygame 的代码和教材很多，但是我们要讲的，不仅仅是 Pygame 代码是如何编写的，而是要从 Pygame 的代码中，分析 2D 游戏的编写逻辑和编程思想。在这个过程中，Pygame 只是一个载体。

4.GDI

GDI，全称Graphics Device Interface，也是 Windows 下的图形设备接口。它所做的就是处理 Windows 程序的图形输出，负责在 Windows 系统和绘图程序之间进行信息的交换。使用 GDI 的人已经越来越少，从编程的方便性和硬件加速等功能来看，GDI 被 GDI+ 取代是显而易见的。

5.GDI+

在 Windows 下，大部分接触过图形编程的程序员都会用过 GDI+。而 GDI+ 其实就是 GDI 的进阶版本。

GDI+ 是有硬件加速功能的，而 GDI 没有；GDI 是以 C 语言接口的形式提供的，而 GDI+ 则是 C++ 和托管类的形式提供；从接口代码的层次上说，GDI+ 对程序员更友好，使用起来也更顺手。

GDI+ 还提供了图像处理的接口，比如提供了 Image、Bitmap 等类，可以用于读取、保存、显示，操作各种类型的图像，比如 BMP、JPG、GIF 等。

GDI 和 GDI+ 的绘图操作也存在差别。GDI 中存在一个称为“当前坐标”（MoveTo）的位置。“当前坐标”的存在是为了提高绘画的效率。

我还拿画线的过程来举例。有一条新的线连着一条老的线画，如果有了“当前坐标”的设置，逻辑上可以避免每次画线都要给出两个点的坐标（开始和结束）；如果每次都以该“当前坐标”做为起始点，线条绘制结束后，线的结束位置就成为“当前坐标”。

事实上，这种方式的存在是有历史原因的。有一种说法来自很早的 Logo 语言。这种语言针对儿童进行寓教于乐的编程教育。它的绘画逻辑是，如果有“当前坐标”这个概念，只需要一个递归就可以不停地画线，最终组成一个图形。所以后期很多的绘画接口都沿用这种方式去做。但实际到了 2000 年左右，人们发现这种方式并不方便，因此 GDI+ 取消了这个“当前坐标”。

一个原因是不方便；另一个原因是，如果无法确定“当前坐标”，绘图就会出现差错。而用 GDI+ 绘制线条，则可以直接在 DrawLine 函数中指定起始点和结束点的坐标位置。

如何直接使用绘图接口进行游戏开发？

通过上面的介绍，你是否对 Windows 下几大流行的绘图接口有了大致的了解呢？接下来你或许会问，那我了解这些图形接口的编程接口后，是不是就可以直接用这些接口进行游戏的开发呢？

答案当然是可以的。由于 SDL 的开发便利性和通用性，所以我拿 SDL 编程接口作为例子，来阐述一下究竟怎样通过图形接口直接进行游戏的开发。

从最基础的开始，我们先要从 SDL 的网站下载 SDL 的最新版本，下载网址是： http://www.libsdl.org/download-2.0.php （写作这篇文章的时候，最新的版本是 2.0.8 稳定版）。

在下载的网站页面，我们可以看到 Source Code 一栏，这是 SDL 的源代码。有一定编程基础的同学可以下载源代码，直接使用 VC++、MinGW 等编译器进行编译，编译完的头文件和库文件直接就可以使用。

如果你对编译不熟悉，可以选择下载 Development Libraries，也就是编译完成后的开发包。网站已经将 Windows 下的开发环境分为 VC++32 位版和 64 位版、MinGW32 位版和 64 位版。为了教学方面和统一，也照顾各种平台的用户，我建议使用 MinGW 的 32 位版。因为 64 位 Windows 可以兼容 32 位的应用。至于 MinGW 编译器和 IDE 的下载安装细节，我将会在后续的专栏文章中介绍。

下载完成后，将压缩包解压缩到任意目录，头文件和库文件使用解压缩出来的“i686-w64-mingw32”这个目录下的“include”和“lib”。

接下来，我们在 IDE 中设置 include 路径和 lib 路径，链接程序的时候需要在 IDE 设置包含库文件 libsdl.a、libsdlmain.a，就可以开始在 IDE 中编写代码了。

在开始开发的时候，首先使用 SDL_Init 来进行初始化。用这个方法传入一个 unsigned int 类型的参数，参数列表就像这样：

其中“初始化所有系统”这个选项，除了“忽略任意错误”外，包含了以上所有不同的初始化系统，一般使用 SDL_INIT_EVERYTHING 即可。

随后，我们要使用 SDL_CreateWindows 来创建一个窗体。SDL_CreateWindows 支持六个参数，分别是：窗体名称、在 Windows 屏幕显示的 x 坐标、在 Windows 屏幕显示的 y 坐标、宽、长、显示方式。

然后将使用 SDL_CreateRenderer 创建一个 SDL 的渲染器（SDL_Renderer）。渲染器的参数是：

随后可以使用 SDL_RenderClear 来清空 SDL 渲染器、使用 SDL_RenderPresent 方法将渲染的结果显示出来。然后我们需要建立一个大循环，在这个循环内，你可以把 SDL 支持的图形图像函数或者其他逻辑代码往里面填写，完成游戏的程序内容，具体的操作我会在之后的文章详细介绍。

在这个大循环内，我们要用到 SDL_Event 事件系统。在循环内捕捉用户事件，比如要退出这个循环就必须点击右上角的 X 关闭按钮才行。如果你点击了 X 按钮，就会被 while 内的 event 事件捕捉到，并且匹配是不是退出事件，如果是退出事件就退出程序。

最终退出程序的时候，使用 SDL_Quit 清除资源退出程序。

我们结合这张流程图来看一下将这些内容串联起来的代码：

#include  <SDL.h>

int main(int argc,char *args[])

{

    SDL_Window* window;

    SDL_Renderer* render;

    SDL_Event e;

    bool q = 0;

    int sdl=SDL_Init(SDL_INIT_EVERYTHING);

初始化完成后，我们要建立窗体，并编写后续的步骤：

if(0 <= sdl ){

        // 当 SDL 初始化完成后创建一个标题为 "SDL Window" 的窗口, 窗口对齐方式为居中对齐，分辨率为 640x480 的窗口

        g_pWindow=SDL_CreateWindow("SDL Window",

            SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,

            640,480,SDL_WINDOW_SHOWN);

        if(0 != window)

            render=SDL_CreateRenderer(window,-1,0);

    }

    SDL_SetRenderDrawColor(render,0,255,255,255);

    SDL_RenderClear(render);

    SDL_RenderPresent(render);

接下来是游戏主循环的内容：

while( 0 == q )

    {

       while( 0 != SDL_PollEvent( &e ) )

                {

                    // 检测到用户需要退出

                    if( e.type == SDL_QUIT )

                        q = true;

                }

    }

    SDL_Quit();

    return 0;

}

这个简单的例子说明了如何直接利用 SDL 接口编写游戏。直接利用其他图形接口编写游戏，也基本是这样的步骤。

小结

我来给今天的内容做一个总结，你只需要记住这些内容即可：

绘图接口绘图接口其实就是使用 C/C++ 语言或汇编语言，通过操作系统的底层，调用诸如显卡、内存这些绘图设备，最后做成接口；

SDL 拥有统一封装的绘图接口，你可以在各个平台无缝编译和使用。

现在，你是不是对游戏开发的一部分流程有点了然于胸了呢？

给你留个小思考题吧。

我们提到了直接利用绘图接口编写游戏，请问，如果这样，还需要“游戏引擎”吗？如果需要的话，这个“引擎”应该放在哪里呢？

欢迎留言说出你的看法，我在下一节的挑战中等你！

第5讲 | 构建游戏场景的武器：地图编辑器

今天，我想和你聊一聊基础知识模块的最后一部分，地图编辑器。为了给之后的内容铺垫，我还会顺带介绍一些游戏 Demo 的知识。

我们先来看一幅图。这看起来是一款 FPS 射击游戏对不对？也对也不对。说对，因为这确实是一款 FPS 游戏；说不对，这其实只是一幅地图，但是这幅地图来自《魔兽争霸 3》。更准确地说，这是使用《魔兽争霸 3》的地图编辑器制作出来的一幅游戏地图。

《魔兽争霸 3》发布之初，没人能想到并且能做出这样“变态”且“不走寻常路”的地图。《魔兽争霸 3》的地图编辑器之强大，直到它发布五、六年后，才有人开始制作出这样令人匪夷所思的地图。

地图编辑器在游戏开发的前期扮演着非常重要的角色，它的主要功能就是用来制作游戏地图。 因为我们现在是一个人来开发一款游戏，不涉及分工的问题。而常规的团队在进行游戏开发的过程中，用地图编辑器制作地图是由美术来负责的。

地图编辑器本身一般会由专业的地图编辑器程序员来开发完成。地图编辑器可以使用原生引擎所支持的语言来编写，比如 C/C++；也可以使用引擎加嵌入脚本语言的方式来进行编写，比如 Lua 等。但是现在，一些成熟的游戏引擎都会自带地图编辑器。所以，大多数时候地图编辑器不需要你自己开发。

地图编辑器有哪些种类？

地图编辑器并非千篇一律。由于游戏类型的不同，地图编辑器也不相同。比如我们做一个《大富翁》类的游戏，我们就需要做一个斜 45 度角的地图编辑器。

如果是俯视角度的游戏，地图编辑器的视角呈现方式也类似斜 45 度角的游戏。这两种编辑器制作地图的方式极其类似，唯一不同的是，斜 45 度角是用菱形的地图块拼接而成，俯视视角是用矩形的地图块拼接而成的。

我们做一个横版卷轴游戏，就需要在地图编辑器内摆放横版的地图。如果地图是重复地图，类似《管道鸟》，那就需要程序员重复贴图，而不需要在地图编辑器重复编辑相同的地图。

3D 游戏就会有 3D 地图编辑器，就像前面的《魔兽争霸》编辑器，就是一个 3D 视角。地图编辑器的种类不同，保存的地图类型和文件结构也不一样。有些地图编辑器保存的是图片和位置，而有一些保存的却是一个脚本文件，方便直接载入游戏程序。

为什么要了解地图编辑器的编写过程？

我刚才说过，大多数时候地图编辑器不需要你自己开发。但是我们依然要了解地图编辑器是如何编写出来的，至于了解这些究竟有什么用呢？我继续来给你讲。

1.编写地图编辑器的过程，能让你更好地了解游戏引擎。 从编写地图编辑器开始，就是对游戏引擎的一次全面应用。为什么这么说呢，因为在编写地图编辑器的过程中，除了音乐模块或许暂时不会被用到外，利用引擎编写的地图编辑器、地图资源的载入代码、保存代码等等，都会被用在正式的游戏中。

2.编写地图编辑器的过程，可以优化游戏开发流程。 程序员之间会讨论关于资源载入、游戏内容加载和读取这些基础问题，并将最基础的流程完整地梳理一遍。在游戏正式开始开发之前，把这些流程梳理一遍，能极大提升开发过程中的流畅性。

3.编写地图编辑器的过程，涉及 UI、按钮和键盘事件的使用。地图编辑器开发完毕后，主要是给美术人员使用，所以 UI 的点击、鼠标键盘事件是必不可少的。在地图编辑器中运用 UI、按钮、键盘事件后，在正式游戏开发中，这些代码可以直接移植到游戏代码中使用。

4.编写地图编辑器的过程，能起到团队磨合的作用。 在编写的过程中，你会接触到策划、美术等很多人的意见，程序员之间也会对引擎的应用有很多的讨论。

地图编辑器与关卡编辑器

说到地图编辑器，不得不提到关卡编辑器。关卡编辑器是一种主要给策划使用的编辑器，它涉及地图中的关卡内容。 一般情况下，关卡编辑器不涉及具体的地图细节，但是它涉及总体的游戏地图和游戏流程。

简单来说，地图编辑器的功能就相当于房屋的结构朝向图。我们可以从这张图里，看到每个房间的俯视图。里面包括门的位置、阳台的位置，甚至厕所的位置。而装修公司设计家居，就相当于关卡编辑器的功能。设计师会在你已有房间的结构内，摆放各种桌子、椅子、床、柜子、灯泡等具体的家居用品。

在实际开发过程中，很多时候，只有一些成体系的大游戏才会把关卡编辑器单独分出来。很多时候，对一些小游戏来说，关卡编辑器和地图编辑器经常是在一起的，你可以在地图编辑器里面直接来编写关卡。这张 《坦克大战》的地图编辑器中就包含了关卡编辑器的内容。

这里有一个 3D 游戏的关卡编辑器的界面。我们常见的关卡编辑器的界面一般都是这样的。这个界面最左侧的对话框，是物件的选择界面，具体包括关卡内物件的选择和摆放；界面右侧的对话框是属性选择界面，定义这个物件的属性，比如这个物件是否可以弹跳、爆炸等等；界面右下角是物件 X、Y、Z 轴的编辑，可以自定义物件的拉伸和缩放。

我们拿 FPS 枪战游戏来举例。我们可以先在编辑器内画完所有俯视角的地图通路，然后在这个地方放上某种武器，在那个地方放上某种升级包。这就是一个关卡的总体架构。

随后，你需要在编辑器里编写一些简单的脚本程序。这些脚本程序会涉及玩家在进入游戏后总体的游戏进程和逻辑。比如，你可能会写“分数达到多少之后，等级 +1”“击杀敌人达到多少之后，分数 +10”“失血达到多少出现什么情况”等等。

总之，关卡编辑器和地图编辑器的功能，就好比家装设计图和房屋结构朝向图，一个是在地图内设置物件的摆放位置和具体的表现形式；一个是设计地图本身。

为什么要做游戏 Demo？

按照传统的标准游戏开发流程，在编写完地图编辑器之后，就要开发游戏了。事实上，开发游戏的初期，我们必须要制作这款游戏的 Demo。很多商业游戏制作之前都是要先做游戏 Demo 的。你或许会问，为什么不立刻开发一个正式的游戏呢？我总结了一下，有以下几方面的原因：

1.游戏 Demo 可以完成游戏核心框架。 绝大部分情况下，最终完成的游戏成品，都会和游戏 Demo 有较大的差异。这些差异体现在美术画面的优化细节、操作的优化细节、网络优化上等等。游戏 Demo 所肩负的任务，就是完成该游戏总体的核心内容框架。 在这个框架内，我们可以一窥游戏的总体逻辑、想呈现给玩家的是什么内容。

比如，《三国志》题材的游戏，游戏 Demo 就会呈现在大地图上进行城市内政的管理、军队的管理，以及战斗画面这些游戏的核心内容；比如 FPS 射击游戏，游戏 Demo 呈现出的就是，主角在一个有限的空间内进行射击和击杀敌人的操作，因为这些就是游戏的核心。至于游戏是不是好玩，画面是不是炫酷，音乐是不是好听，并不在游戏 Demo 中呈现。

2.游戏 Demo 可以测试 bug。 在游戏开发中，地图编辑器做完之后，会载入经地图编辑器制作的地图，这些地图在游戏中是否有 bug，是否需要调整，游戏体验好不好。这些内容在游戏 Demo 中将被测试出来，并且在正式开发中进行调整。

3.游戏 Demo 可以磨合开发团队。 前面我们提到，有一些引擎自带地图编辑器，所以大多数情况下，地图编辑器不需要你自己编写。这个时候，你敲下的第一行代码就是游戏 Demo 的代码，所以游戏 Demo 就肩负另一个使命，就是团队人员的磨合。和地图编辑器一样，第一次编写游戏 Demo，会有来自策划、美术、程序等各方面的意见和讨论，而制作游戏 Demo 的过程对磨合团队是非常有利的。

4.游戏 Demo 最终会被舍弃。 在开发过程中，Demo 版本是不稳定的，是有 bug 的。只要不是特别严重的，一般不会去进行特别的修正。但是一般来说，bug 会在正式版本中被修正。所以，游戏 Demo 肩负的另一个任务，就是“试水”。程序代码在游戏中跑起来是不是有瓶颈，bug 出现在哪里，哪些部分可以从 Demo 中提取出来，在正式开发中使用，哪些部分可以在正式版本中优化，这些都是游戏 Demo 所背负的“使命”。

很多开发团队为了节省时间，很多公司老板为了节省成本，在地图编辑器完成之后，就跳过了游戏 Demo 开发这个步骤，或者有些开发 leader 根本就不知道要做 Demo 这个流程，所以一开始就去做正式版。这是个很危险的动作。

因为按照流程，如果一上来就开始编写正式版本的游戏，很有可能会出现无法预估的 bug 和无法修正的问题。 比如，地图编辑器中保存的地图，在正式游戏中就出现极大的错误，连修正的时间都没有，最终修修补补急急忙忙地上线，玩家一片怨声载道。游戏死亡。

当然，由于游戏类型不同，地图编辑器、关卡编辑器和游戏 Demo，并非是必须要做的内容，就像我们要做的“打飞机”这样简单的游戏，甚至贪吃蛇、俄罗斯方块、象棋、围棋等等，这些都不需要任何编辑器，这些单一简单的游戏，开发流程简单，如果出现问题直接重新编写就可以了。

小结

这一节，我讲了地图编辑器、关卡编辑器和游戏 Demo 相关的内容，你只需要记住这些重点：

开发地图编辑器的过程可以帮助开发人员了解引擎、优化开发流程、测试 bug、磨合团队；

关卡编辑器之于地图编辑器，就好比装修设计图和房屋朝向图之间的关系；

游戏 Demo 的使命是完成游戏核心框架，测试地图编辑器等问题，Demo 最终会被舍弃；

在游戏开发的正规流程中，我们要经历地图编辑器、关卡编辑器、游戏 Demo、正式开发等几个流程，每一个流程都肩负不一样的任务。但简单如贪吃蛇类的游戏，我们可以直接上手编写代码。

最后，给你留一道小思考题吧。

用地图编辑器制作出来的地图有没有取巧的保存方法，让游戏程序读取地图时更方便？

欢迎留言说出你的看法，我在下一节的挑战中等你！

第6讲 | 从0开始整理开发流程

在第一模块的基础知识中，我已经讲过开发一款游戏需要的一些背景知识。对于 2D 游戏和 3D 游戏的区别、从程序到美术、从 2D、伪 3D 到 3D 等方面，我都进行了逐一地阐述。除此之外，对于任何类型的游戏开发都非常重要的三个概念：游戏引擎、底层绘图接口、地图编辑器，我也进行了一些我个人的解读。

有了这些背景知识，这一节，我来带你整理一下整个游戏开发流程以及流程中所需要的工具。

1. 选择开发环境

我们默认是在 Windows 环境下进行开发，至于是什么版本的 Windows 不需要做更多地阐述，你愿意用什么版本就用什么版本，因为几乎所有流行的 Windows 环境都能进行编程工作。至于我为什么选择 Windows 环境进行开发，那是因为：

首先，在 Windows 环境下，拥有比较方便的调试工具。不管是 Python 脚本语言还是 C/C++ 语言，都可以使用图形界面进行调试；

其次，Windows 下的IDE 开发环境也比其他平台更多，你拥有更多的工具可供选择。另外，在开发游戏的时候，你可以选择 OpenGL、DirectX 或者 SDL 等图形库进行编程。作为游戏开发，DirectX 几乎是不可或缺的标准，而我在第四节讲述底层绘图接口的时候说过，它是由微软提供的游戏编程接口，在 Windows 下提供了更为方便的底层调用。

除了 Windows 外，Linux 平台的图形显卡驱动几乎是不完善的，无法发挥显卡的最大优势。苹果平台又一家独大，开发人员只能为其定制专一的代码，开发难度比之 Windows 就大了不少。

2. 下载脚本工具

在开发过程中，我们需要用到 Python、Lua 或者 Ruby 等脚本工具。我们可以直接用 Python 或者 Ruby 开发简单的游戏模块的 Demo。由于脚本语言代码的简单和高可读性，所以由脚本语言入手，进行早期示例的代码剖析，是一个不错的选择。

Python 我们可以从 python.org 下载，Lua 我们可以从 lua.org 下载，相应地，Ruby 也可以在 ruby-lang.org 下载。为了考虑兼容性，Python 建议使用 2.7.x 版本。Lua 下载最新的版本即可。Windows 下 Python 的源代码编译并不方便，所以建议下载 MSI 安装包，直接安装即可。因为之后我要使用 Pygame 进行示范，所以建议你使用 32 位的 Python 版本。

3. 选择编程语言版本

在开发的过程中，一定会用到 C/C++ 语言。

如果你要使用 VC++ 的话，会涉及购买、安装和配置等情况。为了使这个专栏的内容尽量简洁、可用，我建议使用 Windows 下，移植版本的 GCC 和 G++ 进行编译（也就是 MinGW 移植版），GCC 版本为 4.2.x 或者以上版本。有人说这些版本太老了，我认为，初学阶段，版本越新，意味着你需要知道的编译器内容和编译器开关就越多，因此建议你选择较为稳定的 4.2.x 或以上版本。

对于 C++ 而言，我们也不需要用到最新的 C++ 标准，比如 C++11 等。对于 C 语言，我们默认使用 C89 或者 C99 都是可以的。简洁、高效、显而易见，是我一向遵从的原则。

4. 下载编译器

关于 C/C++，你可以去 MinGW 官网下载 4.2.x 版本。当然如果你希望使用其他更新的版本也不是不行，你可以直接下载安装器，来获取编译器的版本。下载地址是这个：https://sourceforge.net/projects/mingw/files/Installer/

你也可以按照你的需求定制下载。如果要成为完整的编译器，必须下载这些内容：

MinGW (C/C++) Compiler

Binutils

Windows32 API

MinGW Runtime Libraries

GNU Debugger (GDB)

GNU Make

一般来讲，使用安装器下载的编译器都是最新版本的，如果你需要下载特定的版本号，你可以在这个网址 https://sourceforge.net/projects/mingw/files 下，找到相应的编译工具目录和对应的版本号。

这样，C/C++ 编译器就下载完成了。如果你是自己下载特定版本号的话，需要将所有包解压缩在一个指定的目录下，解压缩出来的目录结构一般有这几个常用子目录：bin、include、lib、share、 local、etc、var。

5. 选择 C/C++ 和 Python 的 IDE

接下来，我们需要一套 IDE 来帮助我们进行 C/C++ 和 Python 的开发。

C/C++ 方面，我选择使用免费的 MinGW Studio 来完成。MinGW Studio 的界面绝大部分模仿了经典的 VC6 的 IDE 界面。虽然相对于更时髦的收费编译器来说，MinGW Studio 没有特别智能的代码提示，但是可以方便我们完成程序的调试。

我们可以通过搜索引擎搜索到并且顺利地下载 MinGW Studio。有一些 IDE 是自带 C/C++ 编译器的，这种包也没有问题。如果你对 C/C++ 这部分设置比较熟悉，你也可以自由选择其他 IDE，比如 DevCpp、CodeLite、CodeBlocks 等等。

至于 Python 方面，我们可以使用 Wing IDE。这是一个付费项目。也可以使用国内程序员编写的 Ulipad，另一个付费软件 Komodo，用来做 Python、Ruby 的 IDE 都很合适。至于 Wing IDE，我们可以在 wingware.com 下载最新版本。

6. 带你一起测试编译器的运作

首先，我们需要先测试编译器是否运作顺利，所以我选择 Lua 来进行编译。在将来，需要使用 Lua 的时候，必须将之编译为静态库或者可执行文件。

我们打开 MinGW Studio，界面是这样的：

我们可以在 Edit->Options->Compiler 选项里设置具体的编译器路径，以便让 IDE 找到编译器来开始工作。

一般编译器的路径都会自带 bin 目录，所以设置的目录必须在 bin 目录的上级目录。比如我们设置的 MinGW 编译器路径为 C:\MinGW，那么 bin 目录就是 C:\MinGW\bin，所以在 IDE 的设置下，只需要设置为 C:\MinGW 就可以了。

我们将下载到的 Lua5.x.x.tar.gz 解压缩到某个目录。在我写文章的时候，Lua 的最新版本是 5.3.4。在这个目录下，并没有我们所需要的 MinGW Studio 的项目文件，所以我们需要手工建立一个。我们在 File->New->Projects 选项下，建立一个 Win32 Static Library，也就是 Windows 静态库，将名字设为 lua。

然后我们将文件添加到项目里面，在项目虚拟目录里面，点击鼠标右键。

在弹出的选择文件对话框里，选中刚才解压缩出来的 Lua 目录，选择 src 目录下的所有或椎为.c 的文件，随后，我们将 lua.c 排除在外（选中，右键，选择移除出项目）。因为我们制作静态库的时候，可以不用这个文件。

我们可以点击 Build->Build 选项来进行编译，编译完成后，如果编译成功的话，我们会在 Debug 目录下看到一个.a 文件。如果你的项目名叫 lua，那么制作出来的静态库就是 liblua.a，所以个文件就是我们以后要用到Lua 静态库。

如果你有一定的编程经验的话，可能已经看到，我们现在编译出来的是 Debug，是调试版本，我们暂且不去管它。这个在后面我们会进行详细地探讨，目前我们只需要知道这一系列的使用方式和方法就可以了。

我们已经将 Lua 编译完毕了，后续的文章中我会教你使用 Lua 静态库。

接下来，我们尝试使用 Python 语言。你可以使用任何一个上述推荐的专业 IDE 来编写 Python 代码。实际上，Python 的 IDE 不需要过多的配置。因为安装在 Windows 机器上后，Python 的路径会被注册到系统。通常 IDE 会自动找到 Python 执行文件，并且，IDE 的 Shell 窗口将会正确地找到 Python 并看到其互动窗口，就像这张图的内容：

现在，我们尝试在 IDE 中编写一小段 Python 测试代码，然后跑一下。程序运行结果就是打印一个 test 字符串。

import os, sys

if __name__ == '__main__':

   print 'test'

最后，将该文件保存后缀为.py 的文件，这就是 Python 源代码。

7. 专为 Python 开发的游戏库 Pygame

在这里，为你介绍一个专门为 Python 开发的游戏库 Pygame。至于为什么选择 Pygame，我在第四节讲底层绘图接口的时候已经解释了一些。这里再说一下。

Pygame 包装了 SDL 的实现。在编写 2D 游戏方面，它的表现可以用三个词来形容：成熟，稳定，简单。它把一些细枝末节隐藏在了 Python 语法背后，当然也有 Ruby 语言封装的 RubyGame，但是很多人对于这种魔幻的语言并不是特别了解，所以选择简洁的 Python 语法 +SDL 库封装是最合适的选择。

今后我们会编写游戏的示例 Demo，一些轻量级的、游戏的某一部分的说明和介绍，我会使用 Pygame 进行简单的阐述。Windows 版本我们点击这个网址下载这个版本的源代码。 http://www.pygame.org/ftp/pygame-1.9.1release.zip 如果你不愿意下载源代码，也可以根据自己对应的 Python 版本号下载对应的二进制包，支持 Python 2.4 到 3.2 的版本。

pygame-1.9.1.win32-py2.7.msi 3.1MB

pygame-1.9.1release.win32-py2.4.exe 3MB

pygame-1.9.1release.win32-py2.5.exe 3MB

pygame-1.9.1.win32-py2.5.msi 3MB

pygame-1.9.1.win32-py2.6.msi 3MB

pygame-1.9.2a0.win32-py2.7.msi 6.4MB

pygame-1.9.1.win32-py3.1.msi 3MB

pygame-1.9.2a0.win32-py3.2.msi 6.4MB

如果你安装的是 64 位 Windows 和 64 位 Python，注意 Pygame 版本和 Python 都需要是 32 位的，才能完美兼容和使用。

小结

好了，这节内容差不多了。我来总结一下。在这一节中：

我先从各操作系统下的调试工具、IDE 开发环境、显卡驱动等三个方面，分析了为什么选择在 Windows 环境下进行开发；

然后，我还带你梳理了一遍开发所需要的语言和工具，并且提供了下载的网址和安装的方法；

之后，我还带你测试了 Lua 脚本语言在编译器中的编译并且生成了静态库文件。

最后给你介绍了 Pygame，今后将会用到这个 Python 下的 2D 游戏开发引擎。

最后，给你留一个思考题吧。

你可以结合之前几节的内容，思考一下，Pygame 绑定 SDL 绘图接口是如何实现的？

欢迎留言说出你的看法，我在下一节的挑战中等你！

第7讲 | 如何建立一个Windows窗体？

今天，我要跟你分享开发 Windows 游戏的第一步，建立窗体。

上一节，我讲解 Python 和 C++ 的编译器，以及它们各自对应的 IDE 该如何选择，并且测试了 C/C++ 的运行，编译了一个 Lua 静态库。准备工作基本上算是完成了。

如果你有一些编程功底，应该知道建立 Windows 的窗体所需的一些基础知识。如果你经验稍丰富一些，还应该知道 Delphi、C++Builder、C# 等等。这些工具都可以帮助你非常方便地做出一个空白窗体，但是这些窗体并没有游戏的绘图系统，所以它们只是“建立了一个标准窗体”而已。因此，虽然建立窗体是我们这一节的内容，但我们要探讨的是，在窗体背后，Windows 系统做了什么。

Windows 窗体由哪些部分构成？

我们常规意义上的 Windows 窗体，由下列几个部分组成。

标题栏：窗口上方的鼠标拖动条区域。标题栏的左边有控制菜单的图标，中间显示的是程序的标题。

菜单栏：位于标题栏的下面，包含很多菜单，涉及的程序所负责的功能不一样，菜单的内容也不一样。比如有些有文件菜单，有些就没有，有一些窗体甚至根本就没有菜单栏。

工具栏：位于菜单栏的下方，工具栏会以图形按钮的形式给出用户最常使用的一些命令。比如，新建、复制、粘贴、另存为等。

工作区域：窗体的中间区域。一般窗体的输入输出都在这里面进行，如果你接触过 Windows 窗体编程，就知道在这个工作区域能做很多的事情，比如子窗体显示、层叠，在工作区域的子窗体内进行文字编辑等等。你可以理解成，游戏的图形图像就在此处显示。

状态栏：位于窗体的底部，显示运行程序的当前状态。通过它，用户可以了解到程序运行的情况。比如的，如果我们开发出的窗体程序是个编辑器的话，我按了一下 Insert 键，那么状态栏就会显示 Ins 缩写；或者点击到哪个编辑区域，会在状态栏出现第几行第几列这样的标注。

滚动条：如果窗体中显示的内容过多，不管横向还是纵向，当前可见的部分不够显示时，窗体就会出现滚动条，分为水平滚动条与垂直滚动条两种。

窗体缩放按钮：窗体的缩放按钮在右上角，在窗体编程中属于 System 类目。这些缩放按钮依次为最小化、最大化和关闭按钮。

我们来看一张标准的 Windows 窗体截图，这个软件名是 Notepad++。

这是 MSDN 上对于窗体结构的说明：

typedef struct tagWNDCLASSEX {

  UINT      cbSize; // 结构体大小，等于 sizeof(WNDCLASSEX)

  UINT      style;  // 窗体的风格

  WNDPROC   lpfnWndProc; // 窗体函数指针

  int       cbClsExtra;  // 附加在窗体类后的字节数，初始化是零

  int       cbWndExtra;  // 附加在窗体实例化的附加字节数。系统初始化是零，如果一个应用程序使用 WNDCLASSEX 注册一个通过在资源中使用 CLASS 指令建立的对话框时，必须把这个成员设成 DLGWINDOWEXTRA。

  HINSTANCE hInstance; // 该对象的实例句柄

  HICON     hIcon;     // 该对象的图标句柄

  HCURSOR   hCursor;   // 该对象的光标句柄

  HBRUSH    hbrBackground; // 该对象的背景刷子

  LPCTSTR   lpszMenuName;  // 菜单指针

  LPCTSTR   lpszClassName;  // 类名指针

  HICON     hIconSm;       // 与窗体关联的小图标，如果这个值为 NULL，那么就把 hIcon 转换为大小比较合适的小图标

} WNDCLASSEX, *PWNDCLASSEX;

使用 C/C++ 编写 Windows 窗体

接下来，我将使用 C/C++IDE 来编写代码，完成一个默认窗体的开发，并让它运行起来。

#include <windows.h>

LRESULT CALLBACK WindowProcedure(HWND, UINT, WPARAM, LPARAM);

char szClassName[ ] = "WindowsApp";

int WINAPI WinMain(HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nFunsterStil)

 {

    HWND hwnd;               /* 指向我们窗体的句柄 */

    MSG messages;            /* 保存发往应用的消息 */

    WNDCLASSEX wincl;        /* 前面详细介绍过的 WNDCLASSEX 结构的对象 */

    wincl.hInstance = hThisInstance;

    wincl.lpszClassName = szClassName;

    wincl.lpfnWndProc = WindowProcedure;

    wincl.style = CS_DBLCLKS;

    wincl.cbSize = sizeof(WNDCLASSEX);

上述代码开始给 WNDCLASSEX 结构对象赋值。

 /* 使用默认图标以及鼠标指针 */

    wincl.hIcon = LoadIcon(NULL, IDI_APPLICATION);

    wincl.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

    wincl.hCursor = LoadCursor(NULL, IDC_ARROW);

    wincl.lpszMenuName = NULL; /* 没有菜单栏 */

    wincl.cbClsExtra = 0;                      /* 没有多余的字节跟在窗体类的后面 */

    wincl.cbWndExtra = 0;

    wincl.hbrBackground = (HBRUSH) GetStockObject(LTGRAY_BRUSH);

    if(!RegisterClassEx(&wincl)) return 0;

代码在窗口过程调用函数的时候，将地址赋值给 lpfnWndProc，然后呼叫 RegisterClassEx(&wincl) 注册窗口类，系统就拥有了窗口过程函数的地址。如果注册失败，则返回 0。

 hwnd = CreateWindowEx( 0,            /* 扩展风格为 0*/

           szClassName,         /* 类名 */

           "Windows App",         /* 窗体抬头标题 */

           WS_OVERLAPPEDWINDOW, /* 默认窗体 */

           CW_USEDEFAULT,       /* 让操作系统决定窗体对应 Windows 的 X 位置在哪里 */

           CW_USEDEFAULT,       /* 让操作系统决定窗体对应 Windows 的 Y 位置在哪里 */

           544,                 /* 程序宽度 */

           375,                 /* 程序高度 */

           HWND_DESKTOP,        /* 父窗体的句柄，父窗体定义为 Windows 桌面，HWND_DESKTOP 是系统定义的最顶层的托管的窗体 */

           NULL,                /* 没有菜单 */

           hThisInstance,       /* 程序实例化句柄 */

           NULL                 /* 指向窗体的创建数据为空 */

           );

    ShowWindow(hwnd, nFunsterStil);

    /* 要显示窗体，使用的是 ShowWindow 函数 */

    while(GetMessage(&messages, NULL, 0, 0))

    {

           TranslateMessage(&messages);

           DispatchMessage(&messages);

    }

    return messages.wParam;

}

建立并显示窗体，在循环内将虚拟键消息转换为字符串消息，随后调度一个消息给窗体程序。

LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{

    switch (message)                  /* 指向消息的句柄 */

    {

           case WM_DESTROY:

           PostQuitMessage(0);

           break;

           default:

           return DefWindowProc(hwnd, message, wParam, lParam);

    }

    return 0;

}

最后是消息处理。当窗体程序接收到某些操作的时候，比如键盘、鼠标等等，就会呼叫 DispatchMessage(&messages)；函数将消息回调给系统，系统通过注册的窗口类得到函数指针并且通过函数指针调用函数对消息进行处理。

还有一个经常用到的函数就是 MoveWindow，就是移动已经建立的窗体。MoveWindow 函数用来改变窗口的位置和尺寸，如果窗体本身就按照计算机的屏幕对齐左上角，对于窗体内的子窗体，就对齐父窗体的左上角。

BOOL MoveWindow( HWND hWnd,/* 窗体句柄 */

         int x,  /* 窗体左上角起点 x 轴 */

         int y,  /* 窗体左上角起点 y 轴 */

         int nWidth, /* 窗体宽度 */

         int nHeight, /* 窗体高度 */

         BOOL bRepaint = TRUE /* 是否重新绘制，如果是 true 系统会发送 WM_PAINT 到窗体，然后呼叫 UpdateWindow 函数进行重新绘制，如果是 false 则不重新绘制 */

        );

MoveWindow 会给窗体发送 WM_WINDOWPOSCHANGING，WM_WINDOWPOSCHANGED，WM_MOVE，WM_SIZE 和 WM_NCCALCSIZE 消息。

类似的功能还有 SetWindowPos，SetWindowPos 功能更强大，可以设置更多的参数。

这是基本的使用 C/C++ 绘制 Windows 窗体的流程，也是标准的 Windows 窗体的创建和显示。在后续的分享中，我也会使用 GDI 或者 GDI+ 来绘制一些的内容。

使用 Python 编写 Windows 窗体

说完了 C/C++ 系统编程编写的 Windows 窗体，接下来来看一下，如何使用 Python 来编写 Windows 窗体。

Python 的 Windows 窗体编程一般会使用默认的 Tinker 库。不过用别的窗体库也可一建立一个窗体，比如 Python 版本的 QT 库或者 wxPython。

现在来看一下，使用默认的 Tinker 来建立一个窗体。

import Tkinter

def my_window(w, h):

  ws = root.winfo_screenwidth()

  hs = root.winfo_screenheight()

  x = (ws/2) - (w/2)

  y = (hs/2) - (h/2)

  root.geometry("%dx%d+%d+%d" % (w, h, x, y))

root = Tkinter.Tk(className='python windows app')

my_window(100, 100)

root.mainloop()

运行的结果是这样的。

我们可以看到左上角有一个 Tk 的标识，这是 Tinker 的默认图标。目前，我们只是建立了一个 Windows 的窗体，并不能直接编写游戏。除此之外，我们还必须要知道这些建立窗体的具体的细节。

不过，就像前面的文章所说，OpenGL 并不附带任何关联窗体的编程，所以如果你使用的是 OpenGL 的接口来编写代码，稍微修改一下，这些窗体就能成为游戏屏幕窗体。

游戏所有的内容都是在一个循环内完成的，即我们所有的绘图、线程、操作、刷新，都在一个大循环内完成，类似我们在前面看到的代码。

while(GetMessage(&messages, NULL, 0, 0))

  {

      TranslateMessage(&messages);

      DispatchMessage(&messages);

  }

以及使用 Python 编写的代码的窗体中，也会看到一个循环函数：

root.mainloop()

在这个 while 循环中，消息的派发都在此完成。游戏也一样，我们所有游戏内的代码几乎都在循环内完成。你可以想象一个循环完成一个大的绘制过程，第二个循环刷新前一次绘制过程，最终类似电影一样，完成整个动画的绘制以及不间断的操作。

在建立 Windows 窗体的时候，程序会从入口函数 WinMain 开始运行，定义和初始化窗体类，然后将窗体类实例化，随后进行消息循环获取消息，然后将消息发送给消息处理函数，最后做出相应的操作。

小结

总结一下今天所说的内容，我们编写了一个标准的 Windows 窗体，在编写的过程中：

窗体的结构是在建立窗体之前就定义下来的；

所有长时间运行的程序，包括游戏，包括 Windows 本身都是一个大循环。我们在这个循环里做我们想做的事情，直到循环结束；

如果使用脚本语言的方式编写窗体，就不需要关心那么多的东西，只需要定义坐标、位置和窗体名称即可。

最后，给你留一道小思考题吧。

你经常会看到有一些游戏是需要全屏才能进行的。既然我们在这里建立了一个窗体，那请问你，全屏是怎么做到的呢？

欢迎留言说出你的看法，我在下一节的挑战中等你！

第8讲 | 如何区分图形和图像？

据我所知，很多人可能都分不清图形和图像这两个概念：一种情况是你可能会觉得区分图形和图像这两个概念并没有什么实质的用途，于是就没关心过；另一种情况是，你可能朦胧中对图形和图像的区别有一些了解，但是不够深入或者不够透彻，也说不出一个所以然。没关系，今天我就来深入浅出地给你讲一下，图形和图像背后的那些事儿。

既然我们是做游戏开发，那首先我们得知道，从专业地角度讲，区分图形和图像对我们的开发工作有什么帮助。简单地说，搞清楚了游戏开发中绘制、载入、保存的究竟是图形还是图像，你会对接口函数的使用有一个更深入的认识。

比如，如果是图形接口，可能它的接口函数是：

Surface* DrawSomething(int start_x, int start_y, int finish_x, int finish_y);

如果是图像接口，它的接口函数函数看起来可能是这个样子：

Surface* LoadFromFile(const string& filename);

如何区分图形和图像？

从广义上说，所有我们人肉眼能看到的对象，都是图形。从狭义上说，图形是我们所看到的一种点、线、面的描述对象。

图像，是由数据组成的任意像素点的描述对象。比如我们所看到的照片。在电脑中，图形的显示过程是有一定顺序（比如从左往右）的，而图像则是按照像素点进行显示的。电脑对于图形的编辑、修改更为简单方便，因为单一的图形具有特殊的属性（比如圆圈的直径、颜色等等，因为这些在这个图形建立的时候就固定了下来）。

对于图像进行编辑则非常困难，软件需要用一些特殊的算法来计算图像的色块、区域、描边等等，来安排图像该如何进行编辑，有一些甚至还需要用到深度学习的方法来辨别图像的显示区域、显示的内容等等，所以图像的修改比之图形的修改要困难。

那么你可能就会问了，既然前面说，任何眼睛看到的对象，都是图形，那么我觉得图形也是一种图像，这么说对不对呢？如果按照载体来说，图形也是一种图像，这种说法是对的。因为一张 JPG 图片可能存储的是一幅照片，也可能存储一幅三角形的图形。虽然本质不一样，但是由于存储的形式是以图像的形式存储的，在电脑看来，这个三角形就是一幅图像。但是如果你在游戏中使用函数画出了一个三角形，那就是图形了。

所以，严格来说，图形其实是图像的一种抽象表现形式。一般来讲，图形的轮廓并不复杂，比如一个圆圈、一个方块、一个三角形、一条线、某些几何图形、工程上面使用的图纸和 CAD 等，这些都属于图形。图形的色彩也并不是很丰富。而图像一般都有复杂的轮廓、非常多的细节和颜色（当然也有纯单一的颜色，比如黑白照片）。

所以，准确地说，图形和图像有不同的模式。当然，从计算机最底层的程序（显卡处理）来看，绘制图形和图像所经过的硬件处理几乎是一样的。一般显卡会经过这些流程进行图形、图像计算（2D）、显存，用来存取图形图像内容，GPU 计算图像图像内容并渲染，最后输出到显示器。

从图像的呈现方式讲，只有通过图像的方式去呈现“图形”这个对象，才能看到图形，而在开发游戏的过程中，图形和图像的编程方式是截然不同的。比如我们要画线，那么可能会使用到一个叫 DrawLine 的函数。该函数里面需要输入线条的起始坐标，这就是图形的绘制方式。而在接下来的过程中，我将教你如何绘制图形和图像，并呈现出来。

跟我一起绘制图形和图形

现在，我们先用 Pygame 游戏库来建立一个窗体，然后开始绘制图形、载入图像。

在第五节的时候，我们已经讲过 Pygame 的安装和配置。在第六节的时候，我们讲过如何建立一个 Windows 窗体。现在从上到下，我们一起看一下这段代码。

import pygame

pygame.init()

caption=pygame.display.set_caption('Python App')

screen=pygame.display.set_mode([320,200]) # 窗口大小为 320*200

while True:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            pygame.quit()

    pygame.display.update()

    screen.fill([255,255,255]) # 用白色填充窗体

sys.exit()

在这段代码中，首先，我们需要告诉 Python 我们要引入 Pygame。然后 Pygame 进行初始化（init）。在这个初始化的函数里，Pygame 会初始化屏幕、声音、事件、按钮等一系列需要初始化的东西。随后，我们利用 Pygame 的 display 对象的 set_caption 函数来设置窗体的文字，将这个设置后的对象返回给 caption 变量。随后，再使用 set_mode 函数设置窗口大小，将窗口大小设置为 320x200 分辨率，将返回对象赋值给 screen 变量，最后 screen 拿到窗口句柄后，使用 fill 函数设置填充窗体的颜色，在这里填充的颜色是白色。

我们可以看到，使用 Pygame 游戏库来建立一个 Windows 窗体比前面我们提到的任何一种方式都快。那是因为Pygame 封装了建立窗体的代码和图形显示模块。

我们在前面提到，一个游戏是在一个大循环下形成的，所以这里我们要补上一个大循环以确保这个程序不会立刻退出。

while True:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            pygame.quit()

    pygame.display.update()

    screen.fill([255,255,255]) # 用白色填充窗体

sys.exit()

这段代码的意思是，当条件为真（True）的时候（条件总是为真），进行一个循环。事实上这是个死循环，如果没有下面的退出代码的话。那么在这个循环里，从 Pygame 的 event 事件列表中取出 event 事件，然后进行判断，如果 event 的类型是退出类型（点击右上角的 X 关闭按钮），那么 Pygame 就退出，这个 quit 函数就直接退出 while 大循环了。最终系统也退出 sys.exit。

现在我们要在窗体上放上一个矩形和圆。我们先使用 rect 函数来画一个矩形：

  pygame.draw.rect(screen,[255,0,0],[150,10,0,40],0)

其中，draw 中 rect 的定义为：rect(目标画布, 颜色, 位置, 宽度)。

我们也可以用类似的方法来画一个圆：

  pygame.draw.circle(screen,[0,0,0],[top,left],20,1)

然后我们使用 pygame.draw.circle() 用来画圆形。circle 函数具有 5 个参数：

目标画布，在这里是 screen

颜色

由左侧点和顶部点组成的圆形初始位置

直径

宽度

现在我们将所有的代码合并起来看一下：

import pygame

pygame.init()

caption=pygame.display.set_caption('Python App')

screen=pygame.display.set_mode([320,200]) # 窗口大小为 640*480

while True:

    for event in pygame.event.get():

        if event.type==pygame.QUIT:

            pygame.quit()

pygame.draw.rect(screen,[255,0,0],[150,10,20,40],0)

pygame.draw.circle(screen,[0,0,0],[20,50],20,1)

pygame.display.update()

screen.fill([255,255,255])# 用白色填充窗口

sys.exit()

所以我们很容易就能看出来，在 Pygame 游戏开发库里面，画一个图形是很容易的事情，你不需要知道太多的细节，只要将位置和颜色或者内容填充进去就可以了。

我们可以在 Pygame 中使用 Pygame.image.load 来加载图像文件，这个函数支持各种图片格式。我们使用这个方法来加载一副 PNG 图片：

obj = pygame.image.load("test.png").convert_alpha()

使用 convert_alpha 函数是因为这个函数会使用透明方法来绘制，所以我们在加载一个拥有 alpha 通道的图片的时候（比如 TGA、PNG）的时候，可以使用这个方式。

然后使用 blit 方法将图像绘制出来：

screen.blit(obj, (20,10))

或许你会问，blit 是什么函数，我在这里简单介绍一下，blit 这个函数会以各种函数形式出现在图形引擎的函数里面，比如 FastBlit 等等。这个函数具体负责将图像从某一个平面复制到另一个平面，或者将图像从内存复制到屏幕。简而言之，这个函数的功能就是将图像“绘制”在游戏窗体的屏幕上。

现在继续来看看 blit 函数。blit 函数的第一个参数是加载完成的返回对象，第二个参数是绘制的坐标位置。最后我们需要 update（更新）整个游戏窗体的绘制内容。

我们把载入图像的代码整合到刚才的代码中一块儿看一下。

import pygame

pygame.init()

caption=pygame.display.set_caption('Python App')

screen=pygame.display.set_mode([320,200]) # 窗口大小为 640*480

obj = pygame.image.load("test.png").convert_alpha()

while True:

    for event in pygame.event.get():

        if event.type==pygame.QUIT:

            pygame.quit()

            sys.exit()

    screen.blit(obj, (20,10))

    pygame.display.update()

screen.fill([255,255,255])# 用白色填充窗口

最后呈现的效果是这样的：

小结

这一节，我带你学习了图形和图像的区别，使用 Pygame 绘制了最基础的图形，最后我们通过代码载入一副 PNG 图像并在屏幕上绘制出来。

给你留一个小练习吧。

请你结合上述代码，在游戏执行的大循环内，在游戏的窗体里面，绘制出一个从左到右移动的矩形、圆形或者图像。

之后，针对一些实操性强的内容，我都会适时给你留一些必要的练习。希望你每次都能动手去练习一下。同时，也欢迎你留言，说出你在练习中的疑惑和成果。温故而知新，相信你会有更多的收获！

我在下一节的挑战中等你！

第9讲 | 如何绘制游戏背景？

我在之前的文章中描述了各种基础知识，然后梳理了开发流程，并带你创建了一个窗体，现在我们要做的就是朝这个窗体里添加东西。

我会随着进度逐渐提升难度。就现阶段来讲，我们涉及的只是一些基础知识，并且只需要将大部分的关注点放在我们要做的游戏内容上，并不需要关注过多的底层逻辑代码。

做事情都有先后顺序，做游戏开发自然也是。为什么要学习先绘制游戏背景而不是别的什么，很简单，因为只有先绘制了游戏背景，才能进行后续的游戏图像遮挡、图形图像的显示等等操作。

不管你有没有玩过《超级玛丽》《魂斗罗》《雷电》之类的游戏，但一定对其画面不陌生。和我们要开始做的打飞机游戏一样，这种类型的 2D 游戏，其背景不是左右卷轴，就是上下卷轴。所谓左右卷轴，就是游戏画面是横向的、左右运动的，而上下卷轴就是游戏画面是竖直对的、上下运动的。

像《雷电》这样的经典飞机游戏，就是属于上下卷轴的。上下卷轴的飞机游戏有一个特点，就是它是在空中，从凌驾于飞机之上的视角，往地面俯瞰的。因为是俯视角，所以我们可以很方便地看到游戏的整体地图，包括地面上的敌人、空中的敌人等等，层次感会很强。

因此，可以确定，我们要做的打飞机，也是一个上下卷轴的游戏。这样，我们就可以着手将需要的图片添加进去了。

我们要使用 Pygame，先读取一个图片，让该图片成为游戏背景并载入进去。当下阶段，我们的图片从哪儿获得并不重要，因为在一个完整的游戏开发团队里面，都有专业的美术团队负责作图，但是现在我们没有，所以我就自己贴一幅图来代替正式的游戏背景。所以你现在只需要知道背景是如何贴上去的就好了。

和前面的文章说过的一样，我们需要先载入 Pygame 模块，并且定义一个变量 background。我们将一幅名为 lake,jpg 的图片文件赋值给 backgroud 变量。

import pygame

background = 'lake.jpg'

然后，我们先把 Pygame 的所有组件都初始化。接下来，我们调用 display 类里的 set_mode 函数来对屏幕进行一个初始化。

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

pygame.display.set_caption("pygame game")

这里一共有三个参数，第一个参数是分辨率，比如我这里编写的是 640x480 的分辨率；第二个参数是flag，flag 的参数我放在下面这个表里了；第三个参数是32，32 代表的是颜色深度，这里是 32 位的意思。

在设置完了窗体模式之后，后面的一段代码就是设置窗体的抬头文字，这里显示的是 pygame game。

随后，我们要载入背景的图片。

bg = pygame.image.load(background).convert()

我在前面的文章中也说过，这句话的意义是，载入 backgroud 图片。但是 pygame.image.load 这个函数返回的是一个 surface，而.convert 函数是来自于 surface 对象。你可以参考下面的代码来理解。

surface_temp = pygame.image.load(background)

bg = surface_temp.convert()

其次，bg 这个变量也是一个 surface，而 convert 函数的作用是改变一副图片的像素格式。convert 有四个相同名字的重载函数。如果就像我们的代码里所示，convert 没有任何参数，则表示直接返回一个 surface 对象。

好了，现在我们设置完了背景 bg 的 surface，我们按照上面的文章，开始写一个大循环，并且在循环里面进行检测鼠标事件是不是退出操作，这是最基本的一项检测。

while True:

    for event in pygame.event.get():

        if event.type == QUIT:

            pygame.quit()

和前面的文章一样，我们从 event 里取出事件列表，然后把每一个 event 的类型进行对比，如果发现有 QUIT 事件（鼠标点击 X 关闭按钮后），就直接退出游戏。完成这一步之后，就可以开始使用 blit 函数进行绘制屏幕的操作。

screen.blit(bg, (0,0))

这句话的意思是，使用 blit 将 bg 在以游戏屏幕 x,y 轴为（0,0）的坐标位置在 screen 对象上绘制背景图像。然后我们需要 update 刷新屏幕，添加下面这行代码。

pygame.display.update()

upadate 这个函数是 pygame.display.flip 函数的优化版。因为 pygame,display.flip 是更新整块屏幕，所以如果加载的资源多，效率并不是很高，而 update 如果传递一个矩形值得参数的话，它会只更新这块矩形的内容，所以效率会比较高，但是不传递参数的话，默认还是会更新整块屏幕，但是这个函数不能用在 set_mode 的时候设置为 OpenGL 的模式下。

好了，我们该做的事情基本都做完了，现在我们来运行一下，看看效果。

好了，背景是贴上去了。现在问题来了，要想让背景动起来该怎么做呢？如果在 blit 的时候，改变坐标是不是就可以移动背景图的位置了呢？你再开动脑筋想想，该怎么做才能让背景移动起来？

对的，我们只需要写一个循环，就可以将背景移动起来。

我们来修改一下大循环开始的代码。

y_move = 0

while True:

    for event in pygame.event.get():

        if event.type == QUIT:

            pygame.quit()

    screen.blit(bg, (0,y_move))

    y_move-=1

我们在大循环开始之前，在这段代码里定义了一个 y 值移动的变量，而我们每循环一次，blit 就绘制一次屏幕，y 值都会被减去 1，所以我们每次看到的图片，都会不停往上移动，我们来看一下效果。

发现问题了没有，在移动的过程中，下方的图案居然没有被刷新，直接黏在了屏幕上，看起来是不是很恶心的样子？

我们应该怎么做才能达到正常的效果呢？也就是说，请你思考一下，应该怎样做，我们才可以将这个令人头疼的图像在移动的时候变得正常呢？

我们先来回顾一下，我们在循环里面做了哪些步骤：

检测退出事件；

在屏幕上绘制 bg 对象，坐标初始为（0, y）；

飞机每移动一格，坐标 y 减 1；

更新屏幕。

看起来似乎没有什么问题，我再来带你梳理一下。

首先我们初始化的时候，屏幕是黑屏一块，没有任何图像，然后我们进入大循环，将 bg 对象绘制到屏幕上的时候，你觉得这时候我们的眼睛看到绘制的图像了吗？

如果你说是的话，那就大错特错了，因为这个 blit 的动作，仅仅是绘制，而不是显示。请记住这个区别：绘制不等于显示。

那你可能就要问了，既然绘制了，为什么不显示呢？要什么时候才能显示呢？答案是，要在 update 一次屏幕的时候，才会显示，这就是“更新”的作用。就像电影是一帧一帧的，如果没有下一帧更新，电影就会永远定格在某一秒。

所以问题逐渐就暴露出来了，我们再来重新梳理一下流程：

检测退出事件；

在屏幕上绘制 bg 对象，坐标初始为（0, y）（注意是绘制，不是显示）；

飞机每移动一格，坐标 y 就减 1；

更新屏幕，将第二步绘制的 bg 对象呈现在屏幕上，严谨地说，应该是将在 update 函数之前所有的绘制操作都更新一次并呈现在屏幕上）。

好了，问题很清楚了，update 函数只是将屏幕更新了一次，并未进行填充颜色或者“擦除”背景的操作，也就是我们在移动 y 值的时候，整个屏幕不停地更新，然而没有擦除。那么应该怎么将移动后的画面进行清理呢？

我们在 update 代码之后填入下面的代码。

screen.fill([0,0,0])

fill 操作拥有三个参数，其中第一个参数是填充颜色；第二个参数是填充某一块区域（如果不填入第二个参数，就会填充整个屏幕）；第三个参数是blit 操作的特殊参数，我们暂时可以不用管它。

所以，我们在代码里填充了黑色到整个屏幕，这样一来我们的屏幕操作变成这样：

检测退出事件；

在屏幕上绘制 bg 对象，坐标初始为（0, y）；

坐标的 y 减 1；

更新屏幕；

填充屏幕区域为黑色。

我们再运行一下看一下效果。

嗯，这下看起来正常了，屏幕不断往上移，并且没有拖着尾巴一样的图案了。

小结

我们在写 2D 游戏的时候要注意一点，就是：

我们要想象游戏的每一帧就像电影的每一帧。每一帧做的事情，如果下一帧不去做，那么永远不会更新屏幕内容。

所以，update 的功能是更新调用 update 之前的所有动作，这些动作可以有绘制图像操作，也可以有音乐播放，也可以有动画每一帧的操作等等。只要 update 一次，屏幕的画面就会往前行进一次。

给你留个小思考题吧，我们在 fill 屏幕的时候，怎么做才能让填充的颜色不停变幻呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第11讲 | 如何载入“飞机”和“敌人”？

上周，我向你解释了如何载入背景图片，以及如何使用坐标值的变换来移动背景图片。今天，我们要载入主角“飞机”和一些“敌人”。

导入随机函数

我们选择 Python 做为开发语言。在开始之前，需要用到一个随机函数，这个随机函数会在之后的代码中用到，具体用作什么，我会在后面揭晓。

首先，需要将随机函数导入 Python 的随机函数库 random。代码可以这么写：

import random

然后，我们需要限定一个范围，来生成随机数，比如 10 到 100 的范围，代码可以这么写：

random.randrange(10, 100)

这个函数会接受三个参数：开始、结束、递增数字。

开始：开始随机的指定范围数值，包含在范围内。比如 (10，100)，就包含 10。

结束：开始随机的指定范围数值，不包含在范围内。比如 (10，100)，不包含 100，最多到 99。

递增：指定递增数字。

如果不填写递增值，则按照开始、结束的值随机分配。比如 (10，100) ，那就会在 10～99 之间随机分配任何一个数字。

载入主角飞机

我们说完了随机函数的准备工作，就可以开始载入飞机了。

我们假设主角的飞机是从下往上飞，那它的飞机头应该是朝着电脑屏幕上方，而敌人的飞机是从上往下出现，所以它的飞机头应该朝着电脑屏幕的下方。主角的飞机暂时固定在屏幕下方，敌人的飞机则一直不停从上往下飞。

飞机的图片是我从共享的图片网站上抓取下来，让美术帮我处理和加工了一下。其实就是将飞机从一整块背景图片上抠除下来，让飞机看起来拥有飞机本身的轮廓，而不是一幅“方块”的飞机图片，然后将其图片保存成 png 的格式。

我们来看这里的代码。和载入背景一样，我们需要先定义主角飞机的图片名和敌人飞机的图片名。

plane = 'plane.png'

enemy = 'enemy.png'

使用 png 格式的原因是，png 格式包含 alpha 通道。我们可以将图片抠成透明图，这样将图片贴在背景上面就看不到任何黑色块。

我们先尝试贴一下主角的飞机。

pln = pygame.image.load(plane).convert_alpha()

screen.blit(pln, (40, 350))

pygame.display.update()

我们定义一个叫 pln 的变量，载入 plane 图片，并且将 alpha 通道进行处理，然后在屏幕中绘制 pln，最后我们使用 update 函数更新屏幕。

我们来看一下贴图的效果。

我们已经将这幅图片贴了上去。

在载入的过程中，如果我不使用 convert_alpha 函数会怎样呢？我们也来做一下实验。

pln = pygame.image.load(plane).convert()

screen.blit(pln, (40, 350))

pygame.display.update()

我将 convert_alpha 改成了 convert，来看一下效果。

看到了那个大大的黑色色块没有？这就是我们没有处理 alpha 通道导致的结果，导致了一个大大的抠图色块出现在屏幕，所以要记住：

设计主角图的时候，要将图片抠下来；

在贴图的时候，需要进行 alpha 混合的处理，否则贴上去的图会存在抠图黑块。

载入敌人飞机

接下来，我们要从屏幕上方，贴一架敌人的飞机。

enm = pygame.image.load(enemy).convert_alpha()

screen.blit(enm, (30, 10))

pygame.display.update()

我们将两架飞机前后代码整合起来，再来看一下效果。

这样，我们将两架飞机都贴在了屏幕上了。看起来是不是有点像样了呢？

敌方肯定不止一个飞机，那我们就需要贴更多的敌方飞机。这里我们就需要用到最开始提到的随机函数了。为什么使用随机函数呢？因为我们需要让敌方飞机的排列看起来很随机（笑）。

我们现在要加载相同的敌方飞机图片，加载三次。也就是说，我们会在屏幕上方的一个固定区域范围贴上三次敌人的飞机。我们需要准备三个随机 (x，y) 位置的数字，并且赋值给 blit 函数。

ex1 = random.randrange(20, 600)

ey1 = random.randrange(10, 50)

ex2 = random.randrange(20, 600)

ey2 = random.randrange(10, 50)

ex3 = random.randrange(20, 600)

ey3 = random.randrange(10, 50)

screen.blit(enm, (ex1, ey1))

screen.blit(enm, (ex2, ey2))

screen.blit(enm, (ex3, ey3))

这样，我们就贴上了三幅飞机的图片。

我们再来看一下效果。

是不是看起来很有意思？但是这样并不能让飞机动起来，我们需要用到上一节里，移动背景图片的知识，来让敌人的飞机动起来。我们只需要将这三个 y 值在循环中设置成递增，就可以做到三架飞机的移动了。代码就像这样：

screen.blit(enm, (ex1, ey1))

screen.blit(enm, (ex2, ey2))

screen.blit(enm, (ex3, ey3))

ey1 +=1

ey2 +=1

ey3 +=1

这样我们就完成了敌人飞机不停往下飞的效果了。

后面的内容，也会像现在这样，代码很多，我带你再梳理一下逻辑。

首先，我们需要使用 Python 程序库的随机函数来制作随机数。

通过这个随机函数，来随机载入敌人飞机的位置。当然如果有游戏策划的话，游戏可能会由某种固定的起始点来刷出敌人飞机，这里我们只用到随机函数来刷敌人飞机。

如果想要做得更漂亮的话，我们可以将随机函数的值从屏幕最上方刷出来，这样看起来敌人就是从屏幕最上方飞下来的。比如我们可以设置 y 值为 -10 左右。

如果想要做得更精细的话，我们可以通过程序得到图片的长和宽。通过图片的长和宽来计算刷出飞机的位置，我们可以使用屏幕大小来减去飞机长宽的大小来计算，比如屏幕长是 640，图片的长是 8。那么，我们在设置 x 轴位置的时候，就应该最大只设置到 640-8 这样的位置。这样就不至于我们在编程的时候，只刷出半架飞机，或者根本就看不到飞机。

其次，我们在载入敌人飞机的时候，需要贴三幅图片。

当然，我们可以优化这一系列的代码，比如我们可以将一系列 blit 放在一个函数里面。上述的代码只是一个针对教学用的代码，为的是让你更直观、明了地能看明白如何载入三幅敌人飞机的图片。我们优化了代码后，可以直接使用一段代码和一系列数组就可以完成这个操作。

如果做了一幅 alpha 通道抠图的图片，我们在载入的时候，需要处理 alpha 通道的数据，让其图片达到“透明”的效果，而不是直接贴一幅有黑框的图片。

最后，事实上，我们要将这些内容更加完善，还有许多的工作要做。

这些工作我将在后续的内容中展开讲解。比如我们需要移动背景。这个我们上次已经说明了。在敌人飞机往下飞的过程中，我们需要考虑敌人飞机往下飞的速度，是不是要比屏幕移动的速度更快或者更慢，这样才能体现敌人飞机的等级高低，体现出游戏的难度是随着关卡的变化越来越难的。

我们将游戏背景的图片 blit 函数放在游戏循环的最开始，而载入飞机的代码则放在稍后的部分，那么如果我们将游戏背景的图片放到飞机之后载入会发生什么事情呢？如果你一直在练习我在文中提供的代码，你应该可以知道，这个时候飞机的图片都会不见了，只能看到游戏背景。这是因为 Pygame 是按照 blit 代码的顺序来载入图片的，这部分内容我在后面的内容中讲解。

我们可以将载入的图片资源放到一个资源包中，或者放在一个目录中，这样游戏的目录就不至于看起来乱七八糟，而是非常有序的。比如我们可以将所有和主角飞机相关的内容就放在飞机的目录下，和敌人相关的就放在敌人的目录下，背景和关卡就放在关卡的目录下，这样就看起来就整整齐齐。在编写代码的时候，从目录的名字不同，可以知道载入的是什么内容，比如：‘enemy/plane.png’。

小结

好了，这节内容差不多了。我主要和你讲了三个内容：

随机函数使用 random.randrange 来做，输入开始和结束值，就能随机出这一个范围的数字 ;

让飞机移动起来，需要将 x 或者 y 的值进行加减变化 ;

处理 alpha 混合半透明图片，需要使用 conver_alpha 函数。

最后，给你留一个小思考题吧。

while True:

     ......

ex1 = random.randrange(20, 600)

ey1 = random.randrange(10, 50)

screen.blit(enm, (ex1, ey1))

     pygame.display.update()

如果我们把这段代码的 ex1, ey1 变量放在游戏循环中（本来在循环外面），并且将 ex1, ey1 填入到敌人飞机的 blit 函数中，会出现什么样的结果呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第12讲 | 如何设置图像的前后遮挡？

我们人的肉眼所观察到的世界是属于 3D 世界，有远近大小之分。一个物件 A 被另一个物件 B 遮挡，物件 A 就会看不到，而在 2D 的世界里，都是平面的，没有实际的高度区分，就算做成了斜 45 度角，也是一种视觉呈现，并没有在计算机内形成高度差。

在一般的游戏引擎，或者像 Pygame 这样的游戏库中，基本都是“先绘制的图案先出来”，“后绘制的图案后出来”，而后绘制的图案一定遮挡前面绘制的图案。因为 2D 就是一个平面，从逻辑上讲，按照先后顺序绘制，没有任何问题。

但是如果我们现在做的游戏是斜 45 度角的游戏，类似《梦幻西游》视角的，那么人物和建筑物之间就存在遮挡的问题，如果处理不谨慎，就会出现人物浮在建筑物上，或者建筑物把人挡住了。

所以在一些 2D 引擎中，会有一个 Z 值的概念，Z 值的概念就是在（X,Y）的基本 2D 位置上，加一个高度的概念。这个高度是一个伪概念，它模仿 3D 的 Z 值，只是作遮挡用。但是我们现在使用 Pygame 来编写游戏的话，并没有 Z 值的概念，所以我们需要想一些办法来解决遮挡的问题。

首先，我们从共享资源中抽取一段围墙的图片来进行摆放。

围墙分为两幅图片，都是往右上角延伸的。现在我们需要将这两段围墙连接起来。如果我们像以前的做法，一个图片一个 blit 的话，那是不行的。因为这样需要相当大的代码量，所以我们采取将围墙的代码放入一个 list 中的做法。

首先，我们要定义图片和载入图片。

right_1 = 'right_1.png'

right_2 = 'right_2.png'

r_1 = pygame.image.load(right_1).convert_alpha()

r_2 = pygame.image.load(right_2).convert_alpha()

然后，我们写一个循环，将围墙放入一个 list 中。我们想要将这两段围墙每隔一个放置不同的样式，就需要做一些判断。我们将数字除以 2，如果能除尽，就摆放其中一个，否则就摆放另一个。

total = 10

wall = []

while total > 0:

    if total % 2 == 0:

        wall.append(r_1)

    else:

        wall.append(r_2)

    total-=1

这样，我们就将围墙的对象分割并且放入到了 list 里面，我们就可以在接下来的代码中使用这个 list，来将围墙拼接出来。

在拼接之前，我们还要定义一系列的变量。现在我们已知这个图片的宽度是 62，长度是 195，所以我们需要增加的步长就是“每次拼接加 62 的宽度”。而围墙 1 和围墙 2 在拼接的过程中，是要往右上角倾斜的。经过测量，倾斜的高度是 30，所以每增加一个围墙，就要往 y 轴减去 30 的高度，现在我们要定义初始化的 x 和 y 的起始位置，并且要定义增加步长的 x 值和 y 值，我们可以这么写：

init_x = 0

    init_y = 300

    step_x = 62

    step_y = -30

我们要将这一系列变量放在循环中，因为每循环贴图一次，就需要重新初始化和计算步长，这样看上去就像把一系列墙一直贴在游戏中一样。

我们来看一下代码。

for w in wall:

        screen.blit(w, (init_x, init_y))

        init_x += step_x

        init_y += step_y

这段代码的意思是，遍历 wall 这个 list，取出下标并且赋值给 w 变量，每个 w 变量都是一个 surface 对象，这个对象通过 screen.blit 来贴上去，贴上去的位子使用初始 x 和初始 y，然后初始 x 和初始 y 的位置又变化了，每次增加步长 x 和减去步长 y，进行第二次的贴图，然后继续循环贴，这样我们的围墙就开始连贯了起来。

我们来看一下贴上去的效果。

可以看到，每隔一段贴一幅图，另一段贴另一幅图，这样一整段的围墙就贴完了。一共有十幅图片，每一副图片的 y 值都向上减去 30。

现在我们来总结一下贴这些连贯图片的重点：

将内容放入列表或者数组中。为了编程方便，将需要连续贴图的内容放入列表或者数组中就能够减少编程工作量；

计算好贴图的点，能让我们在连续贴图的过程中，只要控制位置变量就可以完成。

如果我们编写的是地图编辑器，而地图编辑器生成的脚本代码，除非写得非常智能，一般来讲，就是一连串的贴图代码，这样就会有许许多多的 blit 的操作，并不会将相同的元素加入循环或者列表，那是因为脚本代码是电脑生成的，没有更多的优化代码。

接下来，我们要将一个人物放上去。这个人物只是摆设，我们只是为了测试图像遮挡的情况。

player = 'human.png'

plr = pygame.image.load(player).convert_alpha()

然后我们在循环的围墙贴图的代码之后，放入人物。

screen.blit(plr, (62, 270))

我们将人物故意放在围墙的某一个位置，效果看起来是这样的。

﻿﻿ 这样看上去，人物就站在围墙上面了。看起来他似乎有飞檐走壁的功夫，然而事实上，他应该几乎被围墙挡住，但是这个时候问题就来了。虽然我们可以把 blit 的代码放在显示围墙的 blit 代码之下，让围墙遮挡住人物，但是当游戏在进行的时候，人物要往下走，这时候就需要显示在围墙之外，我们不可能在游戏运行的时候改变代码，这是不可能做到的。所以我们还需要改变代码。

事实上，在正式的游戏开发中，我们需要将人物的控制、NPC 的控制等放在不同的线程中去做，而地图则是直接载入地图数据文件。在地图的数据文件中会告诉你，哪些坐标是有物件挡住的，不能走；哪些坐标有哪些物件，你需要走过去的时候被遮挡。但是在我们今天的内容中，为了你能看得更明白，我们将地图和人物的代码都放在游戏的大循环中去做。

我们使用代码来模拟 Z 值的作用，虽然在代码中没有体现 Z 值，但是通过代码你可以理解 Z 值的意义。

首先我们来定义一个函数，这个函数将 blit 代码抽取出来，然后判断传入的参数是不是 list 类型，如果是的话，就连续贴图，否则就贴一张图。

def blit_squences(data, x, y):

    if isinstance(data, list):

        for d in data:

            screen.blit(d, (x, y))

    else:

        screen.blit(data, (x, y))

我们利用 Python 的 isinstance 函数，来判断传入的 data 是不是 list 类型。如果是的话，我们就遍历 data，然后将 data 中的内容进行连续贴图。这是为了模拟我们除了贴人物，还要贴围墙。如果判断不是 list 类型的话，则直接贴上 data。

然后，我们需要改变在游戏循环内的绘制图片代码。我们需要用 blit_sequences 函数来替代这块代码，然后我们在内部做一个判断，判断人物是不是和围墙的位置重叠了，如果是的话，就贴上人物和围墙。

for w in wall:

        if  init_y == 270:

            blit_squences([plr, w], init_x, init_y)

        else:

            blit_squences(w, init_x, init_y)

        init_x += step_x

        init_y += step_y

在这段代码中，我们看到，我们使用了 blit_sequences 这个函数，替代了原本的 surface.blit 代码。在这段代码中，我们需要判断一个位置，这个位置是围墙的 y 值，如果人物走到了这个位置，那么我们就将人物和围墙对象放入到 blit_sequences 中进行绘制。效果就是，人物被遮挡到了围墙外面。

﻿﻿

这段代码起作用的地方是在 [plr, w] 这部分。我告诉 Pygame，要先绘制 plr 然后再绘制 w，但是如果你换一个位置，就是先绘制 w 再绘制 plr。

这一部分是示例代码，正式编写游戏的时候，其实是不太会这么写的。这是为了展示我们如何方便地切换绘制位置。其中，plr 和 w 的 list 部分，事实上就是解释 Z 值所做的工作，如果 plr 的 Z 值高于 w，那么就先绘制 plr，否则就先绘制 w。当然在正式编写类似的游戏的时候，我们需要考虑的是多线程，这些我们将在后续的内容中进行讲解。

一般的做法是，我们会在多线程中绘制人物，然后载入地图，我们会在人物走动的过程中，判断地图上的物件，然后进行 Z 值的调整，或许，Z 值最高的是物件本身，比如围墙和建筑物的 Z 值是 100，而人物的 Z 值一直保持在 20，所以每次走到围墙和建筑物这里，总是先绘制人物，再绘制建筑物，这样就起到了遮挡的效果。

小结

这一节内容差不多了，我来总结一下。

我们其实就讲了一个内容。在做遮挡的时候，要考虑绘制顺序，先绘制的一定会被后绘制的遮挡。

如果做得比较成熟的话，利用 Python，我们需要在外面包裹一层字典。每个物件载入的时候，都告知其 Z 值，然后在绘制的时候，判断 Z 值，安排绘制顺序。

现在给你留一个小问题。

如果在绘制的过程中，两个人物的 Z 值相同的话，人物碰到一起，会出现什么结果呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第14讲 | 如何设置精灵的变形、放大和缩小？

上周四，我给你讲解了图片的遮挡问题。这一节我要和你讲精灵的变形、放大和缩小。如果之前没有做过游戏开发，你肯定会问，什么是精灵？

什么是精灵？

我先来解释一下什么是精灵。精灵当然不是我们传统意义上的什么树林里的精灵。精灵是一个游戏开发中的名词，英文叫 Sprite。

它多用于游戏中的人物和可移动物品，也可以用于显示鼠标指针和输入的文字。如果屏幕上的可移动物体的尺寸比一个精灵图要大，可由若干个精灵图缩放或者拼接而成。

从宏观的概念讲，精灵就是一幅图片。比如我们之前中讲过的那些飞机图、背景图，这些都可以认为是精灵或者是从精灵中派生出来的。它就是一系列可以变化的图片。这些图片可以变形、放大、缩小，或者是一系列的动画帧等等。

从编程的角度讲，精灵是一种管理器。在一个精灵的管理器中，可能会有一系列的方法去操作精灵，比如添有加、删除操作，比如有图像的变形、放大、缩小操作，还有系列帧的显示操作等。

既然，精灵就是图片，那在“打飞机”中，飞机会随着画面的变化、操作的不同，而有变形、放大以及缩小的状态。我现在就来讲这些操作的实现，需要用到哪些函数，以及这背后都有什么技巧。

设置变形、放大和缩小需要用到哪些函数？

Pygame 中的底层，使用的是 SDL 开发库，这个我们在之前的内容中已经讲过，因此，这些变形、放大缩小等操作，都有对应的 SDL 库。

我们要用到的还是之前的飞机图片，为了让你更明确的看清楚，我删除了背景，只呈现飞机的内容。

翻转函数 flip

我们首先要用到的是函数 flip。顾名思义，这个函数就是让你对图片进行翻转，你可以翻转成水平的或者垂直的。所以它拥有两个参数，一个是传入 x，一个是传入 y，并且都需要传入布尔值。如果传入 x 值为真，那就进行水平镜像翻转，如果 y 值为真，那就进行垂直镜像翻转，两个都为真，两方都进行翻转。这个函数会返回一个 surface。

pln_t = pygame.transform.flip(pln, 1, 1)

screen.blit(pln_t, (40, 350))

我们看到的结果是这样：

原本飞机的头是朝上的，现在进行了水平和垂直的翻转。

缩放函数 scale

我们再来看一下缩放的函数 scale。scale 的参数是这样：

scale(Surface, (width, height), DestSurface =None)

其中第一个参数是绘制对象，第二个参数是缩放大小，第三个参数一般不太使用，指的是目标对象。

pln_t = pygame.transform.scale(pln, (220,220))

screen.blit(pln_t, (20, 150))

我们在代码中，将 pln 这个对象放大到 220×220（飞机原本大小为 195×62），然后看一下效果。

你看，飞机变大了。我们再尝试修改一下代码。

pln_t = pygame.transform.scale(pln, (20,20))

飞机就变小了。所以，scale 函数的作用是，只要你传入参数的 width 和 height 值大于原本精灵的长宽值，就变大，否则就变小。

类似，我们还有一个函数 scale2x，你只需要填入绘制对象即可，函数会帮你进行两倍扩大，不需要你计算原本的长宽值并且乘以 2。

旋转函数 rotate

我们再来看一下rotate 旋转函数。它提供一个参数 angle，也就是你需要旋转的角度，正负值都可以。

我们来看一下代码。

pln_t = pygame.transform.rotate(pln, 20)

我们看到的效果就像这样。

这样飞机就朝左侧旋转了 20 度。 相似的，也有整合的函数rotozoom。它该函数提供了旋转和扩大的功能。

如果代码这么写：

pln_t = pygame.transform.rotozoom(pln, 20, 2)

我们能看到的效果就是这样：

剪切函数 chop

接下来的是函数 chop，这个函数提供了图像剪切的功能。我们需要传入一个绘制对象以及一个 rect 矩形，这样就可以将输入的矩形的内容剪切出来。

pln_t = pygame.transform.chop(pln, [20,150,25,155])

    screen.blit(pln_t, (20, 150))

我们看一下代码的内容，我们在 blit 的时候，将 pln_t 放置在 (20,150) 的位置上，所以我们在 chop 的时候，将剪裁 [20,150,25,155] 这样一个矩形进行裁切。

然后我们来看一下效果。

﻿﻿ 这么多函数，是不是容易记不住？我来给这一部分做个总结：

对于精灵的所有放大、缩小或者变换的函数，都在 pygame.transform 模块里。它提供了一系列 2D 精灵的变换操作，包括旋转角度、缩小放大、镜像、描边、切割等功能，让你很方便地能够在游戏中随心所欲地对处理 2D 精灵。

Pygame 中的 Sprite

我们再来看一下 Pygame 本身，Pygame 本身就提供有 Sprite 模块，Sprite 模块提供了 Sprite 类，事实上，Pygame 的精灵类最方便的功能就是将某些序列帧的图片，做成动画，并且保存在 Sprite 的组（group）里面。在 Pygame 里面，Sprite 是一个轻量级的模块，我们需要做的是要将这个模块继承下来，并且重载某些方法。

类 explode

我们现在有一副图片，效果是打击到某个点，开始爆开图案。

这幅图片一共三帧，是一个标准的精灵动画。那么我们需要做的，就是先将这幅图片导入到精灵类当中。我们做一个类 explode：

class explode(pygame.sprite.Sprite):

这个类继承自 Sprite 类，然后我们定义一个初始化函数，并且首先调用上层基类的初始化。

  def __init__(self, target, frame, single_w, single_h, pos=(0,0)):

        pygame.sprite.Sprite.__init__(self)

在这个类当中，我们看到了函数的定义内容，第一个参数self，我就不过多解释了；target是我们需要载入的目标图片；frame是我们需要告诉这个类，我们这个动画有几帧；single_w, single_h 代表了我们每一帧动画的长宽。在这里，我们的每一格动画是 262×262。pos是我们告诉屏幕，将这个动画放置在屏幕的什么位置。

接下来，这是我编写的初始化代码：

def __init__(self, target, frame, single_w, single_h, pos=(0,0)):

        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.image.load(target).convert_alpha()

        self.main_image = self.image

        self.frame = frame

        self.rect = self.image.get_rect()

        self.count = 0

        self.single_w, self.single_h = single_w, single_h

        self.rect.topleft = pos

大部分代码你应该都能理解，但是有几个点，我要特殊说明一下。

第一个是main_image。这个是保存主 image 图片。我们在后续的切换帧的时候，需要在 main_image 中切割后面几帧，并且呈现在屏幕上，这样就会在视觉效果中呈现动画效果。count是每一帧的当前计数。在这里我们一共拥有三帧，这三帧我们记录在 self.frame 里，是总的帧数。

重载函数 update

接下来，我们来看一下 update 代码。

 def update(self):

        if self.count < self.frame-1:

            self.count += 1

        else:

            self.count = 0

        self.image = self.main_image.subsurface([self.count*self.single_w, 0, self.single_w,self.single_h])

Update是一个重载函数。事实上，在 update 函数里，需要判断帧数、当前循环的计数等等。但是，为了能让你能更直观容易地感受代码做了什么内容，所以我直接使用 self.count 来做帧数的计数。

进入函数后，我们使用 self.count 来和 self.frame 的总帧数进行对比。如果帧数不足以切换，那就加 1，否则就置为 0。判断结束后，我们就将 image 变成下一帧的内容。

其中，subsurface 的意思是传入一个 rect 值，并且将这个值的 surface 对象复制给 image 对象，并且呈现出来。

这时候，我们需要将这些内容放入到 group 中。

exp = explode('explode.png', 3, 262,262, (100,100))

group = pygame.sprite.Group()

group.add(exp)

首先，exp 就是我们定义的 explode 类的对象，我们分别传入的内容是图片、帧数、单个帧的宽度、单个帧的高度，并且将这个精灵显示在屏幕的位置。

随后，我们定义一个叫作 group 的对象，并且将 exp 对象填入 group 中。随后，我们在大循环内，写一串代码。

group.update()

    group.draw(screen)

这个 update，调用的就是exp.update 函数。draw 就是在 screen 中绘制我们填入 group 中的内容。由于动画在文章中无法显示，所以我就不将图片放入到文章中来了。

在精灵类中，我们除了动画的呈现，还有碰撞效果的制作。这属于更为复杂的层面，后续的内容，我将会用简单的方式来呈现碰撞的实现。

当然，Sprite 类还有更为高阶的用法，除了碰撞，还有 Layer（层）的概念。group 的添加精灵，事实上是没有次序概念的，所以哪个精灵在前，哪个在后是不清楚的，到了这个时候，你可以使用 OrderUpdates、LayerUpdates 这些类，其中 LayerUpdates 拥有众多方法可以调用，这样就会有分层的概念。

小结

这一节，你需要记住这几个重点。

精灵的变形、缩放以及 pygame 中关于精灵类的一些简单的操作。

你可以直观地感受到，精灵类和 group 类配合起来使用是一件很方便的事情，也就是说，我们忽略了 blit 的这些方法，直接在 group 中，进行 update 和 draw 就可以一次性做完很多的工作。

如果我们单独编写精灵的序列帧动画函数，也不是不行，但是你可能需要编写相当多的代码来代替 Sprite 和 group 类的工作。

现在留一个小问题给你。

结合精灵的变形、放大和缩小，再结合 Pygame 精灵类的内容，要在 update 重载函数里绘制动画帧效果，并且不停地放大、缩小，该怎么实现呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第15讲 | 如何设置淡入淡出和碰撞检测？

我们在前一节，学习了精灵的变形、放大和缩小，并且学习了如何使用精灵类和组的概念来管理精灵，制成动画。今天，我将带你学习淡入淡出和碰撞热点的判断。

所谓的淡入淡出，就是英文的fade-in和fade-out。淡入淡出在电影、游戏、CG、操作系统、手机 UI、应用等等各种地方随处可见。那究竟什么是淡入淡出呢？它在游戏中究竟如何实现呢？在我们的打飞机游戏中，什么时候会用到这个操作呢？

什么是淡入淡出？

不知道你有没有注意，在我们玩过的打飞机游戏中，当每一关游戏开始的时候，都会有个游戏画面逐渐出现的过程。短短几秒，从无到有，整个画面就呈现在你眼前了。同样，每一关结束的时候，也会有个画面逐渐消失的过程。

从画面效果讲，这个画面从有到逐渐屏幕变暗，直到消失，或者反过来，由暗逐渐变亮，到完全进入画面的过程，就叫做淡入淡出。从声音角度讲，也存在淡入淡出，比如音乐从无声到逐渐有声，或者从有声到逐渐无声。

在 Pygame 中并不存在“画面的淡入淡出”这样的函数，需要我们自己去实现这样的功能。

首先，如果我们想给这张图片进行淡入淡出的处理的话，就需要对它进行 alpha 混合处理。我们在前面谈到过 alpha 混合，你可以理解成半透明，但是 alpha 混合究竟是什么呢？

alpha 混合就是将一部分被遮盖的图像进行半透明处理。在游戏引擎或者游戏库中，图像的 alpha 值是可以被修改的。每动态修改一次 alpha 值，就会让图像更透明或者更不透明。通过制作出 alpha 效果，我们可以在游戏中实现各种绚丽的效果。

一般来讲，底层图形接口的颜色为 32 位的值，包含 RGB 以及 A（alpha），其中红色 R、绿色 G 和蓝色 B 各为 8 位，alpha 也为 8 位，所以合起来是 32 位的颜色值。

但是如果不存在 A 通道，那么就是 24 位的颜色值。每个颜色值都有 256 个级别的值，从程序角度是从 0 到 255，而支持 alpha 通道的图片格式有 png、tiff 等。但是如果没有带 alpha 透明通道的图，我们也可以在程序中设置它的 alpha 值来做透明。

如果是 Pygame，在 load image 函数的时候，不要处理 alpha，也就是不要调用 convert_alpha 函数。具体为什么呢？我后面给你揭晓。

如何做出淡入淡出效果？

我们在没有背景图片载入的时候，做淡入淡出效果，就不是使用 alpha 通道了，而是需要用fill 函数来填充背景色。

如果背景色是（0,0,0），也就是纯黑的话，那么就需要将（0,0,0）逐渐变成（255,255,255）来变成纯白，或者你自己定义一个 RGB 值来完成最终淡出后的背景色。

我们现在来看一下这段代码。

pln = pygame.image.load(plane).convert()

a=0

while True：

pln.set_alpha(a)

screen.blit(pln, (20, 150))

if a > 255:

    a=0

screen.fill([a,a,a])

a += 1

这段代码中，我们开始载入飞机图片。注意一下，我们没有用 convert_alpha。如果我们用了 convert_alpha，就会出现设置的 alpha 值没有任何作用。因为，在载入的时候，已经处理了 alpha 值了。

随后，我们定义一个变量 a，这个 a 既作用在 screen.fill 上，将 fill 的 RGB 值进行变换，也作用在 set_alpha 这个函数里，这个函数将图片的 surface 进行 alpha 值的设置，最后 blit 出来，呈现在屏幕上。

我们呈现的效果就是这样。

其他图片也可以做 alpha 混合，我们将最早的背景 jpg 图片传入，进行 alpha 半透明调整，效果是这样的。

如何设置碰撞检测？

说完了 alpha 混合，我们现在要来学习一下碰撞相关的内容。这个很好理解，飞机相撞了，就要用到碰撞。

事实上，在游戏中，碰撞属于物理引擎的一部分。特别是在 3D 游戏当中，物理引擎是独立于图形引擎的一个模块。程序员需要将图形引擎的对象填入到物理引擎中，计算出碰撞的结果，然后再返回给图形引擎，给出画面效果。做得精致的 2D 游戏也有独立的物理引擎，专门检测碰撞、计算重力等等。

但是在今天我们的课程中，我将使用浅显易懂，用你最能看懂的代码来解释碰撞是怎么回事。

事实上，我们今天要讲到的碰撞是两个图片相交之间的碰撞检测，这并不算物理检测，而是图片检测。

既然我们要检测的是图片，那么哪些前置信息是我们需要知道的呢？

首先，我们肯定要知道这两张需要碰撞图片的长宽，才能计算图片是否相交。在计算图片相交的时候，我们首先要知道它所在位置的 x 轴的起点，然后要知道它的图片宽度，然后我们要知道图片位置的 y 起点，以及它的图片长度，这样我们就得到了图片的长宽。

我们用上面的主角飞机图片和敌人飞机图片来做演示。

让两架飞机面对面，敌人的飞机从上往下飞，主角飞机从下往上飞。如果两架飞机碰到，我将在后台的命令行窗口显示一些字符串。

定义碰撞函数

接下来，我们来看一下，如何定义这个碰撞函数。

def collide(a, axy,  b, bxy):

    a_x1, a_x2 = axy[0], axy[0]+a.get_width()

    a_y1, a_y2 = axy[1], axy[1]+a.get_height()

    b_x1, b_x2 = bxy[0], bxy[0]+b.get_width()

    b_y1, b_y2 = bxy[1], bxy[1]+b.get_height()

    a1, a2 = range(a_x1, a_x2) , range(a_y1, a_y2)

    b1, b2 = range(b_x1, b_x2) , range(b_y1, b_y2)

    ct = 0

    for a in a1:

        if a in b1:

            ct = 1

            break

    for a in a2:

        if a in b2:

            if ct == 1:

                return True

    return False

我们来仔细地看一下这段函数。

首先，collide 函数拥有四个参数。第一个参数是第一幅图片的对象，第二个参数接收一个元组，接收第一幅图片所在的 x 轴和 y 轴，第三个参数是第二幅图片的对象，第四个参数接收一个元组，接收第二幅图片所在的 x 轴和 y 轴。

随后，代码进入一个得到长宽的过程。

a_x1 获取 a 图片对象所在屏幕的 x 点的起始位置，这个位置由第二个参数的元组下标 0 提供，a_x2 获取 a 图片对象所在屏幕的 x 点的终止位置（事实上是它的宽度），由于有 x 轴的起始坐标的关系，所以需要起始坐标加上图片宽度，才是它真实的 x 坐标结束点。

a_y2 获取 a 图片对象所在屏幕的 y 点的起始位置，这个由第二个参数的元组下标 1 提供，a_y2 获取 a 图片对象所在屏幕 y 点的终止位置，其实是它的长度，和前面的 x 轴一样，需要加上 y 轴所在屏幕的位置，才是真正的 y 轴的结束点。

和 a 图片是一个道理，b 图片我就不作具体阐述了。

接下来，我们需要知道整个图片所在的屏幕点，那么我们就需要用到range 函数。

Python 的 range 函数，是自动形成的一串整数列表。它的函数原型是这样的。

range(start, stop, [step])

其中步长 step 可以省略。因为默认是 1，所以如果在 range 中输入了开始和结束，就会形成一个列表。如果省略了 stop，就会从 0 开始计数，形成一串列表。比如 range(5)，那就会形成 0，1，2，3，4。

我们在 range 中形成了一串列表，其中 a1 对应的是，a 图片 x 值的起始点到终止点的列表，a2 对应的是 a 图片 y 值的起始点到终止点的列表。接下来的 b1 和 b2 就不做阐述了，和 a1 是相同的代码逻辑。

碰撞的检测

随后，我们就需要进行碰撞的检测了。

首先，我们先要判断 a 图片 x 轴的列表数字里面，是不是存在 b 图片的 x 轴的数字。如果存在，那么就把计数加 1，跳出循环。

接下来，我们再判断 a 图片的 y 轴的列表数字里面，是不是存在 b 图片的 y 轴的数字。如果存在，那么就返回为真（True），就说明碰撞检测成功了。如果计数等于 0 或者计数等于 1 但是并没有通过 y 轴的列表检测，那么就返回假（False）。

我们来看一下传入参数的代码。

y1, y2 = 1, 1

  screen.blit(pln, (100, 300 + y1))

  screen.blit(enm, (100, 20 + y2))

  print collide(pln, (100,300+y1), enm, (100,20+y2))

  y1-=1

  y2+=1

我们在 blit 绘制的时候，y 轴加入了一个变量，就是 y1 和 y2。其中主角的飞机 pln 对象，y 轴始终减 1，敌人的飞机 enm，始终加 1，为的就是让两架飞机对向飞过来并且检测碰撞。

我们将 pln 和 enm 以及它们所在的位置，分别传入 collide 函数，进行检测。我们将在命令行后台打印 True 或者 False。如果是 False 就是没有碰撞，如果是 True 就是碰撞了。

当两架飞机碰到的时候，True 就出现了，那是因为 x 轴和 y 轴都有不同程度的重叠。所以在 collide 函数里面，就返回了 True。

另外，在 Pygame 里，精灵类当中也有碰撞检测的类可以提供使用，但是，使用碰撞检测类可以用来进行球形的判断，而不能用于矩形图片之间的判断。 这是更为高级和复杂的用法，在这里不做更深的阐述了。

小结

今天，我和你讲解了淡入淡出以及碰撞的热点检测。我们需要设置 Alpha 混合和背景填充，来实现淡入淡出，而普通图像碰撞的检测，则是通过判断图像 x 轴和 y 轴是否重叠来实现。

给你留个小问题吧。

如果给你一张图片，需要判断精准的碰撞，比如碰到机翼，或者碰到某一个非矩形的位置，你该如何判断碰撞结果？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第17讲 | 如何制作游戏资源包和保存机制？

我们要做一款打飞机游戏，里面有飞机图片、背景图片、飞机音效、碰撞音效等等非常多的素材。如果将这些资源都放置在一个目录下，将会变得非常混乱。如果按照素材内容来划分目录，程序读取的效率就不高，所以我们需要将这些素材打包在一个资源包内，然后将每个素材都放置在一个虚拟目录内。

因此，今天我们就来如何制作讲解资源包。简单来说，所谓的资源包，就是将游戏的所有资源和素材，进行打包分类，并且进行资源整合，亦或将资源和素材进行压缩，减少游戏体积。

什么是资源包？

我总结了一下，可以从这三个角度来理解什么是资源包。

资源包是一种将游戏的资源和素材进行分类、梳理，并且打包的一种包裹。

资源包可以用来压缩游戏资源和素材，减少游戏体积。

资源包里存在任何可能性，比如它可以包含图片文件、模型文件、音频文件、脚本文件等等，具体要看游戏开发人员的配置需求，来决定资源包里的内容。

现在很多游戏公司都不会编写特殊的资源包格式。因为设计一种资源包格式，需要经过一系列复杂的动作，包括包头、包身和包尾。

关于这个格式的设计，一会儿我会给你仔细分析。因为，和我们自定义网络协议包一样，一个好的资源包，能够很方便进行解包、打包、删除文件、插入文件的操作，以及游戏的在线更新、补丁更新、资源包的解包、打包、删除、插入、更新文件等操作。

而一个好的资源包格式，不会占用主程序大量的时间。因为在游戏中，需要直接读取包文件里面的内容。

比如我们之前在 Pygame 中读取的图片文件，在包裹格式中，可能会这么写伪代码：

load.image(‘package.pack/plane.png’)

其中 package.pack 就是包裹，plane.png 是存在在包裹里面的其中一幅图片文件。这样，打了包裹后的文件，就不会污染目录。一般一个包裹文件中存在大量资源，而我们只要按照包裹路径读取就可以了。

如果不编写特殊的资源包格式，那应该怎么制作资源包呢？答案是，使用现成的压缩软件库，进行打包压缩，直接在程序内使用。比如我们最常用的 zip 文件、rar 文件，都是可以拿来做资源包文件的。在 Python 中有内置 zip 模块，可以直接读取 zip 文件。我们可以直接在 Pygame 中结合 zip 模块进行编程。

资源包的格式

我们要讲解的是资源包的制作，我将会用一种较为通用和简单易懂的方法，解释资源包都包含哪些内容，同时让你理解资源包是怎么制作的。

首先，从编程的格式来理解资源包，你需要了解下列这些内容。

资源包头，是一种标记，存放在包裹里最开始的几个字节，一般是 2～4 个字节。资源包头可以用来辨别这个资源包是哪个公司出品的。例如我后面准备举的一个例子，这里面就有 INFO 这样的标记，INFO 可能是这家游戏公司的名字或者是缩写等等。

资源包版本，这个不是必须的。如果考虑到一款游戏各个版本之间变化很大，未来可能会修改资源包的格式，那么这个时候就需要版本号。版本号一般会使用 2 个字节的 short 类型来存储，或者直接用十六进制编辑器能看明白的字符串，来代表版本号，比如用 10 表示 1.0。所以，结合资源包头，我们现在所看到的结构是 INFO10。

资源包是否进行压缩，这个也不是必需的，但是有一些资源包会说明白，究竟是不是压缩资源包。如果是压缩就是 1，不是压缩就是 0。至于压缩格式，一般在编程的时候，就会指定清楚，不需要特别说明在资源包内。

资源包的目录结构以及素材名文件名偏移量，资源包内的目录结构都是虚拟的，所以你可以定义在资源包内类似于 /game/res 这样的目录结构。但是事实上，这只是方便程序调用，事实上目录是不存在的，这是一种只存在在包裹内的虚拟目录。

然后，我们需要规定素材的文件名和偏移量。比如 /game/res/background.jpg。这是告诉我们在 /game/res 虚拟目录下，拥有 background.jpg 这个文件。随后需要告诉程序偏移量是多少，一般是存储 4 个字节的整型数字。

到目前为止，资源包的格式看起来可能是这样的：

INFO100/game/res/background.jpg,[四个字节的偏移量]

在这里，我们看到，偏移量之前多加了一个逗号“,”。这是一个分隔符，也就是告诉程序，这一段在哪里结束。

随后是四个字节的偏移量。所谓的偏移量，就是告诉程序，你要到这个包裹的第几个字节去寻找这个文件的具体内容。

资源包的素材本体。每个本体都可能是一个二进制文件、文本文件或其他任何文件。这些文件的文件名在资源包的素材文件名中都被定义好了。在资源包的素材本体中，我们可能会碰到各种各样的二进制字符，那么我们怎么知道这些素材是从哪里开始哪里结束的呢？

资源包的素材长度，规定素材的长度有两种方法，一种方法是在定义资源包的目录结构以及素材偏移量的时候，再加上一个素材长度，也是四个字节的整型数字。这种方法的好处是，不需要添加某个分隔符告诉程序，这个素材的本体到这里结束。第二种方法是在本体结束的位置添加分隔符，比如一个逗号或者分隔符号|。这种方法的好处是，不需要知道文件长度是多少。但是坏处是，分割符号可能会和素材本体重叠。

比如素材的本体是个二进制文件，分隔符比如是!@#，素材的本体里面也存在!@#，素材的本体里面也存在!@# 这样的内容，这样的情况下，就会出现读取中断，因为程序以为素材内的!@#$ 就是结束符号，事实上这只是素材本身的内容而已。

资源包结束符，这个也不是必须的。我们要结束资源包，必须在资源包的结尾添加结束符，这个结束符是告诉程序，资源包已经结束了。

我们来看一个完整的资源包，大概是什么样子的。

[资源包头][版本号][是否压缩][资源包目录 / 素材文件名 A][文件 A 偏移量][文件 A 长度]…[资源包目录 / 素材文件名 N][文件 N 偏移量][文件 N 长度][素材 A 本体]….[素材 N 本体][结束符]

了解了资源包的格式内容，我们可以很方便地利用 Python 或者 C 语言等来编写相应格式的资源包。

我来给这部分做一个总结：

资源包的存在，有两个目的，一是让游戏目录干净整洁，不然看上去都是乱七八糟的图片和各种配置，二是让游戏程序能更快地从内存中读取游戏资源制作的包裹文件，加速游戏的运行效率。这个包裹文件中含有虚拟目录、资源、资源位置、资源名字等等信息。我们不需要从文件目录中去读取单一文件，只需要从内存中载入的资源包中取出某个文件即可。

如何制作游戏的保存机制？

每一个游戏几乎都有保存和载入的机制。首先你需要知道，只有保存了数据，我们才能载入数据。那么游戏的保存机制是怎么做的呢？

事实上，游戏的保存和游戏的地图编辑器中保存地图的原理，可以说是异曲同工。如果一个游戏中，有地图、坐标、人物、装备、分数，这些都需要被记录下来，那么我们不可能将地图、坐标、人物、装备、分数等全部转换成二进制文件记录下来。那应该怎么做呢？

首先，如果是记录地图，有地图 1 或者地图 2，我们只需要记录地图的 ID 就好了。假如是地图 2，坐标是（x，y）。人物只需要记录人物的 ID，再关联到人物。一个游戏中，玩家建立了一个人物角色，就会将这个人物角色进行保存，不至于丢失人物角色。所以，在读取游戏的时候，需要先读取人物角色，再读取保存的游戏内容。

至于分数就很好记录了，记录分数其实就是记录数字，所以记录起来会很方便。

那么装备呢？如果是装备，一般会将装备的所有内容记录下来，如果做得精致的游戏，还会将地图中那些掉落的装备和死去的 NPC 进行记录。

还有一种做法是，将游戏保存的文件直接导出成一个脚本文件，以后每次读取数据就只需要使用程序读取脚本就可以了。

小结

今天我讲解了资源包的制作以及游戏进度的保存，你需要你记住这些内容。

制作资源包的目的是为了厘清游戏素材以及游戏素材的存放结构。资源包的结构与压缩包的结构比较相似，但是为了更贴合游戏程序读取，会对虚拟目录和素材文件名等，做一些修改。

另外，为了方便保存游戏进度，我们可以做成游戏脚本，第二次打开游戏直接载入保存的脚本即可。

给你留一个小思考题吧。

在《GTA》中，汽车会有不同程度的损毁，当你保存完游戏重新进入的时候，汽车又复原了，请问这是为什么呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第18讲 | 如何载入背景音乐和音效？

好的音乐总是伴随着游戏一起，一直被玩家所记忆。在游戏中播放音乐和音效并不是什么困难的事情，但是究竟什么时候播放什么音效，具体怎么实现，这恐怕就需要一些技巧了。比如，我今天要讲的，我们可以和某些函数捆绑在一起实现。

Pygame 支持 mp3、ogg、wav 音频和音效的播放。音乐的模块都在 pygame.mixer 中，这里面包括音乐和音效。

我们在使用音频部分模块的时候，需要先初始化一次。

pygame.mixer.init()

这个初始化应该在 pygame.init() 的初始化之后。

我们来看一下具体的函数，这些函数，存在在 pygame.mixer.Sound 模块下。

我们再来看一下 Pygame.mixer.music 音乐模块。我们可以尝试一下载入音频并且播放。

pygame.mixer.music.load('bgm.mp3')

pygame.mixer.music.set_volume(0.5)

pygame.mixer.music.play()

s1 = pygame.mixer.Sound('a.wav')

s1.set_volume(0.5)

s2 = pygame.mixer.Sound('b.wav')

s2.set_volume(0.5)

我来解释一下这段代码。

刚开始，我们载入了一个名叫 bgm 的 mp3 文件，告诉程序需要载入这个文件，然后调整音量到 0.5，随后就是 play，也就是播放，播放是在程序的后台播放，然后程序会接着跑到下面的代码行。

随后，我们使用 Sound 模块，Sound 模块初始化会载入 a.wav，然后返回一个对象，这个对象设置音量为 0.5，随后再初始化一次，载入 b.wav，然后设置音量为 0.5。

到这里为止，我们已经将所有的初始化、设置都在游戏的循环外做好了。

随后，我们需要结合前几节的内容，在循环里面，对飞机碰撞进行声音的操作，比如出现爆炸声的时候，播放什么声音；碰撞结束，播放另一种的声音。

if True == collide(pln, (100,300+y1), enm, (100,20+y2)):

s1.play()

else:

  s2.play()

for event in pygame.event.get():

   if event.type == QUIT:

     pygame.quit()

   if event.type == KEYDOWN:

     if event.key == K_p:

       pygame.mixer.music.pause()

     if event.key == K_r:

       pygame.mixer.music.unpause()

首先，我们使用collide 函数。这在前面几章有过详细的说明。

这是一段检测飞机碰撞的代码，如果飞机碰撞了的话，就会返回 True，如果返回 True 的话，我们就播放 s1 音频，否则就播放 s2 音频。当然，这个 s2 音频可能会一直在播放（因为一直没有碰撞）。

随后就是事件监测，如果检测到 K_p，就是按下键盘 p，就让音乐停止，使用 pause 函数；如果按下 r 键，就恢复播放。

我们在 Pygame 上的操作已经基本结束了，但是，音频和音效的内容并没有结束。

在游戏编程中，我们需要嵌入音频和音效，特别是在没有 Pygame 的时候，如果有一些游戏引擎没有提供音频库的话，我们就需要自己使用第三方的音频库。虽然可以使用耳熟能详的 ffmpeg，但是感觉有点大材小用了，所以我们需要一个专门的音频库。

在这里，我推荐BASS 音频库。你可以去 http://www.un4seen.com 下载开发库。这个音频库是不开源的，如果你只是自己开发游戏玩玩，非商业目的，就可以使用。如果是商业使用，那就需要购买证书。

在这个页面上，我们点击 download 按钮，就会下载最新版本的开发库。解压缩下来，会出现对应几个语言的开发目录。

其中 bass.dll 文件是动态链接库，要使用的话，可以在 c 文件夹下，使用 lib 库和 bass.h 进行头文件包含进行编程。

我们来看一下，如何使用 C/C++ 语言加入 Bass 引擎的代码。

BASS_Init(-1, 44100, 0, hwnd, NULL);

HSTREAM s = BASS_StreamCreateFile(false, "a.mp3", 0, 0, 0);

BASS_ChannelPlay(s, false);

BASS_StreamFree(s)

首先，我们将 BASS 库初始化，初始化的参数是：设备、输出比率、标志位（比如 8 位音质、立体声、3D 等等）、Windows 句柄。你也可以输入 0。最后一个是 clsid，就是用于初始化 DirectSound 的类的 ID，一般会使用 NULL。

随后，开始从文件建立一个流，BASS_StreamCreateFile 函数，返回一个 HSTREAM。HSTREAM 其实是一个 DWORD 类型。

这个函数里的参数，我也解释一下。

第一个参数是内存。如果传入 true 的话，就将这个流保存在内存中；否则的话，就不保存在内存中。

第二个参数是音频文件名。这个参数和第一个参数会联动。当第一个参数保存在内存中的时候，就填入内存地址，否则就填入文件名。

第三个参数是偏移量，也就是文件从哪里开始播放。当然这个参数只在第一个参数为 false，不保存在内存的情况下起作用。

第四个参数是长度，如果填入 0，就是所有长度。

最后一个是标志位，填入的是创建模式，比如是循环播放方式，还是软件解码模式等等。

接下来就是开始播放，第一个填入的是刚才返回的流的句柄，第二个参数是是否重新开始播放。最后一个就是播放完后进行回收资源，删除句柄。

float v; DWORD r;

BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, 100);

v = BASS_GetVolume();

v = 200;

BASS_SetVolume(v);

r = BASS_ChannelIsActive(s);

if(r == BASS_ACTIVE_PAUSED)

...

else if(r == BASS_ACTIVE_PLAYING)

...

else if(r == BASS_ACTIVE_STOPPED)

...

else if (r == BASS_ACTIVE_STALLED)

  ..

接下来就是调整音量以及获取播放的状态功能。

其中 BASS_SetConfig 中，第一个参数是选项，第二个参数是调整音量的值，BASS_CONFIG_GVOL_STREAM 的意义是全局的流的音量。

随后我们就开始取得音量，BASS_GetVolume 是获取系统的音量，并不是流的音量，第五行代码就是设置系统音量。

接下来，我们就要获取播放的状态。在 BASS_ChannelIsActive 的函数内填入流的句柄，随后获取返回值，然后使用返回值进行比较，其中 BASS_ACTIVE_PAUSED，就是播放状态暂停，BASS_ACTIVE_PLAYING 是正在播放中或者录音状态，BASS_ACTIVE_STOPPED 是停止状态，或者流句柄并不是有效的，BASS_ACTIVE_STALLED 是停滞状态。

一般的原因是，播放的状态缺少样本数据，流的播放停滞了，如果数据足够播放的话，就会自动恢复。

BASS 库还有许许多多的函数和功能，就不在这里过多阐述了。

小结

我来总结一下。今天我们讲解了 Pygame 中音频和音效的播放。你应该记住这些东西。

在 Pygame 中，播放音乐是不需要进行多线程控制的。它本身就会在后台进行播放。

所有的音乐和音效都在 pygame.mixer 模块中，如果载入的是音乐，就使用 music 模块；如果载入的是音效，就使用 Sound 模块。

随后我们介绍了 BASS 音频库。这几乎是最专业的音频库了。由于是 C 接口，所以通用多种语言，你可以使用.NET 或者 VB 等语言来应用。当然如果要进行后台播放、多个频道播放等功能，你需要编写多线程的代码，并没有 Pygame 那么轻松，这里面很多事情需要自己去做。

现在给你留一个小问题。

在 pygame.mixer.music 模块中，如何播放一首音乐后立刻播放另外一首音乐？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第20讲 | 如何在游戏中载入UI和菜单？

今天我们要在游戏中载入 UI 和菜单，在开始之前，我们先调整一下，我们之前讲过的游戏代码部分的内容。

首先我们需要更改游戏的背景图片，使之看起来更像是一款打飞机的游戏，而不是最早之前我们随便用的一幅山水图。我们先将游戏背景修改为正常的游戏背景，并且贴上飞机图像。

这里，我想到一个问题，之前有人留言问我，程序员需不需要有美术功底。我在这里说一下我的看法。如果你只是要做一个程序员，那我可以告诉你，不需要。但是，如果你不是只想做一个“码农”，你想有更多的发展，那各方面的知识，比如策划、美术，你都需要去了解。

UI 的两种呈现形式

言归正传，我们需要在这个游戏画面上面，加一系列的内容来代表 UI 的呈现。UI 的呈现有两种形式：

第一种就是美术画好 UI 图片，然后直接贴图，用鼠标控制一系列的点击和按下操作；

另外一种是自己画 UI 界面，比如画一个框，并且在框里面写上文字。

这两种方式各有利弊。

如果使用美术 UI 图贴图的方式，优点就是可以减少程序员的工作量。这样在版本迭代的时候，美术改一幅图就可以修改界面，方便快捷，你就不需要做多余的工作。但是这样的缺点就是，增加了游戏安装包的大小，毕竟 UI 是一幅图，只要是图就会有一定的体积，就会增加安装包的大小。

如果是程序员自己绘制的 UI 界面，好处就是主程序体积变得稍大一点，而游戏安装包不会变大。但是这样的缺点也很明显，就是程序员的工作量会增加很多。而且当游戏需要迭代的时候，或者界面需要更新的时候，程序员需要重新绘制或者重新编写 UI 源代码，大大增加了工作量。

我们现在是自己来开发，那我就讲一讲程序员绘制 UI 的方法。

我们通过模拟按钮的方式来摆放 UI 界面。首先，我们要在 UI 界面上摆放一系列字符，我们要实现的效果是，只要使用鼠标点击到这个字符，就会变换字符的内容。这个过程，我们会用到鼠标操作、绘制矩形、字体和字符绘制相关的知识，下面我就来具体给你讲。

鼠标操作

我们先来看一下鼠标操作的知识。在 Pygame 中，鼠标操作用到的模块是 pygame.mouse。在这个模块里面，点击事件的函数是 get_pressed. 假如有返回按钮 1、按钮 2 和按钮 3 等等很多按钮, 我们随便选一个点，假如说选了按钮 1，那代码可以这么写：

pygame.mouse.get_pressed()[0]

这条语句不需要在事件语句里面操作，写在别的地方也可以，而且鼠标的操作在循环里一直是实时监测的。

绘制矩形

随后，我们要绘制矩形。 绘制矩形的目的是为了模拟一个按钮。 矩形绘制的代码是 Rect，但是我们需要绘制在一个 surface 上，这个 surface 需要新建，然而在 pygame 中，如果使用 pygame.surface.surface 初始化一个 surface 的话，不能指定位置，x 值是从 0 开始的。

所以在屏幕上看到的新建的 surface 是一个长条状的图层，所以我们需要将生成一个图层的子图层，并且，如果使用这个子图层的话，在 blit 的时候将会提示被锁定，所以，我们还需要将这个子图层进行拷贝。所以，我们的代码看起来是这个样子的。

the_rect = Rect(200, 100, 150, 40)

block_surface = screen.subsurface(the_rect).copy()

首先第一行代码是建立一个矩形，分别是左侧起始值是 200，顶部从 100 开始，宽度 150，长度 40。随后，使用 screen 这个图层来建立一个子图层，子图层的大小按照 the_rect 这个矩形大小来建立。随后的一个 copy 函数，是将子图层进行拷贝，在后续的使用中，不会出现锁定图层的情况。

绘制字体和字符

之后我们要开始编写文字处理的代码，字体我们要用到 pygame.font 模块，我们先初始化一个字体，这个字体在安装 pygame 游戏库的时候就包含在了 pygame 里面，我们直接就可以拿来使用。现在我们初始化字体，并且将字体大小调整到 25：

fnt = pygame.font.Font('freesansbold.ttf',25)

其中，freesansbold.ttf 是 pygame 安装的时候默认存在的 ttf 字体文件，随后，我们在第二个参数设置为大小 25。

我们拿到了 fnt 对象，然后使用这个对象调用 render 函数。这其实就是渲染，将文字渲染在屏幕，并且形成一个文字图层，函数原型是这样的：

Font.render(text, antialias, color, background=None)

其中第一个参数 text 是文字内容，第二个参数 antialias 是抗锯齿，第三个内容 color 是文字颜色，最后一个是背景颜色，默认可以忽略。

tsurf = fnt.render(text, True, (255,255,255))

我们将颜色设置为白色，所以是（255，255，255）。

tsurf 是 render 返回的一个文字的图层（surface），我们之后要按照这个图层，来确定它的矩形框。

trect = tsurf.get_rect()

随后我们需要将文字摆在这个 trect 矩形框的中央，所以我们要进一步将 trect 确定在中央的位置，计算完中央的坐标值并且赋值过去。

trect.center = ((block_surface.get_width()/2),(block_surface.get_height()/2))

我们将最开始的复制的子图层的宽度和高度除以 2，就得到了中心点的位置。

最后我们要做的就是在封装的函数内将 blit 部分包含进去，现在我们来看一下完整的包装函数代码。

def text_out(text):

    fnt = pygame.font.Font('freesansbold.ttf',25)

    tsurf = fnt.render(text, True, (255,255,255))

    trect = tsurf.get_rect()

    trect.center = ((block_surface.get_width()/2),(block_surface.get_height()/2))

    block_surface.blit(tsurf, trect)

我们看到，在函数最后，我们 blit 了 block_surface 这个被拷贝的图层。

随后我们在游戏的大循环里面，需要判断鼠标的点击事件，我们之前所定义的矩形，代码是这样：

the_rect = Rect(200, 100, 150, 40)

所以这 x 的起始位置和结束位置是 200 和 350，y 轴的起始位置和结束位置是 200 和 240，为什么 y 轴也是 200 开始呢？因为起始点 200 既是 x 轴开始的点也是 y 轴开始的点。我们在代码里面这么判断鼠标的点：

txt = "Pause"

x, y = pygame.mouse.get_pos()

    if pygame.mouse.get_pressed()[0]:

        if x >=200 and x <= 350 and y >= 200 and y <= 240:

            txt = "Clicked

我们将 txt 定义为 Pause 字符串，并且判断是不是鼠标左键点击的。如果是的话，判断是不是在 x 轴和 y 轴进行点击，如果不是的话，就将 txt 改为 Clicked。

随后我们绘制按钮框，并且将按钮框背景设置为绿色，然后输出文字，并且绘制。

screen.blit(block_surface, (200, 200))

    block_surface.fill([0,20,0])

    text_out(txt)

将 block_surface 这个图层绘制在（200,200）的坐标点，并且将之涂为绿色，最后调用 text_out 函数，由于 text_out 里面已经编写了 blit 函数，所以不需要再次 blit 了。

我们来看一下效果图：

我们看到了一个绿色的按钮放置在屏幕上，并且有一个白色的 Pause 字样放在按钮上，如果是鼠标左键点击在这个按钮上，就会变成 Clicked 字样。

到这里，你可能会问了，为什么没有解释怎么输出中文呢？

在这种情况下，输出中文有两种解决方案。

第一种是比较底层的方案，就是根据中文进行点阵绘制，这需要很底层的代码操作，效率也不太高，所以这种方案我们不作讨论。

第二种就是改变字体，我们可以在初始化字体的时候，下载一些网上的中文字体先进行尝试，或者我们可以使用系统字体。在使用系统字体的时候，我们可以使用 SysFont 来初始化字体。

fnt = pygame.font.SysFont('微软雅黑',32)

另外，还需要修改一个地方。我们需要在 Python 代码最开始的地方添加编码方式，并且将中文文字前面添加 u 字样来告诉解释器是 Unicode 的。

# encoding: utf-8

txt = u" 暂停 "

我们来看一下效果：

至此，中文的输出也已经完成了。

到现在为止，我们编写完了 UI 的按钮部分。至于菜单部分，我们也可以通过相同的方式来编写菜单效果。

作为菜单这些高级操作，比如点击出现下级菜单、隐藏菜单这些动态效果，可以使用图片的方式来制作菜单，并且进行载入。如果使用程序来编写菜单的效果，工作量就太大了。而如果是图片形式的话，只需要载入并且控制鼠标点击和绘制子菜单就可以了。

小结

今天我们学习了 UI 部分的编写以及文字的输出、鼠标的移动和抓取鼠标按键的实现，我来总结一下今天的内容。

不管何种类型的游戏引擎，鼠标的操作基本有 3 个值需要控制，左键、中键和右键。

按钮可以用常规的方法来绘制。如果想做出更好的效果，比如画框，可以在框里面再画一个小框，看起来像是有凸出感觉的样子。这就需要你平时多注意观察一些细节，然后去分析如何用我们讲过的简单的操作来实现这些内容。

在 2D 游戏中，很多游戏引擎都不支持中文的输出。如果要输出中文，假如你的引擎支持，那你可以使用系统字体或者其他中文字体；如果引擎不支持，可以使用一个一个点阵绘制的方式在屏幕上绘制中文。最后一种方式，也就是比较极端的方式，那就是使用图片来直接贴上中文字符，这种方式直接粗暴，但是图片资源量太大，而且如果你要在游戏中进行网络聊天，这里面其实还是没有从根本上解决中文输出的问题。

现在给你留一个小问题。

如果让你在上述的代码中，将按钮变成菜单，也就是点击按钮，就在下方出下一个下拉框，你会如何实现？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第21讲 | 如何用鼠标和键盘来操作游戏？

如果一款游戏不能用鼠标和键盘操作，那我们只能当动画来看。

所以在一款游戏中，鼠标和键盘的操作是必不可少的，有时候甚至鼠标和键盘都要同时操作，比如 FPS 游戏，比如即时战略等等。鼠标和键盘的操作在 Pygame 中需要进行实时检测，这个上一节我有提到过，然后我们就可以对游戏画面进行操作。

我们在 Pygame 中有两种方式可以检测和编写键盘事件，一种是使用event 事件操作，一种是使用keypressed 函数来进行键盘的判断操作。

我们先来尝试一下使用事件进行操作的键盘事件。我们在之前的代码中，已经使用事件来判断是不是退出，我们来看如下代码：

for event in pygame.event.get():

        if event.type == QUIT:

            pygame.quit()

在这段代码里面，event.type 的类型如果是 QUIT 的话，就让 pygame 退出，那么举一反三，我们也可以在里面写上如下代码：

if event.type == KEYDOWN:

    if event.key == pygame.K_w:

        .....

在这里，我们判断事件的类型是 KEYDOWN，也就是键盘按下的事件，随后我们再在下面的代码里, 判断 event 所返回键盘 key 值，是 pygame.K_w，这个 K_w 是 pygame 里面定义的虚拟键盘的按键，代表的是 Key 值为键盘 w 的按键，所以你只要按下 w 键，就会出现对应的操作。

我们来写下一系列的操作代码，在编写代码之前，我们首先要来定义一下规则。

我们的目的，是要让主角的飞机移动起来，所谓的飞机的移动，我们在前面几篇课程里面都有阐述。如果我们要让飞机在画面上移动起来，就需要修正飞机的 x 轴和 y 轴。

相应的，如果飞机往左侧飞，就需要减少飞机的 x 轴；如果飞机往右侧飞，就要增加飞机的 x 轴；如果往上面飞，就要减少飞机的 y 轴；如果往底下飞，就要增加飞机的 y 轴。我们先理清楚了这些内容之后，就可以编写键盘操作代码了。

我们先来修正飞机的 x 轴和 y 轴。我们要在游戏的循环之外，定义两个变量 xx 和 yy，以修正键盘操作后的飞机坐标。

xx = 0

yy = 0

在定义完了这些内容后，我们再来看看按键的定义。

if event.type == KEYDOWN:

            if event.key == pygame.K_w:

                yy -= 1

            if event.key == pygame.K_s:

                yy += 1

            if event.key == pygame.K_a:

                xx -= 1

            if event.key == pygame.K_d:

                xx += 1

首先，和普通的游戏一样，我们将电脑键盘上的 WSAD 按键用作上下左右的操作按键，所以我们判断了一系列的按键值，比如 K_w，K_s 等等，然后我们看到，xx, yy 的一系列操作，然后我们进行飞机的贴图和操作：

screen.blit(pln, (100+xx, 300+yy))

我们看到，基础坐标值是（100，300）。我们经过键盘操作，对 xx 和 yy 进行位置的修正。到这里为止，我们可以看到，只要我们按下 WSAD 中的任意一个按键，飞机就会往指定的位置移动。

所以如果你认为到这里按键的内容就结束了，那就错了，就像我们今天开头所说的，Pygame 下的键盘类，还有另外一种方式可以检测，你可以考虑下面的代码。

key = pygame.key.get_pressed()

    if key[pygame.K_w]:

        yy -= 1

    if key[pygame.K_s]:

        yy += 1

    if key[pygame.K_a]:

        xx -= 1

    if key[pygame.K_d]:

        xx +=

是的，我们看到了 pygame.key.get_pressed(); 函数。这个函数返回一个 Key 值。和 event 事件不同的是，我们直接可以在每一次循环内进行判断。返回的 Key 是一个 tuple 类型，在里面存放了各种按键对应的值。如果没有按键，所有值都是 0；如果有按键，其中一个值是 1。

再来看接下来的代码，如果 key 值的 tuple 里正好是 pygame.K_w 的话，那么判断结果就是真，我们来将这个内容打印出来看一下。

print key

我们打印了 key，并且按下 w 按键，随后，我们可以在游戏界面命令行看到如下内容输出：

﻿找到那个 1 没有？那个 1，就是对应的 K_w 值，在 key[pygame.K_w] 判断的时候，返回一个 1，也就是 True，那么就产生 yy-=1 的操作。接下来就是类似的代码了，我就不再作重复的阐述了。

看到这里，或许你就要问了，那有很多的游戏都有组合键，比如我按下 Ctrl 键，再按下 w 键，就会出现对应的操作，这又该怎么实现呢？

你思考一下，我们是不是可以把两个按键写在同一个判断语句下？是的，你没有猜错，确实可以这么写，这就是组合键的效果。

if key[pygame.K_w] and key[pygame.K_LCTRL]:

    yy -= 2

在这里我们看到，只要同时按下了 w 和左侧 CTRL，（LCTRL 的意思是 Left Control，就是左侧 Control 的意思），那么 yy 的坐标值就减去 2，我们操作一下就知道结果了。所以，组合键可以在同一个判断里面，使用 and 连接起来。

然后，事情并没有到这里结束，请你将这些代码写在自己的电脑里，并做一下实验，第一种方式是事件判断，第二种方式是按键判断。这两种方式的区别是什么？

看到区别了吗？如果你按照我说的去做，你会发现，第一种方式，只要按下一个键，飞机就会往指定方向移动一格，然而如果你一直按着这个键，飞机是不会移动的，要等你再按下键盘才行。而第二种方式，只要你一直按着这个键，飞机就会一直不停往指定位置移动。

问题究竟出在哪里呢？

问题在于，事件判断首先判断了 KEYDOWN，当你按下按键的时候，KEYDOWN 已经被判断了，随后我们再进入 event 的按键类型的判断，但是如果你这时候一直按着键盘，KEYDOWN 事件并没有被唤起，所以一直按着按键并没有起到作用，所以你要按下键盘，松开，再按下一次，飞机才会移动。

而第二种方式，在循环里面，只要键盘按下去，就会一直返回一个 tuple 给 key，然后在继续做判断，所以，我们只要一直按着键盘，一直会做判断，直到按下的键盘是 WSAD 为止。

接下来，我们要做一下鼠标的操作。鼠标的操作我们在前几次的课程中也进行了介绍，我们再来温习一遍，并且添加一些新的内容进去。

我们今天要把一幅图片贴在鼠标的位置，并且随着鼠标的移动而移动，我们先来看下列代码：

mouse = 'mouse.png'

mouse_cursor = pygame.image.load(mouse).convert_alpha()

mouse_scale = pygame.transform.scale(mouse_cursor, (40, 40))

While True:

    # 获取 x, y 值

    x-= mouse_scale.get_width() / 2

    y-= mouse_scale.get_height() / 2

    screen.blit(mouse_scale, (x, y))

    ...

首先我们要定义的一幅图片名叫 mouse.png，随后载入图片并且处理 alpha 混合，这在我们先前的课程中都有过阐述。

随后我们看到了一个叫 pygame.transform.scale 的函数。这个函数的意思是，我们要重新将 mouse 这个 surface 进行缩放，其中缩放的大小长宽是（40，40），并且返回一个新的 surface。

随后在循环里，我们获取到这个 surface 的中心点，也就是计算需要绘制鼠标的 x，y 值，我们需要得到图片的长宽，并且除以 2，最后 blit 开始贴图，我们看到的效果是这样的。

那么我们如果要判断鼠标的按键怎么办呢？我们来温习一下上一次所讲的内容，鼠标的按键，也是类似的判断方式：

x, y = pygame.mouse.get_pos()

  if pygame.mouse.get_pressed()[0]:

      ....

前面那段代码用到的 x，y 的值，在这里进行代码获取。

我们看到，pygame.mouse.get_pos() 函数，获取两个值，x 和 y 坐标，后面一段代码就是获取鼠标点击的内容，其中 get_pressed 函数下标 0 返回是不是左键点击，下标 1 返回是不是中键点击，下标 2 返回是不是右键点击，最后再做出判断。

小结

今天的内容基本到这里了，我带你将内容梳理并总结一下。

首先是键盘事件判断，这里会出现按一下键盘做一下操作的情况，问题出在 KEYDOWN 事件判断上。但是如果你需要一直按键的判断，可以使用 get_pressed 函数。

组合键可以写在同一个判断下，使用 and 连起来做判断。

get_pressed 会返回一个 tuple，里面存放了所有的 key 值，只要判断 key 值是不是为 True 就是判断了有没有按键。

鼠标操作也可以使用 get_pressed 函数，也是返回 tuple，其中下标 0、1、2 分别代表了左、中、右三个按键。

最后，给你留一个小问题。

如果将组合键写在第一个按键的判断下，会出现什么情况？

if key[pygame.K_LCTRL]:

    if key[pygame.K_w]:

        ...

欢迎留言说出你的看法，我在下一节的挑战中等你！

第23讲 | 如何判断客户端的网络连接？

由于涉及到网络、脚本语言等等，这一节起，我要开始讲一些基础的、看起来比较枯燥的知识。我会尽力写得有趣生动，但是，知识的获取并不容易，即便我已经在努力去讲解，还是需要你用更多的时间去摸索和学习。

我们在前面说了 Pygame 的一些客户端知识，如果你想让这款游戏能够在网络上传输数据，接下来，那就需要编写服务器端，并且在客户端做客户端连接的准备。

前面我们已经用 Pygame 讲解了很多内容，那我们客户端的网络连接，我也继续使用 Python 来编写，当然这已经和 Pygame 没有关系了。因为网络连接是独立的代码，所以只需要直接写在游戏里就可以了。

在开始编写网络部分之前，我们需要整理一下网络的基础知识。如果我们一上来就编写网络代码，对于你来说，可能会有一些概念上的模糊。

对于网络编程，或许你已经接触到了，或许你只是有点概念，或许你一点都没接触过。但是你或许听说过 Socket 套接字、TCP/IP 协议、UDP 协议、HTTP、HTTPS 协议等等，那么这些协议是不是都属于网络编程范畴？还是这里面哪些部分是属于网络编程范畴呢？

网络，从模型上讲，一共分为七层，从底层到最上层，分别是：物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

我来分别解释一下。

物理层：所谓的物理层，你可以理解为我们看到的各种网络线，也就是人肉眼能看到的物理线路，包括光纤、以前连接调制解调器的电话线等等。这些线路就是物理层。物理层有物理层的规范，比如电流、编码、帧、连接头等等。你只需要知道物理层也是存在规范的，就可以了。物理层最主要的功能就是网络的物理连接。

数据链路层：所谓的数据链路层，就是建立逻辑连接，然后进行硬件上的寻址操作、差错的校验，然后将二进制的字节进行组合，通过 MAC 地址进行访问，比如网卡、交换机等等。你需要记住的是，在这一层，要通过 MAC 地址来进行访问，进行硬件寻址操作。

网络层：网络层进行逻辑地址的寻址操作和数据链路层不同。数据链路是使用硬件寻址操作，而网络层是使用逻辑地址寻址，比如路由器、防火墙、多层交换机等等。我们最熟悉的 IPv4（202.101.253.233）、IPv6、ARP 等等都属于这一层。你在这里需要记住的是，网络层是逻辑寻址操作，会用到 ARP、IPv4 等等协议。

传输层：在编程中最常用到的 TCP、UDP 等等协议，都在这一层进行操作，它首先定义了数据传输的协议端口号以及一些错误的检测。

会话层：会话层在传输层之上，它就在客户端和服务器端。严谨地说，就是本地机器和远端机器之间的会话，比如要进行断点续传这些操作，就属于会话层的范畴。

表示层：表示层很容易理解，就是数据的传输，然后展现在电脑上。比如图片的传输和显示、网络地址的加密等等。

应用层：应用层就是提供给电脑用户的各种网络应用，比如你自己编写的网络软件、FTP 软件、Telnet、浏览器等等。

以上这些点你要硬性记住的话，会比较困难。我教给你一个方法。

首先，我们想象一段从网线过来一段数据，网线就是“物理层”，那么数据需要找到一个门牌号，这个门牌号是一个硬件地址，可能是你的电脑网卡，也可能是你公司的交换机。这些数据需要把这些门牌地址连接起来，这就是“数据链路层”。

随后，这些数据找到门牌号后，就需要分发到逻辑地址，比如路由器或者你的 IP 地址，这些逻辑地址就是网络地址，这就是“网络层”。

经过网络层后，就要看这是什么数据，是 TCP 协议的，还是 UDP 协议的。知道了协议后，才可以传输数据，所以这个是“传输层”。

那么在传输的过程中，可能会中断，所以我们需要登录服务器，断点续传进行重新传输，这些属于机器和机器之间的会话，所以是“会话层”。

传输完数据后，我们就会在电脑里显示这个内容，是一幅图片呢，还是一段电影？这个需要表示出来，所以是“表示层”。

最后，我们将这个一整套的东西，写成了一个应用，这就是“应用层”。

虽然这么表述起来，有许多不精确不严谨的地方，但是通过这段话能让你很快记住这个七层网络模型，对你将来的编程有很大的帮助。

Python 支持 Socket 编程，也就是支持 TCP、UDP 等协议的编程，也支持更上层的编程，比如 HTTP 协议等等。在今天的内容中，我们要用到 TCP 编程。至于为什么要使用 TCP，有这样几个原因：

TCP 保证连接的正确性。在建立 TCP 连接的时候，需要经过三次握手，连接这一方发送 SYN 协议，被连接方返回 SYN+ACK 协议，最后连接方再返回 ACK 协议；

TCP 保证如果在一定时间内没有收到对方的信息，就重发消息，保证消息传输的可靠性；

TCP 可以进行流量控制。它拥有固定大小的缓冲池，TCP 的接收方只允许另一方发送缓冲池内所接纳的数据大小。

TCP 还有其他更多的保证传输可靠性的内容和标准，我在这里不做更多的阐述。另外，使用 TCP 可以进行长时间的连接，在客户端和服务器端之间进行不停地交互。在交互过程中，服务器端发送数据给客户端，客户端就能做出相应的回应。

在 Python 中编写 TCP 协议的代码比之使用 C/C++ 更为方便。因为 C/C++ 需要初始化一系列的内容，然后进行顺序的流程化绑定，设置网络参数，最后进行发送和接收操作，在结束的时候进行资源的回收。而在 Python 这里，只需要设置协议和 IP 地址就可以实现 TCP 协议编程。我们来看一段代码。

import socket

class go_sock(object):

    ip = ""

    port = 0

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def __init__(self):

        object.__init__(self)

    def connect(self, ip, port):

        self.ip = ip

        self.port = port

        self.sock.connect_ex((ip, port))

    def close(self):

        self.sock.close()

我在这里编写了一个类，这个类将 TCP 的内容封装在了类中，这样，我们的网络代码能在游戏中方便地初始化，使用起来就很方便。

首先，我们看到在类里面定义了 ip、port、sock 这三个变量，这三个变量分别是对应 IP 地址、端口号以及 socket 句柄。在类里，我们直接将 sock 初始化为 socket 类，其中 socket 类填写的内容中，参数 1 是服务器之间的网络通信，参数 2 是流 Socket，这里指的是 TCP 协议。

在初始化完成了之后，我们看到 connect 函数。在函数里面，我们看到参数对变量的初始化，其中 sock 句柄调用了标准 socket 函数 sock.connect_ex，这个函数负责与对方进行一个连接。

最后的函数是 close 关闭操作，在任务完成之后，你可以调用 close 函数进行 socket 句柄的关闭。

我们可以这样使用这个类。

_inet = go_sock()

_inet.connect("115.231.74.62", 21)

_inet.sock.recv(100)

在这里，我们可以简单测试一下某些应用服务器，然后接收返回内容。这个类的封装工作到此就告一个段落，更多的网络服务和交互的编写，我将在下一节阐述。

小结

今天我们学习了网络的七层模型结构，以及我们将要在游戏中使用的 TCP 协议的编程。

我用了一个传输过程介绍了七层每一层做的事情，这个你一定要牢记。

我们使用 Python 封装了 Socket 库的细节内容，只需要直接编写 connect 代码就可以进行数据的接收和发送操作了。

选择 TCP 协议是因为它安全可靠，能保证游戏传输的过程中不出错。

现在，我给你留一个小问题吧。

如果我们要使用 UDP 来编写这个网络服务，该如何保证数据的准确性呢？选择 UDP 协议的优势在哪里？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第24讲 | 如何嵌入脚本语言？

从 2005 年开始，逐渐流行使用 C/C++ 语言结合脚本语言（Lua、Python、Ruby 等等）编写游戏。这是因为用 C/C++ 编写游戏的传统方式，硬代码太多，而使用硬代码编写的游戏，更新难度很大，除非重新编译一次程序。

于是，就有人开始使用配置文件来做活动逻辑。比如填写好配置表、玩家等级多少、攻击力如何、等于多少的伤害力等等，一开始就将这些内容都读取进代码，在游戏中实时计算出来。

但是这种方法其实也并不方便。很久以前的游戏，由于硬件资源限制，所以一般都加载 WAV 格式。而加载 MP3 则需要机器对音乐文件进行解压缩再播放，如果机器硬件计算能力不好的话，会由于解压缩而导致整个游戏的运行效率下降。

脚本语言也是如此，如果机器硬件能力不好的话，会由于脚本语言的虚拟机要解释程序，导致游戏运行效率下降。随着电脑硬件的提升，我们在游戏中加载 MP3 音乐文件成为可能，而在游戏中加载脚本语言进行逻辑编写当然也是可以的。

《魔兽世界》就是使用 Lua 脚本语言编写的。类似《GTA》等大型游戏，都拥有一套自己的脚本语言和体系。 使用脚本语言，是为了能够在编写硬代码的同时，也能很方便地、不需要重新编译地编写逻辑代码。 事实上，现在很多大型游戏都使用这种方式来编写代码，甚至一些游戏引擎本身，也支持脚本语言和引擎本身所提供的语言分离编写。比如引擎用 C++ 语言编写，脚本语言用 Lua 编写。

为什么使用 Lua 脚本嵌入 C/C++ 硬代码？

今天我就来教你使用 Lua 脚本来嵌入 C/C++ 硬代码。为什么我要选择 Lua 脚本语言来编写代码呢？

因为Lua 脚本足够轻量级，几乎没有冗余的代码。Lua 虚拟机的执行效率几乎可以媲美 C/C++ 的执行效率。如果选择 Python、Ruby 等常用脚本语言来嵌入，并不是不行，而是要付出执行效率作为代价。因为 Python、Ruby 的执行效率远逊于 Lua。

如果没有非常多的编码经验，你可能会问，为什么 Python、Ruby 的执行效率远逊于 Lua 呢？这个问题，用一本书的篇幅恐怕才能彻底讲明白。我这里只简要说一下原因。

Lua 的虚拟机很简单，指令设计得也精简，Lua 本身是基于寄存器的虚拟机实现，而 Python 等其他脚本语言是基于堆栈的虚拟机，而基于寄存器的虚拟机字节码更简单、高效。因为字节码，一般会同时包含指令、操作数、操作目标等内容。

另一方面，Python、Ruby 之所以应用范围广，是因为它们拥有大量的成熟库和框架，而 Lua 只是一种很纯粹的脚本语言。因为 Lua 没有过多的第三方库，只提供最基础的 I/O 处理、数学运算处理、字符串处理等，别的与操作系统相关度密切的，例如网络、多线程、音频视频处理等等都不提供。

我在第 6 讲里，已经非常详细地讲过，如何将 Lua 脚本编译成为静态库，如果不记得的话，可以回去复习一下。编译好静态库 liblua.a 之后，我们就可以在编程中使用它了。

你也可以选择在解压缩出来的目录内，使用 make 命令来直接编译，编译会生成 Lua 虚拟机的执行文件 lua.exe、luac.exe，当然这需要一整套 MinGW 的环境支持。

开始，我们还是使用 MinGW Development Studio 来创建一个工程。由于只是示例，所以名字可以任意取。我取一个叫作 lua_test 的工程名，并且将工程设置为 Win32 Console Application。你可以看这个示例图。

建立好了工程之后，我们新建一个 test.c 文件。这个文件位于 lua 源代码路径下。我们将 liblua.a 文件也放到同一个目录下，以方便后续链接时候调用。

在包含 Lua 头文件之前，我们需要将头文件写在某一个.hpp 文件下，以便一次性包含进去，我们的代码可以这么写。

#ifdef __CPLUSPLUS

extern "C" {

#endif

#include "src/lua.h"

#include "src/lualib.h"

#include "src/lauxlib.h"

#ifdef __CPLUSPLUS

}

#endi

你可以看到，这里面包含了三个代码。这三个代码来自 src 目录下，其中最后一个 lauxlib.h 包含了大量的 C 语言形式的接口以及扩展接口。而定义 extern "C" 的意思是，使用 C 的方式进行链接，前置条件是，你的语言是 C++ 语言（ifdef __CPLUSPLUS）。

定义好了这个 hpp 文件后，我们可以在 C 或者 C++ 语言中进行包含。

#include “lua.hpp”

你需要了解三个 Lua 语言的细节问题

写完定义之后，我们就可以开始对 Lua 进行一系列的绑定操作了。在编程之前，我先用一些你能看得懂的语言，对 Lua 语言的细节进行一些描述。有三个点，需要你着重记一下。

首先，Lua 的下标都是以 1 为最初始的值（当然反向可以使用 -1 为下标），而不是我们所熟悉的 0。有个传言说，是因为作者当时编写最初版本的 Lua 时，计算错误才导致的，所以就这么一直沿用下来了，这个说法虽然不可考，但也算是一种解释。

其次，在 C/C++ 内嵌 Lua 的做法中，Lua 有两种读取脚本的方法。

一种方式是读取后直接运行，调用的函数是 luaL_dofile。使用这个函数，脚本会在读取完毕后直接运行。当然如果出现错误，你也不知道错误的具体位置在哪里，调试起来不是很方便。

第二种方式是将脚本代码压到栈顶，然后使用 pcall 操作运行脚本，这个函数叫 luaL_loadfile。事实上第一种方式也是使用这种方式并且将 pcall 操作直接调用起来，第一种方式的代码一看你就能明了。

#define luaL_dofile(L, fn) \

      (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))

这行代码在 lauxlib.h 中能找到。这段代码写得非常精妙，它的意思是，如果 loadfile 成功，那么就运行 pcall 函数，中间这个 || （或者）已经直接判断了 loadfile 是否成功。因为 loadfile 函数操作成功就返回 0，否则就返回 1。

而在“或者”这个逻辑判断下，只要是 0，就继续往下判断；只要是 1，就直接返回条件为真。所以，在这行代码下，只要是 1，就中断 dofile 这个宏的操作；只要是 0，就进行 pcall 操作。

最后，我要说一下Lua 的堆栈。理解了堆栈的计数方式，就能很容易地理解我后续要讲解的代码中的计数方式。Lua 的堆栈可以从这个图里看出来，从栈底往上表示可以用 1、2、3、4、5，而从栈顶往下表示是 -1、-2、-3、-4、-5。

如何使用 Lua 以及 liblua.a 来进行与 C 语言的绑定操作？

我们现在开始使用 Lua 以及 liblua.a 来进行与 C 语言的绑定操作。

首先，我们需要包含之前我们所定义的 lua.hpp 头文件，随后我们开始在 main 入口函数处，定义一些变量。

#include "lua.hpp"

int main(int argc, char ** argv)

{

      int r;

      const char* err;

      lua_State* ls;

       ….

}

在这里，我们定义了三个变量，其中 r 是用来接收返回值的；err 是一个常量字符串，用来接收错误字符串并打印出来；而 lua_State* ls 就是 Lua 虚拟机的指针了。

我们再来看接下来的代码。

ls = luaL_newstate();

luaL_openlibs(ls);

在这两行代码中，首先初始化一个虚拟机（在 Lua 5.1 中，使用的函数是 lua_open 来新建虚拟机），并且将虚拟机地址赋值给 ls 指针。随后，我们拿到这个指针之后，就在之后的代码中“打开”Lua 所需要用到的各种库。我们用到 luaL_openlibs。我现在只是给你示范，你可以一个一个库单独打开。

我们新建了虚拟机，并且打开了 Lua 类库。我们继续看下面的代码。

r = luaL_loadfile(ls, argv[1]);

if(r)

{

err = lua_tostring(ls, -1);

if(err)

printf("err1: %s\n", err);

return 1;

}

r = lua_pcall(ls, 0, 0, 0);

if(r)

{

err = lua_tostring(ls, -1);

if(err)

     printf("err2: %s\n", err);

return 1;

}

lua_close(ls);

我来具体解释一下。这段代码中，argv[1] 的是命令行输入的第一个内容。比如我们的程序叫 lua_test，那么我们在 Windows 命令行中，输入 lua_test a.lua，那么其中 a.lua 就是 argv[1] 这个内容。

luaL_loadfile 我们在前面介绍过，就是载入文件并不运行。当然在这个期间，它会检查基础的语法。如果你少一个括号或者多一个引号，就会在这个时候给你一个错误信息，这个错误信息就是利用 r 这个变量判断的。如果 r 的返回值不等于 0 的话，那就是出错了。出错的时候，Lua 会将出错信息压栈顶，而栈顶是从 -1 开始表示，所以我们要取出栈顶的错误信息 lua_tostring(ls, -1);，并且将它赋值给 err，最后由 err 打印出来。

认为没有错误之后，就是过了这一关。第二关我们需要使用 lua_pcall 函数，来调用 Lua 脚本文件，其中第一个参数是虚拟机指针，第二个参数是传递多少参数给 Lua，第三个参数是这个脚本返回多少值，第四个是错误处理函数，可以是 0，那就是无处理函数。

pcall 的返回值也是一样，如果不是 0 的话，就说明出错了。和之前的 luaL_loadfile 不同，这时候一般是运行时错误，比如运行时类型错误等等。同样的，pcall 也会把错误信息压到栈顶，我们直接去将栈顶的内容转成 string 就可以打印出来了。最后，我们将 Lua 虚拟机通过 lua_close 关闭。

按常理来说，我们现在可以来运行一下效果了，你可以先等等，我们先写一段错误的 Lua 代码，来看看执行起来会发生什么情况。

print "test running")

我们故意少写一个括号，然后将源代码命名为 a.lua，我们来运行看看。会出现一个这样的错误信息：

在发现语法错误后，程序就会报错，另外，如果你输入了一个根本不存在的文件，比如我们这么运行，test_lua xxx.lua，也会在 loadfile 的时候出错。

小结

我们今天的内容就到这里。下次我会进一步把 Lua 的脚本嵌入的细节呈现在你面前。我们来总结一下今天的内容。

因为 Lua 脚本足够轻量级，几乎没有冗余的代码。Lua 虚拟机的执行效率几乎可以媲美 C/C++ 的执行效率。所以我们选择使用 Lua 脚本来嵌入 C/C++ 硬代码。

Lua 脚本在 C/C++ 语言里面嵌入，需要先声明一个虚拟机并且赋值给指针。

Lua 脚本需要先 loadfile 再 pcall 调用脚本文件，loadfile 会检查最基本的脚本文件内容，比如文件是否存在，比如脚本代码是否出错，而 pcall 会在运行时出错的时候将错误压至栈顶。

Lua 错误会将错误压制栈顶，我们要取出来，需要使用 -1 下标取出栈顶的内容，并转成 string 打印。

给你留一个小问题吧。

如果直接使用 luaL_dofile，相对于把 loadfile 和 pcall 分开写，这样有什么优劣呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第26讲 | 脚本语言在游戏开发中有哪些应用？

上一次，我们谈到了如何在游戏中嵌入脚本语言，我们用的语言是 Lua。Lua 语言具有轻量级、速度快的特点，而且 API 的调用也非常方便和直观。现在，我们仍然拿 Lua 脚本，试着把它应用在我们开发的游戏中。

我们使用 C 语言来对 Lua 脚本的绑定做一次深入的剖析，然后来看一下，在游戏开发中绑定了脚本语言后，脚本语言能做些什么事情。

首先，我们要明白一点，事实上任何模块都可以使用脚本语言编写。当然在游戏开发的过程中，需要分工明确，如果不分工的话，效率可能会比较低。

在需要某些效率要求非常高的情况下，一般是用 C、C++ 或 ASM 语言，将底层模块搭建好，然后将一些逻辑部分分出来，给脚本语言处理。比如我们熟知的服务器端，可以使用 C/C++ 来编写服务器端的 IOCP 或者 epoll 处理；而接收、发送、逻辑处理等等，都可以使用绑定脚本的方式编写。

我们在编写的过程中，需要对 C/C++ 的语言和代码有个了解，我们需要先考虑这个函数。

int test_func(lua_State *L)

{

     return 0;

}

这只是一个空的 C 函数，在这个函数里面，我们看到它的传入参数是 lua_State，接受一个指针 L。随后，这个函数返回一个 0。

lua_State 是 Lua 虚拟机的对象指针，也就是我们需要把前面 new 出来的一个虚拟机传进去，才可以保证在这个函数里面，使用的是一致的虚拟机。

这个函数的作用是，只要注册到了 Lua 虚拟机里面，它就是 lua 的一个函数，其中在 lua 函数中，传入的参数由函数内部决定。

比如我可以这么写：

int test_func(lua_State *L)

{

     const char *p1 = lua_tostring(L, 1);

     const char *p2 = lua_tostring(L, 2);

     // .... do something

     lua_pushstring(L, "something");

     return 1;

}

这里面，lua_tosting 就是这个函数的传入参数，传入的是一个字符串的参数；第二个参数也是字符串参数，其中 lua_tosting 的第二个参数 1 或者 2，表明的是在 Lua 虚拟机的堆栈中从栈底到栈顶开始计数，一般先压入的参数在第一个，后压入的在第二个，以此类推。返回 1 的意思是，这个函数会返回一个参数，这个参数就是我们前面 lua_pushstring 后压入的这个内容 something，这就是返回的参数。

那么这个函数究竟怎么注册成为 Lua 函数呢？我们来看这段代码。

lua_register(L, "test", &test_func);

lua_register 函数的功能是，注册 C 函数到 Lua 虚拟机。其中 L 是虚拟机指针。这个在前面的代码都有说到，而第二个参数 test 就是注册在 Lua 虚拟机中的函数名，所以这个函数名叫 test。第三个参数是函数指针，我们把 test_func 这个函数传入到 lua_register 函数中。这样，一个函数就注册好了。

那么，如果我们在游戏中有许多许多的函数需要注册到 Lua 中，那么这种写法是不是太慢了，有没有一种快捷的写法来支持注册等操作呢？

如果你没有 C/C++ 的语言基础，或者 C/C++ 语言基础比较薄弱，下面的内容可能需要花一点时间消化，我也会竭尽所能解释清楚代码的意思，但如果你已经是个 C/C++ 程序员，那么下面的代码对你来说应该不会太难。

我们需要使用 lua_register，我们先看它里面有什么参数。第一个是字符串，也就是char*；第二个是函数指针，也就是int ()(lua_State) 这种形式的。

那么，我们需要定义一个 struct 结构，这个结构可以这么写：

   #define _max 256

    typedef struct _ph_func

    {

          char ph_name[_max];

          int (*ph_p_func)(lua_State*);

    } ph_func;

我们定义了一个 struct 结构，这个结构的名字叫 _ph_func，名字叫什么并没有关系，但是最开始有一个 typedef，这说明在这个结构声明完后，接下来最后一行 ph_func 就是替代最初定义的那个 _ph_func 的名字，替代的结果是，ph_func 等同于 struct _ph_func，这在很多 C 语言的代码里面经常能见到。

接下来，我们看到 char ph_name[_max]。其中 _max 的值为 256。我相信你应该能理解这句话。第二个变量就是我们所看到的函数指针，其中 ph_p_func 是函数指针，其中函数指针指向的内容目前暂时还没有确定，我们将在后续初始化这个结构变量的时候进行赋值。

我们来仔细看一下这两段宏的内容。

#define func_reg(fname) #fname, &ph_##fname

#define func_lua(fname) int ph_##fname(lua_State* L)

其中 func_reg 是在给前面那个结构体初始化赋值的时候使用的，因为我们知道，如果我们需要给这个结构体赋值，看起来的代码是这样：

ph_func pobj =  {"test", &test_func};

那么由于我们有大量的函数需要注册，所以我们将之拆分为宏，其中 #fname 的意思是，将 fname 变为字符串，而 ph_##fname 的意思是使用 ## 字符，将前后内容连接起来。

通过这个宏，比如我们输入一个 a 赋值给 fname，那么 #fname 就变成字符串 "a"，通过 ph_##fname，结果就是 ph_a。

接下来的代码，是方便在代码中编写一个一个 lua 注册函数用的，所以很明显，和上述的宏一样，我们只需要输入 a，那么这个函数就变成了 int ph_a(lua_State* L)；

定义好了这两个宏，我们怎么来应用呢？

func_lua(test_func);

ph_func p_funcs[] =

{

      { func_reg(test_func) },

};

func_lua(test_func)

{

     const char *p1 = lua_tostring(L, 1);

     const char *p2 = lua_tostring(L, 2);

     // .... do something

     lua_pushstring(L, "something");

     return 1;

}

void register_func(lua_State* L)

{

      int i;

      for(i=0; i<sizeof(p_funcs)/sizeof(p_funcs[0]); i++)

      lua_register(L, p_funcs[i].ph_name,  p_funcs[i].ph_p_func);

}

首先，联系上面的宏，第一行代码是使用 func_lua，所以 func_lua 输入的宏参数是 test_func。于是，通过这个宏，我们最终得到的函数名字是 int ph_test_func(lua_State* L); 。

ph_func p_funcs[] =

{

      { func_reg(test_func) },

};

这段代码，使用的是 func_reg 的宏。test_func 最终在宏里面，变成了 “test_func”，以及 &ph_test_func 函数指针。

最后我们来看一个重要的函数，register_func，这个函数在后续将会输入一个 Lua 虚拟机指针，但是我们要知道它在函数内部它做了什么东西。

int i;

for(i=0; i<sizeof(p_funcs)/sizeof(p_funcs[0]); i++)

      lua_register(L, p_funcs[i].ph_name,  p_funcs[i].ph_p_func)

在循环里面，我们计算 p_funcs 的结构数组的长度，怎么计算的呢？

首先，我们使用 sizeof 编译器内置函数，来取得 p_funcs 整个数组的长度，这整个长度等于 sizeof(ph_func) 的值乘以数组长度。而 ph_func 结构体拥有一个字符串数组，每个数组长度是 256，加上一个函数指针为 4 字节长，所以长度是 260。而如果有两个数组元素，那就是 520 的长度。

以此类推，/sizeof(p_funcs[0] 的意思是，我们取出第一个数组的长度作为被除数。事实上就是结构体本身的长度，所以就是结构体数组总长度除以结构体长度，就是一共有多少数组元素，随后进行循环。

在循环的过程中，我们看到，我们填入了结构体里面的两个变量 ph_name 以及 ph_p_func，这样一来，我们只需要通过宏加上一些小技巧，就可以把 Lua 的函数都注册到 C 程序里面，我们假设这个 C 程序就是游戏的话，那么我们很容易就可以和 Lua 进行互通了。

小结

我总结一下今天所讲的内容。

在 Lua 与 C 的结合过程中，C 语言需要新建一个 Lua 虚拟机，然后使用虚拟机的指针来操作 Lua 函数。

在程序的应用中，使用 C 语言中的一些宏的技巧，可以使代码能够便利地应用在程序里。

最后，给你留一个小问题。

如果使用 Lua 往 C 语言传递一些内容，比如从 C 语言获取 Lua 脚本中某个变量的值，应该怎么做？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第27讲 | 如何使用脚本语言编写周边工具？

上一节，我们讲了脚本语言在游戏开发中的应用，我列举了很多 C 语言代码，这些代码做了这样一些事情：

使用 C 语言和 Lua 语言进行沟通；

在 C 语言代码里，使用了宏和结构，方便批量注册和导入 C 语言函数；

Lua 代码如何传输内容给 C 语言；

Lua 虚拟机堆栈的使用。

这一节，我们要用 Lua 脚本来编写一个游戏周边工具 Makefile。游戏周边工具有很多种，并没有一个统一的说法，比如在线更新工具、补丁打包工具、人物模型编辑工具、游戏环境设置工具等等。

你或许就会问了，那我为什么选择 Makefile 工具来编写，而不选择别的周边工具来编写呢？

因为这个工具简单、小巧，我们可以将 Lua 脚本语句直接拿来用作 Makefile 语句，而在这个过程中，我们同时还可以通过 Lua 语句来了解 Lua 的工作机理。 而且这个编写过程我们一篇文章差不多就可以说清楚。

而别的周边工具编写起来可能会比较复杂，比如如果要编写类似 Awk 的工具的话，就要编写文本解析和文件查找功能；如果编写游戏更新工具的话，就必须涉及网络基础以及压缩解压缩的功能。

简单直白地说，Makefile 是一种编译器的配置脚本文件。这个文件被 GNU Make 命令读取，并且解析其中的意义，调用 C/C++（绝大部分时候）或者别的编译器（小部分）来将源代码编译成为执行文件或者动态、静态链接库。

我们可以自己定义一系列的规则，然后通过顺利地运行 gcc、cl 等命令来进行源代码编译。

我们先定义一系列函数，来固定我们在 Lua 中所使用的函数。

int compiler(lua_State*);

int linker(lua_State*);

int target(lua_State*);

int source_code(lua_State*);

int source_object(lua_State*);

int shell_command(lua_State*);

int compile_param(lua_State*);

int link_param(lua_State*);

int make(lua_State*);

这些都是注册到 Lua 内部的 C/C++ 函数。我们现在要将这些函数封装给 Lua 使用，但是在这之前，我们要将大部分的功能都在 C/C++ 里编写好。

随后，我们来看一下，在 Lua 脚本里面，具体是怎么实现 Make 命令操作的。

target("test.exe");

linker("c:\\develop\\dm\\bin\\dmc.exe");

compiler("c:\\develop\\dm\\bin\\dmc.exe");

source_code("c.cpp", "fun.cpp", "x.cpp");

source_object("c.obj", "fun.obj", "x.obj");

compile_param( "$SRC", "-c",

                      "-Ic:/develop/dm/stlport/stlport",

                    "c:/develop/dm/lib/stlp45dm_static.lib");

link_param("$TARGET", "$OBJ");

make();

shell_command("del *.obj");

首先，第一行对应的就是目标文件 target 函数，后续的每一个 Lua 函数都能在最初的函数定义里找到。

在这个例子当中，我们使用的是 DigitalMars 的 C/C++ 编译器，执行文件叫 dmc.exe。我们可以看到，在 linker 和 compiler 函数里都填写了 dmc.exe，说明编译器和链接器都是 dmc.exe 文件。

现在来看一下在 C/C++ 里面是如何定义这个类的。

struct my_make

{

      string target;

      string compiler;

      string linker;

      vector<string> source_code;

      vector<string> source_object;

      vector<string> c_param;

      vector<string> l_param;

};

为了便于理解，我将 C++ 类声明改成了 struct，也就是把成员变量改为公有变量，你可以通过一个对象直接访问到。

随后，我们来看一下如何将 target、compiler 和 linker 传入到 C 函数里面。

int compiler(lua_State* L)

{

      string c = lua_tostring(L, 1);

      get_my_make().compiler = c;

      return 0;

}

int linker(lua_State* L)

{

      string l = lua_tostring(L, 1);

      get_my_make().linker = l;

      return 0;

}

int target(lua_State* L)

{

      string t = lua_tostring(L, 1);

      get_my_make().target = t;

      return 0;

}

在这三个函数里面，我们看到，get_my_make 函数就是返回一个 my_make 类的对象。这个具体就不进行说明了，因为返回对象有多种方式，比如 new 一个对象并且 return，或者直接返回一个静态对象。

随后，我们直接使用了 Lua 函数 lua_tostring，来得到 Lua 传入的参数，比如如果是 target 的话，我们就会得到”test.exe”，并且将这个字符串传给 my_make 对象的 string target 变量。后续的 compiler、linker 也是一样的道理。

我们接着看下面两行。

source_code("c.cpp", "fun.cpp", "x.cpp");

source_object("c.obj", "fun.obj", "x.obj");

这两行填入了 cpp 源文件以及 obj 中间文件，这些填入的参数并没有一个固定值，可能是 1 个，也可能是 100 个，那在 C/C++ 和 Lua 的结合里面，我们应该怎么做呢？

我们看到一个函数 lua_gettop。这个函数是取得在当前函数中，虚拟机中堆栈的大小，所以返回的值，就是堆栈的大小值，比如我们传入 3 个参数，那么返回的就是 3。

接下来可以看到，使用 Lua 的计数方式，从 1 开始计数，并且循环结束的条件是和堆栈大小一样大，然后就在循环内，将传入的参数字符串，压入到 C++ 的 vector 中。

随后的 source_object、compile_param 和 link_param 都是相同的方法，将传入的参数压入到 vector 中。

你可能要问了，我在 Lua 的代码中看到了 TARGET、TARGET、OBJ、$SRC 等字样的字符串，这些字符串的处理在哪里，这些字符串又是做什么的呢？

这些字符串是替代符号，你可以理解为 C 语言中 printf 函数的格式化符号，例如 “%d %s”等等，虽然在这里，这些符号都是自己定义的，但是我们仍然需要解析它们。

其实解析的步骤并不难，我们只需要将 vector 内的内容提取出来，对比是不是字符串 $TARGET 等，如果是的话，就被替代为前面我们在 target 函数或者 source_code 函数中所定义的内容。

我们拿 source_code 部分来举例，来看一下部分代码。

void run()

      {

               string command_line;

               string src = "$SRC";

               string tar = "$TARGET";

               string obj = "$OBJ";

        for(int i = 0; i < source_code.size(); i++)

         {

         ..............

        for(int j=0; j<c_param.size(); j++)

                        {

                                 if(c_param[j] == src)

                                 {

                                 command_line += source_code[i];

                                        .....

                                         }

                       }

          }

在这部分的代码里面可以看到，我们将压入的 source_code 内容进行循环。在循环之后，必须对 c_param（compile_param），也就是编译参数进行循环。当我们发现编译参数里面出现了 $SRC 这个替代字符串的时候，就将 source_code 的内容（其实就是源代码文件）合并到 command_line（命令行）里面去，然后整合成为一个完整的、可以运行的命令行。

随后我再贴一部分代码，可以看到别的可替代字符串是怎么做的。

else if(c_param[j] == obj)

{

      command_line += source_object[i];

}

else if(c_param[j] == tar)

{

      command_line += target;

}

我们对替代字符串做了相同的比较，如果是一致的话，就将被替代内容添加到 command_line 变量里面，组成一个完整的可运行命令行。

这个 run 函数其实就是在 make 的时候调用的函数。至于如何调用这一串 command 命令，在 C 里面最简单的方式就是调用 system 函数，或者使用 execl 函数系列。注意，这个 execl 并不是来自微软的 excel 表格，而是 C 语言的函数。

我们封装完了 Lua 部分的代码之后，就需要将 Lua 的函数注册到 Lua 虚拟机里面，这个我上一节已经具体说过了。

最后，由于我们的 Lua 源代码本身就是一个 Makefile 文件，所以我们不需要做过多的解析，直接将这个源代码输入给 Lua 虚拟机即可。

string makefile;

ifstream in("my_makefile");

makefile = "my_makefile";

if(!in.is_open())

{

in.close();

}

else luaL_dofile(L, makefile.c_str());

在这段代码里面，我们首先使用 C++ 的 fstream 库中的 ifstream 来尝试读取是不是有这个 my_makefile 文件，如果没有的话，就跳过，并且关闭文件句柄，如果存在的话，就把这个文件填入到 Lua 虚拟机中，让 Lua 虚拟机直接运行这个源文件。所以这种方式是最简单快捷的。

代码有点多，不要担心，我带你梳理一下今天的内容。

利用 C/C++ 语言和 Lua 源代码进行交互，从 Lua 代码中获取数据并且在 C 语言里面进行算法的封装和计算，最后将结果返回给 Lua。 我们在 C/C++ 语言里面进行大量的封装和算法提取，并且也利用 C/C++ 进行调用和结果的呈现，这是一种常用的方式，也就是 C 语言占比 60%～70%，Lua 代码占比 30%～40%。

另一种比较好的方式是，使用 C/C++ 编写底层实现逻辑，随后将数据传输给 Lua，让 Lua 来做逻辑运算，最终将结果返回给 C 语言并且呈现出来。这是很多人在游戏开发中都会做的事情，比如我们编写地图编辑器，先在 Lua 中编写好逻辑，用 C 语言在界面中呈现出来即可。如果反过来做的话，那就会出现大量的硬代码，是很不合适的。所以这种情况下，C 语言占比 30%～40%，Lua 代码占比 60%～70%。

Lua 可以是一种胶水语言。严谨地说，像 Python、Ruby 等脚本语言，都是合格的胶水语言。 在这种情况下，胶水语言起到的作用就是粘合系统语言（C/C++）和上层脚本逻辑。所以，使用胶水语言，就像是一种动态的配置文件。
按照普通的配置文件来讲，你需要手工解析比如类似 INI、XML、JSON 等配置文件，随后按照这些文件的内容来做出一系列的配置，但是胶水语言不需要，它本身就是一种动态的语言。
你也可以把它当作一种配置的文件，就像今天讲的 Makefile，它可以不需要你检测语法问题，这些问题在 Lua 虚拟机本身就已经做掉了，你需要做的就是将我们脑海里想让它做的事情，通过 C 和 Lua 的库代码进行整合，直接使用就可以了。所以，胶水语言的本身就是一个配置文件，同时它也是一个脚本语言源代码。

小结

在使用 C/C++ 结合脚本语言的时候，需要梳理这些内容，比如哪些是放在 C/C++ 硬代码里写的，那些可以放到脚本语言里写，梳理完后，就可以将脚本语言和 C/C++ 结合起来，编写出易于修改脚本逻辑（如果有不同需求，可以很方便地改写脚本而不需要动 C/C++ 硬代码）、易于使用的工具。

现在给你留一个小问题吧。

在 Lua 当中有 table 表的存在，如何在 C 语言中，给 Lua 源代码生成一个 table 表，并且可以在 Lua 中正常使用呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第29讲 | 如何选择合适的开发语言？

有许多编程语言可以用来开发服务器端。一些语言对于网络开发有先天优势，一些语言没有先天优势，但是能完成任务，而有一些语言，则不太适合。今天，我就来具体讲一讲这些语言来开发网络服务的优劣势。

你了解这些编程语言吗？

C/C++ 是最标准的开发语言，速度快，并发性能高，能最大程度利用机器资源完成任务。现在 C/C++ 层面拥有无数网络开发库和 SDK，知名的有 ACE、Boost/Asio、ICE 等等。但是缺点是，开发效率不比别的语言来得高，出错后常常只有熟练的程序员才能定位问题并且作出修复。

Go是 2009 年新出现的语言。Go 天生适合编写网络程序。它也是一种系统级的语言，可以直接编译为执行文件，当然由于封装了大量的语法糖，所以编译出来的文件会更大，它天生支持并发操作，所以很多时候你不需要像 C/C++ 一样手工去处理。缺点是，Go 语言仍然存在许多语法方面的坑，你可以去 https://studygolang.com/ 学习最新的资料。

Java是公认的编写网络服务的第一大语言。在运行性能和开发效率上，有很好的折中效果。Java 拥有众多的中间件和开发库，调试方便，一般的运维人员也有极为广泛可用的第三方维护工具可以使用。缺点是，Java 的运行效率虽然有了质的飞跃，但因为中间隔了一层虚拟机，所以仍然比不上系统开发语言编写的软件。另外，Java 的发布和部署需要众多的依赖包和库，软件体积庞大也是其重要弊病。

如果深入理解，Python、Ruby这两种语言的相似程度以及对系统的支持程度，可以用 C 和 C++ 的相似程度来相比。你或许会很疑惑，毕竟 Python 和 Ruby 的语法几乎不一样，Python 需要格式化源代码，而 Ruby 并不需要；Python 更严谨，Ruby 更开放；Python 用户更多，Ruby 用户更少。

不可否认的是，两种语言编写网络程序都非常方便，也非常高效。两种语言都可以在 100 行内编写出一个简单的、完全可以直接使用的网络服务器程序。但是这两种语言的弊病也很明显，那就是速度不够快。

比之 Java，或许运行效率更慢一点，但由于目前机器硬件水平的提升，软件效率不足的缺点一部分已经被硬件所弥补，但是仍然不能否认，Python、Ruby 语言适合 IO 密集型开发，而不适合计算密集型的开发。

Python 的书籍比 Ruby 多好几倍，然而你如果仔细去看的话就会发现，Ruby 的书籍质量明显比 Python 高几个等级，所以如果要看好的脚本语言的书籍，Ruby 相关的书籍是首选，我这里推荐一本Programming in Ruby，有兴趣的话可以找来看看。

Node.js从前端语言变成后端语言，让编程界眼前为之一亮。随后的发展大家也有目共睹，Node.js 由于使用 JavaScript 语言语法，所以我们一般采用事件驱动的形式，以及非阻塞的模型来进行网络开发。因为这些特点，它非常适合做分布式的、数据密集型的工作。但是缺点也很明显，Node.js 是单线程，无法很好地使用多核 CPU，这个问题在 Python、Ruby 语言中也很明显。

或许你没有听说过Erlang这种语言，这种语言最初是由爱立信开发的。它的初衷是让程序员应对大规模并发、分布式、软件实时并行系统进行编程。最早期的版本在 80 年代就出现了，但是一直到 1998 年才开源。

Erlang 也不是系统语言，它有自己的虚拟机和解释器。当然和 Java 一样，Erlang 也支持将代码编译为 Native Code 来运行。Erlang 的缺点就是类型问题，它并非强类型语言。由于是事件编程，所以导致会在运行时无法匹配类型而出错，不过这些问题可以使用规范的编程方法来规避。

这么多种编程语言，整合起来看，大致可以把他们分为三类。

系统级编程语言，诸如汇编、C、C++。这种编程语言执行效率快，并发量也比较高，作为编写网络服务的第一语言，一台服务器就能支撑许多人。缺点是开发效率不够高，需要几年以上经验的程序员才能搞定。

专门为网络服务器开发的语言，诸如 Go、Erlang。这种语言编写高并发和开发效率都不是问题，有很好的折中效果。缺点就是语言比较新，有许多的坑等着后来的程序员去填，而且语言、语法等系统机制要随着进一步的发展才能稳定下来。

解释型脚本语言，诸如 Python、Ruby。 这类语言的开发效率非常高效，在现在的服务器硬件上，也能支撑不少用户，但是唯一的缺点是，运行效率低下。虽然也有解决方案，但仍然不能对抗高性能的系统编程语言和专业网络开发的语言。

如何选择一种合适的语言来编写网络服务？

Web 服务

现在有一种流行的说法叫前后端分离。对于编写 C/S 结构的程序员，听到这种说法应该会比较蒙，客户端和服务器端难道不是本来就分离的吗？

很长的一段时间里，在 Web 的世界中，前后端都是混合在一起编写的，比如 PHP 的方式，只有用到 Ajax 交互的时候，才需要用到后端的代码。但是前后端一分离，后台就需要做更多的工作了，当然前端的工作也不会变少。

编写 Web 服务，需要 HTTP 和 HTTPS 的服务体系，那么在这种情况下，使用 nginx、Apache 作为静态页面路由，Java、Tomcat、Python、Ruby 等脚本语言就有了用武之地。因为页面只需要使用 JSON 交互即可。

所以，编写 Web 服务，我们可以选择 Java、Python、Ruby。但是如果公司财力物力有限，再考虑到招人成本的问题，次选也可以是 Java 语言，第一是写 Java 的人够多，第二是 Java 成熟的类库够多，因此，一旦出问题，有解决经验的人也比较多。

Socket 服务

传统 TCP/IP 和 UDP 服务，或者最近的 WebSocket 等，都需要快速响应和并发操作，在这种情况下，系统级编程语言和网络编程语言就可以派上用场了。

如果公司的项目需要更快更高效，并且财力也允许，那么选择 C、C++、Go、Erlang 等编程语言未尝不是一种选择。当然 Java 也能很好地提供服务，但是从业务上来讲，既然选择了 Socket 服务模式，那么就必然是对并发量有一定的要求，所以选择上述这些语言更合适。

混合模式

这类业务，既有 HTTP/HTTPS 的服务，也有 Socket 服务，那么如何平衡两者之间的语言成本？如何平衡程序员之间技术栈的问题呢？

如果要做一款短期内必须上线的产品，我建议选择成熟的、有大量解决方案的，开发人员不短缺的语言，比如 Java；或者能快速做出原型的语言，比如服务器专有语言 Go。如果是长期发展的产品，并不那么着急成型，那么选择稳定成熟的，人员素质高的语言，比如 Python、Java 等。

至于平衡技术栈的问题，首先要选择网上有众多解决方案的语言，其次是找成熟的语言，比如 Python、Java、Ruby。如果针对某种特殊的产品，比如并发要求特别高的，那么只有选择系统语言或者专门的语言，比如 Go、C++ 等。

看到这里，你是不是觉得 Java 语言是一种万能药，或者是银弹？错了，这个世界上没有银弹。Java 虽然有其独特的优势，但是其被人诟病的地方，也是有不少的。

第一点莫过于速度。就算拥有 JIT 编译，总体速度仍然比不上 C/C++，但是事实上这些因素综合考虑并不算特别大的弊病，因为硬件资源提升后，速度这些问题已经可以“得过且过”了。

那么从语言本身来看，如果说 C/C++ 语言本身的弊病是因为系统平台导致的，那么 Java 语言的弊病就是因为继承自 C++，却没有做更彻底的改革而导致的。

我随便举一个例子，比如说 switch case 判断语句，硬生生地从 C/C++ 处直接继承了下来，因为 C/C++ 只允许使用 int、enum（其实是 int）、char（提升为 int）作为判断类型，而 Java 也是直接将这套规范继承了下来。

再比如，在 Java 里面，异常检查也是一个痛苦的根源，程序员不得不写下无数 try catch 语句以使得将捕获的异常，转变为运行时的异常，然后再将之抛出去，这样一来，使用 Java 编写的 API 将缺少灵活和扩展性。

那如果选择了 Python 或者 Ruby 等脚本语言进行开发，却需要大量高并发的操作该怎么办呢？我们可以选择多进程（不是多线程）编程的方式进行开发，代码尽量简洁、高效，一个进程兼顾一个任务，进程之间的通信方式要尽量高效、简洁，比如可以使用自定义的队列等方式。

小结

学完这一节，你应该对使用各种编程语言来编写网络服务有了一个更深的了解。我主要讲了以下几个内容。

编程语言可以大致分为三类，系统级编程语言、专为网络服务器开发的编程语言和解释型脚本语言。

在编写网络服务的时候，可以根据要编写的是 Web 服务、Socket 服务，还是混合模式，来选择合适的编程语言。

给你留一个小问题吧。

如果让你来使用 C/C++ 粘合 Lua 脚本来编写网络服务器，你会怎么设计这个程序框架？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第30讲 | 如何定制合适的开发协议？

什么是开发协议？说得简单一点，就是一种客户端和服务器端的网络沟通协议（Protocol）。

广义上说，协议是计算机各种设备之间沟通的桥梁。比如网络之间需要协议，ping 一个网站是否通顺也需要协议，广播地址也需要协议。我们甚至可以说键盘鼠标操作事件也需要协议，Dubbo 架构也需要协议沟通等等。

从狭义上说，协议指的就是网络协议。比如在网络游戏中，客户端和服务器端之间的内容交互，就需要网络协议；在 Web 网站中，前端和后端的交互，也需要协议；再比如，邮件服务的网络交互也需要协议的交互等等。可以说，任何与网络相关的服务都少不了协议的支撑。

在游戏开发中，我们可以自定义一套自己的开发协议，也可以把现成的开发协议拿来使用。具体怎么做呢？我们先来看现在网上用得比较多的几种协议。

三种最流行的开发协议

XML

XML 几乎是网络上最早出现的传输协议之一。在最早的 Web 开发中，XML 可以作为网络协议，也可以用作配置文件。比如某些游戏或者某些应用的配置文件，都可以使用 XML 来编写。

从人类的角度讲，它的可读性比较强，解析也比较方便。我们先来看几种解析方式。

解析方式是这些协议被程序理解的一种方式，按照这种方式解析，和我后面要说的自定义协议的解析和剖析结合起来，乃前后呼应之奇效。

PULL 方式：PULL 解析是一种专门为安卓设备解析 XML 文件设计的解析方式。这种解析方式更适用于移动设备。PULL 解析和我们下面要说的 SAX 解析比较类似，不一样的地方是 PULL 读取 XML 文件后，触发相应的事件调用方法返回的是 number。另外，PULL 方式可以控制程序的解析停止位置，不需要全部解析，可以随时停止。

SAX 方式：SAX（Simple API for XML）采用事件驱动型方式。语法解析从发现元素开始、元素结束、文本、文档的开始或结束等，就开始事件的发送，程序员编写响应这些事件的代码，就可以直接将数据保存下来。所以优点是，不需要载入整个文档，资源占用比较少。
SAX 解析器的代码比 DOM 解析器的代码更精简，一般应用于 Applet 技术。缺点就是，这种解析方式并不持久，等事件消息过去后，如果你没有保存数据，那么数据就丢失了；从事件的消息中我们只能得到文本数据，但不知道这个文本属于哪个元素。但是，我们只需 XML 文档的少量内容就可以解析出我们所需的数据，所以耗费的内存更少。

DOM 方式：DOM（Document Object Model）是最传统的解析方式。解析器读入整个文档，然后在内容中构建一个 XML 的树结构，使用代码就可以操作这个树结构。优点是整个文档树在内存中，便于操作；而且支持删除、修改、重排等多种功能。缺点是将整个文档调入内存比较浪费计算机的时间和空间，但是如果一旦解析了文档，还需多次访问这些数据的话，这种方式就可以起到作用了。

JSON

其实，目前 XML 已经不太流行，取而代之的是 JSON。JSON 是一种轻量级的数据交换格式。它用完全独立于编程语言的文本格式来存储和表示数据。

比之 XML，它看起来更加简洁和清晰，层次结构分明；JSON 易于阅读和编写，在程序方面，也易于机器解析和生成，同时也提升了网络传输效率。这些优点使得 JSON 很快在程序员中流行起来，成为理想的数据交换语言。

它也是移动端比较常见的网络传输协议。相对于前面所说的 XML 格式，它更为简单，体积更小，加之对网络流量和内存的需求更小，所以 JSON 比 XML 更适合移动端的使用。

我们来看一下 JSON 的几种流行的解析程序库。

Gson 是谷歌开源的一种解析方法，使用 Java 编写，你可以通过提供的 JAR 文件包，使用静态方法直接将 JSON 字符串解析成 Java 对象，这样使用起来简单方便。

FastJSON 是阿里开源的一个解析 JSON 数据的类库。

JSONObject 也是一个解析 JSON 字符串的 Java 类。第二、第三这两种用的人都比较少，我就不多介绍了。

当然，支持别的语言的库也有很多，由于 JSON 比较流行，所以各种语言都有其支持的类库版本，比如 Python、C++、Ruby 等等。

ProtoBuf

ProtoBuf 全称 Google Protocol Buffer， 是谷歌公司开发的内部混合语言数据标准。目前正在使用的有接近五万种报文格式定义和超过一万两千多个.proto 文件。它们都用于 RPC 系统和持续数据存储的系统。

这是一种轻便、高效的结构化数据存储格式，可以用于结构化数据的序列化操作。它很适合用作数据存储或 RPC 数据交换格式。可以用于通讯协议、数据存储等领域。由于是独立的协议系统，所以它和开发语言、运行平台都没有关系，可以用在扩展的序列化结构数据格式。目前提供了 C++、Java、Python 、Ruby、Go 等语言的开发接口 API。

ProtoBuf 方便的地方在于，它有一款编译器可以将.proto 后缀的协议文件，编译成 C++、Java、Python 等语言的源代码。你可以直接看到和利用这些源代码，且不需要自己去做解析，所以不同语言之间使用 ProtoBuf 的协议规范都是一样的，但是有一个问题是，ProtoBuf 存储的文件格式是二进制的，由于是二进制的，所以程序员需要调试其保存的内容就有点麻烦，当然这可能只是对于某些人来说的瑕疵吧，对于大部分人来讲，方便性还是大于瑕疵的。

ProtoBuf 的编码风格是这样的，花括号的使用类似 C/C++、Java。数据类型的命名方式使用驼峰命名，比如 DataType、NewObject。字段的变量小写并使用下划线连接，类似 GNU 规范，比如 proto_buf、user_name。枚举类型使用大写，并使用下划线连接，比如 MY_HOME，BEST_FRIEND。

Protobuf 并不是针对大型数据设计的，Protobuf 对于 1M 以下的 message 有很高的效率，但是当 message 大于 1M 的时候，Protobuf 的效率会逐步降低。

如何自己定义协议包？

我们讲完了三种目前最流行的开发协议，接下来我们要讲讲如何自己定义协议包。

我们所说的协议包，是在 TCP 和 UDP 传输之上的协议包，也就是通过字符串的形式发送的协议包。这些协议包在客户端和服务器之间做了约定，也就是说，客户端和服务器都能通过拿到协议包来进行解包操作，并且进行一系列的逻辑运算并返回结果，当然结果也是协议包的形式发送出去。

一个好的协议不仅能节约网络带宽，也能让接收端快速拿到和解析需要的内容。设计协议包，必须保证安全性和完整性。

为了保证完整性，接收方需要知道协议的长度，或者知道协议的尾部在哪里。

我们可以给协议最末尾添加分隔符，该分隔符需要特殊字符。不能被传输的内容所混淆，又要能达到方便接收方辨认，因此，该特殊字符需要具有唯一性。比如我们可以将“!@#$”这四个字符做为分隔符，那么协议看起来可能是这样：

[协议头][协议体][协议结尾分隔符]

你可能要问了，在传输的过程中，我知道了协议长度，不需要协议头，只需要协议长度就可以？是的。因为有了协议长度，协议尾部有没有分隔符就不重要了。如果我们固定好输出协议长度的字节数，就可以忽略协议头。在这种情况下，协议看起来像是这样：

[协议长度 2 字节][协议体]

这样简单地就能定义整个协议的内容。

在读取的时候，我们只需要读取开头的两个字节，转换为一个 short 的长度，或者四个字节一个 int 的长度，在第三个字节开始就是协议体。让程序开始计算长度，如果长度少于协议长度所定义的长度，那就继续接收，如果接收长度超过协议所定义的长度，切割协议体，并将下一段开始的协议存储到内存中留待下一次取出。这种方式是最方便的。

我们在保证协议完整性的同时，也要保证协议不被破坏和篡改，也就是所说的安全性。在这种情况下，最直接的方式，就可以将协议内容进行加密。比如 SHA-256 或者 AES 等等加密方式将内容加密，随后传输过去，最简单的做法就是将密码在客户端和服务器端协商好就可以了。

看起来可能是这个样子：

[协议长度 2 字节][加密协议体]

小结

这节内容差不多了，我们总结一下。我和你介绍了这几个内容。

我介绍了三种的开发协议 XML、JSON 和 ProtoBuf，以及它们对应的解析方式。XML 是网络上最早出现的传输协议之一。

游戏或应用的配置文件，都可以使用 XML 来编写，但是目前 XML 已经不太流行，取而代之的是 JSON。

ProtoBuf 适合用作数据存储或 RPC 数据交换格式，缺点是保存比较麻烦，但是总体来讲还是比较方便的。

自己定义协议包需要考虑完整性和安全性。接收方需要知道协议的长度，或者知道协议的尾部在哪里，就可以保证协议包的完整性。而最直接的给协议包加密，就可以保证安全性。

最后，给你留一个小问题吧。

在自定义协议中，如果使用添加协议结尾的方式来做协议，如何才能保证协议结尾分割字符串不和协议本身的二进制内容重复？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第32讲 | 不可忽视的多线程及并发问题

既然我们说到了服务器端的开发，我们就不得不提起多线程和并发的问题，因为如果没有多线程和并发，是不可能做网络服务器端的，除非你的项目是 base 在 Nginx 或者 Apache 之上的。

多线程和并发究竟有什么区别和联系？

提到并发，不得不提到并行，所以我就讲这三个概念：并发、并行，以及多线程。作为初学者，你或许不太明白，多线程和并发究竟有什么区别和联系？下面我们就分别来看看。

并发出现在电脑只有一个 CPU 的情况下，那如果有多个线程需要操作，该怎么办呢？CPU 不可能一次只运行一个程序，运行完一个再运行第二个，这个效率任谁都忍受不了啊！所以，就想了个办法。

CPU 将运行的线程分成若干个 CPU 时间片来运行。不运行的那个线程就挂起，运行的时候，那个线程就活过来，切换地特别快，就好像是在同时运行一样。

你可以想象这个场景，有一个象棋大师，一个人对十个对手下棋，那十个人轮流和他下。大师从 1 号棋手这里开始下，下完 1 号走到 2 号的棋手面前，下 2 号棋手的棋，一直轮流走下去，直到再走回 1 号棋手这里再下一步。只要象棋大师下象棋下得足够快，然后他移动到下一位棋手这里又移动得足够快，大家都会觉得好像有十位象棋大师在和十个对手下棋。事实上只有一位象棋大师在下棋，只是他移动得很快而已。

并行和并发不同，并行是出现在多个物理 CPU 的情况下。在这种情况下，并行是真正的并发状态，是在物理状态下的并发运行。所以，并行是真的有几位象棋大师在应对几个对手。当然在并行的同时，CPU 也会进行并发运算。

而多线程是单个进程的切片，单个进程中的线程中的内存和资源都是共享的，所以线程之间进行沟通是很方便的。

多线程的意义，就好比一个厨师，他掌管了三个锅，一个锅在煮排骨，一个锅在烧鱼，另一个锅在煮面，这三个锅内容不同，火候不同，但是所有的调料和资源，包括菜、油、水、盐、味精、糖、酱油等等，都来自同一个地方（也就是资源共享），而厨师自己是一个进程，他分配了三个线程（也就是三个锅），这三个锅烧着不同的东西，三个食物或许不是同时出锅的，但是厨师心里有数，什么时候这个菜可以出锅，什么时候这个菜还需要煮。这就是多线程的一个比喻。

我们在编写网络服务器的时候，多线程和并发的问题是一定会考虑的。我们说的网络并发和 CPU 的并发可以说是异曲同工，也就是说，网络并发的意义是，这个网络服务器可以同时支撑多少个用户同时登陆，或者同时在线操作。

为什么 Python 用多个 CPU 的时候会出现问题？

那么我们又回头来看，为什么 Python、Ruby 或者 Node.js，在利用多个 CPU 的时候会出现问题呢？这是因为，它们是使用 C/C++ 语言编写的。是的，问题就在这里。

我们后续的内容还是会用 Python 来写，所以我们先来看看 Python 的多线程问题。Python 有个GIL（Global Interpreter Lock，全局解释锁），问题就出在 GIL 上。

使用 C 语言编写的 Python 版本（后面简写为 C-Python）的线程是操作系统的原生线程。在 Linux 上为 pthread，在 Windows 上为 Win thread，完全由操作系统调度线程的执行。

一个 Python 解释器进程内有一条主线程，以及多条用户程序的执行线程。即使在多核 CPU 平台上。由于 GIL 的存在，所以会禁止多线程的并行执行。这是为什么呢?

因为 Python 解释器进程内的多线程是合作多任务方式执行的。当一个线程遇到 I/O（输入输出）任务时，将释放 GIL 锁。计算密集型（以计算为主的逻辑代码）的线程在执行大约 100 次解释器的计步时，将释放 GIL 锁。你可以将计步看作是 Python 虚拟机的指令。计步实际上与 CPU 的时间片长度无关。我们可以通过 Python 的库 sys.setcheckinterval() 设置计步长度来控制 GIL 的释放事件。

在单核的 CPU 上，数百次间隔检查才会导致一次线程切换。在多核 CPU 上，就做不到这些了。从 Python 3.2 开始就使用新的 GIL 锁了。在新的 GIL 实现中，用一个固定的超时时间来指示当前的线程放弃全局锁。在当前线程保持这个锁，且其他线程请求这个锁的时候，当前线程就会在五毫秒后被强制释放掉这个锁。

我们如果要实现并行，利用 Python 的多线程效果不好，所以我们可以创建独立的进程来实现并行化。Python 2.6（含）以上版本引进了 multiprocessing 这个多进程包。

我们也可以把多线程的关键部分用 C/C++ 写成 Python 扩展，通过 ctypes 使 Python 程序直接调用 C 语言编译的动态库的导出函数来使用。

C-Python 的 GIL 的问题存在于 C-Python 的编写语言，原生语言 C 语言中，由于 GIL 为了保证 Python 解释器的顺利运行，所以事实上，多线程只是模拟了切换线程而已。这么做的话，如果你使用的是 IO 密集型任务的时候，就会提高速度。为什么这么说？

因为写文件读文件的时间完全可以将 GIL 锁给释放出来，而如果是计算密集型的任务，或许将会得到比单线程更慢的速度。为什么呢？事实上 GIL 是一个全局的排他锁，它并不能很好地利用 CPU 的多核，相反地，它会将多线程模拟成单线程进行上下文切换的形式进行运行。

我们来看一下，在计算密集型的代码中，单线程和多线程的比较。

单线程版本：

from threading import Thread

  import time

  def my_counter():

      i = 0

      for x in range(10000):

          i = i + 1

      return True

  def run():

      thread_array = {}

      start_time = time.time()

      for tt in range(2):

          t = Thread(target=my_counter)

          t.start()

          t.join()

      end_time = time.time()

      print("count time: {}".format(end_time - start_time))

  if __name__ == '__main__':

      run()

多线程版本：

from threading import Thread

  import time

  def my_counter():

      i = 0

      for x in range(10000):

          i = i + 1

      return True

  def run():

      thread_array = {}

      start_time = time.time()

      for tt in range(2):

          t = Thread(target=my_counter)

          t.start()

          thread_array[tid] = t

      for i in range(2):

          thread_array[i].join()

      end_time = time.time()

      print("count time: {}".format(end_time - start_time))

  if __name__ == '__main__':

      run()

当然，我们还可以把这个 ranger 的数字改得更大，看到更大的差异。

当计步完成后，将会达到一个释放锁的阀值，释放完后立刻又取得锁，然而这在单 CPU 环境下毫无问题，但是多 CPU 的时候，第二块 CPU 正要被唤醒线程的时候，第一块 CPU 的主线程又直接取得了主线程锁，这时候，就出现了第二块 CPU 不停地被唤醒，第一块 CPU 拿到了主线程锁继续执行内容，第二块继续等待锁，唤醒、等待，唤醒、等待。这样，事实上只有一块 CPU 在执行指令，浪费了其他 CPU 的时间。这就是问题所在。

这也就是 C 语言开发的 Python 语言的问题。当然如果是使用 Java 写成的 Python（Jython）和.NET 下的 Python（Iron Python），并没有 GIL 的问题。事实上，它们其实连 GIL 锁都不存在。我们也可以使用新的 Python 实作项目 PyPy。所以，这些问题事实上是由于实现语言的差异造成的。

如何尽可能利用多线程和并发的优势？

我们来尝试另一种解决思路，我们仍然用的是 C-Python，但是我们要尽可能使之能利用多线程和并发的优势，这该怎么做呢？

multiprocess 是在 Python 2.6（含）以上版本的提供是为了弥补 GIL 的效率问题而出现的，不同的是它使用了多进程而不是多线程。每个进程有自己的独立的 GIL 锁，因此也不会出现进程之间，CPU 进行 GIL 锁的争抢问题，因为都是独立的进程。

当然 multiprocessing 也有不少问题。首先它会增加程序实现时线程间数据通信和同步的困难。

就拿计数器来举例子。如果我们要多个线程累加同一个变量，对于 thread 来说，申明一个 global 变量，用 thread.Lock 的 context 就可以了。而 multiprocessing 由于进程之间无法看到对方的数据，只能通过在主线程申明一个 Queue，put 再 get 或者用共享内存、共享文件、管道等等方法。

我们可以来看一下 multiprocess 的共享内容数据的方案。

from multiprocessing import Process, Queue

  def f(q):

      q.put([4031, 1024, 'my data'])

  if __name__ == '__main__':

      q = Queue()

      p = Process(target=f, args=(q,))

      p.start()

      print q.get()

      p.join()

这样的方案虽说可行，但是编码效率变得比较低下，但是也是一种权宜之计吧。

小结

我们来总结一下今天的内容。

我首先介绍了几个概念。并发是单个 CPU 之间切换多线程任务的操作。并行是多个 CPU 同时分配和运行多线程任务的操作。线程是进程内的独立任务单元，但是共享这个进程的所有资源。网络的并发指的是服务器同时可以承载多少数量的人数和任务。

而 C 语言编写的 Python 有 GIL 锁的问题，会让其多线程计算密集型的任务效率更低，解决方案有，利用多进程解决问题 或者 更换 Python 语言的实现版本，比如 PyPy 或者 JPython 等等。

给你留一个小问题，如果 Python 以多进程方式进行操作，那么如果我们网络服务器是用 Python 编写的，其中一个 Python 进程崩溃或者报错了，有什么办法可以让其复活？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第33讲 | 如何判断心跳包是否离线？

在初学网络，编写过阻塞和非阻塞网络代码的时候，有一个问题，那就是在非阻塞的情况下，不知道对方的网络何时断开。

因为在非阻塞的情况下，如果没有接收到消息，recv 的数值一直会是 0。如果以这个来判断，显然是错误的。而在阻塞情况下，只要对方一断开，接收到 0 就说明断开了，那么我们怎么才能在非阻塞的情况下确定连接是断开还是没断开呢？

我们可以采用离线超时的方案来判断对方连接是否断开。那什么是离线超时呢？

我们都知道，人累了就要休息。你在休息的时候，有没有注意过这么一个现象，那就是你在快要睡着的时候，忽然脚会蹬一下，或者人会抽一下，这是为什么呢？

有一种说法流传很广，说，其实大脑是在不停地检测人有没有“死”，所以发送神经信号给手和腿。抽动一下，检验其是否死亡。这个就有点儿像我们检测超时，看看有没有反应。

现在我们先看一段 Python 代码，让它运行起来。

import socket

import time

def server_run():

   clients = []

   my_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

   my_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

   my_server.bind(("", 1024))

   my_server.listen(256)

   my_server.setblocking(False)

这是我节选的一部分代码。其中，在函数 server_run 里面，我们先定义了一个 clients，这是一个列表，用于后面保存客户端连接用。my_server 获得 socket 句柄，并且将之设置为 TCP 模式，随后我们绑定地址为本地（bind 函数），端口号为 1024，并且开始侦听，随后我们看到 setblocking 函数，将之设置为非阻塞模式。

while True:

      time.sleep(1)

      try:

         client, addr = my_server.accept()

         print client

         client.setblocking(False)

         clients.append(client)

      except Exception as e:

         print "no client incoming"

      for cli in clients:

         try:

            data = cli.recv(1024)

            if data:

               print data

            else:

               cli.close()

               clients.remove(cli )

         except Exception as e:

            print "no data from ", cli

   my_server.close()

在一个大循环内，我们做了如下几件事情：第一个是 accept，只要有客户端进来，我们就 accept，如果没有客户端进来，一直等待状态下，就打印 no client incoming 字符串，如果有客户端进入的话，就直接将新客户端放入列表。

我们在启动函数的时候，如果没有客户端连接，就会出现这样的字样：

然后我们使用 Windows 下的 telnet 命令来模拟客户端。输入 telnet 127.0.0.1 1024，服务器端代码会出现这样的字符串：

我们打印新的客户端连接的对象地址，并且将新的客户端连接句柄放入列表里面。随后，循环进入到了取出新客户端列表，并且做出判断，每次接收 1024 字节。如果没有，则显示 no data from <xxxx 地址 >；如果有，那就显示输入的字符串。

好了，现在我们打开 Windows 任务管理器，找到拥有 telnet 的程序，并且“杀死”它。

随后，我们会发现，命令行提示符出现了如下内容的字符串：

按照道理，服务器不是应该断开连接了吗？它应该能知道客户端断开了不是吗？

错，服务器端根本不知道对方已经被“杀死”了，所以它的状态仍然在接收中。由于是 TCP 握手，除非你正常将 telnet 程序关闭，才会让服务器端正常接收到客户端关闭的消息，否则，你永远不知道对方已经退出连接了。

所以心跳包的作用就在这里，心跳包允许你每隔多少毫秒发送数据给服务器端，告诉服务器我还活着，否则服务器就当它已经死了，确认超时，并且退出。

事实上，在 TCP/IP 的协议层中，本身是存在心跳包的设计的，就是 TCP 协议中的 SO_KEEPALIVE。

系统默认是设置 2 小时的心跳频率。需要用 setsockopt 选项将 SOL_SOCKET.SO_KEEPALIVE 设置为 1，打开，并且可以设置三个参数 tcp_keepalive_time，tcp_keepalive_probes，tcp_keepalive_intvl，分别表示连接闲置多久开始发 keepalive 的 ACK 包、发几个 ACK 包不回复就当连接端“死”了。

这种心跳检测包是属于 TCP 协议底层的检测机制，上层软件只是解析显示网口的有用数据包，收到心跳包报文属于 TCP 协议层的数据，一般软件不会将它直接在应用层显示出来，所以用户是看不到的。以太网中的心跳包可以通过以太网抓包软件分析 TCP/IP 协议层的数据流看到。报文名称是 TCP Keep-Alive。

当然，我们也可以做应用层的心跳包检测，我们在编写游戏服务器的时候，就可以自定义心跳服务，TCP 层的心跳服务是为了保持存活的，但是应用层的心跳，则是拥有更明确或者其他的目的（比如对方是否还活着）。

我们专门独立一台服务器做心跳服务器，连接客户端和真正的游戏逻辑服务器，那么我们希望逻辑服务器的同步率和心跳服务器统一，也就是说，心跳服务器负责的就是发送心跳包和客户端数据给逻辑服务器，逻辑服务器每一次获取数据，也是从心跳服务器获得的，那么心跳服务器能做的事情就会变得很多。

为了调试方便，我们可以利用心跳服务器，将客户端传送过去的数据包存储在本地磁盘上。如果应用或者游戏在测试的时候，就可以看到那些发送的内容，甚至可以回滚任意时段的数据内容，这样调试起来就相对方便，而不需要客户端大费周章地不停演练重现出现的错误。代码看起来是这样：

 def SendToServer(is_save = 0):

      package = socket.recv(recv_len)

      ticktock()

      if is_save:

          SaveToDisk(package)

      server_socket.send(package)

在逻辑服务器内部，每一次接收数据，都根据心跳服务包的心跳来接收，这样做的好处就是，可以随时调整心跳的频率，而不需要调整逻辑服务器的代码。

在应用层的心跳模式下，我们会有两种策略需要进行选择。

我们假定把逻辑运算设为 A，心跳时间（比如代码的 Sleep 或者挂起）设为 B。

第一种是运算时间 A 和心跳时间 B 相对固定。也就是说，不管 A 运算多久，B 一定是固定挂起多久。

第二种策略是运算时间 A 和心跳时间 B 是实时调整。A 运算时间长，挂起时间就短，如果 A 运算时间加上 B 挂起时间超过约定心跳总时间，那 B 就不挂起，直接进行另一个 A 运算。这两种策略究竟哪种好呢？

在 CPU 负载并不是那么严重的情况下，策略二是比较好的选择。

假设心跳 Sleep 时间是 1000ms，运行时间规定为 2000ms。如果运行时间小于等于 2000ms 的话，Sleep 时间不变；如果运行时间超过 2000ms 的话，那么 Sleep 时间就等于 Sleep 时间 - (运行时间 - 2000ms)。

这样一来，平均心跳有了保障，但是在运算量加大的时候，Sleep 时间已经完全被运行时间所占据，那么心跳 Sleep 时间就会减少到最少甚至不存在，CPU 的负载就会变得很高，这种时候就需要用到策略一。

你可以这么理解。策略一是说，不管我们的运行时间多久，Sleep 时间始终是一致的 1000ms，这种方式保证了服务器一定会进行心跳，而不会导致负载过高等情况。

当然这只是一种简单的模型，在进行大规模运算，或者有多台服务器的时候，我们可以将两种方式合并起来进行策略交互。任务不繁重的时候采用策略二，当服务器发现任务一直很多且超过 Sleep 时间几次，就切换到策略一，这样可以保证心跳时间基本一致。

我们可以将心跳服务和逻辑服务分开运行，而是否放在同一台物理机并不是首要的问题，这样心跳服务器只提供心跳包，而逻辑服务通过心跳包自动判断并且调整运行频率。

小结

好了，我给今天的内容做一个总结。

判断非阻塞模型的网络是否断开，可以使用心跳包和计算超时的方式进行断开操作，比如 30 秒没收到心跳包，则可以强制关闭 Socket 句柄断开。

心跳包是一种服务器之间交互的方法，也可以用作服务器数据调试和回滚的策略方案。心跳包有两种策略，第一种就是运算时间 A 和心跳时间 B 相对固定，第二种策略是运算时间 A 和心跳时间 B 是实时调整。CPU 的负载很高的时候用策略一，CPU 负载并不是那么严重的情况下，策略二是比较好的选择。

最后，给你留一个思考题吧。

如果编写的是阻塞方式的服务器代码，心跳包还有存在的意义吗？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第35讲 | 如何用网关服务器进行负载均衡？

我们费劲心思做了一款游戏，那肯定希望有很多人来玩。一旦玩家数量开始多起来，服务器需要承担的压力就会变大，我们就需要做负载均衡了。

所谓的负载均衡，就是对外有一个公共地址，在请求过来的时候，通过轮询、随机分配等方式，定点到不同的服务器，以分摊服务器的压力。

两种常用的负载均衡技术

常用的负载均衡技术有软件模式和硬件模式。

其中，硬件模式用得比较多的是 F5。F5 是 F5 Networks 公司提供的一个负载均衡器专用设备， F5 BIG-IP LTM 的官方名称叫本地流量管理器，可以做 4～7 层负载均衡，具有负载均衡、应用交换、会话交换、状态监控等等全备的强大网络功能。

F5 硬件负载均衡的优点是，负载均衡能直接通过智能交换机实现, 处理能力更强，与操作系统无关，负载性能强，适用于一大堆设备、大访问量，其缺点也是很明显的，那就是购买成本高，设备的配置冗余，有些用不上的都给默认配置了。另外，由于设备只有单个，所以单个负载均衡配置无法有效掌握所有服务器及应用状态。

硬件的负载均衡，是从网络层来判断负载规则，所以有时候软件的系统处理能力出现问题，网络硬件还可以作出负载的动作。

软件模式的话，比较常用的软件的有 LVS、Nginx、HAProxy。

LVS 是四层负载均衡，根据目标地址和端口选择内部服务器。Nginx 这种负载均衡工具是七层负载均衡。而 HAProxy 同时支持四层、七层负载均衡，还可以根据报文内容选择内部服务器。

因此，LVS 分发路径优于 Nginx 和 HAProxy，性能上也要高些。但 Nginx 和 HAProxy 则更具配置性，比如说可以用来做动静分离。所谓动静分离，就是根据请求协议的特征，去选择静态资源服务器还是应用服务器。

很久以前，游戏服务器只是简单的对应客户端的服务，就像使用编程语言写了一个多线程的程序，每个线程接收一个客户端，然后把该存储的数据放到数据库去保存。到了后期，大量的网游开始兴起，玩的人越来越多，所以一些老式游戏服务器框架已经无法满足更大更现代化的网络游戏的需求了。

到了 2005 年左右，这种情况愈演愈烈，不改变现状不行了。于是，程序员和游戏开发厂商设计出了新的一种服务器的框架模型。这种模型几乎是延用到今天，这种模型甚至延伸到各行各业的服务框架。

我们甚至可以说 Nginx 反向代理的想法也是类似这种模型的一种表现形式。尽管我们不能说 Nginx 学的就是这种模型，但是与这种反向代理的模型的做法实在太类似了。

这种服务器模型的最大改变，就是加了一个 gateway，可以称作网关。这当然不是传统意义上的网关路由器，只是在服务器的应用层面，做的事情类似网关路由器，所以我们仍然把它称为网关。

我们可以在 Web 端称它为会话（Session），也可以称它为Link Server，总之道理是一样的。

这个网关服务器所做的工作可以分为两种，对应网关服务器实现不同功能的服务。每一种功能不同，后台逻辑服务器的传输数据内容也会不同，不能相互混合使用。

网关服务器有哪些功能？

1. 中转功能

网关服务器作为一种代理，所有玩家从客户端传输到真正的游戏逻辑服务器的内容，都需要通过网关服务器，用该服务器作为中转。也就是说，假设有 A 客户端到 B 服务器，网关为 G 的话，就是 A 到 G 到 B，然后 B 服务器完成逻辑计算后，返回给 G 网关，网关再一次返回给 A、B 到 G 到 A。

这样做的好处是，网关可以随时询问它底下的真实逻辑服务器到底哪一台趋于饱和，可以将玩家移动到不饱和的游戏服务器，但是缺点也是很明显的，那就是玩家和服务器之间隔了一层网关，需要消耗更长的时间，传输速率相对低。

2. 负载均衡

网关服务器作为查询网关，也就是说，网关服务器会和底下所有服务器做一个长连接，或者随时询问的连接，这个连接所询问的内容，可以放到一个缓存里面，所查询的内容就是它底下所有服务器哪一台有空，在这种功能模式下，网关服务器只做了负载均衡的工作。

那么当客户端 A 要连接到游戏服务器的时候，需要先询问网关服务器 G，模型看起来会是这样：

A- 询问 G，G 通过查询缓存表，告知 A 客户端，C 服务器有空，于是通知 A，你去连 C 服务器，IP 地址和端口号是多少多少，于是 A 从网关 G 关闭连接，去连接 C 服务器。如果连接失败（因为是缓存查询，从逻辑上讲有可能滞后），那么再次询问网关，直到成功连接某一台服务器为止。

这个模型，网关服务器只做了负载均衡的动作，客户端和网关之间不会保持一个长连接，在这个基础上，一台网关服务器支撑同时七千人以上都不是什么太大的问题。但是它的缺点也很明显，那就是一台游戏逻辑服务器只能负责一个游戏世界，不能进行分块。如果要进行分块，则需要其他模型的服务器模块，这个我一会儿会说。

Nginx 的反向代理也是类似这种负载均衡的网关模型，这种模型大量运用在很多应用服务器、HTTP 连接的网络服务器上。但是，这项技术到了上升时期开始遇到了瓶颈，人们发现就算加上网关，也无法负担体量更大的游戏地图。于是，我们需要对这样的模型进行修改。

如何优化负载均衡的网关模型？

首先，需要将网关服务器增加为几个网关服务器。每个网关服务器都做相同的工作，也就是管理它所下属的所有逻辑服务器。客户端在启动的时候，随机抽取某一个网关服务器，进行询问，使用网关服务器做代理进行中转。

如果游戏地图特别大，这样的模型可以将游戏地图分割成几块，分割好的地图放到下属的各个逻辑服务器中，网关做中转服务，比如服务器 A 负责浙江省，服务器 B 负责安徽省等等。

客户端在连接到网关服务器后，随着游戏进度的走向，网关服务器可以选择连接负责哪一块地图的逻辑服务器，这样在玩家看来就像是连接了一台服务器，而客户端并不用考虑切换服务器的工作。

当然为了减轻服务器的压力，增加更多的人流量，后期这样的模型被逐步细分。比如可以将聊天服务放到一台独立的服务器进行存放，把用户数据独立到一台数据服务器存放，把商品交易放到另一个独立的服务器，或者把私信等等这些和主游戏逻辑无关的内容都放到一个独立的服务器上。

这样一来，主游戏逻辑的服务器的负载就会减轻，然而客户端就不得不多连接几台服务器，要不停获取用户数据或者聊天信息等等，某些负载就转嫁到客户端上了。

这样的游戏逻辑服务器的模型一直沿用到现在。某一些稍微轻量级的，只是使用网关当成负载均衡使用，有一些重量级的，加上地图分割，就会增加网关服务器，但是付出的代价就是，如果要加一台新的游戏逻辑服务器的话，势必会增加部署难度。

不仅网关服务器的配置文件要重新部署，每个游戏节点服务器和被分割的诸如聊天等服务都需要进行重新配置，这样付出的代价也是巨大的，当然很多游戏公司靠着这样的服务器框架使用了好多年，其思想也被延伸到各个行业领域的服务器架构中。

小结

这节内容差不多了，我来总结一下。

我首先讲了两种常用的负载均衡技术，软件模式和硬件模式。硬件模式用得比较多的是 F5。软件模式的话，比较常用的软件的有 LVS、Nginx、HAProxy。

网关服务器有中转功能和负载功能。Nginx 的反向代理用的是负载均衡的网关模型，但是这种模型无法负担更大体量的内容。为了减轻服务器的压力，也为了增加更多的人流量，可以通过增加网关，分割业务逻辑到独立的服务器，分摊服务器压力，这种经典类型的服务器模型被大量沿用并使用至今。

现在给你留一个小问题吧。

我们使用网关服务器这样的模型，如果网关服务器宕机了，或者网关服务器很久没有响应的情况下，有什么办法让客户端能顺利连上网关服务器之下的逻辑服务器呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第36讲 | 如何制作游戏内容保存和缓存处理？

我们在打完游戏的关卡之后，需要保存游戏进度。单机游戏的进度都保存在本地磁盘上，如果是网络游戏的话该怎么办呢？这一节，我就来讲这个内容。

首先，我们要了解游戏内容的保存，需要先了解缓存处理。

为什么要了解缓存的处理呢？那是因为在大量用户的情况下，我们所保存的内容都是为了下次读取，如果每一次都从硬盘或者数据库读取，会导致用户量巨大数据库死锁，或者造成读取速度变慢，所以在服务器端，缓存的功能是一定要加上的。

Redis 不仅是内存缓存

缓存机制里有个叫 Redis 的软件。它是一种内存数据库，很多开发者把 Redis 当作单纯的内存缓存来使用，事实上，这种说法并不准确，Redis 完全可以当作一般数据库来使用。

Redis 是一种 key-value 型的存储系统。它支持存储的 value 类型很多，包括字符串、链表、集合、有序集合和哈希类型。这些数据类型都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作，而且这些操作都具有原子性。

Redis 还支持各种不同方式的排序。为了保证效率，数据一般都会缓存在内存中，而 Redis 会周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件，并且在此基础上实现 master-slave（主从）的同步。

说到 Redis，就不得不说缓存机制的老前辈 Memcached。同样是缓存机制，Memcached 的做法是多线程，非阻塞的 IO 复用的网络模型。

多线程分监听线程和工作子线程。监听线程监听网络连接，接受请求了之后，将连接描述字使用管道传递给工作线程，进行读写。网络层的事件使用 libevent 封装。多线程模型可以发挥多核的作用。Memcached 所有操作都要对全局变量加锁，进行计数等工作，所以会有性能损耗。

而 Redis 使用单线程 IO 复用模型，自己封装了一个简单的事件处理框架，对于单纯只有 IO 操作的模型来说，单线程可以将速度优势发挥到最大，但是 Redis 也提供了一些简单的计算功能，比如排序、聚合等。

Redis 还可以在某些场景下对关系数据库（比如 MySQL）起到较好的补充作用。它提供了多种编程语言的接口，开发人员调用起来也很方便。

Redis 支持主从同步。通过配置文件，可以将主服务器上的数据往任意数量的从服务器上同步，从服务器 A1 也是主服务器 B（B 是关联到其他从服务器 B1，B2 的主服务器，同时又是主服务器 A 的从服务器 A1）。

这种做法就使得 Redis 可以执行单层的树结构的复制。Redis 实现了发布 / 订阅（publisher/subscriber）的机制。所谓发布和订阅，就是订阅者接收发布者的消息的时候，发布者和订阅者都不用去管对方是什么状态，只管各司其职就好了，在这种状态下，可以订阅一个频道并接收主服务器完整的消息发布记录。

编写 Redis 接口代码

我们尝试使用 Python 编写 Redis 接口的代码。

要使 Python 支持 Redis 编程，必须安装一个包“redis”，在使用的时候 import 一下。

import redis

然后我们开启 Redis 服务，在 Windows 下可以运行 redis-server.exe，使用默认配置即可。

现在，我们尝试使用代码连接一下数据库服务，并且往数据库存放并取出、删除内容。

       r = redis.Redis(host='127.0.0.1', port=6379, db=0)

       r.set('foo', 'my_redis')

       print r.get('foo')

       r.delete('foo')

       print r.dbsize(

运行结果为输出 my_redis 和 0。

当然，如果我们没有运行 Redis，则会抛出一个异常：

r 对象为连接 Redis 服务器的对象，其中 db=0 表示使用 redis 的 0 号数据库，可以随你喜欢切换为 1 号、2 号等等。如果 Redis 设置了密码，还可以在初始化的时候输入密码。

Redis 的初始函数是这样定义的：

__init__(self, host='localhost', port=6379, db=0, password=None, socket_timeout=None, connection_pool=None, charset='utf-8', errors='strict', decode_responses=False, unix_socket_path=None)

在之后的代码中，r.set 表明将 key 为 foo，value 为 my_redis 的内容写入数据库。

最后输出 0 号数据库的内容长度。

值得一提的是，Redis 对于存储的内容是来者不拒，有什么扔什么，所以你如果往 Redis 里插入二进制、UTF-8 编码、图片等等，任何东西都可以。理论上只要不超过内存大小的数据都可以往里面扔。

最后，我们可以这么写：

 r.save()

强制 Redis 往硬盘里写入数据，这样我们就能保证数据不会因为电脑发生异常而丢失。这样就将内存的数据同步了下来。

我们常说的木桶理论其实在这里也适用。比如电脑的速度取决于电脑设备中最慢的那个设备，就像水在桶中的高度始终取决于水桶里面最下方的那个漏水处。而磁盘 I/O 始终是拖慢电脑速度的重要力量。

前面我们介绍了 Redis，所以我们可以使用 Redis 对文件进行缓存。Redis 可以当作普通缓存也可以当作文件缓存，在 Redis 中放入任何东西，当然也包括放入二进制文件，Redis 也不会有任何异常出现，从 Redis 缓存中取出二进制文件的速度也非常快，因为是直接从内存中取出数据。

我们假设网络游戏保存下来的数据很大，因为有人物属性、人物装备、地图 NPC 位置和怪兽等等。这些玩家退出后，游戏保存的数据文件，被保存在关系型数据库中，或者保存在服务器硬盘的文件中。我们不可能每次都去读取关系数据库中的游戏内容或者硬盘文件内容，所以，可以用一种方案来存放游戏保存的文件和缓存。

如何存放文件和缓存？

这套机制并不局限于读取保存文件，某些大文件，或者数据文件的读取和缓存上，都可以使用这种思路去做。

首先我们假定文件存放在某一个目录，所有的负载均衡服务器都存放有这个目录的副本，其他分布式服务器存放其他文件和目录，我们先暂定 A 服务器存放文件 A1、A2、A3。

这些都是游戏的保存文件，在服务器初始启动的时候，Redis 并不读取任何文件，当有请求过来的时候，服务器程序通知 Redis 读取某个文件。

这时，我们需要一个机制，为了保证服务器的内存开销，也为了保证缓存速度，我们必须保证被读取量最大的文件被缓存，而不是所有文件，这时候，Python 程序可以另开一个线程或者进程，暂且命名为 T 线程，记录某文件被缓存。

服务器程序每次得到请求的时候，都会将需要递交的被读取文件告诉 Python 线程 T，说文件 A1 被缓存了 N 次，文件 A2 被缓存了 N 次，在这种策略下，T 线程通过几个小时或者几天的计数，就能明确知道 ，比如 A2 文件被递交次数最多，于是它始终通知 Redis 将 A2 文件进行缓存，而 A1 由于到了某一天递交次数下降，在某一个时间节点上，线程 T 就告知 Redis A1 文件可以从缓存文件中撤出来，节省内存开销，让位给读取频次更高更高的文件。

这样，一套完整的缓存计数和缓存的解决方案就出现了。

当然，并不是说 MySQL 等关系型数据库不能做这些工作，但从效率和开发成本来讲，Redis（缓存）的开发成本和效率显然更胜一筹。因为在几十万几百万甚至上亿等级用户量的时候，就算是 Redis，在这种量级的情况下也是吃不消的，所以如果不在上层做更多层的缓存，底层数据库一定是会死锁或者出现各种各样的问题。

那么你可能会说，我可以做索引啊，要知道在连接数足够多的时候，做索引、读写分离，主从数据库等方案，也只是救急只用，无法真正实现稳固的架构体系。

小结

我来总结一下今天的内容。

Redis 不仅仅可以用作普通的缓存机制使用，也可以当作正常的数据库使用，Redis 也支持主从同步，要按照应用场景不同来配置不同的 Redis 使用场景。

缓存机制不仅仅针对读取游戏保存文件这么一种方案，也可以用作各种数据文件的读取和写入操作。

使用现成的 Redis 等缓存数据软件，是一个好的方案。而设计好的框架、好的缓存机制、好的网络模型，是一款好网游必不可少的条件。

现在给你留一个小问题吧。

有没有可能将网络游戏的内容保存在客户端本地的电脑上，如果可以的话，请问如果玩家换了一台电脑，怎么同步内容呢？保存在客户端本地的意义是什么？

欢迎留言说出你的看法。

第10讲 | 热点剖析（一）：HTML5技术是如何取代Flash的？

本周开始，我将会在每周六更新一篇“热点剖析”，阐述我对 2D 游戏开发之外的热点，比如 HTML5 游戏、移动端游戏、AR 和人工智能在游戏领域的应用，以及我对微信小游戏、移动游戏、独立游戏开发者的一些观点和看法。

我已经为你讲解了核心的开发知识，对于这些相对热门领域的知识，你可以根据兴趣进行选择学习。

本周及接下来两周的周六，我会依次为你介绍 HTML5 游戏，以及如何选择 HTML5 的游戏引擎，并带你编写一款 H5 小游戏。

从前几年开始，H5 这个技术就开始蓬勃发展。不管是懂行的还是不懂行的，都开始以“H5”这个字眼来描述产品。比如老板会说“我们就做个 H5 的页面吧”，或者“这个游戏是 H5 的吗”。很多人已经把 H5 等价于“手机端页面”了，这样的理解显然是错误的。

那么 H5 究竟是什么？它的优点在哪里？为什么现在大家都在谈论 H5？你真的知道 H5 是什么，并真的深入理解了它吗？

Flash 是什么？

首先，在说 H5 之前，我想先介绍一下 Flash 技术。

Flash 是由 Adobe 公司开发的一种富媒体技术，起初是一种放置在浏览器中的插件，填补了当时 HTML 页面平淡的空白，增强了网页交互的能力。你可以在 Flash 中做出任何东西，也可以访问本地电脑中的东西。后来，Adobe 公司推出了播放器，在电脑上不打开浏览器，也可以观看或者游戏 Flash 程序员编写出来的产品。乃至今日，依然有大量应用于 Flash 的富媒体应用，比如视频的播放，比如独立的小游戏，比如网页游戏，甚至桌面应用，都是使用 Flash 开发的。

虽然 Flash 拥有大量优点，并在当时弥补了网页端的很多不足，但是 Flash 的缺点也是很明显的。

首先，它的浏览器插件的运行效率并不高，而且访问电脑资源也很不方便，导致很多程序员在开发的过程中出现许多的问题，就算成功发布了一款 Flash 应用，浏览器也会被 Flash 插件拖得很慢，电脑会因此变得很卡，所以 H5 技术出现后，很多人就开始转投到 H5 的门下。

另一方面，由于 Flash 技术掌握在 Adobe 公司下，一家独大，所以从商业角度讲，很多大公司并不会坐视不管，就开始从 Flash 中抽取内容并制定公有的 Flash 规范，到了后面就添加到了 H5 规范下，成为大家都能掌握的规范。

谷歌苹果等大公司都不支持 Flash 应用，所以在大公司的压力下以及现在 H5 的流行，Adobe 公司决定在 2020 年终止 Flash 技术的提供。

H5 技术究竟是什么？

有了这个铺垫，我们再来看看 H5 技术究竟是什么，它为什么会取代 Flash。

首先，H5 大量应用在手机端的网页上面，但这并不等于“H5 等于手机网页”，因为普通的 HTML 页面也可以适配手机端屏幕。

H5 全称是 HTML5，HTML 是超文本标记语言的首字母缩写。而 HTML4.01 的标准在 1999 年年底发布。HTML5 的草案，前身是网络应用 1.0（Web Application），而 HTML5 的第一份草案于 2008 年 1 月月份公布，定稿是在 2012 年的年底。

在 H5 标准出现后，各大浏览器纷纷开始支持 H5，比如火狐、谷歌的 Chrome、IE9、苹果的 Safari，Opera 等等。事实上，H5 技术很大程度上是为了移动平台所设计，能在移动平台支持多媒体，当然取代 Flash 这是顺理成章的事情，但是，H5 技术并不等同于移动平台网页，我们在很多的 PC 端的网页也能看到 HTML5 的技术。

H5 技术有什么新特性？

H5 技术拥有很多最新的特性。

在语法方面，HTML5 的语法给了网页编写程序员拥有更好的语法结构以及更加丰富的标签。比如，video 标签就可以直接在 H5 页面中嵌入播放器。H5 也支持更多的设备兼容，比如可以支持摄像头，支持麦克风（移动端的或者电脑端的都可以）。

H5 也支持本地存储的功能，我们可以使用 H5 技术来开发网页 app，这些都来自本地存储的缓存技术，所以 H5 网页的载入速度会比传统网页更加迅速和便捷，连接也变得更快。H5 支持多媒体这是必然的，包括音频、视频、摄像头等功能，事实上使用 H5 技术的多媒体技术已经完全可以取代 Flash 技术了。

在编程方面，以前我们需要跨域的技术，在 H5 中，可以使用 XMLHttpRequest 来解决跨域问题。而且 H5 页面一经修改就能直接更新上去，基本上刷新页面就能看到效果，而如果使用 Flash 来做，需要进行编译和发布，并且替换 Flash 页面，才可以看到 Flash 应用的更新，从时间效率讲，H5 技术又是技高一筹。

另外，如果有专业的 SEO 人员，也可以很方便地通过 H5 来做 SEO 优化，做网站的索引和搜索引擎的抓取优化工作，这在 Flash 来说几乎是不可能做到的工作。

用 H5 编写游戏有什么优点？

使用 H5 技术编写的应用和游戏，可以很方便地嵌入到苹果或者安桌的 App 中。这种方式可以免去原生开发两套 App 的麻烦，只需要编写一套类似 App 的 H5 页面，然后使用类似 WebView 的方式来嵌入到原生应用中。

由于H5 页面可以随时更改，所以现在苹果商店对于 H5 内嵌应用的审核比较严格，所以 H5 页面必须做得好，而且逻辑流程都不能有问题。所以在 H5 发展的阶段，有很多声音说，H5 除了取代 Flash，还会取代 App，事实上这是不可能的。虽然 H5 取代 Flash 已经毋庸置疑，但是 H5 取代原生 App 还是有很多的掣肘，比如 H5 只能编写轻量级的 App，如果需要那种画面效果特别好的 App，或者对设备进行深层次的操控，那就只有原生可以办到了，所以现在有许多的 App 开发，都选择 H5 和原生混合开发，这样会减轻一定的工作量，并且在实现其他深层次的功能的时候也能使用原生代码。

用 H5 比 Flash 编写游戏更快速，占用资源也更少，所以以前利用 Flash 技术编写的游戏，现在都改成 H5 技术编写，当然 H5 编写的游戏仍然比不上原生编写的游戏。毕竟原生有对底层的操控能力，直接而且快速，而 H5 毕竟需要通过浏览器的解释和渲染，所以它的速度基本取决于浏览器，代码优化地再好，也无法直接穿过浏览器这一层去做事情。

所以，用 H5 编写的游戏，如果是在电脑端，我们可以编写大一点的网络游戏，因为电脑的资源能随心所欲地利用；而如果在手机端，H5 技术适合编写小型游戏，比如电商领域、推广平台等营销类的小游戏，再比如玩家操作不是特别复杂的游戏，比如战棋类的、益智类的、策略类的，或者静态画面比较多的游戏（比如选择一个正确答案，猜一个成语那样的等等）。这样的游戏，占用的资源不大，动态资源载入不多，移动平台也不会有太多的卡顿感，各种手机都可以适配和游戏，这样小型的 H5 游戏是比较适合移动平台的。

H5 游戏的传播能力比 Flash 更好。首先 Flash 技术需要安装一个插件才可以使用，然而 H5 游戏只需要浏览器支持即可，所以从这方面讲，只需要你通过浏览器分享一个 H5 游戏到微信、QQ 等社交软件，就可以直接进行传播，而不像 Flash 那样需要安装额外的东西。所以利用 H5 技术编写的营销工具现在占领了绝对的主流，所以才会有本篇文章最初所说，很多人认为“H5 等价于移动平台网页”这样的误解。

小结

总结一下今天的内容：

H5 游戏拥有比 Flash 更好用更开放的规范，开发和发布也更方便，修改代码后放到网上，只需要刷新一下页面就可以看到修改的内容；

另外，Flash 技术需要安装一个插件，而 H5 技术直接在浏览器就可以呈现。所以 H5 技术完全替代 Flash 只是时间问题。

现在留一个小问题给你。

我们都知道浏览器渲染出来的网页都是 2D 的页面和游戏，但是 H5 技术也可以编写 3D 视觉效果的内容。你知道 H5 技术所拥有的 3D 效果是用什么技术实现的吗？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第13讲 | 热点剖析（二）：如何选择一款HTML5引擎？

上周六，我详细介绍了HTML5 技术相比 Flash 技术的优势，相信你已经对 HTML5 技术有一个大致的了解。下周六，我会带你编写一个 HTML5 游戏，在这之前，我们需要先了解几种常见的 HTML5 游戏引擎。

一些比较成熟的引擎，比如 Cocos2d-JS、白鹭等，它们都提供有系列化的工具，比如编辑器、IDE 等周边。但是其实大部分 HTML5 游戏引擎都只有图形引擎而已，比如 legend.js。而且很多 HTML5 引擎只是个人编写的开源引擎，所以漏洞还是比较多的。

HTML5 游戏引擎在编写的时候，除非用 DOM（Document Object Model）纯原生编写，绝大部分都是使用 JavaScript 编写的。但是为了考虑各种程序员的需求，现在也有使用 TypeScript、CoffeeScript、LiveScript 等语言编写的 HTML5 引擎。

几款常见的 HTML5 游戏引擎

我们现在来看一下几款常见的 HTML5 游戏引擎。

Construct 2

这是一款收费的引擎，当然也有免费的版本，但是免费的版本不可用于商业用途。那么既然是商用引擎，那它一定会比免费开源的产品更加完善和易用。这里有一幅 Construct 2 的截图，你可以看一下它的界面。

我们可以清晰地看到，这个界面类似 Office 的操作界面。左侧是物件的属性，比如长宽高等；右侧上方是物件的层，包括背景、物件；右侧下方是物件选择器，你可以选择各种物件摆放到编辑器内。这种类型的编辑器，在成熟的商业引擎中比较常见，但是由于编写编辑器的人力物力成本，所以免费的引擎一般不提供这种成熟的编辑器。

从使用角度，我总结了一下它的特点，大概有这几条，供你自己制作 HTML5 游戏的时候选择：

可以设计任何类型的 2D 游戏，而且不需要编程的技能（可以由事件系统控制）；

使用简单的拖拽和事件系统就可以设计游戏逻辑；

拥有一个物理引擎，可以用来计算运动、旋转和碰撞反映；

可以在 iOS、Android、Windows Store 等多个平台上发布游戏；

拥有很完备的文档和教程案例。

在软件方面，它拥有一个非常直观的编辑器，可以开发高级的视觉效果，可以支持视差滚动的游戏，并且在编辑器中集成了调试器和 profiler。做出来的游戏支持多人游戏，也支持 2D 的阴影，还支持 JS SDK 的插件。

Construct 2 的不同版本，对应的功能也不相同。

第一个是免费版本的，它的使用没有时间限制，你可以使用 100 个事件、4 个图层和 2 个着色器，随意导出成 HTML5 网页游戏，但是这些编写出来的游戏不可以用作商业用途。

第二个是个人版或者教育版：这个版本移除了免费版本的限制，甚至可以将项目导出至 iOS、Android 或者其他平台。如果你用作商业用途，可以允许你在商业用途中获得最高 5000 美元的收入。它可以使用调试器观察 tab、profiler 和事件断点，还可以制作多人游戏，在应用程序内可以嵌入内购功能。

最后一个是商业版本。所有特点与个人版本相同，但是没有上面说的那些限制。

Construct 2 的缺点就是相关的中文教程不是很多，需要你自己去摸索。但是它支持多平台，简单易用，还提供了很强悍的事件系统，可以不通过代码来控制逻辑，所以不需要编程知识就可以使用。

白鹭引擎

白鹭引擎是我们国内自己研发的 HTML5 引擎。最初，它也是一套图形引擎，后来资本化之后，白鹭引擎就开始着手将引擎变成一套解决方案。

它不仅提供 HTML5 游戏引擎，也提供了各种周边工具和产品。除了图形引擎之外，它还提供了可视化编辑工具（Egret Wing）、动画制作工具（Dragon Bones）、HTML5 游戏加速器（Egret Runtime）。

可视化编辑工具提供了代码调试功能。可以在文件中打断点、单步调试、跳入和跳出、查看局部变量、添加观察表达式等。当然你也可以自己编写扩展程序，可以对任何语言进行调试。可以让开发者用 Node.js + HTML 来开发 IDE 的扩展插件，然后实现各种提升效率的工具。

你还可以自定义语法高亮、智能提示和调试。它还内置了版本控制系统 Git，集成命令行工具，可以同时打开多个命令行标签，执行需要的命令。

它甚至可以在 Wing 中直接运行 Vim。它还内置多种主流的代码配色方案，满足大部分开发者配色习惯。开发者还可以自己编写配色扩展，你可以根据自己习惯，定制你的 IDE。

白鹭的动画制作工具提供了动画和动漫的解决方案，打通了动画设计和游戏开发的工作流，也支持导出各种动画格式，提供了可交互动态漫画编辑器模式。你甚至不需要动画基础，轻松实现丰富特效。DragonBones 可以输出多种格式，视频、网页、动画数据，可以用于几乎目前所有的主流游戏引擎和编程语言，提供了各个平台的运行库。

另外，还有一个HTML5 加速器，这是一款支持 3D 的 HTML5 游戏加速器，解决低端机对 HTML5 标准支持不佳、体验差的弊端，加速器可以适配不同的系统让 HTML5 游戏效果媲美原生游戏，解决设备不统一，移动应用市场的设备、操作系统和屏幕的不统一的问题很严重， 已成为 HTML5 游戏开发者面临的难题，适配也成为美工最头痛的问题。

另外，越来越多的游戏和应用使用移动 QQ、微信、微博等客户端作为入口。 不同客户端软件所携带的浏览器内核不同， 也直接影响 HTML5 游戏在平台上的表现。

而 HTML5 加速器直接支持 HTML5 游戏运行所需的底层功能，解决了屏幕适配和性能问题，并且也从渠道角度解决了 HTML5 游戏接入流程复杂， 定制化功能多的问题，实现一次接入，随时上线。

我们再来讲讲白鹭引擎本身。白鹭引擎支持 2D、3D 游戏，引擎本身采用模块化设计，egret 也实现了脏矩阵方法，可以很大提升渲染性能。脏矩阵，简单描述就是，系统只渲染动画变化的部分，不变化的部分并不进行渲染更新。

白鹭引擎本身的功能和周边工具都很强大，中文教程和论坛也比较成熟，且支持 2D、3D 游戏的制作，现在也支持直接编写最流行的微信小游戏。在商业化方面，白鹭是做的比较成功的，所以现在的用户也非常多。

Cocos2d-JS

最后，我们来说一下 Cocos2d-JS。

Cocos2d-JS 是 Cocos2d-x 的一个分支，只要对 HTML5 游戏有所了解的话，基本都听说过 Cocos2d-JS，很多开发人员也会选择 Cocos2d-JS 来进行开发 HTML5 游戏。

根据官方说法，它跨全平台，采用 JavaScript 语言，可发布到包括 Web、iOS、Android、Windows Phone8、Mac、Windows 等平台，引擎基于 MIT 开源协议，完全开源、免费、易学易用，拥有活跃的社区支持。

Cocos2d-JS 让 2D 的游戏编程门槛更低，使用更加容易和高效。和其他类似游戏引擎相比，它定义了更加清晰的 2D 游戏编程的基本组件、易学易用的 API，并且具备原生性能的脚本绑定技术，实现游戏的跨原生平台发布，开发效率更高，使用简单。

它本身融合了 Cocos2d-HTML5 和 Cocos2d-x JavaScript Bindings（JSB），支持 Cocos2d-x 的所有核心特性并提供更简单易用的 JavaScript 风格 API，基于 Cocos2d-JS 的框架，可以方便地使用 JavaScript 语言进行游戏开发，快速构建原型进行测试和验证。

由于 Cocos2d-html5 的 API 和 Cocos2d-x JSB 的 API 高度一致，开发出来的游戏不用修改代码或者修改少量代码就可打包为原生性能表现的混合游戏，发布到原生应用市场，这就是一次编码全平台部署的效果。

小结

这一节，我给你介绍了最流行的三款 HTML5 游戏引擎。

其中一款是商业引擎，另外两款可以自由使用。从难易角度讲，Construct 2 拥有更方便的开发体验；从流行度讲，Cocos2d-JS 和白鹭都属于国内最流行的 HTML5 游戏引擎。

你可以根据自己的需求来选择，如果美术熟悉 Cocos2d-JS，或者想要快速上手 2D 游戏，可以选择 Cocos2d-JS；如果对编程不太熟练，可以选择 Construct 2；如果是拥有完整流程的开发公司，Cocos2d-JS 和白鹭都可以选择。

最后，给你留一个小问题吧。

现在流行的微信小游戏，目前只支持 3M 左右的大小，否则就审核不过，那么如果在游戏资源大的情况下，有什么方案可以让游戏维持在 3M 大小呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第16讲 | 热点剖析（三）：试试看，你也可以编写一款HTML5小游戏！

前两周，我分析了HTML5 的核心竞争力以及HTML5 游戏的引擎。选择好了 HTML5 引擎，我们就可以开始制作游戏了。

对于编写 HTML5 小游戏，你或许会很有兴趣，因为 HTML5 现在已然是一个潮流，而且利用引擎编写 HTML5 游戏已经变得相当方便。

如何选择一款引擎？

我选择的是比较容易上手的 lufylengend 引擎。为什么要选择这款引擎呢？因为它只需要下载一个压缩包，并且不需要特别繁琐的说明和设置就能直接上手，用作我们的教学示例是最合适的。

如果使用白鹭引擎或者 Cocos-2d 引擎这些比较有名的引擎，可能会有这些问题。

这些引擎从工具到套件都非常成熟，你直接下载一个引擎启动器或者组件管理器，就可以一应俱全地在启动器里面下载，配置到默认路径。但是，这些工具拥有纷繁复杂的界面，你连上手都要费一段时间，更别说短时间内熟练使用并制作出一款游戏。

这些引擎需要引入的库或者使用的方式极为系统，所以你需要系统地引入库文件，才可以使用。事实上我要做的示例，并不需要很多复杂的东西，你只需要跟我从头到尾走一遍，就能明白编写 HTML5 游戏是怎么回事。

这些引擎需要别的工具支持，比如 node.js。作为新手来说，光配置 node.js 就是一项比较麻烦的工作。所以我选择了 lufylengend 引擎这一个比较“单纯的”引擎来作为教学引擎。

几个简单的说明

你可以从这个地址下载最新版本：https://github.com/lufylegend/lufylegend.js/archive/lufylegend-1.10.1.zip 。下载下来的安装包大约有 30M 大，解压缩后有 36M 左右。解压缩后，我们会看到一系列的 js 文件。

我先对这些文件做一些说明，并且对最基础的编程流程做一个简单的梳理。

压缩包内包含 lufylegend- 版本号.js 和 lufylegend- 版本号.min.js 这两个完整版本，还有 lufylegend- 版本号.simple.js 和 lufylegend- 版本号.simple.min.js 这两个缩减版本，其中带 min 字样的是去除了回车和空格的压缩版本。如果你使用 JavaScript 编程，缩减版本对于你来说再熟悉不过的了。

其中，simple 缩减版本与完整版本的区别在于，它将 LBox2d、LQuadTree、LTransitionManager、LoadingSample1、LoadingSample2、LoadingSample3、LoadingSample4、LoadingSample5 等几个类与 HTML5 引擎的常用部分分离，缩减了引擎的体积。如果需要用到被分离的部分功能的话，可以手动进行加载。

随后，可以在 HTML 代码中将 legend 引擎的库件引入。调用 LInit 函数，初始化库件。然后开始游戏代码的编写。剩下，你只需要有一款合适的 IDE 就可以开始编程了。

对制作 HTML5 游戏来说，你首先要做的是，将游戏场景在浏览器中呈现出来。把 Windows 的窗体，从系统客户端程序搬到浏览器上，呈现的窗体从可执行文件变成了浏览器。从这个角度讲，** 浏览器担负了应用层的工作，浏览器本身担负了解释程序，并且渲染的过程，** 所以，从理论上讲，相同的游戏类型和游戏内容，HTML5 游戏的渲染速度是比不上客户端游戏的渲染速度的。

一起动手制作吧！

很凑巧的是，lufylengend 引擎也拥有一款打飞机 demo。我们只需要在 lufylegend 引擎目录的 examples/demo 下，找到 barrage 目录。

这一款打飞机的游戏，打开后是这个样子。

这和我们之前讲述的内容非常类似。那我就借解释一下这款游戏的代码，来教你制作。

在目录下，有一个 index.html，这就是游戏的开始页面。在这个页面下，我们先来看一下部分代码。

<script type="text/javascript" src="../load_lufylegend.js"></script>

<script type="text/javascript">

if(LGlobal.canTouch){

      LGlobal.stageScale = LStageScaleMode.EXACT_FIT;

      LSystem.screen(LStage.FULL_SCREEN);

}

</script>

<script type="text/javascript" src="./js/Main.js"></script>

如果你熟悉 web 编程，对于这些代码肯定非常熟悉。在开始的地方，我们看到载入的 JavaScript 代码是 load_lufylegend.js，这个 js 文件包含在打飞机游戏的上一层目录，内部就只有一行代码。

document.write('<script type="text/javascript" src="../../../lufylegend-1.10.1.min.js"></script> ');

我们看到，在这个 js 文件内，包含了 lufylegend 的原始引擎文件。至于为什么要这么做，为什么要使用两个文件包含引擎，是因为这样可以保持游戏代码的干净。如果你要修改引擎的包含路径，不需要修改游戏本体文件，只需要修改 load_lufylegend.js 包含路径即可。

而 LGlobal.canTouch 这段话的意思是，如果是移动版本的话，设置缩放模式为适应屏幕，并且为全屏，代码是：

LSystem.screen(LStage.FULL_SCREEN)

最后，导入游戏脚本 Main.js 文件。

在 Main.js 里面，我们看到，它还包含了三个别的 js 文件，代码是这样。

imgData.push({type:"js",path:"./js/Global.js"});

      imgData.push({type:"js",path:"./js/Bullet.js"});

      imgData.push({type:"js",path:"./js/Plain.js"});

它包含了一个共有类 Global.js、子弹类 Bullet.js 以及飞机类 Plain.js。之后的代码是这样的。

loadingLayer = new LoadingSample1();

addChild(loadingLayer);

其中 LoadingSample1 是“载入进度条”类，我们可以在下面的代码看到载入的实现。

LLoadManage.load(

imgData,

function(progress){

loadingLayer.setProgress(progress);

},

function(result){

imglist = result;

removeChild(loadingLayer);

loadingLayer = null;

gameInit();

}

);

在载入结束后，就开始 gameInit 函数的调用，也就是游戏初始化。

function gameInit(event){

// 游戏底层实例化

backLayer = new LSprite();

addChild(backLayer);

ctrlLayer = new LSprite();

addChild(ctrlLayer);

LSprite 是引擎的基本显示列表构造，里面包含了显示内容的列表节点，addChild 就是将显示内容添加到节点列表。

// 添加游戏背景

bitmapdata = new LBitmapData(imglist["back"]);

bitmap = new LBitmap(bitmapdata);

backLayer.addChild(bitmap);

这几段代码就是将背景图片也添加到显示节点列表。

// 得分显示

pointText = new LTextField();

pointText.color = "#ffffff";

pointText.size = 20;

pointText.text = point;

backLayer.addChild(pointText)

这是一个得分的显示，所以需要新建一个文本类，并设置颜色和大小，并将之放到显示节点的列表。

// 加入玩家

player = new Plain("player",0,200,600,[5]);

backLayer.addChild(player);

我们需要新建一个玩家类。新建玩家，其实就是新建一个飞机类型，所以我们在这里看到一个 plain 类的创建。

这个创建函数的实现原型是这样的。

function Plain(name,belong,x,y,bullets)

你可能会觉得奇怪，Plain 是什么意思，在它的 demo 里面，Plain 是飞机的意思，然而可能是作者的一时疏忽或者是英文“捉急”，所以就把 Plane 写成了 Plain。以下所有和飞机相关的代码都是 Plain，虽然并不影响代码的运行，但是出于严谨考虑，我在这里更正一下，Plain 等于 Plane。

第一个参数是名字，第二个参数是飞机所属，表明是属于敌人还是玩家，随后两个参数（x,y）是飞机在 2D 画布上所显示的位置，最后一个 bullets 是子弹的数组。

// 添加帧事件，开始游戏循环

backLayer.addEventListener(LEvent.ENTER_FRAME,onframe);

// 添加控制事件

backLayer.addEventListener(LMouseEvent.MOUSE_DOWN,ondown);

backLayer.addEventListener(LMouseEvent.MOUSE_UP,onup);

}

在初始化的最后，我们需要添加鼠标事件，将鼠标事件 LMouseEvent 的鼠标上下操作事件都放入到事件侦听函数内：addEventListener。

看到这里，你不禁会问，按照我们所教学的，游戏应该会有一个大循环来完成游戏的显示、操作等内容。那这个循环在哪里呢？

事实上这个循环，就在上面这串代码中。

backLayer.addEventListener(LEvent.ENTER_FRAME,onframe);

其中，LEvent 为事件类。和传统客户端游戏不同，在 HTML5 游戏引擎中，循环采用了事件代码来完成，只要你在侦听器函数中注册了事件，都会一帧一帧不停地调度这个事件，以此达到循环的效果。

在这里，注册的侦听事件函数就是 onframe 函数。查看它的源代码你可以看到 onframe 函数的实现细节，我就不在这里进行阐述了。

小结

今天，我们使用 lufylegend 引擎剖析了 HTML5 游戏的编写方式。我来总结一下。

HTML5 游戏的启动页是一个 HTML 文件。这个文件包含引擎的 js 文件。在其他别的引擎中，也是使用类似的方式来加载引擎。包含了引擎，就可以针对引擎所封装的接口进行游戏的开发。

HTML5 游戏的循环方式和传统游戏的循环方式不同。由于 HTML5 游戏引擎绝大部分是使用 JavaScript 脚本编写的，而 JS 本身就是以事件驱动的方式来工作的，所以使用事件驱动是 HTML5 游戏引擎的一个很大特点。我们在事件侦听函数中注册一个事件函数，在这个函数内编写“游戏循环”内的代码，就能起到传统游戏循环的作用。

在 HTML5 游戏中，我们几乎不需要关心图片的刷新或者清空操作。这些操作比之传统客户端游戏更为简洁，我们只需要关心游戏的图片呈现以及操作即可。

简单来说，**HTML5 游戏就是一种使用事件驱动模式并渲染在网页上的一种技术，省却了传统游戏的底层操控。** 你在制作的时候，可以把更多的关注点放在游戏逻辑和可玩性上。

最后，留一个小问题给你。

在类似这样的 HTML5 打飞机游戏中，碰撞检测是怎么做的呢？请结合我以前的讲过的内容给出一个答案。

欢迎留言说出你的看法。我在下一节的挑战中等你！

第19讲 | 热点剖析（四）：安卓端和苹果端游戏开发有什么区别？

所谓的移动端，指的是可以随时拿在手上游戏和工作的，自带直流电源的设备，所以从游戏角度讲，手机、掌上游戏机、iPad 都属于移动端设备。

我们今天的侧重点在安卓和苹果端的手机游戏，在开始说手机游戏之前，我们先简单了解一下掌上游戏机，因为这毕竟是移动端游戏的鼻祖。

在游戏发展的历史中，游戏机从 8 位发展到 32 位、64 位；游戏存储的媒体，从芯片到卡带到光盘再到网络下载。从电视游戏机到掌上游戏机，游戏存储的容量也越做越大，游戏机也变得越来越小巧、轻薄。

在如今的手机游戏流行之前，各种掌上游戏机在游戏界的竞争可谓硝烟弥漫。GB、GBA、GBASP、NDS、3DS、PSP、PSV，这是很多人都耳熟能详的掌上游戏主机。当然在这之中也有一些只是昙花一现。加上智能手机的出现，能在手机上玩到如此多种多样的游戏，在此之前，这些东西人们从来没有想过。

因为在当时的条件下，GameBoy 系列、NDS 系列、3DS 系列，每一款都是惊艳世界的产品。然而到了智能机时代，这些产品的优势逐渐被削弱，一大部分掌机玩家被分流去了手机和平板的市场。

而自从 2007 年第一代 iPhone 发布到今天，智能机经历了多次的变革。随着硬件的提升，移动端的游戏画面逐渐超越了 2000 年左右次世代 32 位机，比如 PS2 以及同时期的次世代游戏机。所以今天我们可以在智能机上玩到相当多的 2D、3D 游戏，尽管移动端都是小屏幕，但呈现结果其实已经不输给几年前的高端电脑。

智能机发展到现在，操作系统从苹果、安卓、塞班、黑莓、Windows 等，大战最终剩下两大操作系统阵营，苹果和安卓。而苹果和安卓两大生态系统，一个是封闭式的系统，一个是开放式的系统，有相同点，也有不同点，所以在这两大系统下编写游戏，有很多内容可以讲。

首先，从编程语言这方面说，苹果系统大量使用 Objective-C、Swift 来开发游戏，而安卓大量使用的是 Java。

而苹果公司选择 Objective-C 的原因是因为苹果收购了 NEXTSTEP。NEXTSTEP 是由 NeXT. 公司所开发的操作系统。这套系统以 Mach 和 BSD 为基础，Objective-C 作为原生语言，拥有先进的 GUI 界面。后来苹果公司将 NeXT 买下，成为 Mac OS X 的基础，抛开语言特性不说，Objective-C 比 C++ 出现年代更早，更为动态），使用 Objective-C 也有很大因素是这一历史原因。

而安卓刚诞生的时候，要与苹果展开竞争，目的肯定是要降低开发门槛，吸引更多开发者加入，才能带动安卓的普及。所以选择 Java 入门比 C++ 来得快，在跨平台的能力上也比 C++ 来得容易，庞大活跃的 Java 社区可以提供足够的第三方资源库。所以 Java 成为了安卓平台的首选开发语言，选择 Java 是一个在执行效率和开发效率之间均衡的选择。

在安卓和苹果端开发游戏，除了语言区别之外，还有审核区别。如果我们说的是中国开发者，那苹果开发者的开发工作就复杂多了，不仅要申请开发者帐号，递交开发者年费，还要申请游戏运营资质等等。在安卓端，你就省却了申请开发帐号，开发年费的问题，但是其他资质一样是需要申请的。

不管两者平台的开发语言如何，在安卓和苹果端，使用游戏引擎编写游戏的话，除了使用的语言不同之外，编程接口、引擎的周边工具、美术资源以及最终构建完成后的游戏效果都是一样的。

在 iOS 的环境下，我们同样也可以使用游戏引擎来编写游戏。在苹果的体系下，2D 游戏比较流行的引擎是 SpriteKit。

SpriteKit 是苹果公司在 iOS 7 的 SDK 中新加入的一个 2D 游戏框架，包括物理引擎、视频、滤镜、遮罩等等。IDE 中对 SpriteKit 也有比较好的集成，因为减少了开发者的工作。

SpriteKit 里面有几个比较常用的类：

SKSpriteNode，用来绘制精灵的纹理；

SKLabelNode ，文本渲染类；

SKVideoNode，视频播放的类；

SKEmitterNode，粒子系统的创建和渲染；

SKShapeNode，基于 Core Graphics 路径形状的渲染；

SKEffectNode，使用遮罩来裁剪子节点的类。

因为是苹果公司推出的 2D 游戏引擎，所以你可以在苹果的机器上编写和调试代码，并且在 iPhone 上进行预览和测试。

在编写游戏的过程中，安卓使用工具是 Android Studio，而苹果使用的是 XCode 等编程工具。除了苹果特有的游戏库之外，有不少通用的游戏引擎也可供选择，比如 Cocos2d-x 之类的，在这两个平台都能编写，比如 Unity，比如 libGDX 游戏库，也是目前在安卓、苹果上编写游戏适合选择的一个游戏库。

所以如果使用跨平台的游戏引擎，在两大平台下开发，除了开发语言的区别之外，底层的内容并没有什么区别，比如 Cocos2d-x、libGDX 等等，都是跨平台生成不同目标代码的引擎。

我们继续拿 libGDX 来做说明。libGDX 兼容多种平台系统，自然也包括安卓和苹果。

这款引擎兼容性强，为调试和开发提供了便利。为什么这么说呢？你可以在电脑上编写测试和调试游戏，但是不需要打开安卓模拟器。开发应用的同学应该知道，安卓应用一般是在电脑上编写、测试、调试，并且到模拟器或者安卓硬件上进行预览运行，但是 libGDX 解决了这个问题，你可以直接在桌面上进行开发测试和调试。

libGDX 引擎是由 audio（音频操作）、files（文件读取）、graphics（2D/3D 图像）、math（数学绘图运算）、physics（Box2D 物理引擎封装）、scenes（2D/3D 场景组件）、utils（内置工具）这些主要模块所组成。

libGDX 主要是用 Java 编写的，还用了少部分的 C/C++ 代码。至于这部分 C/C++ 代码，是为了针对一些性能做出优化而编写的，比如音频、物理引擎等等。在 Java 层面，它已经封装了所有的本地代码，相比别的安卓游戏引擎，libGDX 的执行效率也比较突出。

libGDX 使用 jni 封装了 Box2D 物理引擎的 C++ 版本，所以比之 JBox2D 速度更快，目前比较流行的几个包含物理引擎安卓游戏引擎（如 Andengine、Rokon 等）都在用 libGDX 的封装版本。

libGDX 的开发工具也是非常地丰富，比如粒子编辑器、文字生成工具、贴图合并工具，一些 UI 库等。

另外，在网络部分，libGDX 最早的版本提供了单机游戏的编写或者一些联网程度不是特别高的游戏。

在后续的版本里面，由于市场和竞争的原因，现在新版本的 libGDX 也提供了专门的网络模块。在一款将虚拟环境与现实地理位置信息结合在一起的手机游戏《Ingress》推出之后，也有部分国内公司基于 libGDX 网络模块编写的手机网络游戏。

另外，libGDX 拥有很强大的第三发工具，比如第三方物理引擎的属性编辑器 PhysicsEditor、贴图资源打包工具 TexturePacker 以及 Spine。Spine 是制作 2D 骨骼游戏的动画制作软件，功能非常强大，它的作者也是 libGDX 的核心代码编写者之一。

从文档看，libGDX 的文档数量已经比较完善了，但是大部分都是英文，对于部分开发者来说学习难度较大。虽然 libGDX 拥有非常活跃的官方讨论社区，但是大部分还是英文的。

你可以通过 libGDX 来编写苹果和安卓的游戏，当然如果你编写的是网络游戏，那么服务器端就需要你自己来编写。

小结

我来总结一下今天的内容。

从游戏角度讲，手机、掌上游戏机、iPad 都属于移动端设备。而进入智能机时代之后，掌上游戏机逐渐退居幕后，成为少部分游戏发烧友的选择。

安卓端和苹果端在开发游戏中的区别，主要体现在开发语言上。苹果系统大量使用 Objective-C、Swift，而安卓大量使用的是 Java。

之后，我介绍了苹果端的比较流行的引擎 SpriteKit 以及适用于各种平台的 libGDX。使用 libGDX 在各个平台编写出的游戏，表现几乎是一样的，除了执行效率或者耗电水平会有细微的差别，但这和引擎其实关系不大。

最后，给你留一个小问题。你知道什么是 2D 骨骼游戏吗？能否举个例子？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第22讲 | 热点剖析（五）：如何选择移动端的游戏引擎？

上一次，我和你探讨了移动端游戏的一些区别和特点，我特别介绍了 libGDX 这款游戏引擎。因为这款游戏引擎是多平台的，且各种第三方工具都很完备，比如物理引擎、特效、2D 骨骼动画工具等等，所以 libGDX 目前已经被越来越多的公司和个人接受和使用。

在苹果端，我们也讲到了，苹果公司推出的 SpriteKit 游戏开发框架，在苹果体系下，使用 XCode 就可以很方便地使用 SK 工具开发出好玩的 2D 移动游戏。

从目前最新的游戏引擎的趋势看，一次编码、到处兼容已经成为了市场的主流，多种语言、一次打包（多种语言都可以编写相同的代码，然后配合打包成目标平台的游戏），也是程序员喜闻乐见的一种编程方式。

如果你准备编写移动端的游戏，或者你已经是 app 程序员，有编码经验但是想要转型游戏程序员，那么你要对移动端的游戏引擎有一定的了解，才能开始你的游戏编程之旅。

五种常见的移动端引擎

我今天介绍五种常见的移动端引擎，分别是 Unity、虚幻、App Game Kit、Corona SDK、Cocos2d-x。

Unity

和虚幻之类有名的游戏引擎一样，Unity 最初是从 3D 网页引擎的样子进化成现在这样的。Unity 普通版是 400 美元，但是 Pro 版本要 1500 多美元，但是如果你购买 Pro 版的话会得到更强大的功能和编辑器等等。Unity 不仅支持电脑和移动端多平台，还支持游戏机甚至掌上游戏机，比如我们熟知的 PS 系列和 PSV 之类的，比如《杀出重围》《神庙逃亡》等，都是使用 Unity 引擎制作的。

Unity 的编程可以使用 C#、JavaScript、Java、Boo 等脚本语言。对于 Flash，Unity 已经放弃支持这个平台了。事实上，Unity 在版本 4.0 之前是可以支持将游戏转换为 Flash 平台的，4.0 后已经不支持转成 Flash 游戏了。至于 Flash 被各家放弃的原因我在之前的文章已经讲过，这里就不多说了。

Unity 拥有大量的所见即所得的编程界面开发工具。比如支持 3D 骨骼和动画的导入、贴图的材质可以转换为自身的 U3D 格式等等。

在底层方面，它支持 OpenGL 以及 DX 系列。它自带的物理引擎、粒子系统，性能也极其高效，还支持网络系统，所以你可以使用 Unity 来编写单机或者网络游戏。

虚幻

这个大佬级别的引擎，你一定不陌生。它现在已经支持在移动平台使用了。虚幻有免费版本和付费版本，但是只有付费版本你可以获取引擎全部的 C++ 源代码。最为可贵的是，出品虚幻的 Epic 公司有发布完整的教学视频，而开发人员也能从网络社区获取大量的学习资源。

这款引擎不仅仅在游戏引擎技术方面比较优秀，运营方面、市场方面，在行业内都是属于前列。耳熟能详的《蝙蝠侠》《阿卡姆之城》等等用的都是虚幻引擎。

虚幻引擎有完整的数据属性编辑功能。关卡设计人员可以自由地对游戏中的物件进行设置，也可以通过脚本编写的形式进行优化设置。关于关卡编辑器的功能，我在之前的文章中有详细介绍。

虚幻的资源管理器可以进行快速准确地查找、观看，并对游戏开发中的各种资源进行整理组织。地图编辑器可以让美术开发人员自由地对地形进行升降调节。更强大的是，它可以通过带有 alpha 通道的笔刷对地图的层进行融合修饰，可以在地图编辑中生成碰撞数据和位移贴图，你看到的可以说是游戏场景的一种演示版也不为过。

它的编辑器还为美术制作人员提供了完整的模型、骨骼和动画数据导出工具，并可以编辑游戏事件所需要的声音文件、剧情脚本。

App Game Kit

我个人认为，App Game Kit 比较适合刚进阶的游戏开发者。这款引擎使用非常容易上手，你可以去它的官网看最新发布的消息。有一款 Driving Test Success 的应用就是使用 AGK 编写的，当然这是一个商业化的付费引擎。这款游戏引擎也支持树莓派上编程，可谓是各个平台都兼容。

Corona SDK

如果你深入学习过或者接触过移动端游戏开发的话，你一定知道这款游戏引擎。这款引擎不仅仅是跨平台支持，甚至还支持 Kindle 电子书这类的平台。

它的编程语言为 Lua，我们在之后的文章会介绍 Lua 和 C 语言的绑定，我在这里只是简单说一下。相对于别的语言来说，Lua 更加轻量级，对初学者来说也更容易上手。

Corona SDK 的客户除了个人，也有很多知名的大厂，比如日本的南梦宫等等。当然收费也并不便宜，专业版就接近 600 美元，企业版接近 1000 美元，当然如果是企业用户的话，这点投入比之自己编写引擎来说，划算得多。

Cocos2d-x

国内的朋友对这款引擎非常熟悉，与此同时，它的同胞兄弟 Cocos2d 系列，我也在前面做过一些介绍。

很多人以为 Cocos2d-x 是中国人编写的。事实上 Cocos2d 的作者是一个叫 Richardo 的阿根廷人。Cocos 是在阿根廷的一个叫 Los Cocos 的地方诞生的。由于 Richardo 的学习曲线是汇编、C/C++、Python，所以这个时候 Cocos 版本是 Python 的。他们将研究成果在 PyCon 2008 和 EuroPython 2008 上都做了展示。

2008 年的时候，他们抓住 iPhone 发布的机遇，在 2008 年 6 月宣布支持 iPhone，然后沿用了 Python 版本的 Cocos2d 相同的设计思维，用 Object-C 重新编写了 iPhone 版的 Cocos2d 并且发布了 0.1 版。智能手机刚起步的时候，iPhone 用户数量明显多过安卓，所以 Cocos2d 刚开始就笼络了大批 iPhone 开发者，随后又有了安卓版。所以，现在用 Cocos2d 引擎编写的游戏，几乎随处可见。

在 2009 年的时候，Cocos 团队编写了 Cocos2d 的 World Editor，Cocos2d-Python。这个编辑器用起来非常方便，随后 Cocos2d 的各种平台和各种语言的移植版本也逐渐被各类程序员拿去开发并且出现在开源社区。

我们可以看到几个主流语言的绑定版本：

Ruby 版本：ShinyCocos

安卓版本：Cocos2d-Android

.NET 版本：CocosNet（Mono based）

Windows 版本

这里有一幅来自它的官网的产品分支图，你可以比较清晰地了解 Cocos2d 的各个分支。

同时经过几年的努力，Cocos 系列也拥有了非常优秀的编辑器，例如 SpriteSheet Editors（精灵序列图编辑器）、Particle Editors（粒子编辑器）、Font Editors（字体编辑器）、Tilemap Editors（瓦片地图编辑器）。

CocosStudio 这款工具集套件于 2012 年发布，它是 Cocos2d-x 团队官方推出的游戏开发工具。CocoStudio 根据开发团队自己在游戏开发中的经验，为移动游戏开发者和团队定做了这么一套集成工具，用意在降低游戏开发的门槛，提高开发效率；当然最主要还是为了 Cocos2d-x 占领游戏引擎市场。

如何选择引擎？

今天我介绍了这五款引擎，在开发的过程中，究竟该如何选择呢？如果接触游戏开发不久的话，你肯定还是会有点懵。 其实只要有选择，就有取舍，你只要明确自己的需求，然后结合自己的需求和引擎自身的特性，来对比选择就可以了。 这里我把这五种引擎的一些特点，总结了一下，列了一个表格。你在选择的时候，可以作为参考。

首先，如果我们只是编写 2D 游戏，那么 Cocos 系列一定符合你的预期，而且能方便地制作出跨平台的游戏。

如果我们编写的是 3D 游戏，如果是商业购买多话，那么 Unity 和虚幻一定是不错的选择。在财力足够的情况下，选择这两个大厂级别的游戏引擎，一定会让你满意。

但是如果你需要编写休闲的、规模不大的游戏，那么 Corona、App Game Kit 就可以用。

小结

我们总结一下今天的内容。

首先，移动平台的游戏引擎有一个最大的特点，游戏引擎已经逐渐演变成跨平台能力的游戏引擎，如果不这么做，很块就会被别的竞争对手所淘汰，我们先前讲到的 HTML5 游戏引擎，都可以编译出不同的跨平台的目标代码。

其次，移动游戏引擎，要挑选适合自己的才行，引擎规模有大有小，做出来的游戏质量也都不同。游戏开发大厂，可以选择虚幻引擎；中等公司可以选择 Unity；个人团队可以选择 Cocos2d 或者 App Game Kit；个人开发者或者轻量级开发可以选择 Corona SDK。

最后，给你留一个小思考题吧。

如果用移动平台的游戏引擎编写移动应用，是否可以实现呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第25讲 | 热点剖析（六）：AR和人工智能在游戏领域有哪些应用？

在 2014 年左右，VR、AR、人工智能开始逐渐火热起来，我也曾在它最火的时候深入接触过这几项技术。

如果你不是在这个领域从业的话，除了 VR 之外，有很多名词你可能都没有听说过，那我们就先来看一下这些名词是什么意思。通过这几个名词，我们来看一下它们究竟是如何在游戏领域应用的。

VR 是什么？

VR本身是Virual Reality的缩写，意思就是虚拟现实。VR 相对于 AR、MR 等技术来说，在技术上是最为简单的。简单地说，它只需要将 3D 画面做成可以通过 3D 眼镜呈现的内容即可，那么 VR 眼镜是怎么播放 VR 画面的呢？

VR 基于不同视觉场，在局部空间中营造出对不同画面的深度感知，在用户的大脑视觉系统中形成一个虚拟现实视场，而决定虚拟现实视场的主要因素是透镜，而非我们人眼的瞳孔。因此，为了得到更宽广的视场，制作者需要缩短用户眼球与透镜间的距离，或增加透镜的大小。

﻿

VR 眼镜的结构一般都是透镜加上屏幕成像的方式，透镜放置在眼前 2～3 厘米的地方，屏幕距离透镜 3～6 厘米，成像在眼前 25～50 厘米左右。

除此之外，在 VR 中，两个眼睛的立体视觉起的作用很大。用户双眼看到的不同图像是分别产生的，显示在不同的镜面上。用户带上 VR 眼镜后，一只眼睛只能看到奇数帧图像，另一只眼睛只能看到偶数帧图像，奇、偶帧之间的不同，也就因为视差产生了立体感。

人的眼睛有两方面用途，一个是判断左右距离，一个是判断前后深度距离。VR 眼镜相当于通过透镜，欺骗了你的眼睛，让你的眼睛认为自己是在真实的场景下工作，而有些人对于距离和景深比较敏感，就会有眩晕的副作用出现，毕竟是模拟真实的效果，而非完全真实的场景。

如何区分 AR 和 MR？

随着 VR 的兴起，AR 也逐渐进入了人们的视野。AR（Augmented Reality）就是增强现实。简单地描述，所谓增强现实，就是利用摄像头或者头盔，将现实世界外部影像结合内部 3D 计算的影像，将内容混合。

比如我们走在一个空旷的屋子里面，忽然在 AR 视觉效果里面，出现一个怪兽，向你扑过来，你需要拿起手柄，将手柄变成武器，将怪兽杀死，这里面就涉及 AR，它将现实中不存在的东西模拟出来，变成增强了效果的现实场景。

至于MR，它是混合现实（Mixed Reality）。混合现实的意义和 AR 非常相似，比如将现实混合在视觉效果中，这是两者都在做的事情。同时，AR 和 MR 中比较重要的一个技术是定位技术。

基于 2D 定位的是 AR 最成熟的技术。2D 定位是基于平面的识别和定位，比如利用手机 App 拍摄图书中的某个图片，然后虚拟物件就会出现在手机中的对应位置。这种 App 可以识别一幅图片或者图形，作为定位点，随后手机生成的虚拟物体会围绕这个点，融入到被拍摄的现实环境中。

3D 环境的动态实时解析是当前 AR 在技术中最活跃的功能，这其中不得不说的就是 SLAM 定位。所谓 SLAM 就是“同时定位与映射”（Simultaneously Localization And Mapping）。

AR 中的 SLAM 比其他别的领域中的难度大很多，因为移动端的硬件计算能力和资源比起其他硬件平台来说弱很多，所以难度比资源充足的硬件平台更大，所以有一些做法是利用网络交互，到主服务器去计算 SLAM 场景，但是网络交互又有实时性无法保证的问题。计算 SLAM 最好是本地计算，这样才能保证随时随地都能计算，而如果通过网络交互的话，你可能需要等好久才会拿到计算结果。

SLAM 的问题可以这么理解：打开摄像头，你要知道“我（摄像头）在哪”。然后进行定位，定位完后需要对周围的环境进行映射和虚拟环境的构建。然后一边走，一边对周边环境进行虚拟模型的构建，确定本身在所建地图中的定位。所以 AR SLAM 需要在走的过程中，一方面把所见到的地方连起来成模型图，另一方面找到自己在地图上的定位， 这就是 SLAM 的原理。

AR 和 MR 的原理大致一致，不一样的是，MR 是在 AR 的基础上将 3D 物件更精确地展现出来。那究竟该怎么区分 AR 和 MR 呢？有一个方法就是，看虚拟物件是否会跟着真实场景一起动。如果是的话，就是 MR；如果不是的话，就是 AR。

所以，MR 比 AR 更“先进”的地方就是，它看起来更像现实，所以是“混合现实”。当然 MR 还有更多事情可以做，比如它可以让你听到、感受到来自虚拟世界的感受，这是混合现实的另一层含义。

AR 在游戏开发中的应用

说完了这些名词，我们来看一下实际使用 AR 技术制作的游戏吧。

AR 最出名的就是前些年的《PokeMon》和 LBS 结合版本的 AR 游戏，这款游戏风靡一时，可以说是 AR 领域一次比较成功的尝试。它利用 SLAM 技术，结合相机拍摄的真实场景，让虚拟小宠物出现在真实场景下，然后进行 App 端的操作。

VR 就不用说了，现在 Steam 上拥有大量的 VR 游戏，比如前阵子非常火爆的《Beat Saber》，就是一款非常成功的 VR 音乐游戏。还有《刀剑神域》也是成功的 VR 游戏。

但是所谓的 VR 游戏，用到的 VR 头戴式设备，用的其实还是“MR”设备。因为所谓的混合现实头戴设备，就是将手中的手柄，眼里看到的内容进行混合，比如手柄变成剑或者手枪，这就是混合现实的一部分，所以使用 MR 设备也无可厚非。

所以在视觉效果的增强下，目前 VR 在游戏中的使用范围是最广的，其次是 AR 和 MR。

人工智能在游戏开发中的应用

至于人工智能，那也是最近比较火爆的一个行业风口。但是从理论讲，我觉得人工智能、AR、VR 并不会像 HTML5 技术、Flash 技术等一样变成全面颠覆产业的技术，而是会成为各个行业的辅助。

人工智能涉及的领域很多，每一个分支进行细分，都是一个研究课题和研究方向，比如导航当中的语音识别、手机的人脸识别、专家系统、大数据分析、机器学习等等。在游戏领域，能使用到人工智能的地方，或者说最能体现人工智能的地方，就是游戏中的 NPC。

NPC 是最简单，但也是最难做好的。如果做得简单，你随便写几行脚本语言，就可以完成一段 NPC，或者一整个城市 NPC 的编写。但是如果要写得好，利用简易的人工智能，就能将 NPC 做的很真实。

比如《老滚》中的 NPC，你可以看到他走向某个地方去和某人说话，或者你跟踪他一段时间会发现他会和森林里某些野兽战斗。在脚本语言里面，就是大量的事件绑定机制来实现人工智能的功能，比如 A 事件和 B 事件组合出现就可以决定某些 NPC 的行为，比如你可以利用类似代码来作为 NPC 的行为准则。

def foo():

    if npc.action == eat and player.action == talk:

        npc.action = talk

早几年，我曾经想做一款只有 NPC，没有玩家的游戏。你可以观察它们的生活起居，也就是所谓的“上帝模式”。这样的游戏要求游戏中的 NPC 足够智能，以至于这里面的人工智能需要做一些最基础的判断，类似“自动”的“模拟人生”。然而当我开始编写逻辑代码的时候，就发现事情并非想象得那么简单。

我们先抛开人工智能本身不说。我们首先从 NPC 入手，从编程角度讲，每个 NPC 一定有一个统一的身体和大脑的类，就像“模拟人生”一样。我们先要塑造这个 NPC 的躯壳，有了躯壳，才能填写数据进去，比如他累了要睡觉，饿了要吃饭，渴了要喝水，到达一定阀值就要去做某件事情。

单从一个 NPC 入手的框架都很难实现，就算真的完成了一个 NPC 的人工智能的框架，一个游戏中有这么多 NPC 需要交互和计算，一台普通的个人电脑估计很难运算过来，所以从这点来讲，这个游戏的编写计划就已经很难实现。

我们最常见的就是还有地图生成、人物运动控制等。

我们可以通过学习算法进行地图的随机生成，比如哪些是河流，河流旁边一定有树木，河流旁边不可能有飞机场等等；另一个领域是利用神经网络实现人物运动控制，你如果有兴趣也可以去这个网站看。

http://theorangeduck.com/page/phase-functioned-neural-networks-character-control

除此之外，也可以用在一些 2D 讲故事类的游戏中，开放式结局的游戏中，有了人工智能，我们就可以把开放式解决做得更生动和有趣，每个人物的结局都可以是不同的，也没有任何死的套路可言，但是从某方面讲，这个要实现起来也有一定的难度。

小结

今天我们讲了 VR、AR 的知识以及人工智能在游戏中的应用。

AR 和 VR 是对于人们在现实世界的一种增强技术，将现实和虚拟结合起来，做出更多优秀和好玩的游戏，而人工智能则能让人们在游戏中的体验能更好，比如智能的 NPC 行为，漂亮的随机地图的生成等等，这些技术的应用能使我们的游戏体验能变得更加丰富。

给你留个小问题，任天堂出的 3DS，是最早的一款裸眼 3D 设备，这种技术是怎么实现的呢？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第28讲 | 热点剖析（七）：谈谈微信小游戏的成功点

在定下这个专栏目录的时候，微信小游戏仅仅出现了唯一一款《跳一跳》。在写下这篇文章的时候，微信小游戏已经在小程序领域占据了绝大部分的江山。大量开发者涌入微信小游戏，为微信生态、为微信小程序的生态增添了无数可能性。作为游戏开发者，免不了要被挤入这场战斗。今天我们就来了解一下微信小程序和小游戏背后的技术，来看看它们成功的背后，有哪些技术上的支持和创新。

微信小游戏的技术创新

最早以前，微信小游戏有 3M 程序大小的限制，而到了现在，这个大小限制已经上升到了 8M，这使得微信小游戏从轻量级的休闲游戏，逐渐往中级、重量级的方向发展。游戏的品质和操作性也有了质的提升。

小游戏的体积为什么会有限制呢？因为体积较小的话，我们可以从服务器快速拉取资源数据来开启游戏。现在，微信平台本身已经可以做到 20M 的体积限制，只是微信并不愿意开放地太快，因为开发者有一个循序渐进的过程，微信平台本身的战略规划也有一个循序渐进的过程。

我在之前的热点剖析部分，分析过 HTML5 技术发展以及 HTML5 游戏引擎，其实微信小游戏、小程序，甚至微信本身都是基于 HTML5 技术的，而微信小游戏用的其实就是 HTML5 技术，只是在小游戏中运用的是 Canvas 2D 的 API 和 WebGL 1.0 的 API。

毕竟微信小游戏也属于小程序的范畴，所以我们先来看一下微信小程序。

微信小程序用的是基于 WebView 的技术。所谓的 WebView，你可以理解为在手机中内置一款高性能 WebKit 内核的浏览器，然后将这个浏览器核心封装成 SDK，供接口方调用。这个浏览器核心没有地址栏和导航栏，只是单纯的在软件需要的地方展示一个网页界面。

微信小程序本身分为页面的视图层和逻辑层。页面的视图层运用的是 WXML 和 WXSS，它们是两种腾讯修改过的 HTML 和 CSS 技术；而逻辑层则分为 Manager 和 API。视图层和逻辑层都呼叫了 JSBridge 技术，更下层则是一些网络服务、离线存储，再下层则是系统层。具体的结构，你可以看我画的这幅图。

微信小游戏脱胎于微信小程序。小游戏兼顾传统 HTML5 技术和小程序技术，但是小游戏却没有用到 WebView 技术，而是修改了 HTML5 规范的一些接口内容，成为腾讯自己的内容。也就是说，同一款游戏，如果要在微信小游戏和普通网站都能运行，需要编写两套代码。

那么你或许就要问了，为什么微信要自己开发一套 Web 体系而不用 Web 本身的标准体系呢，这样不是增加了前端程序员的工作量吗？如果说都是一套 Web 体系的话，大家不就可以皆大欢喜，到这里可以用，移植到那儿也可以用。

同样的问题，我们是不是可以这么问，为什么苹果公司要自己研发 iOS 系统，用最早最成熟的塞班系统不是挺好，诺基亚用得也挺不错。为什么谷歌要开发一套 Go 语言，现成的 Java、Python 不也挺好，都挺成熟，为什么一定要开发新的东西，让工程师入坑呢？

有很多人说，自己开发一套体系是因为微信下的棋很大，野心很大，你可以这么理解，我们今天从技术本身来看看，事情是什么样的。

我们从以下几个方面来看为什么微信要自己定义一套体系。

可以自定义 Web 标准。为什么要自定义 Web 标准呢？我们从结果来看，自然是为了提升用户体验。而从技术层面讲，这和 Web 兼容性有关。Web 标准本身是个庞大的体系。所以如果既要全部兼容 Web 体系又要按照自己的意愿去实现功能，这是很难做到的事情。比如，如果微信本身的小程序浏览器会重塑一套渲染规则，比如播放视频的时候自动屏蔽广告、按钮默认变成椭圆形等等，因此，自定义 Web 标准，可以去做更多的事情。

可以自定义开发标准。微信扔掉了 Web 兼容性以及标准 HTML5 的内容之后，就开始自定义开发标准了，所以微信强制要求开发者按照某种编码规则来编写代码，从而解决了在普通 Web 编程中“如果不用某种规则来编码，就会出现兼容性问题”的难题，这样，就从源头上解决了这个问题。事实上，这也是“强制约束开发者写出素质较高的代码”。

可以有比 HTML5 更强的功能。完全兼容标准 HTML5 的话，并非不可能，如果你熟悉前端开发的话，就会知道这个坑会有多大。因为首先 HTML5 不具备很多功能，比如获取手机设备信息、获取手机罗盘、地图定位等等。但如果用自定义的体系，加上从微信作为 App 本身具有的底层获取功能，就完全可以做到了。

防止刷流量、刷广告。在防止垃圾 HTML5 页面出现的这件事情上，微信做了大量的工作，比如你应该经常可以看到的，如果出现单纯的 IP 地址的页面，微信就给出提示，询问你是否跳转，或者提示你可以举报诱导分享等等，当然这都不能完全避免垃圾 HTML5 页面出现在微信生态下，所以在小程序自定义规则的情况下，你只能按照定义规则来开发，如果想钻空子，最后小程序和小游戏的上线还有一道人工审核的关卡，所以想要出现垃圾 HTML5 页面的情况几乎是不可能出现的。

方便后续优化。由于微信自己那一套体系是高层次抽象层，所以微信小程序团队可以在用户完全没有感知的情况下进行底层优化，而上层不用修改任何代码就可以了。

所以说，微信小游戏其实是基于 HTM5 技术，并在此之上，充分结合自己的需求和产品特性，添加了自己的创新。

微信小游戏成功的原因

微信小游戏建立在微信本体上，因此微信能获取到的移动端的底层功能小游戏基本都能一并获取，比如网络连接、内部存储等等，而 HTML5 做不到这点。微信可以获取底层平台的接口且并不需要授权，因为微信安装上去后已经获取了手机的权限，小游戏想要或者一部分权限是很容易的事情，相比 HTML5 游戏要从浏览器获取权限那就是很麻烦的一件事情。

其次，因为微信本身作为流量入口，对于小游戏的传播是一个极大的便利，独立开发者或者中小游戏开发公司，如果前期没有推广资源和推广渠道，那么通过微信本身这个巨大的流量入口，就可以获取相比传统 HTML5 游戏更好的效果。

除此之外，传统 HTML5 小游戏可以包装成 App 的外壳做成 App 的形式供人下载，因此又多了一个获客途径。到了后期，苹果公司加大了对于 HTML5 应用和游戏包装成 App 这种形式的审核力度，加上网页机制的运行效率限制，HTML5 套壳程序比原生应用的体验感也更差。

传统 HTML5 游戏发布之后，如果需要更新，则需要重新上传一次网页代码。如果用户端还需要重新刷新网页，甚至清除浏览器缓存等复杂的操作，如果做成 App 套壳，那就更需要在各个渠道市场上传一遍程序，这在效率上就慢了一截。而微信作为平台本身，更新游戏后，你重新获取只需要上传到微信平台就可以了，获取新游戏，刷新网页缓存，微信一并帮你做了。

传统 HTML5 游戏的广告接入，是非常麻烦的一件事情，你需要去和广告商去做分成机制、对接广告接口等等。如果是微信小游戏，你只需要对接微信自己的广告渠道就可以，可以说是一键就能对接几种广告机制，广告分成也是透明的、公开的。

微信平台本身拥有打击拷贝游戏的能力，如果有一款游戏被抄袭了，你可以投诉，如果发现属实，微信就会将抄袭的游戏下架，这样一来，就保护了原创游戏，激发广大开发者创作出更好的游戏，给微信带来更好的游戏生态。当然这里所谓的原创机制，是指的微信小游戏本身生态下的原创，因为作为成熟的游戏生态来讲，已经基本不存在狭义上的“原创”这两个字了。

小结

这节内容差不多了，我们来总结一下。我和你讲了这样几个内容。

微信小游戏、小程序都是基于 HTML5 技术的，而微信小游戏的技术就是 HTML5 技术，只是在小游戏中运用的框架并不是普通的 HTML5 的框架。

微信小程序用的是基于 WebView 的技术；小游戏却没有用到 WebView 技术，而是修改了 HTML5 规范的一些接口内容，成为腾讯自己的内容。

微信平台之所以要自定义自己的一套体系，比如 Web 标准、开发标准等等，是因为可以结合微信本身的特性，在此基础上制作出更契合腾讯生态的产品，一句话就是，为了方便自己开发、优化和管理。

微信小游戏基于微信的底层获取功能、广告接入优势和打击拷贝的能力，塑造了一个更好的微信小程序生态。

延伸阅读

腾讯微信高级工程师、后端高级工程师·邹伟，从技术层面分析，利用微信的开放能力《如何开发一款小游戏》。

腾讯游戏云资深架构师余国良的演讲《微信小游戏的架构设计与开发》。

Layabox 合伙人李明，从应用角度讲述《LayaAir 引擎一键发布微信小游戏的技术分享》。

希望这些分享对你有帮助。最后，给你留一个小问题吧。

迄今为止，你最喜欢玩的一款微信小游戏是什么？它吸引你的点是什么？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第31讲 | 热点剖析（八）：谈谈移动游戏的未来发展

随着芯片纳米工艺技术不断提升，我们的电子设备变得越来越小，而随着移动设备的普及，移动游戏一定是未来游戏行业发展的重头，未来游戏玩家将会花更多的时间在移动端的游戏上，所以我们今天就来探讨一下移动游戏的未来发展。

现阶段移动游戏是什么样？

在展望未来之前，我们先来看看现阶段的移动游戏是什么样的。

首先，移动游戏比较轻量级，一局游戏时间快，能很快出结果。像竞技类游戏的一局时间一般不会超过 30 分钟。由于手机屏幕和操作限制，没有游戏机手柄或者电脑鼠标键盘之类的方便操作，所以移动端的游戏操作手法极尽简化，所以需要尽量突出让人第一眼就能耳目一新的画面效果。

各大平台级的 App 都推出了不同的“小程序”技术，只是名称叫法各有不同，除了叫“小程序”，还有“快应用”“轻应用”的说法等等。在这样的聚合平台下，重量级原生游戏已经逐渐减少，转而都去了聚合平台下，比如微信小游戏。

而 HTML5 游戏依然有人在做，只是变成了大中型电脑网页游戏的形式，以及营销推广渠道的一种方法。这其实和以前 Flash 的路径一样。如果现在有人做 HTML5 游戏的话，更多的可能会往微信端、支付宝端等平台级应用上的小程序方向走。

原生移动端游戏往高操控性、高画质、高可玩性这几个方向走，人群更接近核心玩家，更多碎片时间的游戏内容将往 HTML5、微信小游戏等方向走。

什么是移动游戏发展的最大掣肘？

要思考移动端游戏的发展，不得不提硬件的发展。没有硬件的发展，移动游戏始终是这样几种形式。而在硬件问题还不能解决的情况下，移动游戏有哪些路可以走呢？

首先，往更大型的方向发展，逐步取代电脑上一些中等、小型的游戏。手游的硬件发展带动质量的发展，变得更像电脑上的游戏，3D、2D 的精美的游戏将更多出现在市场上。而重度游戏的发展，将会有迎合重度硬核玩家的游戏加入，包括周边设备的联动推出，比如手柄、键盘等等。

AR、VR 等功能游戏的推出，就算是在现在，这几种类型也是一直在市场上推出，在不久的将来，手机硬件的提升会让这类游戏变得越来也多，裸眼 3D 游戏也会变得越来越多。

而更多利用手机硬件特性的游戏将会出现，比如随着苹果前置感应头的出现，前置感应的人体体感游戏，或者脸部感应游戏也会出现，这些其实都是以前 Wii 时代体感游戏的缩小版本。

手机游戏与电视或者电脑游戏联合互动。随着任天堂 Switch 的发布，多屏互动，小屏和大屏游戏联合也会是将来移动游戏的一个方向。如果有新的巨头加入手机竞争，比如微软，那么手机和电视机连接玩耍，或者安卓系统的电视盒子和手机端联动，也将会是一个发展的趋势。

其次，人工智能将出现更好的发展，泛游戏类型和应用，以及游戏内的人工智能，结合 AR、VR 等的游戏也将出现，游戏将会给玩家一个更好的体验。

移动端的硬件发展，除了平板市场之外，手机市场的硬件发展并不是特别快，而平板市场则是下一个移动端硬件发展的风口，比如安卓的拥有独立显卡的平板，这就给深度学习、人工智能提供了良好的运算平台以及商业接口。毕竟高质量的移动游戏需要在更高级的硬件设备上才能呈现。

移动设备未来会有什么样的变化？

在这个基础上，我们的移动设备会有什么变化呢？

移动设备会随着硬件的发展变得越来也小。虽然越来也小，但是计算能力已经遇到了摩尔定律的瓶颈，所以，虽然硬件可以越做越小，但是 CPU 的计算能力却并不见得能变得更为强大。而可能是，今年的普通移动设备 A，在明年会缩小，变成 B。B 的计算能力和 A 一样，只是变小了，而明年做成普通移动设备的 C，计算能力会比 B 更强大一点，然后后年 C 也会缩小，变成 D，D 变成和 C 一样的计算能力，这样一代一代往下发展。

由于移动设备变得越来越小，那么小型移动设备上的游戏也会变得可观，比如智能手表上裸眼 3D 的小游戏，甚至就像科幻片里面，手表上的游戏投影在手臂上方，这种模式不再变得不可能。

我们之前说过，谷歌眼镜并不成功，并不是应用少的问题，而是场景的问题。试问谁会戴着眼镜到处跑，为了利用眼镜识别各种物件，而非常不自然地盯着某个物体看半天？其次，技术也不是特别成熟，上传数据到分辨并且得到信息是有时间差的，无法做到完美实时。

我们不妨做一个实验，打开手机摄像头，拍摄视频，你会看到视频中的影像比现实中大致慢半秒左右，这是由于视频抓取后呈现，流的压缩等各种手机端的计算占据了一定的时间，所以哪怕本地都无法做到完美实时，又如何将网络端交互变成实时的呢？

所以未来移动设备变得小的同时，做的应用和游戏也应该考虑到互动效果和硬件的制约因素。

移动设备并不只是指手机和 PAD，眼镜、手表、车载导航之类的，也可以算是移动设备，这样很多游戏都可以放在不同的移动平台下，比如车载导航内的小游戏，比如手环上的简单游戏，都是发展的方向。

而 AR 和 VR，体感游戏依然是移动端游戏的一个卖点，因为可以随时随地玩游戏，所以结合现实世界和虚拟世界类型的游戏将会越来越多，再结合地图定位，能玩转的游戏类型也会越来越多，如果能结合现实中的头戴式设备，比如类似谷歌眼镜的成熟产品，那么结合 AR VR 等的应用，将会变得非常生动和有意思。

前些年就有 PokeMon 之类的 AR 游戏风靡了一阵子，但是这类游戏，最主要是是结合了一个强大的 IP 之上，其次是新鲜感，当时去新鲜感后，如何让玩家保持继续玩下去的动力，就是游戏公司需要做的事情了。

制约移动设备发展的，并不是技术本身，或者将芯片做小的能力，具体来说就是电池， 不仅仅是移动设备，事实上各种设备上的制约因素都是电池，包括笔记本、电动汽车、无人飞机等等，只有解决了电源问题，科技才可能有质的飞跃。

如果我们有了大容量的电池且不增加额外电池尺寸开销的情况下，移动端游戏会有怎样的发展呢？

如果电池得到了发展，移动端游戏将会出现和 PC 端游戏一样的效果，比如玩 3D 游戏再也不需要半小时一个小时充一次电了，玩再久的游戏都只要两天或者更多天，才充一次电，这在现在是无法想象的，比如智能手表上的游戏，不可能一只手表玩了游戏后，一天充一次电，这实在是非常尴尬的事情。

小结

我们来总结一下今天的内容。

移动端游戏将往更加精品、高画质、高品位的游戏转型。3D、VR、AR 等游戏也会层出不穷，移动端和大屏幕之间的阻隔也会逐渐模糊，两边都可以互相游戏。

另外，如果摆脱了电池的掣肘，那么移动端游戏将往 PC 端游戏的方向发展，更高质量的 3D 游戏也不用担心电池耗尽而半途无法玩了。

给你留个小问题吧。

如果让你来开开脑洞，思考下移动游戏的未来发展方向，你认为移动游戏会怎么走下去呢？移动游戏的未来是往高品质高画质走下去，还是往真实与虚拟结合走下去呢？还是有其他的方向？

欢迎留言说出你的看法。我在下一节的挑战中等你！

第34讲 | 热点剖析（九）：谈谈独立开发者的未来发展

我曾经看过这么一句话，游戏开发是一门异常复杂的综合性艺术创作。作为个人独立开发者，一个人常常需要包揽所有职责，策划、美工、原画，甚至推广。

说到推广，不得不提的就是发布平台。比如，我第一时间想到的就是 Steam，这个由专门开发电子游戏的美国 Valve 公司开发的 PC 游戏界最大的发行平台。对于 Steam，我想你应该非常熟悉了，我这里就不多介绍了。

但是，现在 Steam 的日子也并“不好过”，更别说独立开发者本身了。

以前，一个人在 DOS 下就可以编写出一款小游戏，一支三五人的小团队就可以做出一款游戏，现在来说几乎是不可能的。现代游戏需要大量人力物力去做一系列的基础工作，美术、关卡、引擎等等都是必不可少，甚至可能成为影响一款游戏成功与否的必要因素。即便你有特别优秀的创意，有非常突出的画面和故事情节，但是依旧很容易就被淹没在游戏海洋中。

为什么现在的游戏制作的难度越来越大？请注意是制作，不是开发，制作游戏不仅仅包含开发，还包含创意、资源调配、产品规划等等。不少人跟我讨论过这个问题，我自己从业的这么多年也一直在思考这个问题。

其实，并不是因为游戏开发的难度比先前更难，相反，现在开发游戏的难度，比 DOS 时代小多了。但是，游戏的制作却没有变得更简单。这是为什么呢？

DOS 时代开发游戏，有一个好的 idea 后，最先要考虑的是 DOS 16 位的内存限制，因为很可能一不小心就崩溃了。到了 DOS 游戏后期时代，有了 DOS4GW 这种突破内存限制的“32 位保护模式”的方式开发游戏。虽然可以几乎不用考虑内存限制，但是难度依然很高，要从头绘制鼠标箭头、点击事件。所有玩家在界面上能看到的和不能看到的，都要手工一行一行敲出来。

现在不同了，有了游戏引擎，只需要往引擎里填写需要的内容就可以了。很多情况下，鼠标箭头、UI 点击事件，甚至图像载入，绘制这些底层操作都不需要你关心，游戏引擎一揽子都包装好了。

我曾经看过 Steam 游戏 Descenders 的创意总监 Mike Rose 的一篇采访，大致是说，现在在 Steam 上上架的游戏，利润大部分都被头部游戏赚取。

一个重要的原因是，Steam 上的游戏越来越多，就像苹果商店，如果用户不搜索，基本不可能会有新游戏的展示和宣传位置。即便 Mike Rose 提议最好能整理游戏列表，能下架一系列没人买的僵尸游戏，这样一来就能给更多的新游戏以展示自己的机会，但是，Steam 没有这么做或者说已经大势已去来不及挽回。所以，越来越多的开发者开始转向了主机游戏。

所以以目前的形式看，对于独立开发，我其实更看好主机游戏。

加上 Switch、XBOX 等主机游戏相继都推出了自己的独立游戏计划，吸引了很多 Steam 平台的独立开发者进入。尽管主机游戏并非是独立游戏开发者的第一选择，然而 XBOX 等的开放态度以及 Steam 平台的利益不平等，还是导致很多开发者流向了主机游戏。

我们开发的游戏，最常开发的分别为 PC 游戏、原生 App 游戏、HTML5 游戏（不含 PC 的 MMO 游戏）、微信小游戏。

在微信小游戏的生态中，你只需要注册账号，并且使用游戏引擎导出到微信，并且申请游戏资质就可以上线运营了。

关于 PC 游戏，Steam 依旧是最大的 PC 游戏商店平台。原生 App 游戏，苹果端就不说了，没有别的选择。而安卓端，国内有至少 30 个应用市场可以上架，所以走一圈上架流程、接入到平台 API 就是一个相当麻烦的过程。

如果要做原生安卓游戏，把主要精力放在几个大平台上就行了。HTML5 游戏，也有可以选择的平台，比如比较出名的 4399、4177 等。

从收入情况看，苹果 App 的收费情况比安卓端更好，一个是用户付费习惯早已经在苹果端养成。另一个是苹果端游戏有审核（谷歌市场也有审核，但是没有苹果那么严格），所以游戏质量都能得到保证。

PC 端的 Steam 市场也有较好的收费机制。而微信小游戏则是统一的广告、支付等渠道，用起来也比较方便。所以独立开发者可以选择对自己较为有利的方案进行针对性开发和推广，比如选择微信小游戏，或者苹果端游戏，如果自己的开发成本不是特别充裕，这是一个相对稳妥的选择。

不管是独立游戏开发还是别的，我个人觉得现在的游戏一定要有这样几个特点：轻、快、多。

首先，游戏体量要够轻，够小，打开手机，打开电脑翻开一个网页就能玩，玩完就走。

其次，游戏体验要快速。一盘游戏不能时间太久，一局超过十分钟就会逐渐变成重度游戏，除非有很强的吸引力，否则时间太久的游戏大多数人都不愿意尝试。

游戏版本一定要不停迭代。从玩法、画面，到平衡性，这样才会让玩家保持新鲜感，不停地被游戏粘滞住。

小结

这节我和你聊了独立开发者的未来发展。

DOS 时代的时候，一个人或者一个小团队就可以做出一款游戏，但是这种时代已经过去了。游戏开发难度变小了，但是游戏制作难度变得越来越大了。

尽管 Steam 依旧是目前最大的发布平台，但是由于它本身没有对上架的游戏进行分类、整理，有选择的展示。对独立开发者其实是不利的。加上 Switch、XBOX 等主机游戏相继都推出了自己的独立游戏计划，我其实更看好主机游戏的发展。

其实不管基于什么平台，把游戏本身做好，才是最重要的。

你想成为一个独立游戏开发者吗？你想做出一款什么样的游戏？

欢迎留言说出你的看法。我在下一节的挑战中等你！

复习课 | 带你梳理客户端开发的三个重点

我们用了将近两个月的时间，讲述了游戏客户端的开发流程和代码编写，今天我来带你总结并且回顾一下。

我这里列了一些编写游戏客户端你必须要掌握的知识和注意的事项。希望通过梳理这些知识，能让你对学过的内容有一个全盘的认识和更加深入的理解。

重点一：客户端开发中 4 个重要方法

首先，我们来看，设置前后遮挡效果时要用到的一个虚拟概念，Z 值。 2D 游戏中其实是不存在这个概念的，因为 Z 值是高度概念，一般只存在于 3D 游戏当中。2D 游戏的坐标轴，拥有 X 和 Y 两种位置。X 轴是横向坐标，Y 轴是纵向坐标。在斜 45 度角的游戏，或者需要经常改变前后遮挡位置游戏中，往往才需要加入这个概念。在 2D 开发中，用好了 Z 值，能够帮你计算 2D 游戏中图片的前后位置，在游戏中设置前后遮挡效果的时候就会更加方便。

其次，在 2D 游戏中制作碰撞检测的时候，可以使用图片相交检测的方法，来替代物理引擎。像打飞机游戏这样非常简单的 2D 游戏，是完全不需要使用物理引擎的，我们使用图片相交检测的方法，直接写对应的代码就可以。

什么时候才要使用物理引擎呢？这取决于你的代码量和程序编写的复杂程度。

如果碰撞代码非常复杂，占据整个工程量的 1/5~1/4，那就可以直接使用物理引擎；如果仅仅用一两个函数就可以搞定，那就可以自己模拟物理碰撞效果来写相应的代码。毕竟 2D 游戏不比 3D 游戏，对“拟真”的要求没有那么高。而图片相交检测这样的方法易于编写，且效果也可以满足需求。当然，3D 游戏中，往往需要高精度地模拟真实世界的碰撞，这个时候就直接使用物理引擎就好了。

再次，在游戏开发中，保持低耦合度，能够提高你的开发效率，减少你的工作量。 如果一款游戏引擎没有提供 UI 模块的话，那需要你编写一套 UI 模块或者 UI 库。所谓单独的 UI 模块，指的是仍然使用游戏引擎作为基础编写代码，但是却独立于游戏本身的逻辑之外，将之抽象出 UI 模块和 UI 库。如果 UI 模块直接融合在游戏里，虽然玩家看起来是一样的效果，但是在代码层面，耦合度却更高，可复用性和可修改性就变得更低，代码质量就更差。

同样，在游戏开发中，很多模块都可以提取出来。比如鼠标和键盘的检测，单独提取出来做，变成相应的模块和库；很多的内容都可以进行抽象和独立，有了抽象和独立的模块，这样可以保证游戏版本的迭代不出现问题，你甚至可以拿这一套独立的抽象模块（比如 UI 模块、鼠标键盘模块等等），继续来做第二款游戏，因为里面很多代码都可以复用。

低耦合度，是游戏开发，甚至编程中都非常重要的思想。我这里推荐一本书《设计模式》。这里面对于耦合度的几种模式有详细的讲解。比如工厂模式、代理模式等等，但是不管用什么模式，低耦合度始终是编程中追求的一种代码方式。

所谓低耦合度，简单理解，就是将功能抽象出来，并且写成一组函数或者类（我们简称为接口）。这些接口有传入传出的参数或者返回值，可以通过外界代码传给接口，接口通过计算，将计算后的结果返回给外部代码。这些接口和别的功能关联性不大，替换了接口，就仅仅替换了算法或者内部实现而已，但是核心功能是一样的。

最后，我想来说一下游戏逻辑。将游戏逻辑放在脚本语言里去写，迭代版本的时候可以不更新主程序，只更新脚本。（如果不放在脚本语言里，就直接写在硬代码里面。）我们在使用引擎和原生语言开发游戏的时候，只保持核心内容，其他游戏逻辑、游戏配置，特别是在游戏中可活动、可配置、可调节的部分，都提取出来，放到脚本语言里去编写。

所谓的主程序，在 Windows 下就是 EXE 文件，一般情况下，更新 EXE 执行文件会导致一系列的更新，比如 DLL 动态链接库或者某些资源也会更新，所以更新资源包会变得很大。如果纯粹更新脚本文件，那就是几个脚本程序而已。当然这个也要看具体的需求，并不是说一定不更新主程序，如果有迫切需求更新主程序，也是必须要更新的。

除此之外，关于客户端其他部分的流程和要注意的细节，我放在一起梳理一下。如果有哪些还不是很清楚，你可以回到对应的文章去复习一下。

首先，一款成熟的引擎会包含各种编辑器和工具。如果游戏引擎没有提供相应工具，你就必须根据要做的游戏项目，编写方便策划和美术使用的编辑器，包括地图编辑器、关卡编辑器、场景编辑器等等。

游戏的网络部分，要尽量保持网络连接和数据传输的安全性，游戏的网络协议可以迭代更新，网络传输的内容和逻辑也可以放在游戏脚本里面去做。

游戏的音乐部分，如果引擎不提供，建议使用成熟的音乐库。游戏要合理使用多线程技术，才能保证流畅性。游戏资源打包的包裹格式，如果没有现成的方案，可以使用压缩包的开发库编写，成熟稳定可靠。

每个平台的游戏机制都不相同，比如在 HTML5 游戏里面，它使用的是事件机制，并非普通的循环。

重点二：游戏开发的 3 大模块

这里我按照游戏开发中的三大模块策划、程序、美术，画了一个图。

开发游戏的时候，策划要先行，先出策划稿、UI 图、页面迁徙图等等，然后美术开始跟上，紧接着根据策划案开始制作 UI、原画、人物、场景等等。程序员开始制作各种编辑器和周边工具（如果引擎没有提供的话），随后美术开始制作地图和各种关卡，程序员开始编写 demo。随后就开始内部测试和运行，总结问题和经验，修正和补缺。最后，开始第一个正式版本的开发。正式版本的开发流程和 demo 版本是一样的，我在第 5 讲的时候已经详细说过 demo 的作用。

在专栏中，我用 Pygame 来做教学示例，有很多人在留言里问，我想用别的语言编写游戏难道不可以吗？当然是可以的，Pygame 只是利用 Python+SDL 封装的游戏库，而我只是以 Pygame 为载体，告诉你编写 2D 游戏引擎的一些核心知识是怎样的。有了核心知识，了解了流程，语言层面就简单地多了。举一反三，换作任何语言、任何引擎都不会有问题了。

在这之后，如果你想要进一步学习，或者从事相关的开发工作，你要自己多练习、多思考。如果是在公司里，那就多上手去做项目。游戏的开发流程就是这么回事，要做的东西也就那么多，但是不同的项目，不同种类的游戏，就会有不同的问题出现。每次遇到问题，多换几种思路去思考，多去了解策划、运营的想法，而不要只是在“不断填坑”。

重点三：如何写出过硬的代码？

其实，说了这么多，我现在要说我认为最重要的一点。 无论你是做游戏开发或者别的开发，你都要有过硬的代码基础。 一款游戏，有再好的引擎，再好的策划，如果代码写得不好，也会影响游戏体验，毕竟引擎只是在底层带动游戏的运行，在上层业务上，还是要依赖代码的牢靠和稳固。

我从 96 年写下第一行代码开始，走过许多的弯路，也经历过许多挫折。在写代码方面，我或许可以带给你一些直接的经验和警示之谈。

十几年前，当我刚还是个入门级程序员的时候，经历过一个项目，我负责做一个共享内存的接口。事实上写这个接口很简单，用不了多少代码就能完成，但当时我正在学习 C++ 最新的 Loki 库（后来加入了 Boost），觉得里面的模板技巧简直是无敌，所以我在共享内存接口之上又封装了类似 Loki 模板的方法，最终导致代码过于复杂，很难控制和管理，项目失败。

因此，如果代码不是特别复杂，你大可以直接写功能，不需要从类、框架开始写起，因为这会造成我所犯的那个错误，就是“过度设计”。举个简单的例子，如果一个“hello word”，却有 500 行代码，这很明显就是“过度设计”。

所以，我的多年的习惯是，如果一个正常的软件、游戏，或者模块、接口，我都会将普通的功能、重复性高的功能，抽象出来，做成一个模块，特殊的功能、无法重复的，再做成一个模块。代码尽可能不要长，长的代码分割成几部分，放在几个文件里，阅读起来也会很清晰。能写简洁的绝对不要花里胡哨，能用正常技巧的绝对不用小聪明，实在没办法才用小聪明。

对外提供接口要清晰、易于阅读，逻辑要一条一条理顺。有异常，就写异常处理代码；有错误，就处理错误代码；没有问题，再运行下一条。

好记性不如烂笔头，写之前先想明白思路，将自己即将要写的代码在脑子里跑一遍，然后将思路记录下来，记在笔记本、电脑、手机哪里都可以。这样写出的代码比直接上手写，速度上会慢很多，但是出错概率极低。直接写你可能一下子就写完，但还要修修补补，调试一整天，事实上，这比思考虑好了再写还要慢。

最后，还是那句话，不管是编写大型游戏还是一款小游戏，还是做任何项目，都离不开踏踏实实把代码写好。毕竟这是基础。如果你总想着什么多少天速成法，快速入门、快速编写出一款脍炙人口的游戏，用户上几百万，这是不存在的。游戏开发，应该说任何程序开发，都不适合任何想要走捷径的人。真正聪明的人，都应该知道学习本来也没有什么捷径。

经过这两个月的学习，不知道你对游戏开发有没有更深入的认识？关于游戏开发或者编程，你还有什么想要了解的，你可以继续在留言区写下想法。

我在后面的挑战中继续等待你的到来！

课后阅读 | 游戏开发工程师学习路径（上）

整个开发流程和内容我已经讲完了。你现在已经不是一个完全零基础的小白了。作为一个满心想要进入游戏开发行业的人，或者想要在游戏行业深耕，你可能要问了，后续的路，该怎么走呢？今天，我就来和你聊一聊这个内容。

我会通过这份看似很长的必读书单，先给你搭建一个学习路径，然后从底层知识和游戏开发这两方面分别讲讲，想成为游戏开发工程师，应该掌握哪些知识。

我在专栏里讲了很多具体的操作，作为初学者，把这些彻底掌握，其实已经差不多了。但是想要进阶，成为一名真正的游戏开发工程师，你还有很长的路要走。从业二十余年，我见过不少的年轻人，抱着对游戏的一腔热血，一头扎进游戏开发，想着自己有一天也能开发出一款爆款游戏，从此“走上人生巅峰”。

然而走着走着，发现要学的东西太多，无从下手，直接放弃；或者学了之后，因为一直写不出什么项目，也放弃了。就好像去少林寺学武功的人，方丈让他先捆着沙包登山、挑水，他以为这并不是学武术，因此就放弃了。

事实上游戏开发，甚至编程都是这样，你首先要学好基础知识，有了扎实的基础，才可能去做更多的事。道理我想你都懂，我就不多啰嗦啦。我们进入正题。

还记得，在专栏最开始的时候，我专门写过游戏的分类，按呈现效果，我们可以粗略分为 2D 游戏和 3D 游戏；按硬件，我们可以分为主机游戏、电脑游戏、移动游戏和电视机游戏。但是，如果从真正的开发角度，我会把游戏开发分为原生游戏开发和网页游戏开发。

为什么这么分呢？首先，我这个分类中，并没有把网络服务器端的内容放进来，而只是针对游戏客户端部分。我将它分为原生客户端（PC、手机非 HTML5 原生 App）和网页客户端（PC 端页游、HTML5 游戏、手机端 HTML5 游戏、微信小游戏）。

我们先从原生客户端讲起。

在专栏里，我通过 Pygame 做了示例，但是碍于你可能不仅是个游戏开发新手，还可能是个编程新手，而我们专栏的侧重点在于帮你认识、了解游戏开发流程，知道游戏开发是怎么一回事，对于底层的知识，我其实没有过多的去介绍。

我本身是写服务器端的，对于底层知识研究了很多，所以，我深知底层知识对于一个真正想要在游戏开发之路走得长远的人的重要性。学习的过程，我没有办法替你做，但是我想应该有那么一些经验还可以跟你讲一讲，不至于你在一些可以避免走弯路的地方，浪费掉许多时间。

开发原生客户端游戏需要非常系统的底层知识。这里的底层知识指的是计算机（移动设备）的硬件底层知识，比如内存、CPU、图形显卡等等。当然如果要学得全面，你还可以学习物理引擎、音频等等。

想走捷径，去学 C++ 吧！

如果你的学习时间并不充裕，或者非常迫切地想知道“底层是什么，底层做了点什么”，想相对快速地能学习到底层知识，那么我推荐你直接学习 C++。

首先从执行效率看，C++ 的执行效率仅次于汇编，约等于 C。如果要编写更底层的操作系统，因为语法糖太多，你不知道编译器在背后做了什么事情，C++ 语言将无法“直接”掌控底层设备。所以要写操作系统，需要对 C++ 编译器进行针对性改造。

我们再往上看，如果要编写更上层的应用，比如桌面应用、动态网页应用，这些是 C++ 经历过的“银弹”时代。90 年代到 2007 年左右，几乎电脑上所有应用都有 C++ 的身影，但是现在，桌面应用完全可以使用 C# 来编写。

网页端就更不用说了，PHP、JavaScript、Python 每一个都能比 C++ 做得更快、更完美。但是在游戏开发中，既需要执行速度，相对地，又需要开发速度，综合下来，只有 C++ 最适合。

所以，学习 C++，会让你对底层知识有了一个大致的了解，但是如果你没有更进阶地学习 C 或者汇编，那么你的知识面就停留在类、内存分配、对象等层面。

作为二十多年的 C++ 程序员，我这里有几个学习 C++ 的心得想跟你分享。

首先，C++ 和 C 语言并不是一门语言。它们之间的语法结构看起来虽然“相似”，但 C++ 相对于 C 更像是瑞士军刀和匕首之间的关系。它们都有刀的功能，而 C++ 除了刀的功能，还有更多的功能。

其次，C++ 是做减法而非加法。把 C++ 所有特性学完会耗费你极大的精力，注意是极大，不是大量。每一种特性背后都包裹着无数语法糖衣和编译器的“自动化”动作，使用 C++ 的人当中，有一部分人经常会“走火入魔”，专门研究一些奇技淫巧的技巧。

所以最好的方法是，不要执着于语言特性，因为两个不同的编译器，可能编译结果都会不同，比如一个会出错，一个会通过，但只要略作修改，就能运行。只要不影响具体最终软件的执行，就可以了。C++ 特性太多，选择一种或者两种特性来编写整个软件，才是发挥 C++ 最大功效的方法。

最后，大道至简，这对于整个产品、架构、编码，都是一句适用的箴言。当然对 C++ 也是如此，只有用最简略的方法来编写代码，才会让代码快速成型。C++ 比 C 多了太多快速搭建的语法功能。尽量利用它的优点（比如面向对象、模板等等），就能发挥出语言最大的特长。

走这个路径，基础更扎实！

如果你有较宽裕的时间学习编程，也希望学习到更多的底层知识，对计算机有一个全面的掌握和了解，那么我推荐你用以下的这个更全面的路径来学习。

以我的经验，我建议你先去学习一下 C 语言。为什么呢？因为 C 语言是汇编的封装，C 语言的一句语句，可以是汇编的几条指令，虽然每个 C 编译器最终生成的汇编指令可能不同，但大致方向是一致的。

其次，学习 C 语言能让你从基础的语言语法了解计算机编程是怎么回事。C 语言是 99.9% 的电子设备的基础语言（剩下的 0.01 是汇编）。

学习 C 语言有太多的入门书。在这里我推荐《C Prime Plus》，绝对的 C 语言入门经典教材。《C Programming Language》，读完 《C Prime Plus》之后再去读这本书，绝对会让你了解什么是高手所写的书。

看完了《C Programming Language》之后，你除了是 C 程序员之外，俨然是一个不错的 Linux 用户了。

学习完 C 语言后，你可以学习汇编语言。有了 C 语言的基础，对编程、地址操作、位操作这些基础，学习汇编就会相对容易。

估计看到汇编语言这几个字，很多人就望而却步了。但是，我想说，没有比汇编更适合学习底层知识的语言了。

毕竟在现实中，除非你编写的是芯片或者操作系统（现在编写芯片或者操作系统也大量使用 C 语言），只有占那 1% 的启动部分代码会使用汇编。这么看，使用汇编编程的人真的非常少，而且它本身也不适合编写大型项目。

你可能要问了，为什么你还推荐我从汇编开始学呢？学习汇编的意义在哪里？

我们先来看一段指令。

 pushl %ebp

 movl %esp,%ebp

 subl $8,%esp

在汇编里，这些指令就代表着 CPU 的指令操作。在编写中级、高级语言，需要调试代码的时候，如果没有调试信息，最终都会落到汇编语言这里。这时候，汇编语言就是你的最后一道防线，学习了汇编，你就能快速、深入地定位程序的问题所在。

所以，学习汇编的意义就是，你能在脑中完全掌握电脑的运行规律和运作逻辑，比如内存寻址操作、寄存器操作、加减操作、CPU 指令等等。

学习汇编有一本书，是绝对的入门级好书，能够把深奥枯燥的机器知识讲得生动活泼，那就是王爽的《汇编语言》。

学习了汇编语言，也会对你的 C 语言知识有一个本质上的巩固。所以结合 C 和汇编，指针不再是头疼的问题，因为你已经知道在汇编中的地址是如何操作的。

C 语言看起来就好像把汇编进行了一次朴素的包装，你不再需要强制记忆寄存器和指令，定义一个变量就知道汇编语言是怎么做的，这对于你的 C 语言来说，是有绝对帮助意义的。

一般人提倡的学习路径是由浅入深，如果你想一直在技术领域深耕下去，我建议你走一条由难到易的学习路径。

因为，越高级的编程语言越接近人类的思维方式，这种思维会固化你的编程思维，让你在之后的学习过程中，无法更深层次地理解底层机器语言的沟通方式，学习起来就会比较困难。先学习底层知识，这样你在后续的高级语言的学习中，就属于“降维打击”，学习起来就相对轻松和快速。

除此之外，如果你已经是一名程序员，你应该很能体会，你的工作将会越来越忙，几乎没有时间学习新的知识，而学习能力是一名程序员必备的基本能力。

接下来，你还是需要学 C++ 语言了。具体原因其实我刚才也说了，游戏引擎绝大部分使用 C++ 编写，也有一小部分是使用 C 语言或者汇编编写的，比如 Allegro。

如果你要学习 C++ 语言，学苑出版社出版的 HerbertSchildt 写的《C++ 从入门到精通》，这本书一定不能错过，它是我的 C++ 启蒙书。唯一可惜的是，这本书年头有些久了，我的书还没有找到，但是我至今对书的内容都记忆犹新。这本书后面的小习题，绝对能让你在学完这本书之后，对 C++ 了解的非常透彻和深刻。

另外，《深度探索C++ 对象模型》《C++ Programming Language》《C++ 语言的设计与演化》也是必看书籍。前一本能让你了解 C++ 对象模型，从底层了解 C++ 的机制。后两本出自 C++ 之父 Bjarne Stroustrup 之手。

从 C++ 开始设计到现在，为何会如此演化和设计，语言的背后又是怎样的故事，除了语言的基础知识，也会有一些小小的故事。另外，如果你的知识面和时间允许，我建议你最好去看《C++ Programming Language》的原版，因为只有原版是原汁原味，不会有被翻译偏差影响的。

现在，我们已经将一幢大楼的“地基”都搭建好了。

学完这些，要多久啊？

你可能会问了，从开始学习底层知识，到对知识有了解并且能顺利写出软件需要多久呢？因人而异，有人是计算机系的，有人是野路子，有人对计算机特别有天赋，有人虽然不聪明但是非常勤奋，每个人的学习曲线和学习路径以及最终学会的时间都是不一样的。

但是一般来说，要从 C 到汇编再到 C++，一直到能编写成熟的代码，平均下来需要 5～7 年的时间。我没有做过精细地统计，但是我想，按照大学四年的学习时间，从学习到实践，再加上毕业 1～3 年的实践深入，5～7 年的时间基本是差不多的。你觉得呢？

今天这些内容主要涉及游戏开发的底层知识。说起来很容易，做起来却一点都不容易。有了这些基本功，下一节，我会推荐一些游戏原理和机制相关的书籍。希望我的分享对你有帮助。

课后阅读 | 游戏开发工程师学习路径（下）

上一节，我讲了一些底层知识和编程语言。搭好了底子，接下来，你需要掌握一些游戏的常规原理和机制。这里我也为你准备了一份学习书单。

游戏设计

《游戏机制》，这是一本策划和程序员都可以看的书。在游戏设计中，游戏机制的设计，决定着游戏中的呈现效果，直接影响着玩家体验。而这本书就是教你如何打造出挑战丰富、乐趣十足、平衡良好的可玩性的游戏机制。它结合了很多实用案例，从不同角度介绍和阐述了游戏机制，介绍了用于构建和模拟游戏机制的 Machinations 工具。

《游戏设计的 100 个原理》做游戏开发的人，基本上自己都是非常喜欢游戏的，肯定玩过不少游戏。但是估计你对游戏设计背后的原理性的东西，了解的不会很多。这本书就是讲很多原理性的东西，让你从更专业的角度来看你以前玩过的游戏，毕竟游戏设计其实更像是一门哲学。

《Windows 游戏编程大师技巧》，这是一本经典书籍，属于游戏开发启蒙教材，它涉及了开发的基础知识和理论、开发技巧这些问题。有一点点汇编、C/C++ 语言基础，看这些就不会觉得特别难了。

2D 和 3D 编程

接下来，关于 2D 编程，我们可以看《Cocos2dX 游戏开发技术精解》，这是一本让你一下子就能学习 2D 引擎且直接上手的书籍，几乎是学习 2D 游戏必备。

《Unity 4.x 2D 游戏开发基础教程》是一本利用 Unity 编写 2D 游戏的教程书籍。另外有一本非常难买到的书《2D Graphics Programming for Games》，目前只有英文版，如果你能找到电子书也可以阅读一下。

毕竟 3D 游戏大行其道，但是它和 2D 游戏的路数又是非常不一样的。加上很多人上来就想做 3D 游戏，那关于 3D 开发，这几本书必看不可。

《3D 游戏编程大师技巧》《Unity 3.x 游戏开发实例》《3D 游戏开发大全》。第一本书可以让你很轻松地就了解 3D 游戏的编程基础知识，第二、三本书都是属于利用引擎来编写实例的，你可以在这些例子中获取更多的实战经验。

而《3D 游戏开发大全》以 Torque 引擎来作为切入点，让你看到更多的技术。比如，游戏建模技术、人工智能技术、玩家控制技术等等游戏开发的细节。看完这三本书你就会对 3D 游戏编程有一个本质的了解。

做为 3D 游戏开发，还要知道 3D 模型、贴图、骨骼等一些 3D 美术知识。从基础开始学起的话，我推荐《DirectX 9.0 3D 游戏开发编程基础》。这本书介绍了一些必备的数学工具，涵盖了 Direct 3D 中几乎所有基本运算，以及如何使用 Direct 3D 来实现 3D 游戏。

网页游戏开发

原生游戏客户端开发的学习重点在编程语言上，所以，其实你学了什么编程语言，几乎可以决定你会从事什么样的开发工作。而网页游戏相对来说就简单得多。

想要学习网页游戏制作，其实就是学习 CSS3、HTML5 以及 JavaScript 的知识。这方面的学习资料非常之多，网上一搜就能出来一大堆。其实你只要把每个方面专心学完一本书，把基本知识和核心知识搞懂了，结合一些项目实战有针对性的去练习就可以了，入门还是非常容易的。

比如，看些大家都推荐的经典书籍《响应式 Web 设计》《HTML5 程序设计》等等。

最初，你需要学习最基础的 HTML 语言语法。HTML5 本身对初学者就非常友好。

《JavaScript 语言精粹》也是必读的书籍。JavaScript 语言的书籍非常非常多。这本书的内容就像它的书名，真是是精粹，从语法、函数、对象、数组、正则等等特性来介绍 JavaScript 语言的精髓，让你能真正掌握并高效地使用 JavaScript，非常推荐。

到这里，按照原生游戏开发和网页游戏开发的分类，我们的游戏学习路径和书籍介绍已经写得差不多了，在这里再向你介绍两本与游戏无关却与编程有关的书籍《设计模式》《代码大全》。

这两本书能让你编写代码的水平提升一个层次，提高编程水平是让自己的职业化道路越走越好的一条唯一途径。世上没有捷径，勤学苦练，多看多写才是根本。游戏开发的基础是编程基础，有了扎实的底子，往后走提升得才会比较快。

一百多年前，福楼拜曾在一封信里写到：“谁要能熟读五六本书，就可成为大学问家了。”所以，我列的这些书，你能读完最好。但我知道，对大多数人来说，肯定是读不完的。你可以根据自己的情况做个筛选，有针对、有选择的去学习。

如果你想从事 H5 游戏开发的，你可以着重学习 HTML 和 CSS；如果想学习服务器端开发的话，可以着重学习 Socket 开发和 TCP/IP 相关的书籍；如果想学习客户端知识，比如 3D 客户端的话，可以注重学习 Unity 或者虚幻引擎为载体的书籍。

希望今天的分享对你有帮助。

结束语 | 做游戏的过程就像是在雕琢一件艺术品

我们用三个月的时间，一起创作了《从 0 开始学游戏开发》这个专栏。转眼间，到了说再见的时候，回想起来，只觉得时间过得真快。

说实话，这三个月我是“痛并快乐”着的，接下来就和你“诉诉苦”，也聊一聊我的收获和成长。

“苦”在哪里呢？坦白说，写专栏真是一件费时费力的事。每周三篇稿子，既要保证不断更，又要保证质量，而我平时的工作非常忙，几乎都是在夜晚赶稿。每次都是先构思，然后写代码、验证代码的正确性，才开始写文章，和编辑反反复复修改，还要再去录音。

录音又是一个“苦差”，夏天蝉鸣声非常大，为了找到一个安静的录音环境，我经常在地下车库录，一个人关在车里，一录就是个把小时，一会儿就汗流浃背。

但是，我把这件事坚持做下来了，因为这个过程中我也非常开心，成长了不少。

写专栏前，我刚写完一本书。写这本书用了我将近两年的时间，所以在一开始创作专栏的时候，我一直用写书的感觉来写专栏文章。我想的是以 PC 游戏为突破口，包含客户端和服务器端，按照完整的开发路径和开发顺序，讲解开发一款 PC 游戏最最基本的流程和细节。

但是，专栏开始后，我立刻就收到了很多反馈。我了解到很多同学的编码经验可能不是非常丰富，于是在后续的文章中，我做了一些调整。在写的过程中，我假想面对的是初入 coding 世界的自己，除了之前构思的内容，还对代码环境给出更详细的说明，对为什么要这么做，也写了更多我自己的思考和总结。

这是一个转变，从一开始不知道以什么姿势写专栏，到后期能够不断地梳理和提炼自己的想法和经验，不得不说，写专栏这件事，对我本身的改变是非常大的。

而给我带来这个转变的，是你的反馈。你的每一个留言我都会认真看，感谢这些留言，使我不断想起自己最初做开发的样子，想起那些激情澎湃的日子。

我在博客里写过一个系列的文章叫“代码十年”，记录了我十多年的代码生活。

那时我刚开始学 C++，对这门语言非常感兴趣，每天都发愤看书，看完之后把习题抄下来，然后模拟，自己再写一份。那时候对编程的入迷程度，几乎可以用痴迷来形容。 那时我和一起工作的程序员，对一种算法几乎都可以研究半天。

后来，我开始自己创业，遇到的问题变得越来越多，也越来越复杂。这让我慢慢抛开编程本身，去思考更多的事情。

游戏开发是个复杂的过程，我们时时刻刻都需要学习、更新和积累知识，比如，编码基础知识、编程结构性知识，软件架构、业务架构知识，以及产品设计、市场、推广，乃至整个行业的动态信息。

现如今，十多年过去了，游戏这个行业依然风起云涌。虽然大公司几乎垄断了 70% 以上的游戏资源，但是每年依然有不少中小游戏公司借助小游戏、HTML5 突出重围，而且独立开发者的未来更是可期。

我自己觉得，开发游戏就是在雕琢一件艺术品，从哪里下刀，怎么切入，怎么设计，人物的表情、仪态、动作如何，完全取决于手握刻刀的你。而现在你刻下的每一刀，都决定了这件艺术品以后的生命周期。

专栏虽然已经结束，但这对你我来说，还只是一个开始。我会继续在游戏行业深耕下去，并试着输出更多的内容。我希望，这对你来说，也是一个开始。毕竟这次我们只做了一个小游戏，在游戏世界里，只是戳开了一个小口子，我们还要朝着游戏开发世界的大门，不断前进。

路漫漫其修远兮，吾将上下而求索。希望我们可以一起努力，给游戏领域注入自己的力量。