This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
<audio id="audio" title="复习课 | 带你梳理客户端开发的三个重点" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/9f/4ea1c9e719242d820c064e954978b29f.mp3"></audio>
我们用了将近两个月的时间,讲述了游戏客户端的开发流程和代码编写,今天我来带你总结并且回顾一下。
我这里列了一些编写游戏客户端**你必须要掌握的知识和注意的事项**。希望通过梳理这些知识,能让你对学过的内容有一个全盘的认识和更加深入的理解。
## 重点一客户端开发中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模块、鼠标键盘模块等等继续来做第二款游戏因为里面很多代码都可以复用。
**低耦合度,是游戏开发,甚至编程中都非常重要的思想**。我这里推荐一本书[《设计模式》](https://book.douban.com/subject/1052241/)。这里面对于耦合度的几种模式有详细的讲解。比如工厂模式、代理模式等等,但是不管用什么模式,**低耦合度始终是编程中追求的一种代码方式**。
所谓低耦合度,简单理解,就是将功能抽象出来,并且写成一组函数或者类(我们简称为接口)。这些接口有传入传出的参数或者返回值,可以通过外界代码传给接口,接口通过计算,将计算后的结果返回给外部代码。这些接口和别的功能关联性不大,替换了接口,就仅仅替换了算法或者内部实现而已,但是核心功能是一样的。
**最后,我想来说一下游戏逻辑。将游戏逻辑放在脚本语言里去写,迭代版本的时候可以不更新主程序,只更新脚本。**(如果不放在脚本语言里,就直接写在硬代码里面。)我们在使用引擎和原生语言开发游戏的时候,只保持核心内容,其他游戏逻辑、游戏配置,特别是在游戏中可活动、可配置、可调节的部分,都提取出来,放到脚本语言里去编写。
所谓的主程序在Windows下就是EXE文件一般情况下更新EXE执行文件会导致一系列的更新比如DLL动态链接库或者某些资源也会更新所以更新资源包会变得很大。如果纯粹更新脚本文件那就是几个脚本程序而已。当然这个也要看具体的需求并不是说一定不更新主程序如果有迫切需求更新主程序也是必须要更新的。
除此之外,关于客户端其他部分的流程和要注意的细节,我放在一起梳理一下。如果有哪些还不是很清楚,你可以回到对应的文章去复习一下。
<li>
首先,一款成熟的引擎会包含各种编辑器和工具。如果游戏引擎没有提供相应工具,你就必须根据要做的游戏项目,编写方便策划和美术使用的编辑器,包括地图编辑器、关卡编辑器、场景编辑器等等。
</li>
<li>
游戏的网络部分,要尽量保持网络连接和数据传输的安全性,游戏的网络协议可以迭代更新,网络传输的内容和逻辑也可以放在游戏脚本里面去做。
</li>
<li>
游戏的音乐部分,如果引擎不提供,建议使用成熟的音乐库。游戏要合理使用多线程技术,才能保证流畅性。游戏资源打包的包裹格式,如果没有现成的方案,可以使用压缩包的开发库编写,成熟稳定可靠。
</li>
<li>
每个平台的游戏机制都不相同比如在HTML5游戏里面它使用的是事件机制并非普通的循环。
</li>
## 重点二游戏开发的3大模块
这里我按照游戏开发中的三大模块策划、程序、美术,画了一个图。
<img src="https://static001.geekbang.org/resource/image/63/25/63c518012e92fb2dea46ffc58c1df125.jpg" alt="" />
开发游戏的时候策划要先行先出策划稿、UI图、页面迁徙图等等然后美术开始跟上紧接着根据策划案开始制作UI、原画、人物、场景等等。程序员开始制作各种编辑器和周边工具如果引擎没有提供的话随后美术开始制作地图和各种关卡程序员开始编写demo。随后就开始内部测试和运行总结问题和经验修正和补缺。最后开始第一个正式版本的开发。正式版本的开发流程和demo版本是一样的我在[第5讲](https://time.geekbang.org/column/article/8670)的时候已经详细说过demo的作用。
在专栏中我用Pygame来做教学示例有很多人在留言里问我想用别的语言编写游戏难道不可以吗当然是可以的Pygame只是利用Python+SDL封装的游戏库而我只是以Pygame为载体告诉你编写2D游戏引擎的一些核心知识是怎样的。有了核心知识了解了流程语言层面就简单地多了。举一反三换作任何语言、任何引擎都不会有问题了。
在这之后,如果你想要进一步学习,或者从事相关的开发工作,你要自己多练习、多思考。如果是在公司里,那就多上手去做项目。游戏的开发流程就是这么回事,要做的东西也就那么多,但是不同的项目,不同种类的游戏,就会有不同的问题出现。每次遇到问题,**多换几种思路去思考,多去了解策划、运营的想法**,而不要只是在“不断填坑”。
## 重点三:如何写出过硬的代码?
其实,说了这么多,我现在要说我认为最重要的一点。 **无论你是做游戏开发或者别的开发,你都要有过硬的代码基础。** 一款游戏,有再好的引擎,再好的策划,如果代码写得不好,也会影响游戏体验,毕竟引擎只是在底层带动游戏的运行,在上层业务上,还是要依赖代码的牢靠和稳固。
我从96年写下第一行代码开始走过许多的弯路也经历过许多挫折。**在写代码方面,我或许可以带给你一些直接的经验和警示之谈**。
十几年前当我刚还是个入门级程序员的时候经历过一个项目我负责做一个共享内存的接口。事实上写这个接口很简单用不了多少代码就能完成但当时我正在学习C++最新的Loki库后来加入了Boost觉得里面的模板技巧简直是无敌所以我在共享内存接口之上又封装了类似Loki模板的方法最终导致代码过于复杂很难控制和管理项目失败。
因此,**如果代码不是特别复杂,你大可以直接写功能,不需要从类、框架开始写起,因为这会造成我所犯的那个错误,就是“过度设计”**。举个简单的例子如果一个“hello word”却有500行代码这很明显就是“过度设计”。
所以,我的多年的习惯是,**如果一个正常的软件、游戏,或者模块、接口,我都会将普通的功能、重复性高的功能,抽象出来,做成一个模块,特殊的功能、无法重复的,再做成一个模块。代码尽可能不要长,长的代码分割成几部分,放在几个文件里,阅读起来也会很清晰。能写简洁的绝对不要花里胡哨,能用正常技巧的绝对不用小聪明,实在没办法才用小聪明。**
对外提供接口要清晰、易于阅读,逻辑要一条一条理顺。有异常,就写异常处理代码;有错误,就处理错误代码;没有问题,再运行下一条。
好记性不如烂笔头,写之前先想明白思路,将自己即将要写的代码在脑子里跑一遍,然后将思路记录下来,记在笔记本、电脑、手机哪里都可以。这样写出的代码比直接上手写,速度上会慢很多,但是出错概率极低。直接写你可能一下子就写完,但还要修修补补,调试一整天,事实上,这比思考虑好了再写还要慢。
最后,还是那句话,不管是编写大型游戏还是一款小游戏,还是做任何项目,都离不开踏踏实实把代码写好。毕竟这是基础。如果你总想着什么多少天速成法,快速入门、快速编写出一款脍炙人口的游戏,用户上几百万,这是不存在的。游戏开发,应该说任何程序开发,都不适合任何想要走捷径的人。真正聪明的人,都应该知道学习本来也没有什么捷径。
经过这两个月的学习,不知道你对游戏开发有没有更深入的认识?关于游戏开发或者编程,你还有什么想要了解的,你可以继续在留言区写下想法。
我在后面的挑战中继续等待你的到来!

View File

@@ -0,0 +1,114 @@
<audio id="audio" title="课后阅读 | 游戏开发工程师学习路径(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/91/06/9140756b0f282fa0987322b04fe18f06.mp3"></audio>
整个开发流程和内容我已经讲完了。你现在已经不是一个完全零基础的小白了。作为一个满心想要进入游戏开发行业的人,或者想要在游戏行业深耕,你可能要问了,后续的路,该怎么走呢?今天,我就来和你聊一聊这个内容。
我会通过这份看似很长的必读书单,先给你搭建一个学习路径,然后从底层知识和游戏开发这两方面分别讲讲,想成为游戏开发工程师,应该掌握哪些知识。
我在专栏里讲了很多具体的操作,作为初学者,把这些彻底掌握,其实已经差不多了。但是想要进阶,成为一名真正的游戏开发工程师,你还有很长的路要走。从业二十余年,我见过不少的年轻人,抱着对游戏的一腔热血,一头扎进游戏开发,想着自己有一天也能开发出一款爆款游戏,从此“走上人生巅峰”。
然而走着走着,发现要学的东西太多,无从下手,直接放弃;或者学了之后,因为一直写不出什么项目,也放弃了。就好像去少林寺学武功的人,方丈让他先捆着沙包登山、挑水,他以为这并不是学武术,因此就放弃了。
事实上游戏开发,甚至编程都是这样,你首先要学好基础知识,有了扎实的基础,才可能去做更多的事。道理我想你都懂,我就不多啰嗦啦。我们进入正题。
还记得,在专栏最开始的时候,我专门写过[游戏的分类](https://time.geekbang.org/column/article/8104)按呈现效果我们可以粗略分为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》](https://book.douban.com/subject/1240002/)绝对的C语言入门经典教材。[《C Programming Language》](https://book.douban.com/subject/1139336/),读完 《C Prime Plus》之后再去读这本书绝对会让你了解什么是高手所写的书。
看完了《C Programming Language》之后你除了是C程序员之外俨然是一个不错的Linux用户了。
学习完C语言后你可以学习汇编语言。有了C语言的基础对编程、地址操作、位操作这些基础学习汇编就会相对容易。
估计看到汇编语言这几个字,很多人就望而却步了。但是,我想说,**没有比汇编更适合学习底层知识的语言了**。
毕竟在现实中除非你编写的是芯片或者操作系统现在编写芯片或者操作系统也大量使用C语言只有占那1%的启动部分代码会使用汇编。这么看,使用汇编编程的人真的非常少,而且它本身也不适合编写大型项目。
你可能要问了,为什么你还推荐我从汇编开始学呢?学习汇编的意义在哪里?
我们先来看一段指令。
```
pushl %ebp
movl %esp,%ebp
subl $8,%esp
```
在汇编里这些指令就代表着CPU的指令操作。在编写中级、高级语言需要调试代码的时候如果没有调试信息最终都会落到汇编语言这里。这时候汇编语言就是你的最后一道防线学习了汇编你就能快速、深入地定位程序的问题所在。
所以,**学习汇编的意义就是,你能在脑中完全掌握电脑的运行规律和运作逻辑**比如内存寻址操作、寄存器操作、加减操作、CPU指令等等。
学习汇编有一本书,是绝对的入门级好书,能够把深奥枯燥的机器知识讲得生动活泼,那就是王爽的[《汇编语言》](https://book.douban.com/subject/3037562/)。
学习了汇编语言也会对你的C语言知识有一个本质上的巩固。所以结合C和汇编指针不再是头疼的问题因为你已经知道在汇编中的地址是如何操作的。
C语言看起来就好像把汇编进行了一次朴素的包装你不再需要强制记忆寄存器和指令定义一个变量就知道汇编语言是怎么做的这对于你的C语言来说是有绝对帮助意义的。
**一般人提倡的学习路径是由浅入深,如果你想一直在技术领域深耕下去,我建议你走一条由难到易的学习路径。**
因为,越高级的编程语言越接近人类的思维方式,这种思维会固化你的编程思维,让你在之后的学习过程中,无法更深层次地理解底层机器语言的沟通方式,学习起来就会比较困难。先学习底层知识,这样你在后续的高级语言的学习中,就属于“降维打击”,学习起来就相对轻松和快速。
除此之外,如果你已经是一名程序员,你应该很能体会,你的工作将会越来越忙,几乎没有时间学习新的知识,而学习能力是一名程序员必备的基本能力。
接下来你还是需要学C++语言了。具体原因其实我刚才也说了游戏引擎绝大部分使用C++编写也有一小部分是使用C语言或者汇编编写的比如Allegro。
如果你要学习C++语言学苑出版社出版的HerbertSchildt写的《C++从入门到精通》这本书一定不能错过它是我的C++启蒙书。唯一可惜的是这本书年头有些久了我的书还没有找到但是我至今对书的内容都记忆犹新。这本书后面的小习题绝对能让你在学完这本书之后对C++了解的非常透彻和深刻。
另外,[《深](https://book.douban.com/subject/1091086/)[度探索](https://book.douban.com/subject/1091086/)[C++对象模型》](https://book.douban.com/subject/1091086/)[《C++ Programming Language》](https://book.douban.com/subject/1099889/)[《C++语言的设计与演化》](https://book.douban.com/subject/1096216/)也是必看书籍。前一本能让你了解C++对象模型从底层了解C++的机制。后两本出自C++之父Bjarne Stroustrup之手。
从C++开始设计到现在为何会如此演化和设计语言的背后又是怎样的故事除了语言的基础知识也会有一些小小的故事。另外如果你的知识面和时间允许我建议你最好去看《C++ Programming Language》的原版因为只有原版是原汁原味不会有被翻译偏差影响的。
现在,我们已经将一幢大楼的“地基”都搭建好了。
## 学完这些,要多久啊?
你可能会问了,从开始学习底层知识,到对知识有了解并且能顺利写出软件需要多久呢?因人而异,有人是计算机系的,有人是野路子,有人对计算机特别有天赋,有人虽然不聪明但是非常勤奋,每个人的学习曲线和学习路径以及最终学会的时间都是不一样的。
但是一般来说要从C到汇编再到C++一直到能编写成熟的代码平均下来需要57年的时间。我没有做过精细地统计但是我想按照大学四年的学习时间从学习到实践再加上毕业13年的实践深入57年的时间基本是差不多的。你觉得呢
今天这些内容主要涉及游戏开发的底层知识。说起来很容易,做起来却一点都不容易。有了这些基本功,下一节,我会推荐一些游戏原理和机制相关的书籍。希望我的分享对你有帮助。

View File

@@ -0,0 +1,49 @@
<audio id="audio" title="课后阅读 | 游戏开发工程师学习路径(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/56/ae/56907724b65ca600ea9c87472d8b4aae.mp3"></audio>
上一节,我讲了一些底层知识和编程语言。搭好了底子,接下来,你需要掌握一些游戏的常规原理和机制。这里我也为你准备了一份学习书单。
## 游戏设计
[《游戏机制》](https://book.douban.com/subject/25859579/)这是一本策划和程序员都可以看的书。在游戏设计中游戏机制的设计决定着游戏中的呈现效果直接影响着玩家体验。而这本书就是教你如何打造出挑战丰富、乐趣十足、平衡良好的可玩性的游戏机制。它结合了很多实用案例从不同角度介绍和阐述了游戏机制介绍了用于构建和模拟游戏机制的Machinations工具。
[《游戏设计的100个原理》](https://book.douban.com/subject/26318335/)做游戏开发的人,基本上自己都是非常喜欢游戏的,肯定玩过不少游戏。但是估计你对游戏设计背后的原理性的东西,了解的不会很多。这本书就是讲很多原理性的东西,让你从更专业的角度来看你以前玩过的游戏,毕竟游戏设计其实更像是一门哲学。
[《Windows游戏编程大师技巧》](https://book.douban.com/subject/1230286/)这是一本经典书籍属于游戏开发启蒙教材它涉及了开发的基础知识和理论、开发技巧这些问题。有一点点汇编、C/C++语言基础,看这些就不会觉得特别难了。
## 2D和3D编程
接下来关于2D编程我们可以看[《Cocos2dX游戏开发技术精解》](https://book.douban.com/subject/24733748/)这是一本让你一下子就能学习2D引擎且直接上手的书籍几乎是学习2D游戏必备。
《Unity 4.x 2D游戏开发基础教程》是一本利用Unity编写2D游戏的教程书籍。另外有一本非常难买到的书[《2D Graphics Programming for Games》](https://book.douban.com/subject/20116832/),目前只有英文版,如果你能找到电子书也可以阅读一下。
毕竟3D游戏大行其道但是它和2D游戏的路数又是非常不一样的。加上很多人上来就想做3D游戏那关于3D开发这几本书必看不可。
[《3D游戏编程大师技巧》](https://book.douban.com/subject/1321769/)[《Unity 3.x游戏开发实例》](https://book.douban.com/subject/25916788/)[《3D游戏开发大全》](https://book.douban.com/subject/1488758/)。第一本书可以让你很轻松地就了解3D游戏的编程基础知识第二、三本书都是属于利用引擎来编写实例的你可以在这些例子中获取更多的实战经验。
而《3D游戏开发大全》以Torque引擎来作为切入点让你看到更多的技术。比如游戏建模技术、人工智能技术、玩家控制技术等等游戏开发的细节。看完这三本书你就会对3D游戏编程有一个本质的了解。
做为3D游戏开发还要知道3D模型、贴图、骨骼等一些3D美术知识。从基础开始学起的话我推荐[《DirectX 9.0 3D游戏开发编程基础》](https://book.douban.com/subject/2111771/)。这本书介绍了一些必备的数学工具涵盖了Direct 3D中几乎所有基本运算以及如何使用Direct 3D来实现3D游戏。
## 网页游戏开发
原生游戏客户端开发的学习重点在编程语言上,所以,其实你学了什么编程语言,几乎可以决定你会从事什么样的开发工作。而网页游戏相对来说就简单得多。
想要学习网页游戏制作其实就是学习CSS3、HTML5以及JavaScript的知识。这方面的学习资料非常之多网上一搜就能出来一大堆。其实你只要把每个方面专心学完一本书把基本知识和核心知识搞懂了结合一些项目实战有针对性的去练习就可以了入门还是非常容易的。
比如,看些大家都推荐的经典书籍[《响应式Web设计》](https://book.douban.com/subject/20390374/)[《HTML5程序设计》](https://book.douban.com/subject/10608238/)等等。
最初你需要学习最基础的HTML语言语法。HTML5本身对初学者就非常友好。
[《JavaScript语言精粹》](https://book.douban.com/subject/3590768/)也是必读的书籍。JavaScript语言的书籍非常非常多。这本书的内容就像它的书名真是是精粹从语法、函数、对象、数组、正则等等特性来介绍JavaScript语言的精髓让你能真正掌握并高效地使用JavaScript非常推荐。
到这里,按照原生游戏开发和网页游戏开发的分类,我们的游戏学习路径和书籍介绍已经写得差不多了,在这里再向你介绍两本与游戏无关却与编程有关的书籍[《设计模式》](https://book.douban.com/subject/1052241/)[《代码大全》](https://book.douban.com/subject/1477390/)。
这两本书能让你编写代码的水平提升一个层次,提高编程水平是让自己的职业化道路越走越好的一条唯一途径。世上没有捷径,勤学苦练,多看多写才是根本。游戏开发的基础是编程基础,有了扎实的底子,往后走提升得才会比较快。
一百多年前,福楼拜曾在一封信里写到:“谁要能熟读五六本书,就可成为大学问家了。”所以,我列的这些书,你能读完最好。但我知道,对大多数人来说,肯定是读不完的。你可以根据自己的情况做个筛选,有针对、有选择的去学习。
如果你想从事H5游戏开发的你可以着重学习HTML和CSS如果想学习服务器端开发的话可以着重学习Socket开发和TCP/IP相关的书籍如果想学习客户端知识比如3D客户端的话可以注重学习Unity或者虚幻引擎为载体的书籍。
希望今天的分享对你有帮助。

View File

@@ -0,0 +1,57 @@
<audio id="audio" title="开篇词 | 跟我学,你也可以开发一款游戏!" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/28/99/28928a31b296bf30658023fc5866d599.mp3"></audio>
你好我是蔡能。从96年编写第一行代码开始我在IT行业从业已经有22年的时间。
从程序员到管理者我参与过各式各样的项目也主导过很多项目。当然我经历的大部分项目都是游戏。20多年里我目睹了从最初的像素游戏到如今高清贴图的3D游戏的发展过程。游戏行业乃至整个IT行业都发生了翻天覆地的变化。
当然我本人也非常喜欢玩游戏。我喜欢自由自在的游戏和需要脑力的游戏。比如“GTA系列”“老滚系列”和“三国志系列”。在玩游戏的过程中我体会到各种各样的乐趣同时作为开发者我也会从专业的角度出发思考这款游戏究竟是如何制作出来的。
我发现,喜欢玩游戏的人,都很有兴趣自己做一款游戏。哪怕自己不懂编程,但是自己做做策划,懂美术的话自己做做图,能让自己的游戏跑起来,就是一件幸福的事情。
还有一部分人,可能对游戏并不感兴趣,但是对赚钱感兴趣,那么我的专栏也可以让你知道游戏开发的基础流程和知识,避免走弯路,不花冤枉钱,不掉技术坑。
我看过网上各式各样的“傻瓜式”游戏开发工具比如RPG MAKER。任何不懂编程的人只需要导入规定的模板就能方便地开发出一款游戏。然而这种方法只能开发某一类非常简单的游戏。**如果想要开发出一款真正意义上的游戏,还是要借助编程基础和系统的开发知识。**
我举几个简单的例子,你就明白了。
<li>
在《守望先锋》里,我们看到的子弹射击,对方被击伤,这些都是经过怎样不断地网络优化实现的呢?还有各种网络预判模型,你是否都了解呢?
</li>
<li>
我们玩的《星际争霸》,究竟是每个线程控制一个兵种,还是一个线程刷新全部兵种画面?
</li>
<li>
《魔兽世界》如此庞大,它是如何将海量逻辑写在游戏客户端里?将逻辑编写成硬代码显然不理性,写成配置文件又缺乏机动性,那它是怎么做到的呢?
</li>
<li>
另外,我发现,很多公司在开发游戏的时候,选择引擎也是一个尴尬事。是自己研发一款引擎还是购买别家成熟的引擎呢?引擎的好坏,对开发的游戏来讲有什么影响?很多人也是不知道的。
</li>
这些问题,我都会在专栏文章中一一解答。但前提是你最好具备一定的编程知识和基础。
<li>
**我将会尽量选择简洁的Python语言来编写。**我会从最上层的脚本语言Python的使用开始剖析为什么游戏开发要从这里开始。在用到C/C++代码的地方,我将会尽可能地使用简单易懂的语法来示范,所以,其实你只需要懂点儿英文就可以了(笑)。
</li>
<li>
**我会尽量选择简单的开发工具而不是动辄几个G的开发工具。**比如“VS系列”比如各种复杂的IDE。简单的开发工具足够简单明了地让你了解开发的具体流程快速进入状态而不是在设置工具上浪费大量的时间和精力。
</li>
<li>
**我会尽量避免特别复杂的底层知识,但是也不会一带而过。**我会帮你一步步梳理开发的流程并仔细剖析在C/C++中如何控制计算机的各种设备,操作计算机的各种硬件。或许你还会在编译中遇到问题,我也会一步一步,由浅入深,教你如何解决编译问题。
</li>
<li>
**我将用制作“打飞机”游戏demo的过程来完成游戏开发内容的分解步骤讲解。**之所以选择“打飞机”作为游戏demo是因为它的开发过程几乎涵盖了我要讲述的所有开发流程包括操作、画面的前后遮挡、图像的碰撞和切换、资源包、脚本语言等等。
</li>
对于专栏的具体内容,我将分以下几个方面来阐述。
1. 我会先带你熟悉游戏开发的基础概念。包括开发游戏需要涉及的基础知识和需要准备的工具:游戏引擎、底层图形接口和地图编辑器。
1. 然后从0开始搭建一个游戏窗体往窗体里添加图形、界面和操作。
1. 我还会讲解如何嵌入脚本语言。毕竟使用硬代码编写游戏逻辑并不是一个很好的选择,而脚本语言的嵌入,可以让我们很方便地完成游戏的逻辑开发。我们只需要将底层图形图像的显示、函数等捆绑好,接下来交给脚本语言就好了。
1. 除此之外我还会阐述各种平台上的游戏开发和实现带你学习一些最近热点的游戏开发知识。比如HTML5游戏和手机移动平台游戏。我们已经有了游戏开发的基本知识也掌握了游戏开发的流程再来看这些游戏的开发就会变得很轻松了。
1. 最后,我会与你谈谈,我对游戏行业未来发展的看法。游戏将会如何发展,往哪个方向走?开开脑洞,我们大胆畅想游戏领域的未来。
我希望,学习完这个专栏,**能让你对游戏开发有个实质性的掌握,让你对游戏的开发和理念有一个完整并且系统的了解,并且能很快地着手进行游戏的开发**。万变不离其宗,希望我的分享不会辜负你为此投入的金钱和时间。
让我们一起踏上游戏开发的旅程吧!
<img src="https://static001.geekbang.org/resource/image/56/24/566a01f6857d4a0e42c5f2fb8db59624.jpg" alt="" />

View File

@@ -0,0 +1,90 @@
<audio id="audio" title="第1讲 | 游戏开发需要了解哪些背景知识?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b5/a0/b5f7fccfba8a1824a985a313b3f935a0.mp3"></audio>
这一节开始,我将带你进入游戏开发的世界。
从最初的小霸王到充满混混和烟味的街机厅到PS、Xbox、DC、N64等次世代游戏主机再到电脑游戏再到如今在手机上就能玩到各种各样的游戏。
我会依次为你揭密,这些令人痴迷、沉浸的游戏,究竟是怎么发展的,常见的游戏种类有哪些,这些游戏在开发流程和细节上有什么不同,以及游戏开发究竟要从哪里开始学起。
## 浅述游戏的发展
我们现在公认的第一款电子游戏也就是主机游戏是1952年面世的。游戏玩的是井字棋6年后才出现了简陋的[网球游戏](https://www.bilibili.com/video/av1326850/)。
下面这幅图就是井字棋游戏尽管它与1952年的游戏画面还是有所出入但是游戏内容是一样的。
<img src="https://static001.geekbang.org/resource/image/e6/74/e67d2a821df9f4c625ba2f78ee294974.jpg" alt="">
此后,我们就进入了“雅达利”时期。随后的《太空侵略者》,是最经典的一款游戏。在这之后,我们耳熟能详的任天堂红白机,发布了《吃豆人》《大金刚》等游戏。再之后,就进入了各种游戏主机竞相绽放的阶段。
在电脑游戏方面第一款电脑游戏是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游戏进行讲解呢
主要有两方面的原因:
<li>**2D游戏涉及最基本的图形、图像知识。**从画面方面考虑开发2D游戏只需要你知道X和Y的位置即可其他深层次的知识引擎都可以替你完成。想要制作3D画面你需要掌握更多的知识特别是几何知识这是毋庸置疑的。对于初学者来说我们首先需要掌握核心的开发流程至于过于细节的知识可以在有了一定基础之后再进行拓展学习。
</li>
<li>**2D游戏涉及最核心的网络呈现过程。**网络数据在客户端和服务器端之间传输,经过服务器端计算的数据返回给客户端,客户端进行呈现。比如,从“一颗炸弹消灭几个敌人”,就能知道网络是否有延迟,服务器计算是否正确等等。
</li>
## 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在游戏的开发流程中扮演一个怎样的角色
欢迎留言说出你的看法,我在下一节的挑战中等你!

View File

@@ -0,0 +1,101 @@
<audio id="audio" title="第2讲 | 2D游戏和3D游戏有什么区别" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9b/71/9b666242086c03b1fae01271707bd871.mp3"></audio>
我们知道游戏的体现形式最主要是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游戏。
我们继续拿《暴力摩托》,或者更早期的赛车游戏来举例。
<img src="https://static001.geekbang.org/resource/image/87/da/87483c1fb12cbb8e7691f6d8843959da.jpg" alt="">
这类游戏的内存中存在两种甚至多种主角赛车的形态图像比如近景摩托车、远景摩托车。在判断速度大小之后我们可以对图像进行替换以此让玩家感觉到远近背景和地面则是按照线条来处理也就是将一副背景图片分成N条线而第N+1条线会比第N条放大一倍以此达到给赛道设置不同远近的效果根据玩家赛车的前后位置来判断绘制每一行的“放大”。而玩家控制的赛车则是一个活动块。每个活动块都存放有不同远近大小的图像形态来控制前后图像大小。
国外有不少专门研究这类“伪3D”技术的网站我在这里推荐几个你有兴趣的话可以去看看。
[http://www.extentofthejam.com/pseudo/](http://www.extentofthejam.com/pseudo/)
[http://lodev.org/cgtutor/raycasting.html](http://lodev.org/cgtutor/raycasting.html)
[https://codeincomplete.com/posts/javascript-racer-v1-straight/](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》的画面。
<img src="https://static001.geekbang.org/resource/image/f6/74/f63c7cb07818dc0d0771139008ae4974.jpg" alt="">
<img src="https://static001.geekbang.org/resource/image/04/36/041b56fd0bb54443d32c083c237a7136.jpg" alt="">
你可以看到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系列”和“老滚系列”就是我的选择。
那如何定义一款成功的游戏呢?如何做出一个爆款游戏呢?我想说的是,一款游戏的成功离不开美术、音乐等等这些硬指标,然而这些指标却不能决定这款游戏是否能深得人心。每一个人心中的判断并不一样,大量的人玩《王者荣耀》,但这并不代表它是优秀的游戏,只能说是成功的游戏。毕竟,青菜萝卜各有所爱。
## 小结
这一节内容差不多了,总结一下,你需要记住这几点:
<li>3D和2D游戏的区别主要体现在呈现画面和文件体积上
</li>
<li>借助3D引擎可以提升2D游戏的声光和特效效果
</li>
<li>成功的游戏不一定是优秀的游戏。
</li>
最后,给你留个思考题吧。你觉得一款成功的游戏需要具备什么样的特点呢?
欢迎留言说出你的看法,我在下一节的挑战中等你!

View File

@@ -0,0 +1,119 @@
<audio id="audio" title="第3讲 | 游戏的发动机:游戏引擎" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3b/d7/3bac6a1663adf432f553d31e30b48bd7.mp3"></audio>
估计长期玩游戏的玩家,都能脱口而出几款游戏引擎的名字,比如“虚幻”“寒霜”“狂怒”等等。那你能说说,游戏引擎究竟是什么吗?它在游戏开发中究竟起着什么样的作用呢?
## 游戏引擎是什么?
汽车没有发动机就无法跑起来,人类没有心脏就会死亡。而引擎就是发动机,就是心脏。
我们先来看一下维基百科对于游戏引擎的定义:
游戏引擎是指一些已编写好的可编辑电脑游戏系统或者一些交互式实时图像应用程序的核心组件。这些系统为游戏设计者提供各种编写游戏所需的各种工具其目的在于让游戏设计者能容易和快速地做出游戏程序而不用由零开始。大部分都支持多种操作系统平台如Linux、Mac OS X、Windows。大多数游戏引擎包含以下系统渲染引擎、物理引擎、碰撞检测系统、音效、脚本引擎、电脑动画、人工智能、网络引擎以及场景管理。
这个概念是不是看起来还是不太好理解?我来具体解释一下,游戏引擎到底是什么,以及它究竟能干什么。
<li>**游戏引擎就是图形引擎。**准确地说,游戏引擎约等于图形引擎。玩家口中所说的“虚幻”“寒霜”等,这每一款引擎对于图形渲染的处理方式都不同,所以用某个引擎编写出来的游戏,具体的表现画面也会不同。比如,有的引擎编写出的光影效果特别绚丽,有的则粒子效果特别真实。
</li>
<li>**游戏引擎是一整套游戏解决方案。**其实游戏引擎并不仅仅等同于图形引擎。图形引擎只是游戏引擎中一个占比极大的组成部分。一款好的游戏引擎不仅要看它对于图形图像的处理能力也要看它对于其他部分的处理能力比如对音频、音效的播放、键盘鼠标的处理以及UI界面的编辑和各种处理工具的提供。这里的处理工具包括地图编辑器、关卡编辑器、人物编辑器、资源编辑器、物理碰撞、碰撞检测等。所以从专业的角度讲游戏引擎是完成开发一套游戏的解决方案而不仅涉及图形部分。
</li>
<li>**游戏引擎是一整套编程接口。**要写游戏就必须写程序,所以有游戏引擎就一定需要编程。不管你是直接面对引擎编写代码,还是在引擎提供的编辑器(比如关卡编辑器)基础上编写简单的逻辑代码,写代码都是必不可少的。在引擎提供的编程接口上,你能很容易地调用各种接口完成游戏的开发。
</li>
## 游戏引擎是怎么工作的?
说完了游戏引擎是什么,我们来看游戏引擎具体是怎么工作的。
我先从代码层面来说。这里是一段伪代码:
```
int DrawLine(const Surface&amp; s, int sx, int sy, int fx, int fy, int fit, const Color&amp; color);
```
这段伪代码提供了一个在屏幕上画一个线条的函数。该函数提供了7个传入参数分别是
- 目标图层
- 线在游戏窗口内的起始x点
- 线在游戏窗口内的起始y点
- 线在游戏窗口内的终止x点
- 线在游戏窗口内的终止y点
- 线条粗细
- 线条颜色
在调用了这个函数之后,你就能很容易在屏幕上画出一个线条。同时,在这个函数背后,引擎做了一系列这样的动作:
- 判断传入的Surface图层对象是否正确、是否存在
- 判断传入的起始点、终止点是不是存在负数;
- 判断颜色是不是正确;
- 拆分Color变量为RGB值填入到SDL绘图接口调用SDL绘图接口绘制一条线。
关于绘图接口,我会在下一节的内容中详细讲解。这里,你只需要知道,画线的接口函数在背后分解、组合、计算,然后会将绘制工作交给底层绘图接口就可以了。
之所以要拿这个画线函数举例,是因为它展示了引擎中函数的使用方式。如果我们用一般的画线函数画圆,那首先要知道圆的直径和计算方式;如果画矩形,那就要知道哪里是起始点和终结点。这一系列动作等于将引擎所做的工作包含在了画线函数里,你只需要关心画线的这一系列参数如何使用就可以了。
说完了代码层面,我们来说说非代码层面的东西。
**游戏引擎其实也包括游戏开发的一系列工具,也就是诸如地图编辑器、关卡编辑器、人物编辑器、资源编辑器等。**美术、策划、制作人等,这些开发流程中的责任人,可以往工具内填入需要的东西,制作出需要的内容。
我们拿人物编辑器来举例。如果我们使用通用编辑工具比如3DMAX、Maya等编辑出来内容需要经过格式的转换才能应用到游戏中但是这样不仅耗费转换时间也耗费调试时间。因为通用工具可能并不适合引擎本身所定义的格式需要不停修正和更改。如果引擎本身就提供了这样的制作工具那么制作出的内容直接就能在游戏中使用不需要转换所见即所得。
所以,游戏引擎背后的工作方式是:
- 在代码层面,游戏引擎是对绘图接口、操作系统、音频等接口进行的代码层面的封装;
- 在工具层面,游戏引擎是一整套游戏内容的制作工具,方便你制作针对这个引擎的游戏内容。
## 自己开发引擎还是直接购买?
了解了游戏引擎的概念和工作方式,那么开发者究竟是自己编写引擎还是购买商业引擎比较好呢?
从一般意义上讲,如果有实力,当然是自己开发引擎更贴合公司的情况。然而,这样付出的代价就是,花费大量的人力和财力,而且,以一般小公司或者起步阶段的开发者的开发水平,编写出的引擎,很可能只是一个半成品或者问题多多的残次品。**自研引擎并不是一个不可能完成的任务,但是要看公司的财力和程序员的实力。**
购买商业引擎是现在大部分公司都会走的一条路。购买商业引擎只需要花费一笔钱就能拿到成熟的游戏引擎,直接可以开发游戏,然而付出的代价就是,你可能需要从头开始学习这套引擎的工作原理、工具套装。
如果你非常熟悉3DMAX等通用工具那要从头学习开发工具就会产生许多问题比如
1. 引擎中包含的开发工具基本没有通用性可言。就算吃透了工具,对你今后的能力提升和职业规划也没有明显的帮助;
1. 如果引擎升级或者更换引擎,就需要从头再学一次工具,会耗费大量时间和精力。当然会有一些优秀的商业引擎支持通用工具制作的内容导出和转换,直接在引擎中可以使用,当然这种引擎的购买费用也会更高;
1. 另外商业引擎本身也会存在一些隐藏得很深的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编写的。
## 小结
好了,这一节内容差不多了,总结一下,我主要给你分享了四个内容,分别是游戏引擎的定义、游戏引擎的工作方式、游戏引擎的选择和编写。
你只需要记住以下这些内容即可:
- 游戏引擎是一整套的游戏开发程序接口和开发组件,可以让你更方便、更专注于游戏的开发;
- 游戏引擎控制着游戏的几乎所有内容,游戏的表现质量和游戏引擎直接相关;
- 针对不同的平台,游戏引擎所使用的编程语言也不一样。而了解了游戏引擎的编写,在之后的开发过程中,会有非常多的便利。
最后,给你留一个思考题:
什么情况下,我们可以跳过游戏引擎,直接编写一款游戏呢?
欢迎留言说出你的看法,我在下一节的挑战中等你!

View File

@@ -0,0 +1,161 @@
<audio id="audio" title="第4讲 | 底层绘图接口的妙用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f7/29/f7458e66ab21fa37339b15b05f89c229.mp3"></audio>
上一节,我给你介绍了游戏引擎的概念及其在游戏开发中所起的作用。同时,我也提到了游戏引擎背后的工作方式。在代码层面,游戏引擎是一套对底层绘图、音频、操作系统接口的封装实现。
在此基础上,我还举了一个在游戏屏幕上画线条的例子。在这个例子中,**画线的接口函数在背后分解、组合、计算,并将绘制工作交给底层绘图接口。**这个绘图接口,就是今天要讲的内容。
## 几种常见的绘图接口
前面我已经说过我会针对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](http://www.libsdl.org/download-2.0.php) 写作这篇文章的时候最新的版本是2.0.8稳定版)。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/1e/7c/1e5e2f488c67643776858f564214787c.jpg">
在下载的网站页面我们可以看到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类型的参数参数列表就像这样
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/7b/6d/7b0370e14ed1121d8c37c546888c9e6d.jpg">
其中“初始化所有系统”这个选项除了“忽略任意错误”外包含了以上所有不同的初始化系统一般使用SDL_INIT_EVERYTHING即可。
随后我们要使用SDL_CreateWindows来创建一个窗体。SDL_CreateWindows支持六个参数分别是窗体名称、在Windows屏幕显示的x坐标、在Windows屏幕显示的y坐标、宽、长、显示方式。
然后将使用SDL_CreateRenderer创建一个SDL的渲染器SDL_Renderer。渲染器的参数是
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/bb/6e/bbc37419239a0c9d32d5676acbc6b96e.jpg">
随后可以使用SDL_RenderClear来清空SDL渲染器、使用SDL_RenderPresent方法将渲染的结果显示出来。然后我们需要建立一个大循环在这个循环内你可以把SDL支持的图形图像函数或者其他逻辑代码往里面填写完成游戏的程序内容具体的操作我会在之后的文章详细介绍。
在这个大循环内我们要用到SDL_Event事件系统。在循环内捕捉用户事件比如要退出这个循环就必须点击右上角的X关闭按钮才行。如果你点击了X按钮就会被while内的event事件捕捉到并且匹配是不是退出事件如果是退出事件就退出程序。
最终退出程序的时候使用SDL_Quit清除资源退出程序。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/fd/b6/fd59a32a4b01a53581cee54a16f2f9b6.jpg">
我们结合这张流程图来看一下将这些内容串联起来的代码:
```
#include &lt;SDL.h&gt;
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 &lt;= sdl ){
// 当SDL初始化完成后创建一个标题为&quot;SDL Window&quot;的窗口,窗口对齐方式为居中对齐分辨率为640x480的窗口
g_pWindow=SDL_CreateWindow(&quot;SDL Window&quot;,
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( &amp;e ) )
{
//检测到用户需要退出
if( e.type == SDL_QUIT )
q = true;
}
}
SDL_Quit();
return 0;
}
```
这个简单的例子说明了如何直接利用SDL接口编写游戏。直接利用其他图形接口编写游戏也基本是这样的步骤。
## 小结
我来给今天的内容做一个总结,你只需要记住这些内容即可:
- 绘图接口绘图接口其实就是使用C/C++语言或汇编语言,通过操作系统的底层,调用诸如显卡、内存这些绘图设备,最后做成接口;
- SDL拥有统一封装的绘图接口你可以在各个平台无缝编译和使用。
现在,你是不是对游戏开发的一部分流程有点了然于胸了呢?
给你留个小思考题吧。
我们提到了直接利用绘图接口编写游戏,请问,如果这样,还需要“游戏引擎”吗?如果需要的话,这个“引擎”应该放在哪里呢?
欢迎留言说出你的看法,我在下一节的挑战中等你!

View File

@@ -0,0 +1,100 @@
<audio id="audio" title="第5讲 | 构建游戏场景的武器:地图编辑器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ab/ae/ab5338044f87d475294ef16328057bae.mp3"></audio>
今天我想和你聊一聊基础知识模块的最后一部分地图编辑器。为了给之后的内容铺垫我还会顺带介绍一些游戏Demo的知识。
我们先来看一幅图。这看起来是一款FPS射击游戏对不对也对也不对。说对因为这确实是一款FPS游戏说不对这其实只是一幅地图但是这幅地图来自《魔兽争霸3》。更准确地说这是使用《魔兽争霸3》的地图编辑器制作出来的一幅游戏地图。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/21/28/2180e87327f796c9d69b2b2222399d28.jpeg">
《魔兽争霸3》发布之初没人能想到并且能做出这样“变态”且“不走寻常路”的地图。《魔兽争霸3》的地图编辑器之强大直到它发布五、六年后才有人开始制作出这样令人匪夷所思的地图。
**地图编辑器在游戏开发的前期扮演着非常重要的角色,它的主要功能就是用来制作游戏地图。** 因为我们现在是一个人来开发一款游戏,不涉及分工的问题。而常规的团队在进行游戏开发的过程中,用地图编辑器制作地图是由美术来负责的。
地图编辑器本身一般会由专业的地图编辑器程序员来开发完成。地图编辑器可以使用原生引擎所支持的语言来编写比如C/C++也可以使用引擎加嵌入脚本语言的方式来进行编写比如Lua等。但是现在一些成熟的游戏引擎都会自带地图编辑器。所以大多数时候地图编辑器不需要你自己开发。
## 地图编辑器有哪些种类?
地图编辑器并非千篇一律。由于游戏类型的不同,地图编辑器也不相同。比如我们做一个《大富翁》类的游戏,我们就需要做一个**斜45度角的地图编辑器**。
如果是**俯视角度的游戏**地图编辑器的视角呈现方式也类似斜45度角的游戏。这两种编辑器制作地图的方式极其类似唯一不同的是**斜45度角是用菱形的地图块拼接而成俯视视角是用矩形的地图块拼接而成的。**
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/e8/d1/e86acc386c52fd571b6e879630489bd1.jpg">
我们做一个**横版卷轴游戏**,就需要在地图编辑器内摆放横版的地图。如果地图是重复地图,类似《管道鸟》,那就需要程序员重复贴图,而不需要在地图编辑器重复编辑相同的地图。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/67/cc/679b41409741dfae9f7e28a18cd0aacc.jpg">
3D游戏就会有3D地图编辑器就像前面的《魔兽争霸》编辑器就是一个3D视角。地图编辑器的种类不同保存的地图类型和文件结构也不一样。有些地图编辑器保存的是图片和位置而有一些保存的却是一个脚本文件方便直接载入游戏程序。
## 为什么要了解地图编辑器的编写过程?
我刚才说过,大多数时候地图编辑器不需要你自己开发。但是我们依然要了解地图编辑器是如何编写出来的,至于了解这些究竟有什么用呢?我继续来给你讲。
1.**编写地图编辑器的过程,能让你更好地了解游戏引擎。** 从编写地图编辑器开始,就是对游戏引擎的一次全面应用。为什么这么说呢,因为在编写地图编辑器的过程中,除了音乐模块或许暂时不会被用到外,利用引擎编写的地图编辑器、地图资源的载入代码、保存代码等等,都会被用在正式的游戏中。
2.**编写地图编辑器的过程,可以优化游戏开发流程。** 程序员之间会讨论关于资源载入、游戏内容加载和读取这些基础问题,并将最基础的流程完整地梳理一遍。在游戏正式开始开发之前,把这些流程梳理一遍,能极大提升开发过程中的流畅性。
3.**编写地图编辑器的过程涉及UI、按钮和键盘事件的使用。**地图编辑器开发完毕后主要是给美术人员使用所以UI的点击、鼠标键盘事件是必不可少的。在地图编辑器中运用UI、按钮、键盘事件后在正式游戏开发中这些代码可以直接移植到游戏代码中使用。
4.**编写地图编辑器的过程,能起到团队磨合的作用。** 在编写的过程中,你会接触到策划、美术等很多人的意见,程序员之间也会对引擎的应用有很多的讨论。
## 地图编辑器与关卡编辑器
说到地图编辑器,不得不提到关卡编辑器。**关卡编辑器是一种主要给策划使用的编辑器,它涉及地图中的关卡内容。** 一般情况下,关卡编辑器不涉及具体的地图细节,但是它涉及总体的游戏地图和游戏流程。
简单来说,地图编辑器的功能就相当于房屋的结构朝向图。我们可以从这张图里,看到每个房间的俯视图。里面包括门的位置、阳台的位置,甚至厕所的位置。而装修公司设计家居,就相当于关卡编辑器的功能。设计师会在你已有房间的结构内,摆放各种桌子、椅子、床、柜子、灯泡等具体的家居用品。
在实际开发过程中,很多时候,只有一些成体系的大游戏才会把关卡编辑器单独分出来。很多时候,对一些小游戏来说,关卡编辑器和地图编辑器经常是在一起的,你可以在地图编辑器里面直接来编写关卡。这张 《坦克大战》的地图编辑器中就包含了关卡编辑器的内容。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/07/31/0751315e5a271167ebfc8f7253073831.jpg">
这里有一个3D游戏的关卡编辑器的界面。我们常见的关卡编辑器的界面一般都是这样的。这个界面最左侧的对话框是物件的选择界面具体包括关卡内物件的选择和摆放界面右侧的对话框是属性选择界面定义这个物件的属性比如这个物件是否可以弹跳、爆炸等等界面右下角是物件X、Y、Z轴的编辑可以自定义物件的拉伸和缩放。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/4f/95/4f55227b8c3e5abd0fc67d3224413895.jpg">
我们拿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相关的内容你只需要记住这些重点
<li>开发地图编辑器的过程可以帮助开发人员了解引擎、优化开发流程、测试bug、磨合团队
</li>
<li>关卡编辑器之于地图编辑器,就好比装修设计图和房屋朝向图之间的关系;
</li>
<li>游戏Demo的使命是完成游戏核心框架测试地图编辑器等问题Demo最终会被舍弃
</li>
<li>在游戏开发的正规流程中我们要经历地图编辑器、关卡编辑器、游戏Demo、正式开发等几个流程每一个流程都肩负不一样的任务。但简单如贪吃蛇类的游戏我们可以直接上手编写代码。
</li>
最后,给你留一道小思考题吧。
用地图编辑器制作出来的地图有没有取巧的保存方法,让游戏程序读取地图时更方便?
欢迎留言说出你的看法,我在下一节的挑战中等你!

View File

@@ -0,0 +1,198 @@
今天我们要在游戏中载入UI和菜单在开始之前我们先调整一下我们之前讲过的游戏代码部分的内容。
首先我们需要更改游戏的背景图片,使之看起来更像是一款打飞机的游戏,而不是最早之前我们随便用的一幅山水图。我们先将游戏背景修改为正常的游戏背景,并且贴上飞机图像。
<img src="https://static001.geekbang.org/resource/image/3c/2c/3cbd72b8968c23eaeb57737a9393072c.jpg" alt="">
这里,我想到一个问题,之前有人留言问我,程序员需不需要有美术功底。我在这里说一下我的看法。如果你只是要做一个程序员,那我可以告诉你,不需要。但是,如果你不是只想做一个“码农”,你想有更多的发展,那各方面的知识,比如策划、美术,你都需要去了解。
## 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))
```
我们将颜色设置为白色所以是255255255
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和350y轴的起始位置和结束位置是200和240为什么y轴也是200开始呢因为起始点 200既是x轴开始的点也是y轴开始的点。我们在代码里面这么判断鼠标的点
```
txt = &quot;Pause&quot;
x, y = pygame.mouse.get_pos()
if pygame.mouse.get_pressed()[0]:
if x &gt;=200 and x &lt;= 350 and y &gt;= 200 and y &lt;= 240:
txt = &quot;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了。
我们来看一下效果图:
<img src="https://static001.geekbang.org/resource/image/fb/a4/fb4e358092d3ad315e54f61cb984fca4.jpg" alt="">
我们看到了一个绿色的按钮放置在屏幕上并且有一个白色的Pause字样放在按钮上如果是鼠标左键点击在这个按钮上就会变成Clicked字样。
到这里,你可能会问了,为什么没有解释怎么输出中文呢?
在这种情况下,输出中文有两种解决方案。
<li>
第一种是比较底层的方案,就是**根据中文进行点阵绘制**,这需要很底层的代码操作,效率也不太高,所以这种方案我们不作讨论。
</li>
<li>
第二种就是**改变字体**我们可以在初始化字体的时候下载一些网上的中文字体先进行尝试或者我们可以使用系统字体。在使用系统字体的时候我们可以使用SysFont来初始化字体。
</li>
```
fnt = pygame.font.SysFont('微软雅黑',32)
```
另外还需要修改一个地方。我们需要在Python代码最开始的地方添加编码方式并且将中文文字前面添加u字样来告诉解释器是Unicode的。
```
# encoding: utf-8
txt = u&quot;暂停&quot;
```
我们来看一下效果:
<img src="https://static001.geekbang.org/resource/image/bb/d2/bba42a0cb409eedaab6a78f02cc456d2.jpg" alt="">
至此,中文的输出也已经完成了。
到现在为止我们编写完了UI的按钮部分。至于菜单部分我们也可以通过相同的方式来编写菜单效果。
作为菜单这些高级操作,比如点击出现下级菜单、隐藏菜单这些动态效果,可以使用图片的方式来制作菜单,并且进行载入。如果使用程序来编写菜单的效果,工作量就太大了。而如果是图片形式的话,只需要载入并且控制鼠标点击和绘制子菜单就可以了。
## 小结
今天我们学习了UI部分的编写以及文字的输出、鼠标的移动和抓取鼠标按键的实现我来总结一下今天的内容。
<li>
不管何种类型的游戏引擎鼠标的操作基本有3个值需要控制左键、中键和右键。
</li>
<li>
按钮可以用常规的方法来绘制。如果想做出更好的效果,比如画框,可以在框里面再画一个小框,看起来像是有凸出感觉的样子。这就需要你平时多注意观察一些细节,然后去分析如何用我们讲过的简单的操作来实现这些内容。
</li>
<li>
在2D游戏中很多游戏引擎都不支持中文的输出。如果要输出中文假如你的引擎支持那你可以使用系统字体或者其他中文字体如果引擎不支持可以使用一个一个点阵绘制的方式在屏幕上绘制中文。最后一种方式也就是比较极端的方式那就是使用图片来直接贴上中文字符这种方式直接粗暴但是图片资源量太大而且如果你要在游戏中进行网络聊天这里面其实还是没有从根本上解决中文输出的问题。
</li>
现在给你留一个小问题。
如果让你在上述的代码中,将按钮变成菜单,也就是点击按钮,就在下方出下一个下拉框,你会如何实现?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,185 @@
<audio id="audio" title="第17讲 | 如何用鼠标和键盘来操作游戏?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a2/07/a2860a659da0f8cf1d9a02e777de6f07.mp3"></audio>
**如果一款游戏不能用鼠标和键盘操作,那我们只能当动画来看。**
所以在一款游戏中鼠标和键盘的操作是必不可少的有时候甚至鼠标和键盘都要同时操作比如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_wK_s等等然后我们看到xx, yy 的一系列操作,然后我们进行飞机的贴图和操作:
```
screen.blit(pln, (100+xx, 300+yy))
```
我们看到基础坐标值是100300。我们经过键盘操作对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按键随后我们可以在游戏界面命令行看到如下内容输出
<img src="https://static001.geekbang.org/resource/image/48/4a/480b8b53350ddb06e6a70e6987c84a4a.jpg" alt=""><br>
找到那个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和左侧CTRLLCTRL的意思是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进行缩放其中缩放的大小长宽是4040并且返回一个新的 surface。
随后在循环里我们获取到这个surface的中心点也就是计算需要绘制鼠标的xy值我们需要得到图片的长宽并且除以2最后blit开始贴图我们看到的效果是这样的。
<img src="https://static001.geekbang.org/resource/image/28/a1/2840dec856ac2ed69eb6d3c8956926a1.jpg" alt="">
那么我们如果要判断鼠标的按键怎么办呢?我们来温习一下上一次所讲的内容,鼠标的按键,也是类似的判断方式:
```
x, y = pygame.mouse.get_pos()
if pygame.mouse.get_pressed()[0]:
....
```
前面那段代码用到的 xy的值在这里进行代码获取。
我们看到pygame.mouse.get_pos() 函数获取两个值x和y坐标后面一段代码就是获取鼠标点击的内容其中 get_pressed函数下标0返回是不是左键点击下标1返回是不是中键点击下标2返回是不是右键点击最后再做出判断。
## 小结
今天的内容基本到这里了,我带你将内容梳理并总结一下。
<li>
首先是键盘事件判断这里会出现按一下键盘做一下操作的情况问题出在KEYDOWN事件判断上。但是如果你需要一直按键的判断可以使用get_pressed函数。
</li>
<li>
组合键可以写在同一个判断下使用and连起来做判断。
</li>
<li>
get_pressed会返回一个tuple里面存放了所有的key值只要判断key值是不是为True就是判断了有没有按键。
</li>
<li>
鼠标操作也可以使用get_pressed函数也是返回tuple其中下标0、1、2分别代表了左、中、右三个按键。
</li>
最后,给你留一个小问题。
如果将组合键写在第一个按键的判断下,会出现什么情况?
```
if key[pygame.K_LCTRL]:
if key[pygame.K_w]:
...
```
欢迎留言说出你的看法,我在下一节的挑战中等你!

View File

@@ -0,0 +1,125 @@
<audio id="audio" title="第18讲 | 如何判断客户端的网络连接?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/21/a4/213efd8525d38e60060de7d47c4d5ea4.mp3"></audio>
由于涉及到网络、脚本语言等等,这一节起,我要开始讲一些基础的、看起来比较枯燥的知识。我会尽力写得有趣生动,但是,知识的获取并不容易,即便我已经在努力去讲解,还是需要你用更多的时间去摸索和学习。
我们在前面说了Pygame的一些客户端知识如果你想让这款游戏能够在网络上传输数据接下来那就需要编写服务器端并且在客户端做客户端连接的准备。
前面我们已经用Pygame讲解了很多内容那我们客户端的网络连接我也继续使用Python来编写当然这已经和Pygame没有关系了。因为网络连接是独立的代码所以只需要直接写在游戏里就可以了。
在开始编写网络部分之前,我们需要整理一下网络的基础知识。如果我们一上来就编写网络代码,对于你来说,可能会有一些概念上的模糊。
对于网络编程或许你已经接触到了或许你只是有点概念或许你一点都没接触过。但是你或许听说过Socket套接字、TCP/IP协议、UDP协议、HTTP、HTTPS协议等等那么这些协议是不是都属于网络编程范畴还是这里面哪些部分是属于网络编程范畴呢
网络,从模型上讲,一共分为七层,从底层到最上层,分别是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
我来分别解释一下。
<li>
**物理层**:所谓的物理层,你可以理解为我们看到的各种网络线,也就是人肉眼能看到的物理线路,包括光纤、以前连接调制解调器的电话线等等。这些线路就是物理层。物理层有物理层的规范,比如电流、编码、帧、连接头等等。你只需要知道物理层也是存在规范的,就可以了。**物理层最主要的功能就是网络的物理连接**。
</li>
<li>
**数据链路层**所谓的数据链路层就是建立逻辑连接然后进行硬件上的寻址操作、差错的校验然后将二进制的字节进行组合通过MAC地址进行访问比如网卡、交换机等等。你需要记住的是**在这一层要通过MAC地址来进行访问进行硬件寻址操作。**
</li>
<li>
**网络层**网络层进行逻辑地址的寻址操作和数据链路层不同。数据链路是使用硬件寻址操作而网络层是使用逻辑地址寻址比如路由器、防火墙、多层交换机等等。我们最熟悉的IPv4202.101.253.233、IPv6、ARP等等都属于这一层。你在这里需要记住的是**网络层是逻辑寻址操作会用到ARP、IPv4等等协议。**
</li>
<li>
**传输层**在编程中最常用到的TCP、UDP等等协议都在这一层进行操作它首先定义了数据传输的协议端口号以及一些错误的检测。
</li>
<li>
**会话层**:会话层在传输层之上,它就在客户端和服务器端。严谨地说,就是本地机器和远端机器之间的会话,比如要进行断点续传这些操作,就属于会话层的范畴。
</li>
<li>
**表示层**:表示层很容易理解,就是数据的传输,然后展现在电脑上。比如图片的传输和显示、网络地址的加密等等。
</li>
<li>
**应用层**应用层就是提供给电脑用户的各种网络应用比如你自己编写的网络软件、FTP软件、Telnet、浏览器等等。
</li>
以上这些点你要硬性记住的话,会比较困难。我教给你一个方法。
首先,我们想象一段从网线过来一段数据,网线就是“物理层”,那么数据需要找到一个门牌号,这个门牌号是一个硬件地址,可能是你的电脑网卡,也可能是你公司的交换机。这些数据需要把这些门牌地址连接起来,这就是“数据链路层”。
随后这些数据找到门牌号后就需要分发到逻辑地址比如路由器或者你的IP地址这些逻辑地址就是网络地址这就是“网络层”。
经过网络层后就要看这是什么数据是TCP协议的还是UDP协议的。知道了协议后才可以传输数据所以这个是“传输层”。
那么在传输的过程中,可能会中断,所以我们需要登录服务器,断点续传进行重新传输,这些属于机器和机器之间的会话,所以是“会话层”。
传输完数据后,我们就会在电脑里显示这个内容,是一幅图片呢,还是一段电影?这个需要表示出来,所以是“表示层”。
最后,我们将这个一整套的东西,写成了一个应用,这就是“应用层”。
虽然这么表述起来,有许多不精确不严谨的地方,但是通过这段话能让你很快记住这个七层网络模型,对你将来的编程有很大的帮助。
Python支持Socket编程也就是支持TCP、UDP等协议的编程也支持更上层的编程比如HTTP协议等等。在今天的内容中我们要用到TCP编程。至于为什么要使用TCP有这样几个原因
<li>
TCP保证连接的**正确性**。在建立TCP连接的时候需要经过三次握手连接这一方发送SYN协议被连接方返回SYN+ACK协议最后连接方再返回ACK协议
</li>
<li>
TCP保证如果在一定时间内没有收到对方的信息就重发消息保证消息传输的**可靠性**
</li>
<li>
TCP可以进行**流量控制**。它拥有固定大小的缓冲池TCP的接收方只允许另一方发送缓冲池内所接纳的数据大小。
</li>
TCP还有其他更多的保证传输可靠性的内容和标准我在这里不做更多的阐述。另外使用TCP可以进行长时间的连接在客户端和服务器端之间进行不停地交互。在交互过程中服务器端发送数据给客户端客户端就能做出相应的回应。
在Python中编写TCP协议的代码比之使用C/C++更为方便。因为C/C++需要初始化一系列的内容然后进行顺序的流程化绑定设置网络参数最后进行发送和接收操作在结束的时候进行资源的回收。而在Python这里只需要设置协议和IP地址就可以实现TCP协议编程。我们来看一段代码。
```
import socket
class go_sock(object):
ip = &quot;&quot;
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(&quot;115.231.74.62&quot;, 21)
_inet.sock.recv(100)
```
在这里,我们可以简单测试一下某些应用服务器,然后接收返回内容。这个类的封装工作到此就告一个段落,更多的网络服务和交互的编写,我将在下一节阐述。
## 小结
今天我们学习了网络的七层模型结构以及我们将要在游戏中使用的TCP协议的编程。
<li>
我用了一个传输过程介绍了七层每一层做的事情,这个你一定要牢记。
</li>
<li>
我们使用Python封装了Socket库的细节内容只需要直接编写connect代码就可以进行数据的接收和发送操作了。
</li>
<li>
选择TCP协议是因为它安全可靠能保证游戏传输的过程中不出错。
</li>
现在,我给你留一个小问题吧。
如果我们要使用UDP来编写这个网络服务该如何保证数据的准确性呢选择UDP协议的优势在哪里
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,212 @@
<audio id="audio" title="第10讲 | 如何载入“飞机”和“敌人”?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a8/39/a8939945c3db1a6c8dbcf40f3376d739.mp3"></audio>
上周,我向你解释了如何载入背景图片,以及如何使用坐标值的变换来移动背景图片。今天,我们要载入主角“飞机”和一些“敌人”。
## 导入随机函数
我们选择Python做为开发语言。在开始之前需要用到一个随机函数这个随机函数会在之后的代码中用到具体用作什么我会在后面揭晓。
首先需要将随机函数导入Python的随机函数库random。代码可以这么写
```
import random
```
然后我们需要限定一个范围来生成随机数比如10到100的范围代码可以这么写
```
random.randrange(10, 100)
```
这个函数会接受三个参数:开始、结束、递增数字。
<li>
开始:开始随机的指定范围数值,包含在范围内。比如(10100)就包含10。
</li>
<li>
结束:开始随机的指定范围数值,不包含在范围内。比如(10100)不包含100最多到99。
</li>
<li>
递增:指定递增数字。
</li>
如果不填写递增值,则按照开始、结束的值随机分配。比如 (10100) 那就会在1099之间随机分配任何一个数字。
## 载入主角飞机
我们说完了随机函数的准备工作,就可以开始载入飞机了。
我们假设主角的飞机是从下往上飞,那它的飞机头应该是朝着电脑屏幕上方,而敌人的飞机是从上往下出现,所以它的飞机头应该朝着电脑屏幕的下方。主角的飞机暂时固定在屏幕下方,敌人的飞机则一直不停从上往下飞。
飞机的图片是我从共享的图片网站上抓取下来让美术帮我处理和加工了一下。其实就是将飞机从一整块背景图片上抠除下来让飞机看起来拥有飞机本身的轮廓而不是一幅“方块”的飞机图片然后将其图片保存成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函数更新屏幕。
我们来看一下贴图的效果。
<img src="https://static001.geekbang.org/resource/image/3b/ca/3b13e0a3ff4b7006ee4a1ddbaf8309ca.jpg" alt="">
我们已经将这幅图片贴了上去。
在载入的过程中如果我不使用convert_alpha函数会怎样呢我们也来做一下实验。
```
pln = pygame.image.load(plane).convert()
screen.blit(pln, (40, 350))
pygame.display.update()
```
我将 convert_alpha 改成了convert来看一下效果。
<img src="https://static001.geekbang.org/resource/image/4b/57/4ba2eb42b15c44056099e53489104d57.jpg" alt="">
看到了那个大大的黑色色块没有这就是我们没有处理alpha通道导致的结果导致了一个大大的抠图色块出现在屏幕所以要记住
<li>
设计主角图的时候,要将图片抠下来;
</li>
<li>
在贴图的时候需要进行alpha混合的处理否则贴上去的图会存在抠图黑块。
</li>
## 载入敌人飞机
接下来,我们要从屏幕上方,贴一架敌人的飞机。
```
enm = pygame.image.load(enemy).convert_alpha()
screen.blit(enm, (30, 10))
pygame.display.update()
```
我们将两架飞机前后代码整合起来,再来看一下效果。
<img src="https://static001.geekbang.org/resource/image/ee/31/ee3059e4cb999d4a5f532c634e88d331.jpg" alt="">
这样,我们将两架飞机都贴在了屏幕上了。看起来是不是有点像样了呢?
敌方肯定不止一个飞机,那我们就需要贴更多的敌方飞机。这里我们就需要用到最开始提到的随机函数了。为什么使用随机函数呢?因为我们需要让敌方飞机的排列看起来很随机(笑)。
我们现在要加载相同的敌方飞机图片,加载三次。也就是说,我们会在屏幕上方的一个固定区域范围贴上三次敌人的飞机。我们需要准备三个随机 (xy) 位置的数字,并且赋值给 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))
```
这样,我们就贴上了三幅飞机的图片。
我们再来看一下效果。
<img src="https://static001.geekbang.org/resource/image/b9/7d/b932a985b390ccf359a4a56f25796c7d.jpg" alt="">
是不是看起来很有意思但是这样并不能让飞机动起来我们需要用到上一节里移动背景图片的知识来让敌人的飞机动起来。我们只需要将这三个y值在循环中设置成递增就可以做到三架飞机的移动了。代码就像这样
```
screen.blit(enm, (ex1, ey1))
screen.blit(enm, (ex2, ey2))
screen.blit(enm, (ex3, ey3))
ey1 +=1
ey2 +=1
ey3 +=1
```
这样我们就完成了敌人飞机不停往下飞的效果了。
后面的内容,也会像现在这样,代码很多,我带你再梳理一下逻辑。
**首先我们需要使用Python程序库的随机函数来制作随机数。**
<li>
通过这个随机函数,来随机载入敌人飞机的位置。当然如果有游戏策划的话,游戏可能会由某种固定的起始点来刷出敌人飞机,这里我们只用到随机函数来刷敌人飞机。
</li>
<li>
如果想要做得更漂亮的话我们可以将随机函数的值从屏幕最上方刷出来这样看起来敌人就是从屏幕最上方飞下来的。比如我们可以设置y值为-10左右。
</li>
<li>
如果想要做得更精细的话我们可以通过程序得到图片的长和宽。通过图片的长和宽来计算刷出飞机的位置我们可以使用屏幕大小来减去飞机长宽的大小来计算比如屏幕长是640图片的长是8。那么我们在设置 x 轴位置的时候就应该最大只设置到640-8这样的位置。这样就不至于我们在编程的时候只刷出半架飞机或者根本就看不到飞机。
</li>
**其次,我们在载入敌人飞机的时候,需要贴三幅图片。**
<li>
当然我们可以优化这一系列的代码比如我们可以将一系列blit放在一个函数里面。上述的代码只是一个针对教学用的代码为的是让你更直观、明了地能看明白如何载入三幅敌人飞机的图片。我们优化了代码后可以直接使用一段代码和一系列数组就可以完成这个操作。
</li>
<li>
如果做了一幅alpha通道抠图的图片我们在载入的时候需要处理alpha通道的数据让其图片达到“透明”的效果而不是直接贴一幅有黑框的图片。
</li>
**最后,事实上,我们要将这些内容更加完善,还有许多的工作要做。**
<li>
这些工作我将在后续的内容中展开讲解。比如我们需要移动背景。这个我们上次已经说明了。在敌人飞机往下飞的过程中,我们需要考虑敌人飞机往下飞的速度,是不是要比屏幕移动的速度更快或者更慢,这样才能体现敌人飞机的等级高低,体现出游戏的难度是随着关卡的变化越来越难的。
</li>
<li>
我们将游戏背景的图片blit函数放在游戏循环的最开始而载入飞机的代码则放在稍后的部分那么如果我们将游戏背景的图片放到飞机之后载入会发生什么事情呢如果你一直在练习我在文中提供的代码你应该可以知道这个时候飞机的图片都会不见了只能看到游戏背景。这是因为Pygame是按照blit代码的顺序来载入图片的这部分内容我在后面的内容中讲解。
</li>
<li>
我们可以将载入的图片资源放到一个资源包中或者放在一个目录中这样游戏的目录就不至于看起来乱七八糟而是非常有序的。比如我们可以将所有和主角飞机相关的内容就放在飞机的目录下和敌人相关的就放在敌人的目录下背景和关卡就放在关卡的目录下这样就看起来就整整齐齐。在编写代码的时候从目录的名字不同可以知道载入的是什么内容比如enemy/plane.png
</li>
## 小结
好了,这节内容差不多了。我主要和你讲了三个内容:
<li>
随机函数使用random.randrange来做输入开始和结束值就能随机出这一个范围的数字;
</li>
<li>
让飞机移动起来需要将x或者y的值进行加减变化;
</li>
<li>
处理alpha混合半透明图片需要使用conver_alpha函数。
</li>
最后,给你留一个小思考题吧。
```
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函数中会出现什么样的结果呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,157 @@
<audio id="audio" title="第11讲 | 如何设置图像的前后遮挡?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e5/f3/e5aef9e29a582eb7076e70cf72eb3bf3.mp3"></audio>
我们人的肉眼所观察到的世界是属于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 &gt; 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进行第二次的贴图然后继续循环贴这样我们的围墙就开始连贯了起来。
我们来看一下贴上去的效果。
<img src="https://static001.geekbang.org/resource/image/5e/91/5e15bd7c6cff5c0ff0080090ae1cc391.jpg" alt="">
可以看到每隔一段贴一幅图另一段贴另一幅图这样一整段的围墙就贴完了。一共有十幅图片每一副图片的y值都向上减去30。
现在我们来总结一下贴这些连贯图片的重点:
<li>
将内容放入列表或者数组中。为了编程方便,将需要连续贴图的内容放入列表或者数组中就能够减少编程工作量;
</li>
<li>
计算好贴图的点,能让我们在连续贴图的过程中,只要控制位置变量就可以完成。
</li>
如果我们编写的是地图编辑器而地图编辑器生成的脚本代码除非写得非常智能一般来讲就是一连串的贴图代码这样就会有许许多多的blit的操作并不会将相同的元素加入循环或者列表那是因为脚本代码是电脑生成的没有更多的优化代码。
接下来,我们要将一个人物放上去。这个人物只是摆设,我们只是为了测试图像遮挡的情况。
```
player = 'human.png'
plr = pygame.image.load(player).convert_alpha()
```
然后我们在循环的围墙贴图的代码之后,放入人物。
```
screen.blit(plr, (62, 270))
```
我们将人物故意放在围墙的某一个位置,效果看起来是这样的。
<img src="https://static001.geekbang.org/resource/image/32/a4/32e01951e1b5d28ec1cd0a616b9019a4.jpg" alt="">
这样看上去人物就站在围墙上面了。看起来他似乎有飞檐走壁的功夫然而事实上他应该几乎被围墙挡住但是这个时候问题就来了。虽然我们可以把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中进行绘制。效果就是人物被遮挡到了围墙外面。
<img src="https://static001.geekbang.org/resource/image/5d/1f/5d4eb9552dce19be5a8b184fe347391f.jpg" alt="">
这段代码起作用的地方是在[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值相同的话人物碰到一起会出现什么结果呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,245 @@
<audio id="audio" title="第12讲 | 如何设置精灵的变形、放大和缩小?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7e/2a/7e6a8426d16c7e043876f0acc5d7c42a.mp3"></audio>
上周四,我给你讲解了图片的遮挡问题。这一节我要和你讲精灵的变形、放大和缩小。如果之前没有做过游戏开发,你肯定会问,什么是精灵?
## 什么是精灵?
我先来解释一下什么是精灵。精灵当然不是我们传统意义上的什么树林里的精灵。精灵是一个游戏开发中的名词英文叫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))
```
我们看到的结果是这样:
<img src="https://static001.geekbang.org/resource/image/96/2f/961af51b04e51d3ba802e44a1fd2382f.jpg" alt="">
原本飞机的头是朝上的,现在进行了水平和垂直的翻转。
### 缩放函数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然后看一下效果。
<img src="https://static001.geekbang.org/resource/image/78/66/78b91a5ea30d2eaa2c08fce1ca749b66.jpg" alt="">
你看,飞机变大了。我们再尝试修改一下代码。
```
pln_t = pygame.transform.scale(pln, (20,20))
```
<img src="https://static001.geekbang.org/resource/image/1b/58/1bd5885a91a0462e852ddcbc15132358.jpg" alt="">
飞机就变小了。所以,**scale函数**的作用是,**只要你传入参数的width和height值大于原本精灵的长宽值就变大否则就变小。**
类似,我们还有一个**函数scale2x**你只需要填入绘制对象即可函数会帮你进行两倍扩大不需要你计算原本的长宽值并且乘以2。
### 旋转函数rotate
我们再来看一下**rotate旋转函数**。它提供一个参数angle也就是你需要旋转的角度正负值都可以。
我们来看一下代码。
```
pln_t = pygame.transform.rotate(pln, 20)
```
我们看到的效果就像这样。
<img src="https://static001.geekbang.org/resource/image/62/39/62704bbf6e240e17ade2a907a82d3939.jpg" alt="">
这样飞机就朝左侧旋转了20度。 相似的,也有整合的函数**rotozoom**。它该函数提供了旋转和扩大的功能。
如果代码这么写:
```
pln_t = pygame.transform.rotozoom(pln, 20, 2)
```
我们能看到的效果就是这样:
<img src="https://static001.geekbang.org/resource/image/17/f0/1785173b9a5e6e4f149b1c3d9797dbf0.jpg" alt="">
### 剪切函数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]这样一个矩形进行裁切。
然后我们来看一下效果。
<img src="https://static001.geekbang.org/resource/image/d0/63/d06607b46aa22f6321edf7a76ee7ab63.jpg" alt=""><br>
<br>
这么多函数,是不是容易记不住?我来给这一部分做个总结:
**对于精灵的所有放大、缩小或者变换的函数都在pygame.transform模块里。它提供了一系列2D精灵的变换操作包括旋转角度、缩小放大、镜像、描边、切割等功能让你很方便地能够在游戏中随心所欲地对处理2D精灵。**
## Pygame中的Sprite
我们再来看一下Pygame本身Pygame本身就提供有Sprite模块Sprite模块提供了Sprite类事实上Pygame的精灵类最方便的功能就是将某些序列帧的图片做成动画并且保存在Sprite的组group里面。在Pygame里面Sprite是一个轻量级的模块我们需要做的是要将这个模块继承下来并且重载某些方法。
### 类explode
我们现在有一副图片,效果是打击到某个点,开始爆开图案。
<img src="https://static001.geekbang.org/resource/image/68/e5/68ee7fc3ef87a7fe6a471da3837626e5.jpg" alt="">
这幅图片一共三帧是一个标准的精灵动画。那么我们需要做的就是先将这幅图片导入到精灵类当中。我们做一个类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 &lt; 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拥有众多方法可以调用这样就会有分层的概念。
## 小结
这一节,你需要记住这几个重点。
<li>
精灵的变形、缩放以及pygame中关于精灵类的一些简单的操作。
</li>
<li>
你可以直观地感受到精灵类和group类配合起来使用是一件很方便的事情也就是说我们忽略了blit的这些方法直接在group中进行update和draw就可以一次性做完很多的工作。
</li>
<li>
如果我们单独编写精灵的序列帧动画函数也不是不行但是你可能需要编写相当多的代码来代替Sprite和group类的工作。
</li>
现在留一个小问题给你。
结合精灵的变形、放大和缩小再结合Pygame精灵类的内容要在update重载函数里绘制动画帧效果并且不停地放大、缩小该怎么实现呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,167 @@
<audio id="audio" title="第13讲 | 如何设置淡入淡出和碰撞检测?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3a/2d/3ab5046253a5726e6f86fb57e0d8d32d.mp3"></audio>
我们在前一节,学习了精灵的变形、放大和缩小,并且学习了如何使用精灵类和组的概念来管理精灵,制成动画。今天,我将带你学习淡入淡出和碰撞热点的判断。
所谓的**淡入淡出**,就是英文的**fade-in**和**fade-out**。淡入淡出在电影、游戏、CG、操作系统、手机UI、应用等等各种地方随处可见。那究竟什么是淡入淡出呢它在游戏中究竟如何实现呢在我们的打飞机游戏中什么时候会用到这个操作呢
## 什么是淡入淡出?
不知道你有没有注意,在我们玩过的打飞机游戏中,当每一关游戏开始的时候,都会有个游戏画面逐渐出现的过程。短短几秒,从无到有,整个画面就呈现在你眼前了。同样,每一关结束的时候,也会有个画面逐渐消失的过程。
从**画面效果**讲,这个画面从有到逐渐屏幕变暗,直到消失,或者反过来,由暗逐渐变亮,到完全进入画面的过程,就叫做淡入淡出。从**声音**角度讲,也存在淡入淡出,比如音乐从无声到逐渐有声,或者从有声到逐渐无声。
**在Pygame中并不存在“画面的淡入淡出”这样的函数需要我们自己去实现这样的功能。**
首先如果我们想给这张图片进行淡入淡出的处理的话就需要对它进行alpha混合处理。我们在前面谈到过alpha混合你可以理解成半透明但是alpha混合究竟是什么呢
**alpha混合**就是将一部分被遮盖的图像进行半透明处理。在游戏引擎或者游戏库中图像的alpha值是可以被修改的。每动态修改一次alpha值就会让图像更透明或者更不透明。通过制作出alpha效果我们可以在游戏中实现各种绚丽的效果。
一般来讲底层图形接口的颜色为32位的值包含RGB以及Aalpha其中红色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 &gt; 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出来呈现在屏幕上。
我们呈现的效果就是这样。
<img src="https://static001.geekbang.org/resource/image/2b/1c/2b6bc9023c281fa5ae7f44bbe51ef01c.jpg" alt="">
其他图片也可以做alpha混合我们将最早的背景jpg图片传入进行alpha半透明调整效果是这样的。
<img src="https://static001.geekbang.org/resource/image/9c/ac/9c71cf0a87d6ebb9c2fd6e7f323d09ac.jpg" alt="">
## 如何设置碰撞检测?
说完了alpha混合我们现在要来学习一下碰撞相关的内容。这个很好理解飞机相撞了就要用到碰撞。
事实上在游戏中碰撞属于物理引擎的一部分。特别是在3D游戏当中物理引擎是独立于图形引擎的一个模块。程序员需要将图形引擎的对象填入到物理引擎中计算出碰撞的结果然后再返回给图形引擎给出画面效果。做得精致的2D游戏也有独立的物理引擎专门检测碰撞、计算重力等等。
但是在今天我们的课程中,我将使用浅显易懂,用你最能看懂的代码来解释碰撞是怎么回事。
**事实上,我们今天要讲到的碰撞是两个图片相交之间的碰撞检测,这并不算物理检测,而是图片检测。**
既然我们要检测的是图片,那么哪些前置信息是我们需要知道的呢?
首先,我们肯定要知道这两张需要碰撞图片的长宽,才能计算图片是否相交。在计算图片相交的时候,我们首先要知道它**所在位置的x轴的起点**,然后要知道它的**图片宽度**,然后我们要知道**图片位置的y起点**,以及它的**图片长度**,这样我们就得到了图片的长宽。
我们用上面的主角飞机图片和敌人飞机图片来做演示。
<img src="https://static001.geekbang.org/resource/image/86/68/861154a6a7403ef7113c259f65425d68.jpg" alt="">
让两架飞机面对面,敌人的飞机从上往下飞,主角飞机从下往上飞。如果两架飞机碰到,我将在后台的命令行窗口显示一些字符串。
### 定义碰撞函数
接下来,我们来看一下,如何定义这个碰撞函数。
```
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)那就会形成01234。
我们在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就是碰撞了。
<img src="https://static001.geekbang.org/resource/image/bb/35/bbc8171fa91b1cc49bede8ba38f2ea35.jpg" alt="">
当两架飞机碰到的时候True就出现了那是因为x轴和y轴都有不同程度的重叠。所以在collide函数里面就返回了True。
另外在Pygame里精灵类当中也有碰撞检测的类可以提供使用但是**使用碰撞检测类可以用来进行球形的判断,而不能用于矩形图片之间的判断**。 这是更为高级和复杂的用法,在这里不做更深的阐述了。
## 小结
今天我和你讲解了淡入淡出以及碰撞的热点检测。我们需要设置Alpha混合和背景填充来实现淡入淡出而普通图像碰撞的检测则是通过判断图像x轴和y轴是否重叠来实现。
给你留个小问题吧。
如果给你一张图片,需要判断精准的碰撞,比如碰到机翼,或者碰到某一个非矩形的位置,你该如何判断碰撞结果?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,119 @@
<audio id="audio" title="第14讲 | 如何制作游戏资源包和保存机制?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/26/c5/26abded8e11ef1dde21c5177417b77c5.mp3"></audio>
我们要做一款打飞机游戏,里面有飞机图片、背景图片、飞机音效、碰撞音效等等非常多的素材。如果将这些资源都放置在一个目录下,将会变得非常混乱。如果按照素材内容来划分目录,程序读取的效率就不高,所以我们需要将这些素材打包在一个资源包内,然后将每个素材都放置在一个虚拟目录内。
因此,今天我们就来如何制作讲解资源包。简单来说,所谓的资源包,就是将游戏的所有资源和素材,进行打包分类,并且进行资源整合,亦或将资源和素材进行压缩,减少游戏体积。
## 什么是资源包?
我总结了一下,可以从这三个角度来理解什么是资源包。
<li>
资源包是一种将游戏的资源和素材进行分类、梳理,并且打包的一种包裹。
</li>
<li>
资源包可以用来压缩游戏资源和素材,减少游戏体积。
</li>
<li>
资源包里存在任何可能性,比如它可以包含图片文件、模型文件、音频文件、脚本文件等等,具体要看游戏开发人员的配置需求,来决定资源包里的内容。
</li>
现在很多游戏公司都不会编写特殊的资源包格式。因为设计一种资源包格式,需要经过一系列复杂的动作,包括包头、包身和包尾。
关于这个格式的设计,一会儿我会给你仔细分析。因为,和我们自定义网络协议包一样,一个好的资源包,能够很方便进行解包、打包、删除文件、插入文件的操作,以及游戏的在线更新、补丁更新、资源包的解包、打包、删除、插入、更新文件等操作。
而一个好的资源包格式,不会占用主程序大量的时间。因为在游戏中,需要直接读取包文件里面的内容。
比如我们之前在Pygame中读取的图片文件在包裹格式中可能会这么写伪代码
```
load.image(package.pack/plane.png)
```
其中package.pack就是包裹plane.png是存在在包裹里面的其中一幅图片文件。这样打了包裹后的文件就不会污染目录。一般一个包裹文件中存在大量资源而我们只要按照包裹路径读取就可以了。
如果不编写特殊的资源包格式,那应该怎么制作资源包呢?答案是,**使用现成的压缩软件库,进行打包压缩,直接在程序内使用**。比如我们最常用的zip文件、rar文件都是可以拿来做资源包文件的。在Python中有内置zip模块可以直接读取zip文件。我们可以直接在Pygame中结合zip模块进行编程。
## 资源包的格式
我们要讲解的是资源包的制作,我将会用一种较为通用和简单易懂的方法,解释资源包都包含哪些内容,同时让你理解资源包是怎么制作的。
首先,从编程的格式来理解资源包,你需要了解下列这些内容。
<li>
**资源包头**是一种标记存放在包裹里最开始的几个字节一般是24个字节。资源包头可以用来辨别这个资源包是哪个公司出品的。例如我后面准备举的一个例子这里面就有INFO这样的标记INFO可能是这家游戏公司的名字或者是缩写等等。
</li>
<li>
**资源包版本**这个不是必须的。如果考虑到一款游戏各个版本之间变化很大未来可能会修改资源包的格式那么这个时候就需要版本号。版本号一般会使用2个字节的short类型来存储或者直接用十六进制编辑器能看明白的字符串来代表版本号比如用10表示1.0。所以结合资源包头我们现在所看到的结构是INFO10。
</li>
<li>
**资源包是否进行压缩**这个也不是必需的但是有一些资源包会说明白究竟是不是压缩资源包。如果是压缩就是1不是压缩就是0。至于压缩格式一般在编程的时候就会指定清楚不需要特别说明在资源包内。
</li>
<li>
**资源包的目录结构以及素材名文件名偏移量**,资源包内的目录结构都是虚拟的,所以你可以定义在资源包内类似于/game/res这样的目录结构。但是事实上这只是方便程序调用事实上目录是不存在的这是一种只存在在包裹内的虚拟目录。
</li>
然后,我们需要规定素材的**文件名**和**偏移量**。比如/game/res/background.jpg。这是告诉我们在/game/res虚拟目录下拥有background.jpg这个文件。随后需要告诉程序偏移量是多少一般是存储4个字节的整型数字。
到目前为止,资源包的格式看起来可能是这样的:
```
INFO100/game/res/background.jpg,[四个字节的偏移量]
```
在这里,我们看到,偏移量之前多加了一个逗号“,”。这是一个**分隔符**,也就是告诉程序,这一段在哪里结束。
随后是四个字节的偏移量。所谓的**偏移量**,就是告诉程序,你要到这个包裹的第几个字节去寻找这个文件的具体内容。
<li>
**资源包的素材本体**。每个本体都可能是一个二进制文件、文本文件或其他任何文件。这些文件的文件名在资源包的素材文件名中都被定义好了。在资源包的素材本体中,我们可能会碰到各种各样的二进制字符,那么我们怎么知道这些素材是从哪里开始哪里结束的呢?
</li>
<li>
**资源包的素材长度**,规定素材的长度有两种方法,**一种方法**是在定义资源包的目录结构以及素材偏移量的时候,再加上一个素材长度,也是四个字节的整型数字。这种方法的好处是,不需要添加某个分隔符告诉程序,这个素材的本体到这里结束。**第二种方法**是在本体结束的位置添加分隔符,比如一个逗号或者分隔符号|。这种方法的好处是,不需要知道文件长度是多少。但是坏处是,分割符号可能会和素材本体重叠。
</li>
比如素材的本体是个二进制文件,分隔符比如是!@#$,素材的本体里面也存在!@#$这样的内容,这样的情况下,就会出现读取中断,因为程序以为素材内的!@#$就是结束符号,事实上这只是素材本身的内容而已。
- **资源包结束符**,这个也不是必须的。我们要结束资源包,必须在资源包的结尾添加结束符,这个结束符是告诉程序,资源包已经结束了。
我们来看一个完整的资源包,大概是什么样子的。
```
[资源包头][版本号][是否压缩][资源包目录/素材文件名A][文件A偏移量][文件A长度]…[资源包目录/素材文件名N][文件N偏移量][文件N长度][素材A本体]….[素材N本体][结束符]
```
了解了资源包的格式内容我们可以很方便地利用Python或者C语言等来编写相应格式的资源包。
我来给这部分做一个总结:
资源包的存在,有两个目的,一是让游戏目录干净整洁,不然看上去都是乱七八糟的图片和各种配置,二是让游戏程序能更快地从内存中读取游戏资源制作的包裹文件,加速游戏的运行效率。这个包裹文件中含有虚拟目录、资源、资源位置、资源名字等等信息。我们不需要从文件目录中去读取单一文件,只需要从内存中载入的资源包中取出某个文件即可。
## 如何制作游戏的保存机制?
每一个游戏几乎都有保存和载入的机制。首先你需要知道,只有保存了数据,我们才能载入数据。那么游戏的保存机制是怎么做的呢?
事实上,游戏的保存和游戏的地图编辑器中保存地图的原理,可以说是异曲同工。如果一个游戏中,有地图、坐标、人物、装备、分数,这些都需要被记录下来,那么我们不可能将地图、坐标、人物、装备、分数等全部转换成二进制文件记录下来。那应该怎么做呢?
首先如果是记录地图有地图1或者地图2我们只需要记录地图的ID就好了。假如是地图2坐标是xy。人物只需要记录人物的ID再关联到人物。一个游戏中玩家建立了一个人物角色就会将这个人物角色进行保存不至于丢失人物角色。所以在读取游戏的时候需要先读取人物角色再读取保存的游戏内容。
至于分数就很好记录了,记录分数其实就是记录数字,所以记录起来会很方便。
那么装备呢如果是装备一般会将装备的所有内容记录下来如果做得精致的游戏还会将地图中那些掉落的装备和死去的NPC进行记录。
还有一种做法是,将游戏保存的文件直接导出成一个脚本文件,以后每次读取数据就只需要使用程序读取脚本就可以了。
## 小结
今天我讲解了资源包的制作以及游戏进度的保存,你需要你记住这些内容。
- 制作资源包的目的是为了厘清游戏素材以及游戏素材的存放结构。资源包的结构与压缩包的结构比较相似,但是为了更贴合游戏程序读取,会对虚拟目录和素材文件名等,做一些修改。
- 另外,为了方便保存游戏进度,我们可以做成游戏脚本,第二次打开游戏直接载入保存的脚本即可。
给你留一个小思考题吧。
在《GTA》中汽车会有不同程度的损毁当你保存完游戏重新进入的时候汽车又复原了请问这是为什么呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,159 @@
<audio id="audio" title="第15讲 | 如何载入背景音乐和音效?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d6/68/d6924e7807e48a266c6d8535c4ecd868.mp3"></audio>
好的音乐总是伴随着游戏一起,一直被玩家所记忆。在游戏中播放音乐和音效并不是什么困难的事情,但是究竟什么时候播放什么音效,具体怎么实现,这恐怕就需要一些技巧了。比如,我今天要讲的,我们可以和某些函数捆绑在一起实现。
Pygame支持mp3、ogg、wav音频和音效的播放。音乐的模块都在pygame.mixer中这里面包括音乐和音效。
我们在使用音频部分模块的时候,需要先初始化一次。
```
pygame.mixer.init()
```
这个初始化应该在pygame.init()的初始化之后。
我们来看一下具体的函数这些函数存在在pygame.mixer.Sound模块下。
<img src="https://static001.geekbang.org/resource/image/29/4d/299c0650d736f939189c49b32eb2b54d.jpg" alt="">
我们再来看一下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](http://www.un4seen.com) 下载开发库。这个音频库是不开源的,如果你只是自己开发游戏玩玩,非商业目的,就可以使用。如果是商业使用,那就需要购买证书。
<img src="https://static001.geekbang.org/resource/image/90/e2/9022635f73854d8b464a188c585ee6e2.jpg" alt="">
在这个页面上我们点击download按钮就会下载最新版本的开发库。解压缩下来会出现对应几个语言的开发目录。
其中bass.dll文件是动态链接库要使用的话可以在c文件夹下使用lib库和bass.h进行头文件包含进行编程。
我们来看一下如何使用C/C++语言加入Bass引擎的代码。
```
BASS_Init(-1, 44100, 0, hwnd, NULL);
HSTREAM s = BASS_StreamCreateFile(false, &quot;a.mp3&quot;, 0, 0, 0);
BASS_ChannelPlay(s, false);
BASS_StreamFree(s)
```
首先,我们将 BASS 库初始化初始化的参数是设备、输出比率、标志位比如8位音质、立体声、3D等等、Windows句柄。你也可以输入0。最后一个是clsid就是用于初始化DirectSound的类的ID一般会使用NULL。
随后开始从文件建立一个流BASS_StreamCreateFile函数返回一个HSTREAM。HSTREAM其实是一个DWORD类型。
这个函数里的参数,我也解释一下。
<li>
第一个参数是内存。如果传入true的话就将这个流保存在内存中否则的话就不保存在内存中。
</li>
<li>
第二个参数是音频文件名。这个参数和第一个参数会联动。当第一个参数保存在内存中的时候,就填入内存地址,否则就填入文件名。
</li>
<li>
第三个参数是偏移量也就是文件从哪里开始播放。当然这个参数只在第一个参数为false不保存在内存的情况下起作用。
</li>
<li>
第四个参数是长度如果填入0就是所有长度。
</li>
<li>
最后一个是标志位,填入的是创建模式,比如是循环播放方式,还是软件解码模式等等。
</li>
接下来就是开始播放,第一个填入的是刚才返回的流的句柄,第二个参数是是否重新开始播放。最后一个就是播放完后进行回收资源,删除句柄。
```
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中音频和音效的播放。你应该记住这些东西。
<li>
在Pygame中播放音乐是不需要进行多线程控制的。它本身就会在后台进行播放。
</li>
<li>
所有的音乐和音效都在pygame.mixer模块中如果载入的是音乐就使用music模块如果载入的是音效就使用Sound模块。
</li>
<li>
随后我们介绍了BASS音频库。这几乎是最专业的音频库了。由于是C接口所以通用多种语言你可以使用.NET或者VB等语言来应用。当然如果要进行后台播放、多个频道播放等功能你需要编写多线程的代码并没有Pygame那么轻松这里面很多事情需要自己去做。
</li>
现在给你留一个小问题。
在pygame.mixer.music模块中如何播放一首音乐后立刻播放另外一首音乐
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,153 @@
<audio id="audio" title="第6讲 | 从0开始整理开发流程" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0b/83/0b7bfbe4ae23cf73e4a527cbf658ab83.mp3"></audio>
在第一模块的基础知识中我已经讲过开发一款游戏需要的一些背景知识。对于2D游戏和3D游戏的区别、从程序到美术、从2D、伪3D到3D等方面我都进行了逐一地阐述。除此之外对于任何类型的游戏开发都非常重要的三个概念游戏引擎、底层绘图接口、地图编辑器我也进行了一些我个人的解读。
有了这些背景知识,这一节,我来带你整理一下整个游戏开发流程以及流程中所需要的工具。
## 1.选择开发环境
我们默认是在Windows环境下进行开发至于是什么版本的Windows不需要做更多地阐述你愿意用什么版本就用什么版本因为几乎所有流行的Windows环境都能进行编程工作。至于我为什么选择Windows环境进行开发那是因为
<li>首先在Windows环境下拥有比较方便的**调试工具**。不管是Python脚本语言还是C/C++语言,都可以使用图形界面进行调试;
</li>
<li>其次Windows下的**IDE开发环境**也比其他平台更多你拥有更多的工具可供选择。另外在开发游戏的时候你可以选择OpenGL、DirectX或者SDL等图形库进行编程。作为游戏开发DirectX几乎是不可或缺的标准而我在第四节讲述底层绘图接口的时候说过它是由微软提供的游戏编程接口在Windows下提供了更为方便的底层调用。
</li>
<li>除了Windows外Linux平台的**图形显卡驱动**几乎是不完善的无法发挥显卡的最大优势。苹果平台又一家独大开发人员只能为其定制专一的代码开发难度比之Windows就大了不少。
</li>
## 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/](https://sourceforge.net/projects/mingw/files/Installer/)
你也可以按照你的需求定制下载。如果要成为完整的编译器,必须下载这些内容:
<li>MinGW (C/C++) Compiler
</li>
<li>Binutils
</li>
<li>Windows32 API
</li>
<li>MinGW Runtime Libraries
</li>
<li>GNU Debugger (GDB)
</li>
<li>GNU Make
</li>
一般来讲,使用安装器下载的编译器都是最新版本的,如果你需要下载特定的版本号,你可以在这个网址 [https://sourceforge.net/projects/mingw/files](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界面是这样的
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/e9/13/e9ff8b431d455f4a03cef636dd838e13.jpg">
我们可以在Edit-&gt;Options-&gt;Compiler选项里设置具体的编译器路径以便让IDE找到编译器来开始工作。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/d8/e0/d8affea68aed61a67f25a8bd04d766e0.jpg">
一般编译器的路径都会自带bin目录所以设置的目录必须在bin目录的上级目录。比如我们设置的MinGW编译器路径为C:\MinGW那么bin目录就是C:\MinGW\bin所以在IDE的设置下只需要设置为C:\MinGW就可以了。
我们将下载到的Lua5.x.x.tar.gz解压缩到某个目录。在我写文章的时候Lua的最新版本是5.3.4。在这个目录下并没有我们所需要的MinGW Studio的项目文件所以我们需要手工建立一个。我们在File-&gt;New-&gt;Projects选项下建立一个Win32 Static Library也就是Windows静态库将名字设为lua。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/01/84/016bd92e192c42b2b53aa076c6277f84.jpg">
然后我们将文件添加到项目里面,在项目虚拟目录里面,点击鼠标右键。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/0b/29/0bf22ba55c20fd93198d335260842629.jpg">
在弹出的选择文件对话框里选中刚才解压缩出来的Lua目录选择src目录下的所有或椎为.c的文件随后我们将 lua.c 排除在外(选中,右键,选择移除出项目)。因为我们制作静态库的时候,可以不用这个文件。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/90/d2/90a6974f6a57c8530f52dda064878ad2.jpg">
我们可以点击Build-&gt;Build选项来进行编译编译完成后如果编译成功的话我们会在Debug目录下看到一个.a文件。如果你的项目名叫lua那么制作出来的静态库就是liblua.a所以个文件就是我们以后要用到**Lua静态库**。
如果你有一定的编程经验的话可能已经看到我们现在编译出来的是Debug是调试版本我们暂且不去管它。这个在后面我们会进行详细地探讨目前我们只需要知道这一系列的使用方式和方法就可以了。
我们已经将Lua编译完毕了后续的文章中我会教你使用Lua静态库。
接下来我们尝试使用Python语言。你可以使用任何一个上述推荐的专业IDE来编写Python代码。实际上Python的IDE不需要过多的配置。因为安装在Windows机器上后Python的路径会被注册到系统。通常IDE会自动找到Python执行文件并且IDE的Shell窗口将会正确地找到Python并看到其互动窗口就像这张图的内容
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/05/82/057511edf04d2d71904b44f1462c2182.jpg">
现在我们尝试在IDE中编写一小段Python测试代码然后跑一下。程序运行结果就是打印一个 test字符串。
```
import os, sys
if __name__ == &#39;__main__&#39;:
print &#39;test&#39;
```
最后,将该文件保存后缀为.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](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位的才能完美兼容和使用。
## 小结
好了,这节内容差不多了。我来总结一下。在这一节中:
<li>我先从各操作系统下的调试工具、IDE开发环境、显卡驱动等三个方面分析了为什么选择在Windows环境下进行开发
</li>
<li>然后,我还带你梳理了一遍开发所需要的语言和工具,并且提供了下载的网址和安装的方法;
</li>
<li>之后我还带你测试了Lua脚本语言在编译器中的编译并且生成了静态库文件。
</li>
<li>最后给你介绍了Pygame今后将会用到这个Python下的2D游戏开发引擎。
</li>
最后,给你留一个思考题吧。
你可以结合之前几节的内容思考一下Pygame绑定SDL绘图接口是如何实现的
欢迎留言说出你的看法,我在下一节的挑战中等你!

View File

@@ -0,0 +1,227 @@
今天我要跟你分享开发Windows游戏的第一步建立窗体。
上一节我讲解Python和C++的编译器以及它们各自对应的IDE该如何选择并且测试了C/C++的运行编译了一个Lua静态库。准备工作基本上算是完成了。
如果你有一些编程功底应该知道建立Windows的窗体所需的一些基础知识。如果你经验稍丰富一些还应该知道Delphi、C++Builder、C#等等。这些工具都可以帮助你非常方便地做出一个空白窗体,但是这些窗体并没有游戏的绘图系统,所以它们只是“建立了一个标准窗体”而已。因此,虽然建立窗体是我们这一节的内容,但**我们要探讨的是在窗体背后Windows系统做了什么。**
## Windows窗体由哪些部分构成
我们常规意义上的Windows窗体由下列几个部分组成。
<li>**标题栏**:窗口上方的鼠标拖动条区域。标题栏的左边有控制菜单的图标,中间显示的是程序的标题。
</li>
<li>**菜单栏**:位于标题栏的下面,包含很多菜单,涉及的程序所负责的功能不一样,菜单的内容也不一样。比如有些有文件菜单,有些就没有,有一些窗体甚至根本就没有菜单栏。
</li>
<li>**工具栏**:位于菜单栏的下方,工具栏会以图形按钮的形式给出用户最常使用的一些命令。比如,新建、复制、粘贴、另存为等。
</li>
<li>**工作区域**窗体的中间区域。一般窗体的输入输出都在这里面进行如果你接触过Windows窗体编程就知道在这个工作区域能做很多的事情比如子窗体显示、层叠在工作区域的子窗体内进行文字编辑等等。你可以理解成游戏的图形图像就在此处显示。
</li>
<li>**状态栏**位于窗体的底部显示运行程序的当前状态。通过它用户可以了解到程序运行的情况。比如的如果我们开发出的窗体程序是个编辑器的话我按了一下Insert键那么状态栏就会显示Ins缩写或者点击到哪个编辑区域会在状态栏出现第几行第几列这样的标注。
</li>
<li>**滚动条**:如果窗体中显示的内容过多,不管横向还是纵向,当前可见的部分不够显示时,窗体就会出现滚动条,分为水平滚动条与垂直滚动条两种。
</li>
<li>**窗体缩放按钮**窗体的缩放按钮在右上角在窗体编程中属于System类目。这些缩放按钮依次为最小化、最大化和关闭按钮。
</li>
我们来看一张标准的Windows窗体截图这个软件名是Notepad++。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/cc/af/cc1d248bd1c76405ad73792112c33faf.jpg">
这是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 &lt;windows.h&gt;
LRESULT CALLBACK WindowProcedure(HWND, UINT, WPARAM, LPARAM);
char szClassName[ ] = &quot;WindowsApp&quot;;
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(&amp;wincl)) return 0;
```
代码在窗口过程调用函数的时候将地址赋值给lpfnWndProc然后呼叫RegisterClassEx(&amp;wincl)注册窗口类系统就拥有了窗口过程函数的地址。如果注册失败则返回0。
```
hwnd = CreateWindowEx( 0, /* 扩展风格为0*/
szClassName, /* 类名 */
&quot;Windows App&quot;, /* 窗体抬头标题 */
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(&amp;messages, NULL, 0, 0))
{
TranslateMessage(&amp;messages);
DispatchMessage(&amp;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(&amp;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_WINDOWPOSCHANGINGWM_WINDOWPOSCHANGEDWM_MOVEWM_SIZE和WM_NCCALCSIZE消息。
类似的功能还有SetWindowPosSetWindowPos功能更强大可以设置更多的参数。
这是基本的使用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(&quot;%dx%d+%d+%d&quot; % (w, h, x, y))
root = Tkinter.Tk(className=&#39;python windows app&#39;)
my_window(100, 100)
root.mainloop()
```
运行的结果是这样的。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/65/b8/657a175b08898385f555f7613d1a55b8.jpg">
我们可以看到左上角有一个Tk的标识这是Tinker的默认图标。目前我们只是建立了一个Windows的窗体并不能直接编写游戏。除此之外我们还必须要知道这些建立窗体的具体的细节。
不过就像前面的文章所说OpenGL并不附带任何关联窗体的编程所以如果你使用的是OpenGL的接口来编写代码稍微修改一下这些窗体就能成为游戏屏幕窗体。
**游戏所有的内容都是在一个循环内完成的,即我们所有的绘图、线程、操作、刷新,都在一个大循环内完成**,类似我们在前面看到的代码。
```
while(GetMessage(&amp;messages, NULL, 0, 0))
{
TranslateMessage(&amp;messages);
DispatchMessage(&amp;messages);
}
```
以及使用Python编写的代码的窗体中也会看到一个循环函数
```
root.mainloop()
```
在这个while循环中消息的派发都在此完成。游戏也一样我们所有游戏内的代码几乎都在循环内完成。你可以想象**一个循环完成一个大的绘制过程,第二个循环刷新前一次绘制过程,最终类似电影一样,完成整个动画的绘制以及不间断的操作。**
在建立Windows窗体的时候程序会从入口函数WinMain开始运行定义和初始化窗体类然后将窗体类实例化随后进行消息循环获取消息然后将消息发送给消息处理函数最后做出相应的操作。
## 小结
总结一下今天所说的内容我们编写了一个标准的Windows窗体在编写的过程中
<li>窗体的结构是在建立窗体之前就定义下来的;
</li>
<li>所有长时间运行的程序包括游戏包括Windows本身都是一个大循环。我们在这个循环里做我们想做的事情直到循环结束
</li>
<li>如果使用脚本语言的方式编写窗体,就不需要关心那么多的东西,只需要定义坐标、位置和窗体名称即可。
</li>
最后,给你留一道小思考题吧。
你经常会看到有一些游戏是需要全屏才能进行的。既然我们在这里建立了一个窗体,那请问你,全屏是怎么做到的呢?
欢迎留言说出你的看法,我在下一节的挑战中等你!

View File

@@ -0,0 +1,193 @@
<audio id="audio" title="第8讲 | 如何区分图形和图像?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9b/dc/9b6b806d82b95fb8990a72584daadadc.mp3"></audio>
据我所知,很多人可能都分不清图形和图像这两个概念:一种情况是你可能会觉得区分图形和图像这两个概念并没有什么实质的用途,于是就没关心过;另一种情况是,你可能朦胧中对图形和图像的区别有一些了解,但是不够深入或者不够透彻,也说不出一个所以然。没关系,今天我就来深入浅出地给你讲一下,图形和图像背后的那些事儿。
既然我们是做游戏开发,那首先我们得知道,从专业地角度讲,区分图形和图像对我们的开发工作有什么帮助。简单地说,**搞清楚了游戏开发中绘制、载入、保存的究竟是图形还是图像,你会对接口函数的使用有一个更深入的认识。**
比如,如果是图形接口,可能它的接口函数是:
```
Surface* DrawSomething(int start_x, int start_y, int finish_x, int finish_y);
```
如果是图像接口,它的接口函数函数看起来可能是这个样子:
```
Surface* LoadFromFile(const string&amp; filename);
```
## 如何区分图形和图像?
从广义上说,所有我们人肉眼能看到的对象,都是图形。从狭义上说,图形是我们所看到的一种点、线、面的描述对象。
**图像,是由数据组成的任意像素点的描述对象。**比如我们所看到的照片。在电脑中,图形的显示过程是有一定顺序(比如从左往右)的,而图像则是按照像素点进行显示的。电脑对于图形的编辑、修改更为简单方便,因为单一的图形具有特殊的属性(比如圆圈的直径、颜色等等,因为这些在这个图形建立的时候就固定了下来)。
对于图像进行编辑则非常困难,软件需要用一些特殊的算法来计算图像的色块、区域、描边等等,来安排图像该如何进行编辑,有一些甚至还需要用到深度学习的方法来辨别图像的显示区域、显示的内容等等,所以图像的修改比之图形的修改要困难。
那么你可能就会问了既然前面说任何眼睛看到的对象都是图形那么我觉得图形也是一种图像这么说对不对呢如果按照载体来说图形也是一种图像这种说法是对的。因为一张JPG图片可能存储的是一幅照片也可能存储一幅三角形的图形。虽然本质不一样但是由于存储的形式是以图像的形式存储的在电脑看来这个三角形就是一幅图像。但是如果你在游戏中使用函数画出了一个三角形那就是图形了。
所以,严格来说,**图形其实是图像的一种抽象表现形式**。一般来讲图形的轮廓并不复杂比如一个圆圈、一个方块、一个三角形、一条线、某些几何图形、工程上面使用的图纸和CAD等这些都属于图形。图形的色彩也并不是很丰富。而图像一般都有复杂的轮廓、非常多的细节和颜色当然也有纯单一的颜色比如黑白照片
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/fb/bc/fb2b9c4192fd7147c3346dc0da7423bc.jpg">
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/7d/0b/7d00b8af46c9455a24f5a6a3f77e650b.jpg">
所以准确地说图形和图像有不同的模式。当然从计算机最底层的程序显卡处理来看绘制图形和图像所经过的硬件处理几乎是一样的。一般显卡会经过这些流程进行图形、图像计算2D、显存用来存取图形图像内容GPU计算图像图像内容并渲染最后输出到显示器。
从**图像的呈现方式**讲只有通过图像的方式去呈现“图形”这个对象才能看到图形而在开发游戏的过程中图形和图像的编程方式是截然不同的。比如我们要画线那么可能会使用到一个叫DrawLine的函数。该函数里面需要输入线条的起始坐标这就是图形的绘制方式。而在接下来的过程中我将教你如何绘制图形和图像并呈现出来。
## 跟我一起绘制图形和图形
现在我们先用Pygame游戏库来建立一个窗体然后开始绘制图形、载入图像。
在第五节的时候我们已经讲过Pygame的安装和配置。在第六节的时候我们讲过如何建立一个Windows窗体。现在从上到下我们一起看一下这段代码。
```
import pygame
pygame.init()
caption=pygame.display.set_caption(&#39;Python App&#39;)
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。
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/85/7c/85a8383c033ff7ec997e4e7ad9d1dd7c.jpg">
现在我们要在窗体上放上一个矩形和圆。我们先使用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个参数
<li>目标画布在这里是screen
</li>
<li>颜色
</li>
<li>由左侧点和顶部点组成的圆形初始位置
</li>
<li>直径
</li>
<li>宽度
</li>
现在我们将所有的代码合并起来看一下:
```
import pygame
pygame.init()
caption=pygame.display.set_caption(&#39;Python App&#39;)
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()
```
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/41/4a/41ed7da2761a57bf68d990a660f7014a.jpg">
所以我们很容易就能看出来,**在Pygame游戏开发库里面画一个图形是很容易的事情你不需要知道太多的细节只要将位置和颜色或者内容填充进去就可以了。**
我们可以在Pygame中使用Pygame.image.load来加载图像文件这个函数支持各种图片格式。我们使用这个方法来加载一副PNG图片
```
obj = pygame.image.load(&quot;test.png&quot;).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(&#39;Python App&#39;)
screen=pygame.display.set_mode([320,200]) #窗口大小为640*480
obj = pygame.image.load(&quot;test.png&quot;).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])#用白色填充窗口
```
最后呈现的效果是这样的:
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/9e/97/9e8f1747c6ddcdb3302de41d64a69c97.jpg">
## 小结
这一节我带你学习了图形和图像的区别使用Pygame绘制了最基础的图形最后我们通过代码载入一副PNG图像并在屏幕上绘制出来。
给你留一个小练习吧。
请你结合上述代码,在游戏执行的大循环内,在游戏的窗体里面,绘制出一个从左到右移动的矩形、圆形或者图像。
之后,针对一些实操性强的内容,我都会适时给你留一些必要的练习。希望你每次都能动手去练习一下。同时,也欢迎你留言,说出你在练习中的疑惑和成果。温故而知新,相信你会有更多的收获!
我在下一节的挑战中等你!

View File

@@ -0,0 +1,186 @@
<audio id="audio" title="第9讲 | 如何绘制游戏背景?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/b4/0cc1687b5dcd6be8665c3980679f5ab4.mp3"></audio>
我在之前的文章中描述了各种基础知识,然后梳理了开发流程,并带你创建了一个窗体,现在我们要做的就是朝这个窗体里添加东西。
我会随着进度逐渐提升难度。就现阶段来讲,我们涉及的只是一些基础知识,并且只**需要将大部分的关注点放在我们要做的游戏内容上,并不需要关注过多的底层逻辑代码**。
做事情都有先后顺序,做游戏开发自然也是。为什么要学习先绘制游戏背景而不是别的什么,很简单,因为只有先绘制了游戏背景,才能进行后续的游戏图像遮挡、图形图像的显示等等操作。
不管你有没有玩过《超级玛丽》《魂斗罗》《雷电》之类的游戏但一定对其画面不陌生。和我们要开始做的打飞机游戏一样这种类型的2D游戏其背景不是左右卷轴就是上下卷轴。**所谓左右卷轴,就是游戏画面是横向的、左右运动的,而上下卷轴就是游戏画面是竖直对的、上下运动的。**
<img src="https://static001.geekbang.org/resource/image/2c/b1/2cd9958a20b5aca5650e9d6a99dec0b1.jpg" alt="">
像《雷电》这样的经典飞机游戏,就是属于上下卷轴的。上下卷轴的飞机游戏有一个特点,就是它是在空中,从凌驾于飞机之上的视角,往地面俯瞰的。因为是俯视角,所以我们可以很方便地看到游戏的整体地图,包括地面上的敌人、空中的敌人等等,层次感会很强。
因此,可以确定,我们要做的打飞机,也是一个上下卷轴的游戏。这样,我们就可以着手将需要的图片添加进去了。
我们要使用Pygame先读取一个图片让该图片成为游戏背景并载入进去。当下阶段我们的图片从哪儿获得并不重要因为在一个完整的游戏开发团队里面都有专业的美术团队负责作图但是现在我们没有所以我就自己贴一幅图来代替正式的游戏背景。所以你现在只需要知道背景是如何贴上去的就好了。
和前面的文章说过的一样我们需要先载入Pygame模块并且定义一个变量background。我们将一幅名为lake,jpg的图片文件赋值给backgroud变量。
```
import pygame
background = &#39;lake.jpg&#39;
```
然后我们先把Pygame的所有组件都初始化。接下来我们调用display类里的set_mode函数来对屏幕进行一个初始化。
```
pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)
pygame.display.set_caption(&quot;pygame game&quot;)
```
这里一共有三个参数,第一个参数是**分辨率**比如我这里编写的是640x480的分辨率第二个参数是**flag**flag的参数我放在下面这个表里了第三个参数是**32**32代表的是颜色深度这里是32位的意思。
<img src="https://static001.geekbang.org/resource/image/da/67/daecb1449c14d6f4c9b55922011bd667.jpg" alt="">
在设置完了窗体模式之后后面的一段代码就是设置窗体的抬头文字这里显示的是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的模式下。
好了,我们该做的事情基本都做完了,现在我们来运行一下,看看效果。
<img src="https://static001.geekbang.org/resource/image/5c/95/5c96858d8059e1729ce1e14d1c93fc95.jpg" alt="">
好了背景是贴上去了。现在问题来了要想让背景动起来该怎么做呢如果在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所以我们每次看到的图片都会不停往上移动我们来看一下效果。
<img src="https://static001.geekbang.org/resource/image/13/81/135f2931b3a3168192ec9a5cc0a1ba81.jpg" alt="">
发现问题了没有,在移动的过程中,下方的图案居然没有被刷新,直接黏在了屏幕上,看起来是不是很恶心的样子?
我们应该怎么做才能达到正常的效果呢?也就是说,请你思考一下,应该怎样做,我们才可以将这个令人头疼的图像在移动的时候变得正常呢?
我们先来回顾一下,我们在循环里面做了哪些步骤:
<li>检测退出事件;
</li>
<li>在屏幕上绘制bg对象坐标初始为0, y
</li>
<li>飞机每移动一格坐标y减1
</li>
<li>更新屏幕。
</li>
看起来似乎没有什么问题,我再来带你梳理一下。
首先我们初始化的时候屏幕是黑屏一块没有任何图像然后我们进入大循环将bg对象绘制到屏幕上的时候你觉得这时候我们的眼睛看到绘制的图像了吗
如果你说是的话,那就大错特错了,因为**这个blit的动作仅仅是绘制而不是显示**。请记住这个区别:**绘制不等于显示**。
那你可能就要问了既然绘制了为什么不显示呢要什么时候才能显示呢答案是要在update一次屏幕的时候才会显示这就是“更新”的作用。就像电影是一帧一帧的如果没有下一帧更新电影就会永远定格在某一秒。
所以问题逐渐就暴露出来了,我们再来重新梳理一下流程:
<li>检测退出事件;
</li>
<li>在屏幕上绘制bg对象坐标初始为0, y注意是绘制不是显示
</li>
<li>飞机每移动一格坐标y就减1
</li>
<li>更新屏幕将第二步绘制的bg对象呈现在屏幕上严谨地说应该是将在update函数之前所有的绘制操作都更新一次并呈现在屏幕上
</li>
好了问题很清楚了update函数只是将屏幕更新了一次并未进行填充颜色或者“擦除”背景的操作也就是**我们在移动y值的时候整个屏幕不停地更新然而没有擦除**。那么应该怎么将移动后的画面进行清理呢?
我们在update代码之后填入下面的代码。
```
screen.fill([0,0,0])
```
fill操作拥有三个参数其中第一个参数是**填充颜色;**第二个参数是**填充某一块区域**(如果不填入第二个参数,就会填充整个屏幕);第三个参数是**blit操作的特殊参数**,我们暂时可以不用管它。
所以,我们在代码里填充了黑色到整个屏幕,这样一来我们的屏幕操作变成这样:
<li>检测退出事件;
</li>
<li>在屏幕上绘制bg对象坐标初始为0, y
</li>
<li>坐标的y减1
</li>
<li>更新屏幕;
</li>
<li>填充屏幕区域为黑色。
</li>
我们再运行一下看一下效果。
<img src="https://static001.geekbang.org/resource/image/49/66/4982b8ea20800fba341143140eeb8a66.jpg" alt="">
嗯,这下看起来正常了,屏幕不断往上移,并且没有拖着尾巴一样的图案了。
## 小结
我们在写2D游戏的时候要注意一点就是
我们要**想象游戏的每一帧就像电影的每一帧。每一帧做的事情,如果下一帧不去做,那么永远不会更新屏幕内容**。
所以update的功能是更新调用update之前的所有动作这些动作可以有绘制图像操作也可以有音乐播放也可以有动画每一帧的操作等等。只要update一次屏幕的画面就会往前行进一次。
给你留个小思考题吧我们在fill屏幕的时候怎么做才能让填充的颜色不停变幻呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,88 @@
<audio id="audio" title="第22讲 | 如何选择合适的开发语言?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e1/f6/e1216f72adbe0e7fa57338dc3cfb0cf6.mp3"></audio>
有许多编程语言可以用来开发服务器端。一些语言对于网络开发有先天优势,一些语言没有先天优势,但是能完成任务,而有一些语言,则不太适合。今天,我就来具体讲一讲这些语言来开发网络服务的优劣势。
## 你了解这些编程语言吗?
**C/C++** 是最标准的开发语言速度快并发性能高能最大程度利用机器资源完成任务。现在C/C++层面拥有无数网络开发库和SDK知名的有ACE、Boost/Asio、ICE等等。但是缺点是开发效率不比别的语言来得高出错后常常只有熟练的程序员才能定位问题并且作出修复。
**Go**是2009年新出现的语言。Go天生适合编写网络程序。它也是一种系统级的语言可以直接编译为执行文件当然由于封装了大量的语法糖所以编译出来的文件会更大它天生支持并发操作所以很多时候你不需要像C/C++一样手工去处理。缺点是Go语言仍然存在许多语法方面的坑你可以去 [https://studygolang.com/](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](https://book.douban.com/subject/2032343/),有兴趣的话可以找来看看。
**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等脚本语言进行开发却需要大量高并发的操作该怎么办呢我们可以选择多进程不是多线程编程的方式进行开发代码尽量简洁、高效一个进程兼顾一个任务进程之间的通信方式要尽量高效、简洁比如可以使用自定义的队列等方式。
## 小结
学完这一节,你应该对使用各种编程语言来编写网络服务有了一个更深的了解。我主要讲了以下几个内容。
<li>
编程语言可以大致分为三类,系统级编程语言、专为网络服务器开发的编程语言和解释型脚本语言。
</li>
<li>
在编写网络服务的时候可以根据要编写的是Web服务、Socket服务还是混合模式来选择合适的编程语言。
</li>
给你留一个小问题吧。
如果让你来使用C/C++粘合Lua脚本来编写网络服务器你会怎么设计这个程序框架
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,124 @@
<audio id="audio" title="第23讲 | 如何定制合适的开发协议?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4d/58/4d7926809b5247420e66ede7f381cf58.mp3"></audio>
什么是开发协议说得简单一点就是一种客户端和服务器端的网络沟通协议Protocol
广义上说协议是计算机各种设备之间沟通的桥梁。比如网络之间需要协议ping一个网站是否通顺也需要协议广播地址也需要协议。我们甚至可以说键盘鼠标操作事件也需要协议Dubbo架构也需要协议沟通等等。
从狭义上说协议指的就是网络协议。比如在网络游戏中客户端和服务器端之间的内容交互就需要网络协议在Web网站中前端和后端的交互也需要协议再比如邮件服务的网络交互也需要协议的交互等等。可以说任何与网络相关的服务都少不了协议的支撑。
在游戏开发中,我们可以自定义一套自己的开发协议,也可以把现成的开发协议拿来使用。具体怎么做呢?我们先来看现在网上用得比较多的几种协议。
## 三种最流行的开发协议
### XML
XML几乎是网络上最早出现的传输协议之一。在最早的Web开发中XML可以作为网络协议也可以用作配置文件。比如某些游戏或者某些应用的配置文件都可以使用XML来编写。
从人类的角度讲,它的可读性比较强,解析也比较方便。我们先来看几种解析方式。
解析方式是这些协议被程序理解的一种方式,按照这种方式解析,和我后面要说的自定义协议的解析和剖析结合起来,乃前后呼应之奇效。
<li>
PULL方式PULL解析是一种专门为安卓设备解析XML文件设计的解析方式。这种解析方式更适用于移动设备。PULL解析和我们下面要说的SAX解析比较类似不一样的地方是PULL读取XML文件后触发相应的事件调用方法返回的是number。另外PULL方式可以控制程序的解析停止位置不需要全部解析可以随时停止。
</li>
<li>
<p>SAX方式SAXSimple API for XML采用事件驱动型方式。语法解析从发现元素开始、元素结束、文本、文档的开始或结束等就开始事件的发送程序员编写响应这些事件的代码就可以直接将数据保存下来。所以优点是不需要载入整个文档资源占用比较少。<br>
SAX解析器的代码比DOM解析器的代码更精简一般应用于Applet技术。缺点就是这种解析方式并不持久等事件消息过去后如果你没有保存数据那么数据就丢失了从事件的消息中我们只能得到文本数据但不知道这个文本属于哪个元素。但是我们只需XML文档的少量内容就可以解析出我们所需的数据所以耗费的内存更少。</p>
</li>
<li>
DOM方式DOMDocument Object Model是最传统的解析方式。解析器读入整个文档然后在内容中构建一个XML的树结构使用代码就可以操作这个树结构。优点是整个文档树在内存中便于操作而且支持删除、修改、重排等多种功能。缺点是将整个文档调入内存比较浪费计算机的时间和空间但是如果一旦解析了文档还需多次访问这些数据的话这种方式就可以起到作用了。
</li>
### JSON
其实目前XML已经不太流行取而代之的是JSON。JSON是一种轻量级的数据交换格式。它用完全独立于编程语言的文本格式来存储和表示数据。
比之XML它看起来更加简洁和清晰层次结构分明JSON易于阅读和编写在程序方面也易于机器解析和生成同时也提升了网络传输效率。这些优点使得JSON很快在程序员中流行起来成为理想的数据交换语言。
它也是移动端比较常见的网络传输协议。相对于前面所说的XML格式它更为简单体积更小加之对网络流量和内存的需求更小所以JSON比XML更适合移动端的使用。
我们来看一下JSON的几种流行的解析程序库。
<li>
Gson是谷歌开源的一种解析方法使用Java编写你可以通过提供的JAR文件包使用静态方法直接将JSON字符串解析成Java对象这样使用起来简单方便。
</li>
<li>
FastJSON是阿里开源的一个解析JSON数据的类库。
</li>
<li>
JSONObject也是一个解析JSON字符串的Java类。第二、第三这两种用的人都比较少我就不多介绍了。
</li>
当然支持别的语言的库也有很多由于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_HOMEBEST_FRIEND。
Protobuf并不是针对大型数据设计的Protobuf对于1M以下的message有很高的效率但是当message大于1M的时候Protobuf的效率会逐步降低。
## 如何自己定义协议包?
我们讲完了三种目前最流行的开发协议,接下来我们要讲讲如何自己定义协议包。
我们所说的**协议包**,是**在TCP和UDP传输之上的协议包也就是通过字符串的形式发送的协议包**。这些协议包在客户端和服务器之间做了约定,也就是说,客户端和服务器都能通过拿到协议包来进行解包操作,并且进行一系列的逻辑运算并返回结果,当然结果也是协议包的形式发送出去。
一个好的协议不仅能节约网络带宽,也能让接收端快速拿到和解析需要的内容。设计协议包,必须保证**安全性**和**完整性**。
为了保证完整性,接收方需要知道协议的长度,或者知道协议的尾部在哪里。
我们可以给协议最末尾添加分隔符,该分隔符需要特殊字符。不能被传输的内容所混淆,又要能达到方便接收方辨认,因此,该特殊字符需要具有唯一性。比如我们可以将“!@#$”这四个字符做为分隔符,那么协议看起来可能是这样:
```
[协议头][协议体][协议结尾分隔符]
```
你可能要问了,在传输的过程中,我知道了协议长度,不需要协议头,只需要协议长度就可以?是的。因为有了协议长度,协议尾部有没有分隔符就不重要了。如果我们固定好输出协议长度的字节数,就可以忽略协议头。在这种情况下,协议看起来像是这样:
```
[协议长度2字节][协议体]
```
这样简单地就能定义整个协议的内容。
在读取的时候我们只需要读取开头的两个字节转换为一个short的长度或者四个字节一个int的长度在第三个字节开始就是协议体。让程序开始计算长度如果长度少于协议长度所定义的长度那就继续接收如果接收长度超过协议所定义的长度切割协议体并将下一段开始的协议存储到内存中留待下一次取出。这种方式是最方便的。
我们在保证协议完整性的同时也要保证协议不被破坏和篡改也就是所说的安全性。在这种情况下最直接的方式就可以将协议内容进行加密。比如SHA-256或者AES等等加密方式将内容加密随后传输过去最简单的做法就是将密码在客户端和服务器端协商好就可以了。
看起来可能是这个样子:
```
[协议长度2字节][加密协议体]
```
## 小结
这节内容差不多了,我们总结一下。我和你介绍了这几个内容。
<li>
我介绍了三种的开发协议XML、JSON和ProtoBuf以及它们对应的解析方式。XML是网络上最早出现的传输协议之一。
</li>
<li>
游戏或应用的配置文件都可以使用XML来编写但是目前XML已经不太流行取而代之的是JSON。
</li>
<li>
ProtoBuf适合用作数据存储或RPC数据交换格式缺点是保存比较麻烦但是总体来讲还是比较方便的。
</li>
<li>
自己定义协议包需要考虑完整性和安全性。接收方需要知道协议的长度,或者知道协议的尾部在哪里,就可以保证协议包的完整性。而最直接的给协议包加密,就可以保证安全性。
</li>
最后,给你留一个小问题吧。
在自定义协议中,如果使用添加协议结尾的方式来做协议,如何才能保证协议结尾分割字符串不和协议本身的二进制内容重复?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,143 @@
<audio id="audio" title="第24讲 | 不可忽视的多线程及并发问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f0/db/f0ed6005d2e4957c32774f5f3d393edb.mp3"></audio>
既然我们说到了服务器端的开发我们就不得不提起多线程和并发的问题因为如果没有多线程和并发是不可能做网络服务器端的除非你的项目是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(&quot;count time: {}&quot;.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(&quot;count time: {}&quot;.format(end_time - start_time))
if __name__ == '__main__':
run()
```
当然我们还可以把这个ranger的数字改得更大看到更大的差异。
当计步完成后将会达到一个释放锁的阀值释放完后立刻又取得锁然而这在单CPU环境下毫无问题但是多CPU的时候第二块CPU正要被唤醒线程的时候第一块CPU的主线程又直接取得了主线程锁这时候就出现了第二块CPU不停地被唤醒第一块CPU拿到了主线程锁继续执行内容第二块继续等待锁唤醒、等待唤醒、等待。这样事实上只有一块CPU在执行指令浪费了其他CPU的时间。这就是问题所在。
这也就是C语言开发的Python语言的问题。当然如果是使用Java写成的PythonJython和.NET下的PythonIron Python并没有GIL的问题。事实上它们其实连GIL锁都不存在。我们也可以使用新的Python实作项目PyPy。所以这些问题事实上是由于实现语言的差异造成的。
## 如何尽可能利用多线程和并发的优势?
我们来尝试另一种解决思路我们仍然用的是C-Python但是我们要尽可能使之能利用多线程和并发的优势这该怎么做呢
multiprocess是在Python 2.6以上版本的提供是为了弥补GIL的效率问题而出现的不同的是它使用了多进程而不是多线程。每个进程有自己的独立的GIL锁因此也不会出现进程之间CPU进行GIL锁的争抢问题因为都是独立的进程。
当然multiprocessing也有不少问题。首先它会增加程序实现时线程间数据通信和同步的困难。
就拿计数器来举例子。如果我们要多个线程累加同一个变量对于thread来说申明一个global变量用thread.Lock的context就可以了。而multiprocessing由于进程之间无法看到对方的数据只能通过在主线程申明一个Queueput再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()
```
这样的方案虽说可行,但是编码效率变得比较低下,但是也是一种权宜之计吧。
## 小结
我们来总结一下今天的内容。
<li>
我首先介绍了几个概念。并发是单个CPU之间切换多线程任务的操作。并行是多个CPU同时分配和运行多线程任务的操作。线程是进程内的独立任务单元但是共享这个进程的所有资源。网络的并发指的是服务器同时可以承载多少数量的人数和任务。
</li>
<li>
而C语言编写的Python有GIL锁的问题会让其多线程计算密集型的任务效率更低解决方案有利用多进程解决问题 或者 更换Python语言的实现版本比如PyPy或者JPython等等。
</li>
给你留一个小问题如果Python以多进程方式进行操作那么如果我们网络服务器是用Python编写的其中一个Python进程崩溃或者报错了有什么办法可以让其复活
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,132 @@
<audio id="audio" title="第25讲 | 如何判断心跳包是否离线?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bf/97/bf7f27a06e2bd28ef543a7c5b74cbb97.mp3"></audio>
在初学网络,编写过阻塞和非阻塞网络代码的时候,有一个问题,那就是在非阻塞的情况下,不知道对方的网络何时断开。
因为在非阻塞的情况下如果没有接收到消息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((&quot;&quot;, 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 &quot;no client incoming&quot;
for cli in clients:
try:
data = cli.recv(1024)
if data:
print data
else:
cli.close()
clients.remove(cli )
except Exception as e:
print &quot;no data from &quot;, cli
my_server.close()
```
在一个大循环内我们做了如下几件事情第一个是accept只要有客户端进来我们就accept如果没有客户端进来一直等待状态下就打印 no client incoming字符串如果有客户端进入的话就直接将新客户端放入列表。
我们在启动函数的时候,如果没有客户端连接,就会出现这样的字样:
然后我们使用Windows下的telnet命令来模拟客户端。输入telnet 127.0.0.1 1024服务器端代码会出现这样的字符串
我们打印新的客户端连接的对象地址并且将新的客户端连接句柄放入列表里面。随后循环进入到了取出新客户端列表并且做出判断每次接收1024字节。如果没有则显示 no data from &lt;xxxx地址&gt;;如果有,那就显示输入的字符串。
好了现在我们打开Windows任务管理器找到拥有telnet的程序并且“杀死”它。
随后,我们会发现,命令行提示符出现了如下内容的字符串:
按照道理,服务器不是应该断开连接了吗?它应该能知道客户端断开了不是吗?
服务器端根本不知道对方已经被“杀死”了所以它的状态仍然在接收中。由于是TCP握手除非你正常将telnet程序关闭才会让服务器端正常接收到客户端关闭的消息否则你永远不知道对方已经退出连接了。
所以**心跳包的作用**就在这里,心跳包**允许你每隔多少毫秒发送数据给服务器端,告诉服务器我还活着,否则服务器就当它已经死了,确认超时,并且退出**。
事实上在TCP/IP的协议层中本身是存在心跳包的设计的就是TCP协议中的SO_KEEPALIVE。
系统默认是设置2小时的心跳频率。需要用setsockopt选项将SOL_SOCKET.SO_KEEPALIVE设置为1打开并且可以设置三个参数tcp_keepalive_timetcp_keepalive_probestcp_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时间几次就切换到策略一这样可以保证心跳时间基本一致。
我们可以将心跳服务和逻辑服务分开运行,而是否放在同一台物理机并不是首要的问题,这样心跳服务器只提供心跳包,而逻辑服务通过心跳包自动判断并且调整运行频率。
## 小结
好了,我给今天的内容做一个总结。
<li>
判断非阻塞模型的网络是否断开可以使用心跳包和计算超时的方式进行断开操作比如30秒没收到心跳包则可以强制关闭Socket句柄断开。
</li>
<li>
心跳包是一种服务器之间交互的方法也可以用作服务器数据调试和回滚的策略方案。心跳包有两种策略第一种就是运算时间A和心跳时间B相对固定第二种策略是运算时间A和心跳时间B是实时调整。CPU的负载很高的时候用策略一CPU负载并不是那么严重的情况下策略二是比较好的选择。
</li>
最后,给你留一个思考题吧。
如果编写的是阻塞方式的服务器代码,心跳包还有存在的意义吗?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,86 @@
<audio id="audio" title="第26讲 | 如何用网关服务器进行负载均衡?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/6e/19ec637ff46fb9455604393742440b6e.mp3"></audio>
我们费劲心思做了一款游戏,那肯定希望有很多人来玩。一旦玩家数量开始多起来,服务器需要承担的压力就会变大,我们就需要做负载均衡了。
所谓的负载均衡,就是对外有一个公共地址,在请求过来的时候,通过轮询、随机分配等方式,定点到不同的服务器,以分摊服务器的压力。
## 两种常用的负载均衡技术
常用的负载均衡技术有**软件模式**和**硬件模式。**
其中硬件模式用得比较多的是F5。F5是F5 Networks公司提供的一个负载均衡器专用设备 F5 BIG-IP LTM 的官方名称叫本地流量管理器可以做47层负载均衡具有负载均衡、应用交换、会话交换、状态监控等等全备的强大网络功能。
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-询问GG通过查询缓存表告知A客户端C服务器有空于是通知A你去连C服务器IP地址和端口号是多少多少于是A从网关G关闭连接去连接C服务器。如果连接失败因为是缓存查询从逻辑上讲有可能滞后那么再次询问网关直到成功连接某一台服务器为止。
这个模型,网关服务器只做了负载均衡的动作,客户端和网关之间不会保持一个长连接,在这个基础上,一台网关服务器支撑同时七千人以上都不是什么太大的问题。但是它的缺点也很明显,那就是一台游戏逻辑服务器只能负责一个游戏世界,不能进行分块。如果要进行分块,则需要其他模型的服务器模块,这个我一会儿会说。
Nginx的反向代理也是类似这种负载均衡的网关模型这种模型大量运用在很多应用服务器、HTTP连接的网络服务器上。但是这项技术到了上升时期开始遇到了瓶颈人们发现就算加上网关也无法负担体量更大的游戏地图。于是我们需要对这样的模型进行修改。
## 如何优化负载均衡的网关模型?
首先,需要将网关服务器增加为几个网关服务器。每个网关服务器都做相同的工作,也就是管理它所下属的所有逻辑服务器。客户端在启动的时候,随机抽取某一个网关服务器,进行询问,使用网关服务器做代理进行中转。
如果游戏地图特别大这样的模型可以将游戏地图分割成几块分割好的地图放到下属的各个逻辑服务器中网关做中转服务比如服务器A负责浙江省服务器B负责安徽省等等。
客户端在连接到网关服务器后,随着游戏进度的走向,网关服务器可以选择连接负责哪一块地图的逻辑服务器,这样在玩家看来就像是连接了一台服务器,而客户端并不用考虑切换服务器的工作。
当然为了减轻服务器的压力,增加更多的人流量,后期这样的模型被逐步细分。比如可以将聊天服务放到一台独立的服务器进行存放,把用户数据独立到一台数据服务器存放,把商品交易放到另一个独立的服务器,或者把私信等等这些和主游戏逻辑无关的内容都放到一个独立的服务器上。
这样一来,主游戏逻辑的服务器的负载就会减轻,然而客户端就不得不多连接几台服务器,要不停获取用户数据或者聊天信息等等,某些负载就转嫁到客户端上了。
这样的游戏逻辑服务器的模型一直沿用到现在。某一些稍微轻量级的,只是使用网关当成负载均衡使用,有一些重量级的,加上地图分割,就会增加网关服务器,但是付出的代价就是,如果要加一台新的游戏逻辑服务器的话,势必会增加部署难度。
不仅网关服务器的配置文件要重新部署,每个游戏节点服务器和被分割的诸如聊天等服务都需要进行重新配置,这样付出的代价也是巨大的,当然很多游戏公司靠着这样的服务器框架使用了好多年,其思想也被延伸到各个行业领域的服务器架构中。
## 小结
这节内容差不多了,我来总结一下。
<li>
我首先讲了两种常用的负载均衡技术软件模式和硬件模式。硬件模式用得比较多的是F5。软件模式的话比较常用的软件的有LVS、Nginx、HAProxy。
</li>
<li>
网关服务器有中转功能和负载功能。Nginx的反向代理用的是负载均衡的网关模型但是这种模型无法负担更大体量的内容。为了减轻服务器的压力也为了增加更多的人流量可以通过增加网关分割业务逻辑到独立的服务器分摊服务器压力这种经典类型的服务器模型被大量沿用并使用至今。
</li>
现在给你留一个小问题吧。
我们使用网关服务器这样的模型,如果网关服务器宕机了,或者网关服务器很久没有响应的情况下,有什么办法让客户端能顺利连上网关服务器之下的逻辑服务器呢?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,125 @@
<audio id="audio" title="第27讲 | 如何制作游戏内容保存和缓存处理?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/27/ec/276f423f97325bfb9531878a2b5624ec.mp3"></audio>
我们在打完游戏的关卡之后,需要保存游戏进度。单机游戏的进度都保存在本地磁盘上,如果是网络游戏的话该怎么办呢?这一节,我就来讲这个内容。
首先,我们要了解游戏内容的保存,需要先了解缓存处理。
为什么要了解缓存的处理呢?那是因为在大量用户的情况下,我们所保存的内容都是为了下次读取,如果每一次都从硬盘或者数据库读取,会导致用户量巨大数据库死锁,或者造成读取速度变慢,所以在服务器端,缓存的功能是一定要加上的。
## 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也是主服务器BB是关联到其他从服务器B1B2的主服务器同时又是主服务器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则会抛出一个异常
<img src="https://static001.geekbang.org/resource/image/44/a3/44d41a80ab192dfa5514a25bb66c80a3.jpg" alt="">
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 为 foovalue为 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在这种量级的情况下也是吃不消的所以如果不在上层做更多层的缓存底层数据库一定是会死锁或者出现各种各样的问题。
那么你可能会说,我可以做索引啊,要知道在连接数足够多的时候,做索引、读写分离,主从数据库等方案,也只是救急只用,无法真正实现稳固的架构体系。
## 小结
我来总结一下今天的内容。
<li>
Redis不仅仅可以用作普通的缓存机制使用也可以当作正常的数据库使用Redis也支持主从同步要按照应用场景不同来配置不同的Redis使用场景。
</li>
<li>
缓存机制不仅仅针对读取游戏保存文件这么一种方案,也可以用作各种数据文件的读取和写入操作。
</li>
<li>
使用现成的Redis等缓存数据软件是一个好的方案。而设计好的框架、好的缓存机制、好的网络模型是一款好网游必不可少的条件。
</li>
现在给你留一个小问题吧。
有没有可能将网络游戏的内容保存在客户端本地的电脑上,如果可以的话,请问如果玩家换了一台电脑,怎么同步内容呢?保存在客户端本地的意义是什么?
欢迎留言说出你的看法。

View File

@@ -0,0 +1,76 @@
<audio id="audio" title="第28讲 | 热点剖析HTML5技术是如何取代Flash的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4b/d7/4bf6f809c500e75aebb3afd537b3eed7.mp3"></audio>
本周开始,我将会在**每周六**更新一篇“热点剖析”阐述我对2D游戏开发之外的热点比如HTML5游戏、移动端游戏、AR和人工智能在游戏领域的应用以及我对微信小游戏、移动游戏、独立游戏开发者的一些观点和看法。
我已经为你讲解了核心的开发知识,对于这些相对热门领域的知识,你可以根据兴趣进行选择学习。
本周及接下来两周的周六我会依次为你介绍HTML5游戏以及[如何选择HTML5的游戏引擎](https://time.geekbang.org/column/article/9702)并带你编写一款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全称是HTML5HTML是超文本标记语言的首字母缩写。而HTML4.01的标准在1999年年底发布。HTML5的草案前身是网络应用1.0Web Application而HTML5的第一份草案于2008年1月月份公布定稿是在2012年的年底。
在H5标准出现后各大浏览器纷纷开始支持H5比如火狐、谷歌的Chrome、IE9、苹果的SafariOpera等等。事实上**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等价于移动平台网页”这样的误解。
## 小结
总结一下今天的内容:
<li>
H5游戏拥有比Flash更好用更开放的规范开发和发布也更方便修改代码后放到网上只需要刷新一下页面就可以看到修改的内容
</li>
<li>
另外Flash技术需要安装一个插件而H5技术直接在浏览器就可以呈现。所以H5技术完全替代Flash只是时间问题。
</li>
现在留一个小问题给你。
我们都知道浏览器渲染出来的网页都是2D的页面和游戏但是H5技术也可以编写3D视觉效果的内容。你知道H5技术所拥有的3D效果是用什么技术实现的吗
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,101 @@
<audio id="audio" title="第29讲 | 热点剖析如何选择一款HTML5引擎" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/64/f1/64169576d8fe46d6961c025b4e1bc2f1.mp3"></audio>
上周六,我详细介绍了[HTML5技术相比Flash技术的优势](https://time.geekbang.org/column/article/9298)相信你已经对HTML5技术有一个大致的了解。下周六我会带你编写一个HTML5游戏在这之前我们需要先了解几种常见的HTML5游戏引擎。
一些比较成熟的引擎比如Cocos2d-JS、白鹭等它们都提供有系列化的工具比如编辑器、IDE等周边。但是其实**大部分HTML5游戏引擎都只有图形引擎**而已比如legend.js。而且很多HTML5引擎只是个人编写的开源引擎所以漏洞还是比较多的。
HTML5游戏引擎在编写的时候除非用DOMDocument Object Model纯原生编写绝大部分都是使用JavaScript编写的。但是为了考虑各种程序员的需求现在也有使用TypeScript、CoffeeScript、LiveScript等语言编写的HTML5引擎。
## 几款常见的HTML5游戏引擎
我们现在来看一下几款常见的HTML5游戏引擎。
### Construct 2
这是一款收费的引擎当然也有免费的版本但是免费的版本不可用于商业用途。那么既然是商用引擎那它一定会比免费开源的产品更加完善和易用。这里有一幅Construct 2的截图你可以看一下它的界面。
<img src="https://static001.geekbang.org/resource/image/a7/8b/a725e6389a0636f8f0539a626de2f18b.jpg" alt="">
我们可以清晰地看到这个界面类似Office的操作界面。左侧是物件的**属性**,比如长宽高等;右侧上方是物件的**层**,包括背景、物件;右侧下方是**物件选择器**,你可以选择各种物件摆放到编辑器内。这种类型的编辑器,在成熟的商业引擎中比较常见,但是由于编写编辑器的人力物力成本,所以免费的引擎一般不提供这种成熟的编辑器。
从**使用**角度我总结了一下它的特点大概有这几条供你自己制作HTML5游戏的时候选择
<li>
可以设计任何类型的2D游戏而且不需要编程的技能可以由事件系统控制
</li>
<li>
使用简单的拖拽和事件系统就可以设计游戏逻辑;
</li>
<li>
拥有一个物理引擎,可以用来计算运动、旋转和碰撞反映;
</li>
<li>
可以在iOS、Android、Windows Store等多个平台上发布游戏
</li>
<li>
拥有很完备的文档和教程案例。
</li>
在**软件**方面它拥有一个非常直观的编辑器可以开发高级的视觉效果可以支持视差滚动的游戏并且在编辑器中集成了调试器和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 BindingsJSB支持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大小呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,215 @@
<audio id="audio" title="第30讲 | 热点剖析试试看你也可以编写一款HTML5小游戏" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fc/0d/fc5ac61517f259cb128b2f2bd95e8c0d.mp3"></audio>
前两周,我分析了[HTML5的核心竞争力](https://time.geekbang.org/column/article/9298)以及[HTML5游戏的引擎](https://time.geekbang.org/column/article/9702)。选择好了HTML5引擎我们就可以开始制作游戏了。
对于编写HTML5小游戏你或许会很有兴趣因为HTML5现在已然是一个潮流而且利用引擎编写HTML5游戏已经变得相当方便。
## 如何选择一款引擎?
我选择的是比较容易上手的lufylengend引擎。为什么要选择这款引擎呢因为它只需要下载一个压缩包并且不需要特别繁琐的说明和设置就能直接上手用作我们的教学示例是最合适的。
如果使用白鹭引擎或者Cocos-2d引擎这些比较有名的引擎可能会有这些问题。
<li>
这些引擎从工具到套件都非常成熟,你直接下载一个引擎启动器或者组件管理器,就可以一应俱全地在启动器里面下载,配置到默认路径。但是,这些工具拥有纷繁复杂的界面,你连上手都要费一段时间,更别说短时间内熟练使用并制作出一款游戏。
</li>
<li>
这些引擎需要引入的库或者使用的方式极为系统所以你需要系统地引入库文件才可以使用。事实上我要做的示例并不需要很多复杂的东西你只需要跟我从头到尾走一遍就能明白编写HTML5游戏是怎么回事。
</li>
<li>
这些引擎需要别的工具支持比如node.js。作为新手来说光配置node.js就是一项比较麻烦的工作。所以我选择了lufylengend引擎这一个比较“单纯的”引擎来作为教学引擎。
</li>
## 几个简单的说明
你可以从这个地址下载最新版本:[https://github.com/lufylegend/lufylegend.js/archive/lufylegend-1.10.1.zip](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目录。
这一款打飞机的游戏,打开后是这个样子。
<img src="https://static001.geekbang.org/resource/image/bb/b5/bb3765557be1728cbd606fa54cc4a8b5.jpg" alt="">
这和我们之前讲述的内容非常类似。那我就借解释一下这款游戏的代码,来教你制作。
在目录下有一个index.html这就是游戏的开始页面。在这个页面下我们先来看一下部分代码。
```
&lt;script type=&quot;text/javascript&quot; src=&quot;../load_lufylegend.js&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
if(LGlobal.canTouch){
LGlobal.stageScale = LStageScaleMode.EXACT_FIT;
LSystem.screen(LStage.FULL_SCREEN);
}
&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot; src=&quot;./js/Main.js&quot;&gt;&lt;/script&gt;
```
如果你熟悉web编程对于这些代码肯定非常熟悉。在开始的地方我们看到载入的JavaScript代码是load_lufylegend.js这个js文件包含在打飞机游戏的上一层目录内部就只有一行代码。
```
document.write('&lt;script type=&quot;text/javascript&quot; src=&quot;../../../lufylegend-1.10.1.min.js&quot;&gt;&lt;/script&gt; ');
```
我们看到在这个js文件内包含了lufylegend的原始引擎文件。至于为什么要这么做为什么要使用两个文件包含引擎是因为这样可以保持游戏代码的干净。如果你要修改引擎的包含路径不需要修改游戏本体文件只需要修改load_lufylegend.js包含路径即可。
而LGlobal.canTouch这段话的意思是如果是移动版本的话设置缩放模式为适应屏幕并且为全屏代码是
```
LSystem.screen(LStage.FULL_SCREEN)
```
最后导入游戏脚本Main.js文件。
在Main.js里面我们看到它还包含了三个别的js文件代码是这样。
```
imgData.push({type:&quot;js&quot;,path:&quot;./js/Global.js&quot;});
imgData.push({type:&quot;js&quot;,path:&quot;./js/Bullet.js&quot;});
imgData.push({type:&quot;js&quot;,path:&quot;./js/Plain.js&quot;});
```
它包含了一个共有类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[&quot;back&quot;]);
bitmap = new LBitmap(bitmapdata);
backLayer.addChild(bitmap);
```
这几段代码就是将背景图片也添加到显示节点列表。
```
//得分显示
pointText = new LTextField();
pointText.color = &quot;#ffffff&quot;;
pointText.size = 20;
pointText.text = point;
backLayer.addChild(pointText)
```
这是一个得分的显示,所以需要新建一个文本类,并设置颜色和大小,并将之放到显示节点的列表。
```
//加入玩家
player = new Plain(&quot;player&quot;,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游戏的编写方式。我来总结一下。
<li>
HTML5游戏的启动页是一个HTML文件。这个文件包含引擎的js文件。在其他别的引擎中也是使用类似的方式来加载引擎。包含了引擎就可以针对引擎所封装的接口进行游戏的开发。
</li>
<li>
HTML5游戏的循环方式和传统游戏的循环方式不同。由于HTML5游戏引擎绝大部分是使用JavaScript脚本编写的而JS本身就是以事件驱动的方式来工作的所以**使用事件驱动是HTML5游戏引擎的一个很大特点**。我们在事件侦听函数中注册一个事件函数,在这个函数内编写“游戏循环”内的代码,就能起到传统游戏循环的作用。
</li>
<li>
在HTML5游戏中我们几乎不需要关心图片的刷新或者清空操作。这些操作比之传统客户端游戏更为简洁我们只需要关心游戏的图片呈现以及操作即可。
</li>
简单来说,**HTML5游戏就是一种使用事件驱动模式并渲染在网页上的一种技术省却了传统游戏的底层操控。**你在制作的时候,可以把更多的关注点放在游戏逻辑和可玩性上。
最后,留一个小问题给你。
在类似这样的HTML5打飞机游戏中碰撞检测是怎么做的呢请结合我以前的讲过的内容给出一个答案。
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,96 @@
<audio id="audio" title="第31讲 | 热点剖析(四):安卓端和苹果端游戏开发有什么区别?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fd/ae/fdfd2df5d858c717108b07b300d4f0ae.mp3"></audio>
所谓的移动端指的是可以随时拿在手上游戏和工作的自带直流电源的设备所以从游戏角度讲手机、掌上游戏机、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里面有几个比较常用的类
<li>
SKSpriteNode用来绘制精灵的纹理
</li>
<li>
SKLabelNode ,文本渲染类;
</li>
<li>
SKVideoNode视频播放的类
</li>
<li>
SKEmitterNode粒子系统的创建和渲染
</li>
<li>
SKShapeNode基于Core Graphics 路径形状的渲染;
</li>
<li>
SKEffectNode使用遮罩来裁剪子节点的类。
</li>
因为是苹果公司推出的2D游戏引擎所以你可以在苹果的机器上编写和调试代码并且在iPhone上进行预览和测试。
在编写游戏的过程中安卓使用工具是Android Studio而苹果使用的是XCode等编程工具。除了苹果特有的游戏库之外有不少通用的游戏引擎也可供选择比如Cocos2d-x之类的在这两个平台都能编写比如Unity比如libGDX游戏库也是目前在安卓、苹果上编写游戏适合选择的一个游戏库。
所以如果使用跨平台的游戏引擎,**在两大平台下开发,除了开发语言的区别之外,底层的内容并没有什么区别**比如Cocos2d-x、libGDX等等都是跨平台生成不同目标代码的引擎。
我们继续拿libGDX来做说明。libGDX兼容多种平台系统自然也包括安卓和苹果。
这款引擎兼容性强为调试和开发提供了便利。为什么这么说呢你可以在电脑上编写测试和调试游戏但是不需要打开安卓模拟器。开发应用的同学应该知道安卓应用一般是在电脑上编写、测试、调试并且到模拟器或者安卓硬件上进行预览运行但是libGDX解决了这个问题你可以直接在桌面上进行开发测试和调试。
libGDX引擎是由audio音频操作、files文件读取、graphics2D/3D图像、math数学绘图运算、physicsBox2D物理引擎封装、scenes2D/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来编写苹果和安卓的游戏当然如果你编写的是网络游戏那么服务器端就需要你自己来编写。
## 小结
我来总结一下今天的内容。
<li>
从游戏角度讲手机、掌上游戏机、iPad都属于移动端设备。而进入智能机时代之后掌上游戏机逐渐退居幕后成为少部分游戏发烧友的选择。
</li>
<li>
安卓端和苹果端在开发游戏中的区别主要体现在开发语言上。苹果系统大量使用Objective-C、Swift而安卓大量使用的是Java。
</li>
<li>
之后我介绍了苹果端的比较流行的引擎SpriteKit以及适用于各种平台的libGDX。使用libGDX在各个平台编写出的游戏表现几乎是一样的除了执行效率或者耗电水平会有细微的差别但这和引擎其实关系不大。
</li>
最后给你留一个小问题。你知道什么是2D骨骼游戏吗能否举个例子
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,110 @@
<audio id="audio" title="第32讲 | 热点剖析(五):如何选择移动端的游戏引擎?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/84/79f0034e59b295c2572439296ea10884.mp3"></audio>
上一次,我和你探讨了[移动端游戏的一些区别和特点](https://time.geekbang.org/column/article/10423)我特别介绍了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等脚本语言。对于FlashUnity已经放弃支持这个平台了。事实上Unity在版本4.0之前是可以支持将游戏转换为Flash平台的4.0后已经不支持转成Flash游戏了。至于Flash被各家放弃的原因我在之前的文章已经讲过这里就不多说了。
Unity拥有大量的所见即所得的编程**界面开发工具**。比如支持3D骨骼和动画的导入、贴图的材质可以转换为自身的U3D格式等等。
在**底层**方面它支持OpenGL以及DX系列。它自带的物理引擎、粒子系统性能也极其高效还支持网络系统所以你可以使用Unity来编写单机或者网络游戏。
### 虚幻
这个大佬级别的引擎你一定不陌生。它现在已经支持在移动平台使用了。虚幻有免费版本和付费版本但是只有付费版本你可以获取引擎全部的C++源代码。最为可贵的是出品虚幻的Epic公司有发布[完整](http://study.163.com/course/introduction/185001.htm)[](http://study.163.com/course/introduction/185001.htm)[教学视频](http://study.163.com/course/introduction/185001.htm),而开发人员也能从网络社区获取大量的学习资源。
这款引擎不仅仅在游戏引擎技术方面比较优秀,运营方面、市场方面,在行业内都是属于前列。耳熟能详的《蝙蝠侠》《阿卡姆之城》等等用的都是虚幻引擎。
虚幻引擎有完整的**数据属性编辑功能**。关卡设计人员可以自由地对游戏中的物件进行设置,也可以通过脚本编写的形式进行优化设置。关于关卡编辑器的功能,我在[之前的文章](https://time.geekbang.org/column/article/8670)中有详细介绍。
虚幻的**资源管理器**可以进行快速准确地查找、观看并对游戏开发中的各种资源进行整理组织。地图编辑器可以让美术开发人员自由地对地形进行升降调节。更强大的是它可以通过带有alpha通道的笔刷对地图的层进行融合修饰可以在地图编辑中生成碰撞数据和位移贴图你看到的可以说是游戏场景的一种演示版也不为过。
它的编辑器还为美术制作人员提供了完整的模型、骨骼和动画数据导出工具,并可以编辑游戏事件所需要的声音文件、剧情脚本。
### App Game Kit
我个人认为App Game Kit 比较适合刚进阶的游戏开发者。这款引擎使用非常容易上手,你可以去它的[官网](https://www.appgamekit.com/)看最新发布的消息。有一款Driving Test Success的应用就是使用AGK编写的当然这是一个商业化的付费引擎。这款游戏引擎也支持树莓派上编程可谓是各个平台都兼容。
### Corona SDK
如果你深入学习过或者接触过移动端游戏开发的话你一定知道这款游戏引擎。这款引擎不仅仅是跨平台支持甚至还支持Kindle电子书这类的平台。
它的编程语言为Lua我们在之后的文章会介绍Lua和C语言的绑定我在这里只是简单说一下。相对于别的语言来说Lua更加轻量级对初学者来说也更容易上手。
Corona SDK的客户除了个人也有很多知名的大厂比如日本的南梦宫等等。当然收费也并不便宜专业版就接近600美元企业版接近1000美元当然如果是企业用户的话这点投入比之自己编写引擎来说划算得多。
###
Cocos2d-x
国内的朋友对这款引擎非常熟悉与此同时它的同胞兄弟Cocos2d系列我也在[前面](https://time.geekbang.org/column/article/9702)做过一些介绍。
很多人以为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 EditorCocos2d-Python。这个编辑器用起来非常方便随后Cocos2d的各种平台和各种语言的移植版本也逐渐被各类程序员拿去开发并且出现在开源社区。
我们可以看到几个主流语言的绑定版本:
<li>
Ruby版本ShinyCocos
</li>
<li>
安卓版本Cocos2d-Android
</li>
<li>
.NET 版本CocosNetMono based
</li>
<li>
Windows版本
</li>
这里有一幅来自它的官网的产品分支图你可以比较清晰地了解Cocos2d的各个分支。
<img src="https://static001.geekbang.org/resource/image/18/1c/183105d6284d501f3337c192ffebe11c.jpg" alt=""><br>
同时经过几年的努力Cocos系列也拥有了非常优秀的编辑器例如SpriteSheet Editors精灵序列图编辑器、Particle Editors粒子编辑器、Font Editors字体编辑器、Tilemap Editors瓦片地图编辑器
CocosStudio这款工具集套件于2012年发布它是Cocos2d-x团队官方推出的游戏开发工具。CocoStudio根据开发团队自己在游戏开发中的经验为移动游戏开发者和团队定做了这么一套集成工具用意在降低游戏开发的门槛提高开发效率当然最主要还是为了Cocos2d-x占领游戏引擎市场。
## 如何选择引擎?
今天我介绍了这五款引擎,在开发的过程中,究竟该如何选择呢?如果接触游戏开发不久的话,你肯定还是会有点懵。 **其实只要有选择,就有取舍,你只要明确自己的需求,然后结合自己的需求和引擎自身的特性,来对比选择就可以了。** 这里我把这五种引擎的一些特点,总结了一下,列了一个表格。你在选择的时候,可以作为参考。
<img src="https://static001.geekbang.org/resource/image/52/b5/5207890afc60df1fef5d8ce01edda6b5.jpg" alt="">
首先如果我们只是编写2D游戏那么Cocos系列一定符合你的预期而且能方便地制作出跨平台的游戏。
如果我们编写的是3D游戏如果是商业购买多话那么Unity和虚幻一定是不错的选择。在财力足够的情况下选择这两个大厂级别的游戏引擎一定会让你满意。
但是如果你需要编写休闲的、规模不大的游戏那么Corona、App Game Kit就可以用。
## 小结
我们总结一下今天的内容。
<li>
首先移动平台的游戏引擎有一个最大的特点游戏引擎已经逐渐演变成跨平台能力的游戏引擎如果不这么做很块就会被别的竞争对手所淘汰我们先前讲到的HTML5游戏引擎都可以编译出不同的跨平台的目标代码。
</li>
<li>
其次移动游戏引擎要挑选适合自己的才行引擎规模有大有小做出来的游戏质量也都不同。游戏开发大厂可以选择虚幻引擎中等公司可以选择Unity个人团队可以选择Cocos2d或者App Game Kit个人开发者或者轻量级开发可以选择Corona SDK。
</li>
最后,给你留一个小思考题吧。
如果用移动平台的游戏引擎编写移动应用,是否可以实现呢?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,92 @@
<audio id="audio" title="第33讲 | 热点剖析AR和人工智能在游戏领域有哪些应用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/37/e8/37742211093aeac9a0536259b958d1e8.mp3"></audio>
在2014年左右VR、AR、人工智能开始逐渐火热起来我也曾在它最火的时候深入接触过这几项技术。
如果你不是在这个领域从业的话除了VR之外有很多名词你可能都没有听说过那我们就先来看一下这些名词是什么意思。通过这几个名词我们来看一下它们究竟是如何在游戏领域应用的。
## VR是什么
**VR**本身是**Virual Reality**的缩写,意思就是**虚拟现实**。VR相对于AR、MR等技术来说在技术上是最为简单的。简单地说它只需要将3D画面做成可以通过3D眼镜呈现的内容即可那么VR眼镜是怎么播放VR画面的呢
VR基于不同视觉场在局部空间中营造出对不同画面的深度感知在用户的大脑视觉系统中形成一个虚拟现实视场而决定虚拟现实视场的主要因素是透镜而非我们人眼的瞳孔。因此为了得到更宽广的视场制作者需要缩短用户眼球与透镜间的距离或增加透镜的大小。
VR眼镜的结构一般都是透镜加上屏幕成像的方式透镜放置在眼前23厘米的地方屏幕距离透镜36厘米成像在眼前2550厘米左右。
除此之外在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](http://theorangeduck.com/page/phase-functioned-neural-networks-character-control)
除此之外也可以用在一些2D讲故事类的游戏中开放式结局的游戏中有了人工智能我们就可以把开放式解决做得更生动和有趣每个人物的结局都可以是不同的也没有任何死的套路可言但是从某方面讲这个要实现起来也有一定的难度。
## 小结
今天我们讲了VR、AR的知识以及人工智能在游戏中的应用。
AR和VR是对于人们在现实世界的一种增强技术将现实和虚拟结合起来做出更多优秀和好玩的游戏而人工智能则能让人们在游戏中的体验能更好比如智能的NPC行为漂亮的随机地图的生成等等这些技术的应用能使我们的游戏体验能变得更加丰富。
给你留个小问题任天堂出的3DS是最早的一款裸眼3D设备这种技术是怎么实现的呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,80 @@
<audio id="audio" title="第34讲 | 热点剖析(七):谈谈微信小游戏的成功点" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/99/b6/99d7476394198b0663f8b0fb8e710eb6.mp3"></audio>
在定下这个专栏目录的时候,微信小游戏仅仅出现了唯一一款《跳一跳》。在写下这篇文章的时候,微信小游戏已经在小程序领域占据了绝大部分的江山。大量开发者涌入微信小游戏,为微信生态、为微信小程序的生态增添了无数可能性。作为游戏开发者,免不了要被挤入这场战斗。今天我们就来了解一下微信小程序和小游戏背后的技术,来看看它们成功的背后,有哪些技术上的支持和创新。
## 微信小游戏的技术创新
最早以前微信小游戏有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不也挺好都挺成熟为什么一定要开发新的东西让工程师入坑呢
有很多人说,自己开发一套体系是因为微信下的棋很大,野心很大,你可以这么理解,我们今天从技术本身来看看,事情是什么样的。
我们从以下几个方面来看为什么微信要自己定义一套体系。
<li>
**可以自定义Web标准**。为什么要自定义Web标准呢我们从结果来看自然是为了提升用户体验。而从技术层面讲这和Web兼容性有关。Web标准本身是个庞大的体系。所以如果既要全部兼容Web体系又要按照自己的意愿去实现功能这是很难做到的事情。比如如果微信本身的小程序浏览器会重塑一套渲染规则比如播放视频的时候自动屏蔽广告、按钮默认变成椭圆形等等因此自定义Web标准可以去做更多的事情。
</li>
<li>
**可以自定义开发标准**。微信扔掉了Web兼容性以及标准HTML5的内容之后就开始自定义开发标准了所以微信强制要求开发者按照某种编码规则来编写代码从而解决了在普通Web编程中“如果不用某种规则来编码就会出现兼容性问题”的难题这样就从源头上解决了这个问题。事实上这也是“强制约束开发者写出素质较高的代码”。
</li>
<li>
**可以有比HTML5更强的功能**。完全兼容标准HTML5的话并非不可能如果你熟悉前端开发的话就会知道这个坑会有多大。因为首先HTML5不具备很多功能比如获取手机设备信息、获取手机罗盘、地图定位等等。但如果用自定义的体系加上从微信作为App本身具有的底层获取功能就完全可以做到了。
</li>
<li>
**防止刷流量、刷广告**。在防止垃圾HTML5页面出现的这件事情上微信做了大量的工作比如你应该经常可以看到的如果出现单纯的IP地址的页面微信就给出提示询问你是否跳转或者提示你可以举报诱导分享等等当然这都不能完全避免垃圾HTML5页面出现在微信生态下所以在小程序自定义规则的情况下你只能按照定义规则来开发如果想钻空子最后小程序和小游戏的上线还有一道人工审核的关卡所以想要出现垃圾HTML5页面的情况几乎是不可能出现的。
</li>
<li>
**方便后续优化**。由于微信自己那一套体系是高层次抽象层,所以微信小程序团队可以在用户完全没有感知的情况下进行底层优化,而上层不用修改任何代码就可以了。
</li>
所以说微信小游戏其实是基于HTM5技术并在此之上充分结合自己的需求和产品特性添加了自己的创新。
## 微信小游戏成功的原因
微信小游戏建立在微信本体上,因此微信能获取到的移动端的**底层功能**小游戏基本都能一并获取比如网络连接、内部存储等等而HTML5做不到这点。微信可以获取底层平台的接口且并不需要授权因为微信安装上去后已经获取了手机的权限小游戏想要或者一部分权限是很容易的事情相比HTML5游戏要从浏览器获取权限那就是很麻烦的一件事情。
其次,因为微信本身作为**流量入口**对于小游戏的传播是一个极大的便利独立开发者或者中小游戏开发公司如果前期没有推广资源和推广渠道那么通过微信本身这个巨大的流量入口就可以获取相比传统HTML5游戏更好的效果。
除此之外传统HTML5小游戏可以包装成App的外壳做成App的形式供人下载因此又多了一个获客途径。到了后期苹果公司加大了对于HTML5应用和游戏包装成App这种形式的审核力度加上网页机制的运行效率限制HTML5套壳程序比原生应用的体验感也更差。
传统HTML5游戏发布之后如果需要更新则需要重新上传一次网页代码。如果用户端还需要重新刷新网页甚至清除浏览器缓存等复杂的操作如果做成App套壳那就更需要在各个渠道市场上传一遍程序这在效率上就慢了一截。而微信作为平台本身更新游戏后你重新获取只需要上传到微信平台就可以了获取新游戏刷新网页缓存微信一并帮你做了。
传统HTML5游戏的**广告接入**,是非常麻烦的一件事情,你需要去和广告商去做分成机制、对接广告接口等等。如果是微信小游戏,你只需要对接微信自己的广告渠道就可以,可以说是一键就能对接几种广告机制,广告分成也是透明的、公开的。
微信平台本身拥有**打击拷贝游戏**的能力,如果有一款游戏被抄袭了,你可以投诉,如果发现属实,微信就会将抄袭的游戏下架,这样一来,就保护了原创游戏,激发广大开发者创作出更好的游戏,给微信带来更好的游戏生态。当然这里所谓的原创机制,是指的微信小游戏本身生态下的原创,因为作为成熟的游戏生态来讲,已经基本不存在狭义上的“原创”这两个字了。
## 小结
这节内容差不多了,我们来总结一下。我和你讲了这样几个内容。
<li>
微信小游戏、小程序都是基于HTML5技术的而微信小游戏的技术就是HTML5技术只是在小游戏中运用的框架并不是普通的HTML5的框架。
</li>
<li>
微信小程序用的是基于WebView的技术小游戏却没有用到WebView技术而是修改了HTML5规范的一些接口内容成为腾讯自己的内容。
</li>
<li>
微信平台之所以要自定义自己的一套体系比如Web标准、开发标准等等是因为可以结合微信本身的特性在此基础上制作出更契合腾讯生态的产品一句话就是为了方便自己开发、优化和管理。
</li>
<li>
微信小游戏基于微信的底层获取功能、广告接入优势和打击拷贝的能力,塑造了一个更好的微信小程序生态。
</li>
迄今为止,你最喜欢玩的一款微信小游戏是什么?它吸引你的点是什么?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,74 @@
<audio id="audio" title="第35讲 | 热点剖析(八):谈谈移动游戏的未来发展" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5b/56/5b67d559d069be0cb6d57058f9a69156.mp3"></audio>
随着芯片纳米工艺技术不断提升,我们的电子设备变得越来越小,而随着移动设备的普及,移动游戏一定是未来游戏行业发展的重头,未来游戏玩家将会花更多的时间在移动端的游戏上,所以我们今天就来探讨一下移动游戏的未来发展。
## 现阶段移动游戏是什么样?
在展望未来之前,我们先来看看现阶段的移动游戏是什么样的。
首先移动游戏比较轻量级一局游戏时间快能很快出结果。像竞技类游戏的一局时间一般不会超过30分钟。由于手机屏幕和操作限制没有游戏机手柄或者电脑鼠标键盘之类的方便操作所以移动端的游戏操作手法极尽简化所以需要尽量突出让人第一眼就能耳目一新的画面效果。
各大平台级的App都推出了不同的“小程序”技术只是名称叫法各有不同除了叫“小程序”还有“快应用”“轻应用”的说法等等。在这样的聚合平台下重量级原生游戏已经逐渐减少转而都去了聚合平台下比如微信小游戏。
而HTML5游戏依然有人在做只是变成了大中型电脑网页游戏的形式以及营销推广渠道的一种方法。这其实和以前Flash的路径一样。如果现在有人做HTML5游戏的话更多的可能会往微信端、支付宝端等平台级应用上的小程序方向走。
原生移动端游戏往高操控性、高画质、高可玩性这几个方向走人群更接近核心玩家更多碎片时间的游戏内容将往HTML5、微信小游戏等方向走。
## 什么是移动游戏发展的最大掣肘?
要思考移动端游戏的发展,不得不提硬件的发展。没有硬件的发展,移动游戏始终是这样几种形式。而在硬件问题还不能解决的情况下,移动游戏有哪些路可以走呢?
首先往更大型的方向发展逐步取代电脑上一些中等、小型的游戏。手游的硬件发展带动质量的发展变得更像电脑上的游戏3D、2D的精美的游戏将更多出现在市场上。而重度游戏的发展将会有迎合重度硬核玩家的游戏加入包括周边设备的联动推出比如手柄、键盘等等。
AR、VR等功能游戏的推出就算是在现在这几种类型也是一直在市场上推出在不久的将来手机硬件的提升会让这类游戏变得越来也多裸眼3D游戏也会变得越来越多。
而更多利用手机硬件特性的游戏将会出现比如随着苹果前置感应头的出现前置感应的人体体感游戏或者脸部感应游戏也会出现这些其实都是以前Wii时代体感游戏的缩小版本。
手机游戏与电视或者电脑游戏联合互动。随着任天堂Switch的发布多屏互动小屏和大屏游戏联合也会是将来移动游戏的一个方向。如果有新的巨头加入手机竞争比如微软那么手机和电视机连接玩耍或者安卓系统的电视盒子和手机端联动也将会是一个发展的趋势。
其次人工智能将出现更好的发展泛游戏类型和应用以及游戏内的人工智能结合AR、VR等的游戏也将出现游戏将会给玩家一个更好的体验。
移动端的硬件发展,除了平板市场之外,手机市场的硬件发展并不是特别快,而平板市场则是下一个移动端硬件发展的风口,比如安卓的拥有独立显卡的平板,这就给深度学习、人工智能提供了良好的运算平台以及商业接口。毕竟高质量的移动游戏需要在更高级的硬件设备上才能呈现。
## 移动设备未来会有什么样的变化?
在这个基础上,我们的移动设备会有什么变化呢?
移动设备会随着硬件的发展变得越来也小。虽然越来也小但是计算能力已经遇到了摩尔定律的瓶颈所以虽然硬件可以越做越小但是CPU的计算能力却并不见得能变得更为强大。而可能是今年的普通移动设备A在明年会缩小变成B。B的计算能力和A一样只是变小了而明年做成普通移动设备的C计算能力会比B更强大一点然后后年C也会缩小变成DD变成和C一样的计算能力这样一代一代往下发展。
由于移动设备变得越来越小那么小型移动设备上的游戏也会变得可观比如智能手表上裸眼3D的小游戏甚至就像科幻片里面手表上的游戏投影在手臂上方这种模式不再变得不可能。
我们之前说过,谷歌眼镜并不成功,并不是应用少的问题,而是场景的问题。试问谁会戴着眼镜到处跑,为了利用眼镜识别各种物件,而非常不自然地盯着某个物体看半天?其次,技术也不是特别成熟,上传数据到分辨并且得到信息是有时间差的,无法做到完美实时。
我们不妨做一个实验,打开手机摄像头,拍摄视频,你会看到视频中的影像比现实中大致慢半秒左右,这是由于视频抓取后呈现,流的压缩等各种手机端的计算占据了一定的时间,所以哪怕本地都无法做到完美实时,又如何将网络端交互变成实时的呢?
所以未来移动设备变得小的同时,做的应用和游戏也应该考虑到**互动效果**和**硬件**的制约因素。
移动设备并不只是指手机和PAD眼镜、手表、车载导航之类的也可以算是移动设备这样很多游戏都可以放在不同的移动平台下比如车载导航内的小游戏比如手环上的简单游戏都是发展的方向。
而AR和VR体感游戏依然是移动端游戏的一个卖点因为可以随时随地玩游戏所以结合现实世界和虚拟世界类型的游戏将会越来越多再结合地图定位能玩转的游戏类型也会越来越多如果能结合现实中的头戴式设备比如类似谷歌眼镜的成熟产品那么结合AR VR等的应用将会变得非常生动和有意思。
前些年就有PokeMon之类的AR游戏风靡了一阵子但是这类游戏最主要是是结合了一个强大的IP之上其次是新鲜感当时去新鲜感后如何让玩家保持继续玩下去的动力就是游戏公司需要做的事情了。
**制约移动设备发展的,并不是技术本身,或者将芯片做小的能力,具体来说就是电池,** 不仅仅是移动设备,事实上各种设备上的制约因素都是电池,包括笔记本、电动汽车、无人飞机等等,只有解决了电源问题,科技才可能有质的飞跃。
如果我们有了大容量的电池且不增加额外电池尺寸开销的情况下,移动端游戏会有怎样的发展呢?
如果电池得到了发展移动端游戏将会出现和PC端游戏一样的效果比如玩3D游戏再也不需要半小时一个小时充一次电了玩再久的游戏都只要两天或者更多天才充一次电这在现在是无法想象的比如智能手表上的游戏不可能一只手表玩了游戏后一天充一次电这实在是非常尴尬的事情。
## 小结
我们来总结一下今天的内容。
<li>
移动端游戏将往更加精品、高画质、高品位的游戏转型。3D、VR、AR等游戏也会层出不穷移动端和大屏幕之间的阻隔也会逐渐模糊两边都可以互相游戏。
</li>
<li>
另外如果摆脱了电池的掣肘那么移动端游戏将往PC端游戏的方向发展更高质量的3D游戏也不用担心电池耗尽而半途无法玩了。
</li>
给你留个小问题吧。
如果让你来开开脑洞,思考下移动游戏的未来发展方向,你认为移动游戏会怎么走下去呢?移动游戏的未来是往高品质高画质走下去,还是往真实与虚拟结合走下去呢?还是有其他的方向?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,63 @@
<audio id="audio" title="第36讲 | 热点剖析(九):谈谈独立开发者的未来发展" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/56/57/56e1610d3636fb5dd7b629ca8eae9f57.mp3"></audio>
我曾经看过这么一句话,**游戏开发是一门异常复杂的综合性艺术创作**。作为个人独立开发者,一个人常常需要包揽所有职责,策划、美工、原画,甚至推广。
说到推广不得不提的就是发布平台。比如我第一时间想到的就是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市场也有较好的收费机制。而微信小游戏则是统一的广告、支付等渠道用起来也比较方便。所以独立开发者可以选择对自己较为有利的方案进行针对性开发和推广比如选择微信小游戏或者苹果端游戏如果自己的开发成本不是特别充裕这是一个相对稳妥的选择。
不管是独立游戏开发还是别的,我个人觉得现在的游戏一定要有这样几个特点:轻、快、多。
首先,游戏体量要够轻,够小,打开手机,打开电脑翻开一个网页就能玩,玩完就走。
其次,游戏体验要快速。一盘游戏不能时间太久,一局超过十分钟就会逐渐变成重度游戏,除非有很强的吸引力,否则时间太久的游戏大多数人都不愿意尝试。
游戏版本一定要不停迭代。从玩法、画面,到平衡性,这样才会让玩家保持新鲜感,不停地被游戏粘滞住。
## 小结
这节我和你聊了独立开发者的未来发展。
<li>
DOS时代的时候一个人或者一个小团队就可以做出一款游戏但是这种时代已经过去了。游戏开发难度变小了但是游戏制作难度变得越来越大了。
</li>
<li>
尽管Steam依旧是目前最大的发布平台但是由于它本身没有对上架的游戏进行分类、整理有选择的展示。对独立开发者其实是不利的。加上Switch、XBOX等主机游戏相继都推出了自己的独立游戏计划我其实更看好主机游戏的发展。
</li>
<li>
其实不管基于什么平台,把游戏本身做好,才是最重要的。
</li>
你想成为一个独立游戏开发者吗?你想做出一款什么样的游戏?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,180 @@
<audio id="audio" title="第19讲 | 如何嵌入脚本语言?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7f/48/7fea12ffdf335e92000c5730677ee148.mp3"></audio>
从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讲](https://time.geekbang.org/column/article/8782)里已经非常详细地讲过如何将Lua脚本编译成为静态库如果不记得的话可以回去复习一下。编译好静态库liblua.a之后我们就可以在编程中使用它了。
你也可以选择在解压缩出来的目录内使用make命令来直接编译编译会生成Lua虚拟机的执行文件lua.exe、luac.exe当然这需要一整套MinGW的环境支持。
开始我们还是使用MinGW Development Studio来创建一个工程。由于只是示例所以名字可以任意取。我取一个叫作lua_test的工程名并且将工程设置为Win32 Console Application。你可以看这个示例图。<br>
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/c2/51/c214aaccf9b6cd231d73304beea8ba51.jpg">
建立好了工程之后我们新建一个test.c文件。这个文件位于lua源代码路径下。我们将liblua.a 文件也放到同一个目录下,以方便后续链接时候调用。
在包含Lua头文件之前我们需要将头文件写在某一个.hpp文件下以便一次性包含进去我们的代码可以这么写。
```
#ifdef __CPLUSPLUS
extern &quot;C&quot; {
#endif
#include &quot;src/lua.h&quot;
#include &quot;src/lualib.h&quot;
#include &quot;src/lauxlib.h&quot;
#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有两种读取脚本的方法。**
<li>
一种方式是**读取后直接运行调用的函数是luaL_dofile**。使用这个函数,脚本会在读取完毕后直接运行。当然如果出现错误,你也不知道错误的具体位置在哪里,调试起来不是很方便。
</li>
<li>
第二种方式是**将脚本代码压到栈顶然后使用pcall操作运行脚本这个函数叫luaL_loadfile**。事实上第一种方式也是使用这种方式并且将pcall操作直接调用起来第一种方式的代码一看你就能明了。
</li>
```
#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。<br>
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/af/ad/af00612d1f227cac1900b5c9e153c6ad.jpg">
## 如何使用Lua以及liblua.a来进行与C语言的绑定操作
我们现在开始使用Lua以及liblua.a来进行与C语言的绑定操作。
首先我们需要包含之前我们所定义的lua.hpp头文件随后我们开始在main入口函数处定义一些变量。
```
#include &quot;lua.hpp&quot;
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(&quot;err1: %s\n&quot;, err);
return 1;
}
r = lua_pcall(ls, 0, 0, 0);
if(r)
{
err = lua_tostring(ls, -1);
if(err)
printf(&quot;err2: %s\n&quot;, 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 &quot;test running&quot;)
```
我们故意少写一个括号,然后将源代码命名为 a.lua我们来运行看看。会出现一个这样的错误信息<br>
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/cf/2a/cfad6d423a3c95bacba12b5e8dc3782a.jpg">
在发现语法错误后程序就会报错另外如果你输入了一个根本不存在的文件比如我们这么运行test_lua xxx.lua也会在loadfile的时候出错。
## 小结
我们今天的内容就到这里。下次我会进一步把Lua的脚本嵌入的细节呈现在你面前。我们来总结一下今天的内容。
<li>
因为Lua脚本足够轻量级几乎没有冗余的代码。Lua虚拟机的执行效率几乎可以媲美C/C++的执行效率。所以我们选择使用Lua脚本来嵌入C/C++硬代码。
</li>
<li>
Lua脚本在C/C++语言里面嵌入,需要先声明一个虚拟机并且赋值给指针。
</li>
<li>
Lua脚本需要先loadfile再pcall调用脚本文件loadfile会检查最基本的脚本文件内容比如文件是否存在比如脚本代码是否出错而pcall会在运行时出错的时候将错误压至栈顶。
</li>
<li>
Lua错误会将错误压制栈顶我们要取出来需要使用-1下标取出栈顶的内容并转成string打印。
</li>
给你留一个小问题吧。
如果直接使用luaL_dofile相对于把loadfile和pcall分开写这样有什么优劣呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,166 @@
<audio id="audio" title="第20讲 | 脚本语言在游戏开发中有哪些应用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/de/3b/dee9c9b0ac13b8adef126d53d397b53b.mp3"></audio>
上一次我们谈到了如何在游戏中嵌入脚本语言我们用的语言是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, &quot;something&quot;);
return 1;
}
```
这里面lua_tosting 就是这个函数的传入参数,传入的是一个字符串的参数;第二个参数也是字符串参数,其中 lua_tosting 的第二个参数1或者2表明的是在Lua虚拟机的堆栈中从栈底到栈顶开始计数一般先压入的参数在第一个后压入的在第二个以此类推。返回1的意思是这个函数会返回一个参数这个参数就是我们前面 lua_pushstring 后压入的这个内容something这就是返回的参数。
那么这个函数究竟怎么注册成为Lua函数呢我们来看这段代码。
```
lua_register(L, &quot;test&quot;, &amp;test_func);
```
lua_register函数的功能是注册C函数到Lua虚拟机。其中L是虚拟机指针。这个在前面的代码都有说到而第二个参数test就是注册在Lua虚拟机中的函数名所以这个函数名叫test。第三个参数是函数指针我们把test_func这个函数传入到lua_register函数中。这样一个函数就注册好了。
那么如果我们在游戏中有许多许多的函数需要注册到Lua中那么这种写法是不是太慢了有没有一种快捷的写法来支持注册等操作呢
如果你没有C/C++的语言基础或者C/C++语言基础比较薄弱下面的内容可能需要花一点时间消化我也会竭尽所能解释清楚代码的意思但如果你已经是个C/C++程序员,那么下面的代码对你来说应该不会太难。
我们需要使用lua_register我们先看它里面有什么参数。第一个是**字符串**,也就是**char*<strong>第二个是**函数指针</strong>,也就是**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, &amp;ph_##fname
#define func_lua(fname) int ph_##fname(lua_State* L)
```
其中func_reg是在给前面那个结构体初始化赋值的时候使用的因为我们知道如果我们需要给这个结构体赋值看起来的代码是这样
```
ph_func pobj = {&quot;test&quot;, &amp;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, &quot;something&quot;);
return 1;
}
void register_func(lua_State* L)
{
int i;
for(i=0; i&lt;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”以及&amp;ph_test_func函数指针。
最后我们来看一个重要的函数,**register_func**这个函数在后续将会输入一个Lua虚拟机指针但是我们要知道它在函数内部它做了什么东西。
```
int i;
for(i=0; i&lt;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进行互通了。
## 小结
我总结一下今天所讲的内容。
<li>
在Lua与C的结合过程中C语言需要新建一个Lua虚拟机然后使用虚拟机的指针来操作Lua函数。
</li>
<li>
在程序的应用中使用C语言中的一些宏的技巧可以使代码能够便利地应用在程序里。
</li>
最后,给你留一个小问题。
如果使用Lua往C语言传递一些内容比如从C语言获取Lua脚本中某个变量的值应该怎么做
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,224 @@
<audio id="audio" title="第21讲 | 如何使用脚本语言编写周边工具?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/66/a9/6668f24a830dd8e9f9a604e0878f75a9.mp3"></audio>
上一节我们讲了脚本语言在游戏开发中的应用我列举了很多C语言代码这些代码做了这样一些事情
<li>
使用C语言和Lua语言进行沟通
</li>
<li>
在C语言代码里使用了宏和结构方便批量注册和导入C语言函数
</li>
<li>
Lua代码如何传输内容给C语言
</li>
<li>
Lua虚拟机堆栈的使用。
</li>
这一节我们要用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(&quot;test.exe&quot;);
linker(&quot;c:\\develop\\dm\\bin\\dmc.exe&quot;);
compiler(&quot;c:\\develop\\dm\\bin\\dmc.exe&quot;);
source_code(&quot;c.cpp&quot;, &quot;fun.cpp&quot;, &quot;x.cpp&quot;);
source_object(&quot;c.obj&quot;, &quot;fun.obj&quot;, &quot;x.obj&quot;);
compile_param( &quot;$SRC&quot;, &quot;-c&quot;,
&quot;-Ic:/develop/dm/stlport/stlport&quot;,
&quot;c:/develop/dm/lib/stlp45dm_static.lib&quot;);
link_param(&quot;$TARGET&quot;, &quot;$OBJ&quot;);
make();
shell_command(&quot;del *.obj&quot;);
```
首先第一行对应的就是目标文件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&lt;string&gt; source_code;
vector&lt;string&gt; source_object;
vector&lt;string&gt; c_param;
vector&lt;string&gt; 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(&quot;c.cpp&quot;, &quot;fun.cpp&quot;, &quot;x.cpp&quot;);
source_object(&quot;c.obj&quot;, &quot;fun.obj&quot;, &quot;x.obj&quot;);
```
这两行填入了cpp源文件以及obj中间文件这些填入的参数并没有一个固定值可能是1个也可能是100个那在C/C++和Lua的结合里面我们应该怎么做呢
我们看到一个函数lua_gettop。这个函数是取得在当前函数中虚拟机中堆栈的大小所以返回的值就是堆栈的大小值比如我们传入3个参数那么返回的就是3。
接下来可以看到使用Lua的计数方式从1开始计数并且循环结束的条件是和堆栈大小一样大然后就在循环内将传入的参数字符串压入到C++的vector中。
随后的source_object、compile_param和link_param都是相同的方法将传入的参数压入到vector中。
你可能要问了我在Lua的代码中看到了$TARGET、$OBJ、$SRC等字样的字符串这些字符串的处理在哪里这些字符串又是做什么的呢
这些字符串是替代符号你可以理解为C语言中printf函数的格式化符号例如 “%d %s”等等虽然在这里这些符号都是自己定义的但是我们仍然需要解析它们。
其实解析的步骤并不难我们只需要将vector内的内容提取出来对比是不是字符串$TARGET等如果是的话就被替代为前面我们在target函数或者source_code函数中所定义的内容。
我们拿source_code部分来举例来看一下部分代码。
```
void run()
{
string command_line;
string src = &quot;$SRC&quot;;
string tar = &quot;$TARGET&quot;;
string obj = &quot;$OBJ&quot;;
for(int i = 0; i &lt; source_code.size(); i++)
{
..............
for(int j=0; j&lt;c_param.size(); j++)
{
if(c_param[j] == src)
{
command_line += source_code[i];
.....
}
}
}
```
在这部分的代码里面可以看到我们将压入的source_code内容进行循环。在循环之后必须对c_paramcompile_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(&quot;my_makefile&quot;);
makefile = &quot;my_makefile&quot;;
if(!in.is_open())
{
in.close();
}
else luaL_dofile(L, makefile.c_str());
```
在这段代码里面我们首先使用C++的fstream库中的ifstream来尝试读取是不是有这个my_makefile文件如果没有的话就跳过并且关闭文件句柄如果存在的话就把这个文件填入到Lua虚拟机中让Lua虚拟机直接运行这个源文件。所以这种方式是最简单快捷的。
代码有点多,不要担心,我带你梳理一下今天的内容。
<li>
**利用C/C++语言和Lua源代码进行交互从Lua代码中获取数据并且在C语言里面进行算法的封装和计算最后将结果返回给Lua。** 我们在C/C++语言里面进行大量的封装和算法提取并且也利用C/C++进行调用和结果的呈现这是一种常用的方式也就是C语言占比60%70%Lua代码占比30%40%。
</li>
<li>
另一种比较好的方式是,**使用C/C++编写底层实现逻辑随后将数据传输给Lua让Lua来做逻辑运算最终将结果返回给C语言并且呈现出来**。这是很多人在游戏开发中都会做的事情比如我们编写地图编辑器先在Lua中编写好逻辑用C语言在界面中呈现出来即可。如果反过来做的话那就会出现大量的硬代码是很不合适的。所以这种情况下C语言占比30%40%Lua代码占比60%70%。
</li>
<li>
<p>**Lua可以是一种胶水语言。严谨地说像Python、Ruby等脚本语言都是合格的胶水语言。** 在这种情况下胶水语言起到的作用就是粘合系统语言C/C++)和上层脚本逻辑。所以,使用胶水语言,就像是一种动态的配置文件。<br>
按照普通的配置文件来讲你需要手工解析比如类似INI、XML、JSON等配置文件随后按照这些文件的内容来做出一系列的配置但是胶水语言不需要它本身就是一种动态的语言。<br>
你也可以把它当作一种配置的文件就像今天讲的Makefile它可以不需要你检测语法问题这些问题在Lua虚拟机本身就已经做掉了你需要做的就是将我们脑海里想让它做的事情通过C和Lua的库代码进行整合直接使用就可以了。所以**胶水语言的本身就是一个配置文件,同时它也是一个脚本语言源代码。**</p>
</li>
## 小结
在使用C/C++结合脚本语言的时候需要梳理这些内容比如哪些是放在C/C++硬代码里写的那些可以放到脚本语言里写梳理完后就可以将脚本语言和C/C++结合起来编写出易于修改脚本逻辑如果有不同需求可以很方便地改写脚本而不需要动C/C++硬代码)、易于使用的工具。
现在给你留一个小问题吧。
在Lua当中有table表的存在如何在C语言中给Lua源代码生成一个table表并且可以在Lua中正常使用呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,37 @@
<audio id="audio" title="结束语 | 做游戏的过程就像是在雕琢一件艺术品" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d0/76/d0291902a7a583bcfe400a172383ce76.mp3"></audio>
我们用三个月的时间一起创作了《从0开始学游戏开发》这个专栏。转眼间到了说再见的时候回想起来只觉得时间过得真快。
说实话,这三个月我是“痛并快乐”着的,接下来就和你“诉诉苦”,也聊一聊我的收获和成长。
“苦”在哪里呢?坦白说,写专栏真是一件费时费力的事。每周三篇稿子,既要保证不断更,又要保证质量,而我平时的工作非常忙,几乎都是在夜晚赶稿。每次都是先构思,然后写代码、验证代码的正确性,才开始写文章,和编辑反反复复修改,还要再去录音。
录音又是一个“苦差”,夏天蝉鸣声非常大,为了找到一个安静的录音环境,我经常在地下车库录,一个人关在车里,一录就是个把小时,一会儿就汗流浃背。
但是,我把这件事坚持做下来了,因为这个过程中我也非常开心,成长了不少。
写专栏前我刚写完一本书。写这本书用了我将近两年的时间所以在一开始创作专栏的时候我一直用写书的感觉来写专栏文章。我想的是以PC游戏为突破口包含客户端和服务器端按照完整的开发路径和开发顺序讲解开发一款PC游戏最最基本的流程和细节。
但是专栏开始后我立刻就收到了很多反馈。我了解到很多同学的编码经验可能不是非常丰富于是在后续的文章中我做了一些调整。在写的过程中我假想面对的是初入coding世界的自己除了之前构思的内容还对代码环境给出更详细的说明对为什么要这么做也写了更多我自己的思考和总结。
这是一个转变,从一开始不知道以什么姿势写专栏,到后期能够不断地梳理和提炼自己的想法和经验,不得不说,写专栏这件事,对我本身的改变是非常大的。
而给我带来这个转变的,是你的反馈。你的每一个留言我都会认真看,感谢这些留言,使我不断想起自己最初做开发的样子,想起那些激情澎湃的日子。
我在博客里写过一个系列的文章叫“代码十年”,记录了我十多年的代码生活。
那时我刚开始学C++,对这门语言非常感兴趣,每天都发愤看书,看完之后把习题抄下来,然后模拟,自己再写一份。那时候对编程的入迷程度,几乎可以用痴迷来形容。 那时我和一起工作的程序员,对一种算法几乎都可以研究半天。
后来,我开始自己创业,遇到的问题变得越来越多,也越来越复杂。这让我慢慢抛开编程本身,去思考更多的事情。
游戏开发是个复杂的过程,我们时时刻刻都需要学习、更新和积累知识,比如,编码基础知识、编程结构性知识,软件架构、业务架构知识,以及产品设计、市场、推广,乃至整个行业的动态信息。
现如今十多年过去了游戏这个行业依然风起云涌。虽然大公司几乎垄断了70%以上的游戏资源但是每年依然有不少中小游戏公司借助小游戏、HTML5突出重围而且独立开发者的未来更是可期。
我自己觉得,**开发游戏就是在雕琢一件艺术品,从哪里下刀,怎么切入,怎么设计,人物的表情、仪态、动作如何,完全取决于手握刻刀的你。而现在你刻下的每一刀,都决定了这件艺术品以后的生命周期**。
专栏虽然已经结束,但这对你我来说,还只是一个开始。我会继续在游戏行业深耕下去,并试着输出更多的内容。我希望,这对你来说,也是一个开始。毕竟这次我们只做了一个小游戏,在游戏世界里,只是戳开了一个小口子,我们还要朝着游戏开发世界的大门,不断前进。
路漫漫其修远兮,吾将上下而求索。希望我们可以一起努力,给游戏领域注入自己的力量。
[<img src="https://static001.geekbang.org/resource/image/e4/ad/e43e1d08d65b9c3b3dc21e379afb91ad.jpg" alt="">](http://cn.mikecrm.com/WM3BaeU)

View File

@@ -0,0 +1,10 @@
你好,我是蔡能。
到这里《从0开始学游戏开发》这门课程已经全部结束了。我给你准备了一个结课小测试来帮助你检验自己的学习效果。
这套测试题共有 20 道题目包括16道单选题和4道多选题满分 100 分,系统自动评分。
还等什么,点击下面按钮开始测试吧!
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=187&amp;exam_id=436)