上一讲学习了Python游戏开发的简单方法,并在练习中下载和阅读了几个小游戏的源代码。本讲将自己动手制作游戏,练习编写50行左右的比较复杂的程序,在此过程中将继续学习图形图像相关知识、播放声音,以及运用前面学习的各种数据类型、函数等技术。

15.1 图形图像

15.1.1 原理

早期的计算机和手机都用按键输入,屏幕只能显示文字。后来随着软硬件升级,逐步出现彩色显示器,以窗口界面为主,使用鼠标和触摸屏操作的计算机和手机系统。

现在在屏幕上看到的所有显示都是以图片方式“绘制”出来的。之前讲过软件分为系统软件和应用软件,系统软件的一部分工作是管理当前开启的应用程序(应用软件),并将各个软件的输出“绘制”在屏幕上。因此作为普通开发者,只需要考虑应用软件的输出即可。

上一讲在窗口中绘制图形:窗口是开发者在程序内定义的一个固定大小的区域,绘图位置通过坐标(x,y)指定。

屏幕上显示的画面又分为图形和图像两种,之前学习的绘制矩形、圆形,以及显示文字都属于“图形”,另一种是图像,它可能是拍摄的照片或者用画图软件绘制的卡通人物或风景,在Python程序中从文件读出,并将其内容绘制到某一区域,叫做绘制图像。

15.1.2 图像格式

从绘图的角度看,图像分为两种:带有透明度通道的图片和不带有透明度通道的图片。




python程序可以输出声音吗 python可以输出图形和声音吗_优先级


图15.1 图片透明通道示意图

如图15.1所示,将两张卡通羊图片绘制在背景图上,左侧不带透明通道,因此将其白色背景也贴了出来,右侧带透明通道,并其将卡通羊的背景置为透明,贴图后的效果更加自然。一般情况下,在绘制背景图片,以及后面不需要透出其它图片时,使用不带透明通道的图片即可。而绘制前景图片,比如游戏中需要移动变化的卡通人物,则使用带透明通道图片。

图像一般存储在图片文件中,通过不同的扩展名区分不同的格式,最常见的图片格式有bmp、jpg和png。bmp是早期的图像存储格式,它直接把图片中每个像素的颜色存储在图片文件中,占空间较大;jpg是最常用的图片存储格式,它用压缩算法将图片内容压缩后存储,占存储空间较小,有时也会损失一些图片的质量,在保存图片时可以设置压缩比例,以便在图片的大小和精度之间达到平衡;png采用无损压缩格式,存储的是带透明通道的图片。

15.1.3 获取图像

图形界面或多或少用到图像,比如:网站的Logo(商标)、软件的欢迎界面、小图标等,游戏为了提升视觉效果会用到更多的图像。

图片可以从网络下载,方法是:打开浏览器,百度图片搜索:https://images.baidu.com/, 输入想要查找图片的关键字,找到合适的图片后,在图片中点击右键,选择“另存图像为”,即可将网络上的图片下载到自己的电脑上。如图15.2所示:


python程序可以输出声音吗 python可以输出图形和声音吗_python 高效读取mp3_02

图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所示:


python程序可以输出声音吗 python可以输出图形和声音吗_python程序可以输出声音吗_03

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所示:


python程序可以输出声音吗 python可以输出图形和声音吗_python 高效读取mp3_04

图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三方库实现。它是一个物理计算引擎,很多游戏的运动效果都借助该库实现。

课后练习:(练习答案见本讲最后的小结部分)

练习一:实现简单版愤怒的小鸟,具体要求如下:

  1. 游戏目标:接收用户鼠标事件,根据鼠标按下时长决定投掷小鸟的远近。当小鸟打中猪时,游戏结束。
  2. 前景和背景用图片显示,至少两个前景物体:小鸟和猪。
  3. 带背景音乐和用户操作音效,用户点击鼠标时播放音效。
  4. 提示:参考以下代码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所示:


python程序可以输出声音吗 python可以输出图形和声音吗_播放声音_05

图15.5 抛物线图

本讲内容不多,但是练习的难度较大,对于完全没有编程经验的读者,是一个巨大的考验。编写程序的最终目标是实现完整功能,而不是照猫画虎地做秀,增加难度也是必经的过程。

15.4 思维训练

15.4.1 解决复杂问题

本讲中的习题,不像之前的练习,照猫画虎就能完成,需要把学到的知识打散,再组合。小李同学在放假时做了四遍:第一天两遍、第二天一遍、第三天一遍,每做一遍之后,把源码删掉,再重新写。

一开始做的时候问题不断,以她三年级的英语水平,报错基本都看不懂。在常量、变量、坐标系、列表、表达式,判断和循环的缩进中,暴露出了很多前期学得不扎实的知识点。但随着不断地重复,提出的问题也越来越少。

最后她说她都把这五十多行代码背下来了,让她背了一遍,流程基本都对。有点像多年前流行背《新概念》英语中的例文。

下面分享一下解题思路:

第一步:拆分问题——把一个大问题拆分成几个小问题。

小李把问题分解为:1接收鼠标事件、2打到猪退出程序、3显示图像、4点鼠标播放音效、5根据鼠标时长确定小鸟的飞行距离。在这一步,小朋友切分的粒度不够完美,比如应该把小鸟按抛物线运动单列出来。但框架基本正确。

第二步:确定依赖关系,排列解决“小问题”的顺序。

由于问题之间存在前后的依赖关系,同时还需要考虑难易程度,顺序排列为:3->4->1->5->2。

第三步:按步骤解决“小问题”。

新手程序员常犯的错误是:把所有想到的功能都加上,然后再开始调试。可一旦遇到问题,就很难定位问题出在哪儿。建议每做完一步,调试通过后再做下一步。

对小朋友来说,每解决一个小问题,都是一次小小的成功,这个过程也是不断地肯定自己的过程。在最后一遍练习中,她开始大量使用print语句打印状态信息。基本跳出了瞎蒙的状态。

15.4.2 统筹规划

统筹规划是运用统筹兼顾的基本思想,对错综复杂、种类繁多的工作进行统一筹划,合理安排的方法。对于解决复杂的问题尤为重要。

1.层进式优化

在刚开始学习画画时,老师要求,先确定构图,然后画线稿;铺调子;再一遍一遍逐渐深入印画。


python程序可以输出声音吗 python可以输出图形和声音吗_播放声音_06

图15.6 层进优化

这种方法在软件工程中叫作螺旋模型,它不追求一次到位,而是把整个过程从时间分为多个阶段,每个阶段都包含:计划、执行、检查。 它有以下优点:

  • 如果工程中断,也保留了阶段性的成果,基本可用。
  • 当遇到一些关键错误,能及时发现和调整。
  • 时间规划合理,不会为一些细节的不完美影响大局。
  • 以整体为出发点,不会出太大问题,能更好把握时间和进度。
  • 虽然不要求一次做到最好,但随着工作的深入和进一步的理解,也能把握细节。

一位娴熟的画师从任何一点起笔都能完成一幅作品,因为整个过程都在他的脑中;但是对于摸着石头过河的初学者,仍需要一些步骤和方法。

小朋友常见的问题是:做作业时很可能因为对其中一项内容的精益求精,花掉了大多数时间,发现时间不够之后,其它作业就只能草草了事。导致整体水平偏低。自己也很委屈:“我认真难道还错了吗?”。这种问题只需要在安排上做一些微调就能解决。

2.计划

首先是切分问题,不只是切分工作量,切分步骤,还有切分时间。根据不同情况有不同的选择,需要兼顾各种条件。

切分之后,需要排列优先级,有人选择先做简单的,有的先做难的。更细化地可以分成:难度优先级,步骤依赖关系的优先级,重要性优先级,时间优先级……有时候把问题列出来之后,结果就显而易见了。

当事情太多,可能做不完时,“重要性”是判断标准,需要把不重要的往后排,或者去掉一些不重要的安排。也可以从另一个角度考虑,是一个做完再做一个,还是先把每件事做成60分水平,有时间再进一步优化。

当问题足够复杂,步骤很多时,还需要列出步骤清单。

3.实施

实施就是照着计划做,也叫做执行力,在几个步骤之中好像最为简单,实际对于一个长期或者有难度的计划,成年人也很难做到,比如能减肥成功又保持十年内不反弹的人少之又少。计划是否能正常实施很大程度上取决于计划的难度和合理性。

当然,也与人的意志力自控力强弱有关。

4.设置检查点

想都不想就去做的反面是想得多做得少:我是否找到了最佳方向?坚持努力有没有意义?这个问题在成年人身上更加明显,往往在纠结中浪费了大量的时间和精力。很多时候,如果不深入到一定程度,就无法判断最终结果。

层进式工作,并且在过程中设置检查点,可以有效地解决这一问题,即使出现方向性问题也能及时止损。

有时候我们已经习惯听话,只要是老师说的,领导说的,都必须照做,而缺乏对整体的规划和对解决方案思考。在时间和能力条件都有限的情况下,有时候即使做了,效果也不好。如果总是指哪儿打哪儿,判断力从何而来?任何行业,最重要的都是提出问题和提出方案的人,而最没技术含量的是那些不用思考和判断的简单工作。

规划力是一种基础能力:规划、评价、按计划实现的能力。训练得越早,越能应用到其它领域,变成习惯的思维方式。画画、书法、弹琴、运动、学习都能用到,为什么一个小朋友钢琴五级需要学习三五年时间,而有些成年人花一年就可以练到同样程度?这些能力都是在成长过程中慢慢累计而成的。

规划力可以通过编程训练,通过其它日常活动也能锻炼,比如做一顿饭,设计搭配,规划时间,其中的顺序。采买各种食材,找菜谱,在做的过程中发现和补救……

需要注意的是:整件事需要足够复杂,过程中也需要不断思考和判断,此过程也需要根据年龄由浅入深,给孩子足够的空间,也允许他们在过程中犯错。

15.5 小结

15.5.1 单词

本讲需要掌握的英文单词如表15.1所示。


python程序可以输出声音吗 python可以输出图形和声音吗_加载_07

表15.1本讲需要掌握的英文单词

15.5.2 习题答案

  1. 练习一:实现简单版愤怒的小鸟。
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()