上一讲学习了Python游戏开发的简单方法,并在练习中下载和阅读了几个小游戏的源代码。本讲将自己动手制作游戏,练习编写50行左右的比较复杂的程序,在此过程中将继续学习图形图像相关知识、播放声音,以及运用前面学习的各种数据类型、函数等技术。
15.1 图形图像
15.1.1 原理
早期的计算机和手机都用按键输入,屏幕只能显示文字。后来随着软硬件升级,逐步出现彩色显示器,以窗口界面为主,使用鼠标和触摸屏操作的计算机和手机系统。
现在在屏幕上看到的所有显示都是以图片方式“绘制”出来的。之前讲过软件分为系统软件和应用软件,系统软件的一部分工作是管理当前开启的应用程序(应用软件),并将各个软件的输出“绘制”在屏幕上。因此作为普通开发者,只需要考虑应用软件的输出即可。
上一讲在窗口中绘制图形:窗口是开发者在程序内定义的一个固定大小的区域,绘图位置通过坐标(x,y)指定。
屏幕上显示的画面又分为图形和图像两种,之前学习的绘制矩形、圆形,以及显示文字都属于“图形”,另一种是图像,它可能是拍摄的照片或者用画图软件绘制的卡通人物或风景,在Python程序中从文件读出,并将其内容绘制到某一区域,叫做绘制图像。
15.1.2 图像格式
从绘图的角度看,图像分为两种:带有透明度通道的图片和不带有透明度通道的图片。
图15.1 图片透明通道示意图
如图15.1所示,将两张卡通羊图片绘制在背景图上,左侧不带透明通道,因此将其白色背景也贴了出来,右侧带透明通道,并其将卡通羊的背景置为透明,贴图后的效果更加自然。一般情况下,在绘制背景图片,以及后面不需要透出其它图片时,使用不带透明通道的图片即可。而绘制前景图片,比如游戏中需要移动变化的卡通人物,则使用带透明通道图片。
图像一般存储在图片文件中,通过不同的扩展名区分不同的格式,最常见的图片格式有bmp、jpg和png。bmp是早期的图像存储格式,它直接把图片中每个像素的颜色存储在图片文件中,占空间较大;jpg是最常用的图片存储格式,它用压缩算法将图片内容压缩后存储,占存储空间较小,有时也会损失一些图片的质量,在保存图片时可以设置压缩比例,以便在图片的大小和精度之间达到平衡;png采用无损压缩格式,存储的是带透明通道的图片。
15.1.3 获取图像
图形界面或多或少用到图像,比如:网站的Logo(商标)、软件的欢迎界面、小图标等,游戏为了提升视觉效果会用到更多的图像。
图片可以从网络下载,方法是:打开浏览器,百度图片搜索:https://images.baidu.com/, 输入想要查找图片的关键字,找到合适的图片后,在图片中点击右键,选择“另存图像为”,即可将网络上的图片下载到自己的电脑上。如图15.2所示:
图15.2 下载网络图片
从网络上下载的图片多数是jpg格式,如果需要透明通道,则要使用绘图软件修改,或者在搜索时指定png格式。
下载网络图片简单快捷,但会涉及图片的版权问题,如果仅用于学习和实验,问题不大。也可以自己用电脑绘制图片,如使用Windows自带的绘图工具:开始->所有程序->附件->画图,绘图后保存成图片文件即可在程序中使用。另外,也可以把手机中的照片文件通过微信传到电脑上使用。常用的绘图工具还有用于修图的PhotoShop,和用于绘图的Illustrator等等。
15.1.4 Python绘图
任何编程语言,操作图片一般都需要三步:读取图片文件、调整图片、显示图片。本例使用游戏开发库pygame操作图片:
第一步,使用pygame.image.load()函数读取图片,它支持主流的图片格式,读取文件后将其转换成pygame的内部图片存储格式。
第二步,使用pygame.transform.scale()函数将图片缩放到指定大小。
第三步,使用画布提供的screen.blit()函数将图片按指定的位置显示在画布(绘图窗口)上。
01 import pygame
02
03 WIDTH = 640
04 HEIGHT = 480
05
06 pygame.init()
07 screen = pygame.display.set_mode((WIDTH, HEIGHT))
08
09 back_img = pygame.image.load('bg.jpg')
10 background = pygame.transform.scale(back_img, (WIDTH, HEIGHT))
11
12 running = True
13 while running:
14 for event in pygame.event.get():
15 if event.type == pygame.QUIT:
16 running = False
17 screen.blit(background, (0, 0))
18 pygame.display.update()
19
20 pygame.quit()
第01-07行引入pygame游戏开发模块,初始化,并创建一个宽为640,高为480的窗口。(详见上一讲)
第09行加载当前目录下的图片文件bg.jpg。
第10行将图片缩放成与窗口同样的大小。
第12行设置变量running,用于控制程序主循环。
第13行用while实现程序主循环,当running为True时循环显示,否则退出。
第14-16行接收用户操作,当用户关闭窗口时将running设为False,退出主循环。
第17行用blit方法将背景图片显示在窗口之中(类似于将图像区域复制粘贴到显示区域),显示位置为(0,0)。
第18行更新显示,将内存中的数据显示在屏幕上。
第20行释放pygame资源。
15.2 播放声音
背景音乐和音效也是游戏重要的组成部分,本节介绍播放背景音乐和播放普通音效的方法。
15.2.1 音频格式
与图像一样,声音数据也存储在音频文件中,常见的声音文件有两种格式,常用的扩展名有wav、ogg和mp3。wav也叫波形声音文件,是早期的数字音频格式,占空间较大,不适合较长时间的声音存储,常用于存储长短为几秒钟的音效,例如“叮”的一声响,而mp3是目前比较常用的声音文件存储格式,它使用压缩算法,对音频有一定损失,但相应数据量也较小,便于存储和传输。
15.2.2 获取声音文件
声音文件也可以从网络下载,或者自己录制。Windows系统中也有很多自带的音效文件,在计算机中搜索扩展名为“.wav”的文件即可查找音效文件,使用时可将其复制到程序能访问的位置。搜索方法如图15.3所示:
15.3 查找音效文件
15.2.3 Python播放声音
1. 播放背景音乐
播放声音文件,首先需要初始化声音模块,然后加载要播放的声音文件,最后是播放声音,本例中同样使用pygame游戏开发模块中提供的mixer混音子模块实现播放背景音乐。
使用pygame.mixer.init()初始化声音相关模块。
使用pygame.mixer.music.load()加载常见格式的声音文件。
使用pygame.mixer.music.play()播放声音。
使用pygame.mixer.music.set_volume()调节音量。
程序导入模块和主循环与前一例程相同,此处不再重复。在主循环之前加入以下代码即可循环播放当前目录下的背景音乐back.mp3文件。
01 pygame.mixer.init()
02 back_music = pygame.mixer.music.load('back.mp3')
03 pygame.mixer.music.set_volume(0.5)
04 pygame.mixer.music.play(loops=-1)
第01行初始化声音相关模块。
第02行加载当前目录下的声音文件back.mp3,运行程序前请先将要播放的文件复制到当前目录下。
第03行设置音量为最大音量的50%,以防止背景音乐声音过大。
第04行播放已加载的音乐,并使用loops参数设置播放次数,设置为-1时声音循环播放。
2. 播放其它音效
除了背景音乐之外,有时还需要根据用户的操作或者游戏需要偶尔播放一些音效文件,方法如下:
01 dound = pygame.mixer.Sound('warning.wav')
02 dound.play()
第01行用于加载音效文件。
第02行用于播放音效,默认为播放一次。
需要注意的是:以Sound方式加载的音效文件有一定的格式要求,只能播放简单格式的wav和ogg文件,当格式不符合要求时,程序将报错“Unable to open file”。另外需要注意的是在播放完音效后不能马上结束程序,否则将听不到声音的播放效果。
15.3 综合实例
本节将通过游戏“愤怒的小鸟”,学习游戏常用的程序技术,首先使用git下载源码(git使用方法详见前一讲)。
在git bash中输入以下命令下载“愤怒的小鸟”项目:
$ git clone http://www.github.com/estevaofon/angry-birds-python angry-birds-python
该程序还依赖三方模块pymunk,在Anaconda Prompt中,用以下命令安装:
$ pip3 install pymunk
进入下载后的angry-birds-python目录,可以看到程序包的具体内容,如图15.4所示:
图15.4 愤怒的小鸟项目
其中src为source code的缩写,意思是源代码;resources译为资源,其中包括游戏用到的图片和音效文件,有时也简写为res;README.md是说明文件,用Windows记事本打开该文件,可以看到其中介绍了该软件依赖的三方模块,以及运行该程序的方法;LICENSE是软件的版本信息。请读者自行进入每个目录,通过文件名和文件扩展名了解它们的用途。
之前讲解的例程中,为了方便调用,资源文件、代码文件以及说明文件常存储在同一个目录下面,而当程序中有多个代码文件和资源文件时,目录内容将变得非常混乱,建议读者在之后的项目中利用目录更好地分类和管理文件。
从Readme.md文件中可以看到,程序的入口(主要程序)是main.py,绝大多数项目的主程序名都为main.py,这也是约定俗成的命名习惯。请读者在Spyder或者Jupyter中打开main.py,并运行该游戏,了解“愤怒的小鸟”的游戏规则。
请读者在main.py中找到主循环、用户事件处理、以及绘图的相应代码,上一讲学习了对键盘事件的处理,本讲请读者自学对鼠标事件的处理(提示:鼠标关键字“MOUSE”)。
通过玩游戏可以了解:本游戏除了包含图片和声音之外,主要的难点在于物体的碰撞效果,碰撞效果使用pymunk三方库实现。它是一个物理计算引擎,很多游戏的运动效果都借助该库实现。
课后练习:(练习答案见本讲最后的小结部分)
练习一:实现简单版愤怒的小鸟,具体要求如下:
- 游戏目标:接收用户鼠标事件,根据鼠标按下时长决定投掷小鸟的远近。当小鸟打中猪时,游戏结束。
- 前景和背景用图片显示,至少两个前景物体:小鸟和猪。
- 带背景音乐和用户操作音效,用户点击鼠标时播放音效。
- 提示:参考以下代码06-09行计算抛物线的轨迹:
01 import math as m # 引入数学函数库
02 import matplotlib.pyplot as plt
03 %matplotlib inline
04
05 # v是力度取值(100-1000), a是角度取值(0.1-2), width为窗口宽度
06 def calc(a,v,width):
07 arr = [m.floor(0.5+x*m.tan(a)-x*x/(v*m.cos(a))) for x in range(width)]
08 arr = [i for i in arr if i >= 0]
09 return arr
10
11 arr = calc(1.3,800,640)
12 plt.plot(arr)
13 arr = calc(0.5,800,640)
14 plt.plot(arr)
程序运行结果如图15.5所示:
图15.5 抛物线图
本讲内容不多,但是练习的难度较大,对于完全没有编程经验的读者,是一个巨大的考验。编写程序的最终目标是实现完整功能,而不是照猫画虎地做秀,增加难度也是必经的过程。
15.4 思维训练
15.4.1 解决复杂问题
本讲中的习题,不像之前的练习,照猫画虎就能完成,需要把学到的知识打散,再组合。小李同学在放假时做了四遍:第一天两遍、第二天一遍、第三天一遍,每做一遍之后,把源码删掉,再重新写。
一开始做的时候问题不断,以她三年级的英语水平,报错基本都看不懂。在常量、变量、坐标系、列表、表达式,判断和循环的缩进中,暴露出了很多前期学得不扎实的知识点。但随着不断地重复,提出的问题也越来越少。
最后她说她都把这五十多行代码背下来了,让她背了一遍,流程基本都对。有点像多年前流行背《新概念》英语中的例文。
下面分享一下解题思路:
第一步:拆分问题——把一个大问题拆分成几个小问题。
小李把问题分解为:1接收鼠标事件、2打到猪退出程序、3显示图像、4点鼠标播放音效、5根据鼠标时长确定小鸟的飞行距离。在这一步,小朋友切分的粒度不够完美,比如应该把小鸟按抛物线运动单列出来。但框架基本正确。
第二步:确定依赖关系,排列解决“小问题”的顺序。
由于问题之间存在前后的依赖关系,同时还需要考虑难易程度,顺序排列为:3->4->1->5->2。
第三步:按步骤解决“小问题”。
新手程序员常犯的错误是:把所有想到的功能都加上,然后再开始调试。可一旦遇到问题,就很难定位问题出在哪儿。建议每做完一步,调试通过后再做下一步。
对小朋友来说,每解决一个小问题,都是一次小小的成功,这个过程也是不断地肯定自己的过程。在最后一遍练习中,她开始大量使用print语句打印状态信息。基本跳出了瞎蒙的状态。
15.4.2 统筹规划
统筹规划是运用统筹兼顾的基本思想,对错综复杂、种类繁多的工作进行统一筹划,合理安排的方法。对于解决复杂的问题尤为重要。
1.层进式优化
在刚开始学习画画时,老师要求,先确定构图,然后画线稿;铺调子;再一遍一遍逐渐深入印画。
图15.6 层进优化
这种方法在软件工程中叫作螺旋模型,它不追求一次到位,而是把整个过程从时间分为多个阶段,每个阶段都包含:计划、执行、检查。 它有以下优点:
- 如果工程中断,也保留了阶段性的成果,基本可用。
- 当遇到一些关键错误,能及时发现和调整。
- 时间规划合理,不会为一些细节的不完美影响大局。
- 以整体为出发点,不会出太大问题,能更好把握时间和进度。
- 虽然不要求一次做到最好,但随着工作的深入和进一步的理解,也能把握细节。
一位娴熟的画师从任何一点起笔都能完成一幅作品,因为整个过程都在他的脑中;但是对于摸着石头过河的初学者,仍需要一些步骤和方法。
小朋友常见的问题是:做作业时很可能因为对其中一项内容的精益求精,花掉了大多数时间,发现时间不够之后,其它作业就只能草草了事。导致整体水平偏低。自己也很委屈:“我认真难道还错了吗?”。这种问题只需要在安排上做一些微调就能解决。
2.计划
首先是切分问题,不只是切分工作量,切分步骤,还有切分时间。根据不同情况有不同的选择,需要兼顾各种条件。
切分之后,需要排列优先级,有人选择先做简单的,有的先做难的。更细化地可以分成:难度优先级,步骤依赖关系的优先级,重要性优先级,时间优先级……有时候把问题列出来之后,结果就显而易见了。
当事情太多,可能做不完时,“重要性”是判断标准,需要把不重要的往后排,或者去掉一些不重要的安排。也可以从另一个角度考虑,是一个做完再做一个,还是先把每件事做成60分水平,有时间再进一步优化。
当问题足够复杂,步骤很多时,还需要列出步骤清单。
3.实施
实施就是照着计划做,也叫做执行力,在几个步骤之中好像最为简单,实际对于一个长期或者有难度的计划,成年人也很难做到,比如能减肥成功又保持十年内不反弹的人少之又少。计划是否能正常实施很大程度上取决于计划的难度和合理性。
当然,也与人的意志力自控力强弱有关。
4.设置检查点
想都不想就去做的反面是想得多做得少:我是否找到了最佳方向?坚持努力有没有意义?这个问题在成年人身上更加明显,往往在纠结中浪费了大量的时间和精力。很多时候,如果不深入到一定程度,就无法判断最终结果。
层进式工作,并且在过程中设置检查点,可以有效地解决这一问题,即使出现方向性问题也能及时止损。
有时候我们已经习惯听话,只要是老师说的,领导说的,都必须照做,而缺乏对整体的规划和对解决方案思考。在时间和能力条件都有限的情况下,有时候即使做了,效果也不好。如果总是指哪儿打哪儿,判断力从何而来?任何行业,最重要的都是提出问题和提出方案的人,而最没技术含量的是那些不用思考和判断的简单工作。
规划力是一种基础能力:规划、评价、按计划实现的能力。训练得越早,越能应用到其它领域,变成习惯的思维方式。画画、书法、弹琴、运动、学习都能用到,为什么一个小朋友钢琴五级需要学习三五年时间,而有些成年人花一年就可以练到同样程度?这些能力都是在成长过程中慢慢累计而成的。
规划力可以通过编程训练,通过其它日常活动也能锻炼,比如做一顿饭,设计搭配,规划时间,其中的顺序。采买各种食材,找菜谱,在做的过程中发现和补救……
需要注意的是:整件事需要足够复杂,过程中也需要不断思考和判断,此过程也需要根据年龄由浅入深,给孩子足够的空间,也允许他们在过程中犯错。
15.5 小结
15.5.1 单词
本讲需要掌握的英文单词如表15.1所示。
表15.1本讲需要掌握的英文单词
15.5.2 习题答案
- 练习一:实现简单版愤怒的小鸟。
01 import pygame
02 import time
03 import math as m
04
05 def calc(a,v,width):
06 arr=[m.floor(0.5+x*m.tan(a)-x*x/(v*m.cos(a)))for x in range(width)]
07 arr=[i for i in arr if i >= 0]
08 return arr
09
10 arr=calc(1,800,640)
11
12 x=0
13 y=0
14
15 WIDTH=649
16 HEIGHT=480
17 s=50
18 pygame.init()
19 screen=pygame.display.set_mode((WIDTH,HEIGHT))
20
21 bg_img=pygame.image.load('bg.jpg')
22 bg=pygame.transform.scale(bg_img,(WIDTH,HEIGHT))
23 zhu_img=pygame.image.load('pig.png')
24 zhu=pygame.transform.scale(zhu_img,(s,s))
25 niao_img=pygame.image.load('bird.png')
26 niao=pygame.transform.scale(niao_img,(s,s))
27
28 pygame.mixer.init()
29 back_music=pygame.mixer.music.load('bg.ogg')
30 pygame.mixer.music.set_volume(0.6)
31 pygame.mixer.music.play(loops=-1)
32
33 running=True
34 while running:
35 for event in pygame.event.get():
36 if event.type==pygame.QUIT:
37 running=False
38 if event.type == pygame.MOUSEBUTTONDOWN:
39 b=time.time()
40 if event.type == pygame.MOUSEBUTTONUP:
41 a=time.time()
42 i=a-b
43 dound=pygame.mixer.Sound('warning.wav')
44 dound.play()
45 arr=calc(1,i*500,640)
46 x=0
47 if x>580 and x<640 and y>400 and y<470:
48 running=False
49
50 if x<len(arr)-1:
51 x=x+1
52 y=arr[x]
53 y=HEIGHT-y
54 screen.blit(bg,(0,0))
55 screen.blit(zhu,(580,400))
56 screen.blit(niao,(x,y))
57 pygame.display.update()
58
59 pygame.quit()