从零开始学习PYTHON3讲义(十五)

让画面动起来

《从零开始PYTHON3》第十五讲

虽然看起来绘图和音乐并不相关,但是听过了上一讲的内容你一定知道,这是游戏编程中四个需要处理内容的两部分,这两部分必须同时、并行的处理,不能因为某一项计算的拖延,导致另外一方程序的停滞。要知道人对声音的断续和游戏的卡顿是很敏感的。

在Pygame中进行并行处理的主要手段,一是Pygame中的各种函数,大多是不等待工作完成,只要工作开始进行,就返回主程序,等待下一条命令,而任务会在看不到的后端继续执行,并不停止;另外则是各个并行的任务之间,会通过“消息事件”的方式跟主程序沟通,从而让主程序能够统一调度各项任务的进程。

这是复习上一讲的内容。

并行:指的是在硬件的帮助下,多个任务同时进行,互不影响,最终完成任务的过程。完成的时间取决于最慢的任务。这个硬件帮助,通常是指多核CPU、显卡计算配合CPU计算以及数据传输中的多通道。
串行:指的是完成一项工作,才进行另外一项工作,最后完成的时间是所有任务完成的总和。


游戏绘图

绘图模式

同我们前面学过的科学绘图和海龟绘图相比,游戏绘图在绘图的模式上有较大的区别。

传统程序绘图是顺序方式,每画一笔可以认为这一笔一直都在,直到程序退出或者擦除画面。你可以回忆一下我们在科学绘图和海龟绘图时候所学习的内容。
游戏绘图更类似拍照,一个个角色进入画面,摆好姿态,等待快门按下,这样完成一帧。随后会根据游戏逻辑和输入,调整画面,再拍摄下一张,这样至少达到每秒30帧,才能达成一个动画的效果。

从逻辑上讲,游戏绘图采用的方式似乎应当慢于传统方式。实际上因为这种方式能够得到CPU/显卡以及很多新技术的帮助。很多绘图任务发出后,实际上是进入显卡完成运算的,这时候CPU已经在处理其它内容。这样并行计算的方式,再加上显卡更善于处理图形、图像相关的工作。最终这种方式效率才会高很多。

我们前面讲的科学绘图和海龟绘图,新版本的实现有很多是使用游戏绘图的方式,通过并行的方式完成计算。但因为用户编程接口的兼容性,所以至少从我们编程时所感受到的方式上,还是串行处理的。

坐标系

科技绘图(matplotlib):采用数学坐标系,同显示设备无关,通常原点在屏幕中心。绘图包会自动调整数学坐标系跟窗口分辨率的比例(窗口分辨率是可以在程序中设置的,只是前面的学习中我们基本使用了默认的设置),从而让显示效果最优。
海龟绘图(turtle):原点在窗口中心,跟数学坐标系方向相同,坐标是同显示设备分辨率相关的,但绘图的操作通常是用几何的方式,所以不用太担心显示设备本身的分辨率。
游戏绘图(pygame):原点在窗口左上角,x轴坐标向右侧增大,y轴坐标向下增大,最大值为屏幕分辨率。还有一些更底层的游戏绘图引擎,比如OpenGL会使用统一的1.0*1.0坐标系,然后在不同设备上映射成不同的分辨率。我们本讲的课程采用Pygame所使用的坐标体系。

颜色

在计算机中常用的颜色分类有这么几种:

  • 二值图:仅有黑白两色,比如字体库
  • 灰度图:0-255,共256级灰度,比如黑白照片
  • 伪彩色:0-255,共256种颜色,比如GIF动图、微信表情
  • 真彩色:RGB红绿蓝三色图,每种颜色0-255,按二进制计算,也称为24位色
  • 32位真彩色:RGBA四色,除了红绿蓝之外,A代表透明度,能表现更多的多种颜色互动、遮盖的效果

这些颜色格式Pygame都支持,但最新的游戏通常都已经采用32位真彩色的方式。
在游戏的显示过程中,如果不考虑透明度A的部分,所有颜色都是使用“三基色”来表达的,也就是红、绿、蓝,每个颜色分量可以的取值分为是0到255。0表示完全没有这个颜色,255表示这个颜色最强。当三个颜色都是0的时候显示为纯黑。当三个颜色都是255的时候,显示为纯白。
因为是三个颜色,所以通常的颜色都是使用三个值的“元组”的形式表达的。元组我们第九讲学过了。下面举一些例子,我们在程序中,预定义几个常用的颜色:

#黑色
BLACK =(0, 0, 0)
#白色
WHITE = (255, 255, 255)
#红色
RED = (255,0,0)
#绿色
GREEN =(0,255,0)
#蓝色
BLUE=(0, 0,255)

Pygame程序一般结构

上一讲和本讲开始我们都已经讲过,Pygame的主要工作模式是并行处理,其结构同传统的串行程序就必然有一些差别。这个差别并不大,很类似我们学习互联网编程时候的框架模板,即使不够理解,照抄下来用就可以。下面就是一个一般的结构:

#此代码仅为架构示例,没有具体功能
#作者:Andrew

#引入扩展库
import pygame

width=1280
heigh=556
color=32

#pygame初始化
pygame.display.init()
#创建一个绘图平面,后面参数为设定的窗口分辨率及颜色
screen = pygame.display.set_mode((width, heigh), 0, color)
#声音系统初始化
pygame.mixer.init()

#1...其它自身初始化项目...

#是否要退出标志
requireQuit = False
#程序主循环,在有退出申请之前一直循环
while not requireQuit:
    #2...自己的绘图部分...

    #处理所有事件
    for event in pygame.event.get():
        #用户从窗口菜单选择退出
        if event.type == pygame.QUIT:
            requireQuit=True
            break
        #用户是否有按键?
        elif event.type == pygame.KEYUP:
            #为了可靠,只处理按键松开的动作
            if event.key in [pygame.K_q,pygame.K_ESCAPE]:
                #用户按了q键
                requireQuit=True
                break
        #3...其它事件处理...
    #4...其它程序逻辑...

#优雅的退出,释放各种资源
pygame.mixer.quit()
pygame.display.quit()

上面的代码中,并不包含任何功能,只是一个模板。通常没有特殊需求的程序,只要编写其中的#1/#2/#3/#4部分的程序就可以。 为了程序更便于理解和阅读,还可以对上面的结构进一步的优化,比如把需要继续编程的部分函数化。当然函数化的时候要考虑到变量作用域,避免增加不必要的麻烦。


常用绘图功能

我们介绍几个常用的绘图功能,然后就可以代入到上面的模板代码中来实验了。

一般的几何图形绘制功能,都汇总在pygame.draw包中,比如:

  • 画圆:pygame.draw.circle
  • 矩形:pygame.draw.rect
  • 多边形:pygame.draw.polygon
  • 画线:pygame.draw.line
  • 画弧线:pygame.draw.arc
  • 画矩形:pygame.draw.rect

正常情况下,pygame的显示是在一个窗口中显示的(也可以根据需要设置全屏),窗口可以设置一个标题来表示你当前做的工作,这个命令是:

#设置窗口标题
pygame.display.set_caption('Hello World!')

用于显示的窗口默认是没有颜色,也就是黑色,可以设置窗口的底色:

#用白色填充窗口,既是设置窗口底色,也是把窗口清空,重新绘制下一帧
#pygame绘图是像摄影师拍摄每一帧的照片,还记得吗?
screen.fill(WHITE)

还有一些函数的功能,可以参考help(pygame)。help也可以查看某一个具体的子包,比如:help(pygame.draw)。下面我们通过程序示例代码来看看刚才讲的这些功能:

#我们定义一个函数,来完成画面的绘制
#避免过多的语句挤入到主循环中影响程序的结构
def draw(screen):
    #2...自己的绘图部分...
    #用白色填充窗口
    screen.fill(WHITE)
    #画多边形
    pygame.draw.polygon(screen, GREEN, ((146, 0), (291, 106), (236, 277), (56, 277), (0, 106)))
    #画线
    pygame.draw.line(screen, BLUE, (60, 60), (120, 60), 4)
    pygame.draw.line(screen, BLUE, (120, 60), (60, 120))
    pygame.draw.line(screen, BLUE, (60, 120), (120, 120), 4)
    #画圆
    pygame.draw.circle(screen, BLUE, (300, 50), 20, 0)
        #椭圆
    pygame.draw.ellipse(screen, RED, (300, 250, 40, 80), 1)
    #矩形
    pygame.draw.rect(screen, RED, (200, 150, 100, 50))

    #使用直接操作图形缓存的方法在右下角画四个点
    #这个功能比较底层,除非需要很专业的操作一般用不到
    pixObj = pygame.PixelArray(screen)
    pixObj[480][380] = BLACK
    pixObj[482][382] = BLACK
    pixObj[484][384] = BLACK
    pixObj[486][386] = BLACK
    pixObj[488][388] = BLACK
    del pixObj

    #显示在屏幕上
    pygame.display.update()

上面代码只列出了自己定义的绘图部分,其它部分需要融合到框架模板中去。完整的代码可以参考code2.py源文件。 下面的图片是绘制的效果:
pygameDraw1 程序运行之后,可以按q键退出程序,也可以从菜单选择Quit来退出。

老话题,想掌握学习的知识,只能多练习。
请在上面程序的基础,调整各项参数,增加或者减少绘图的指令,自己练练。看看谁绘制的画面最好看。


挑战

我们已经掌握了基本的绘图知识。可惜游戏没有这么简单,至少游戏需要是以动画的方式为基础,玩起来才会感觉到真实。

我们早已经说过,现代的游戏开发已经是一个团队配合的产物。不管想达成什么样的动画,一般都需要有美工专业人员完成原画的设计制作,提供成素材,随后才能由程序人员来完成让画面动起来的工作。

我们这里已经从网上下载了几个素材:
pygameDraw2 上面包含两个动画元素的素材,上面部分是一只小地鼠,仔细观察这四副图片,他们的脚在不同的位置。四张图片代表动画中的4帧,连续起来,就会出现小地鼠在跑的样子。
下面的箭比较简单,只需要一帧,箭的图片出现在屏幕不同的位置上,感觉起来就是箭飞到了那个位置。
如果你还记得第一讲的演示,你应当能看出来这些素材出自游戏Bunny。

下面我们编程序,来实现小地鼠从屏幕右侧快速跑到屏幕左侧的动画,和羽箭从屏幕左侧飞到右侧的动画。

#使用pygame对图片处理的功能,载入图片到变量
arrow = pygame.image.load("bullet.png")
#地鼠因为包含四帧,我们使用列表格式
badguy = [pygame.image.load("badguy.png"),
    pygame.image.load("badguy2.png"),
    pygame.image.load("badguy3.png"),
    pygame.image.load("badguy4.png")]
#动画动起来,需要一帧帧的变化,下面的变量用于指当前显示的第几帧
badguyIndex = 0


#定义x1/y1和x2/y2两组坐标,
#分别用于表示羽箭和小地鼠在屏幕上的位置

#坐标系还记得吧?左上角是0,y向下变大,x向右变大
x1=0   #羽箭从左侧飞到右侧,开始x坐标是0,表示在左侧
y1=heigh/3#y坐标,在窗口上面的1/3位置

x2=width    #小地鼠一开始在屏幕右侧
y2=heigh/3*2

#定义一个函数,用于计算向左移动时候下一个位置的坐标
def moveLeft(x):
    x -= dx
    if x < 0:
        x += width
    return x
#定义一个函数,用于计算向右移动时候下一个位置的坐标
def moveRight(x):
    return (x+dx) % width

#绘制的函数
def draw(screen):
    screen.fill(WHITE)  #白色填充窗口
    screen.blit(arrow,(x1,y1)) #绘制羽箭
    screen.blit(badguy[badguyIndex],(x2,y2)) #绘制地鼠
    #显示在屏幕上
    pygame.display.update()
    
...

    #4...其它程序逻辑...
    #移动元素坐标位置的工作,应当放到“其它程序逻辑”中
    #这样的方式使得程序逻辑,特别是绘图的逻辑干净易读
    
    x2 = moveLeft(x2)
    x1 = moveRight(x1)
    badguyIndex = (badguyIndex+1) % 4  #地鼠下次使用下一帧

上面代码依然去掉了同前面重复的部分,完整的代码请参考code3.py程序。现在运行一下看看吧: pygameDraw3 截图无法展示动态,你一定要亲自动手来试试,才能看到效果。关键点:

  1. 屏幕绘制部分,根据坐标值,绘制指定的图片。
  2. 在程序逻辑运算的部分,计算下一帧画面的时候,小地鼠和羽箭在屏幕上的新位置。以及地鼠的动画图片下次绘制采用哪一帧图片。

练习时间

  1. 修改上面程序的参数,让地鼠的速度加快一倍,而箭的速度保持不变
  2. 上一讲中的mp3播放器,请实现在播放器播放的时候,显示一张歌曲的封面图片

本讲小结

  • 本讲介绍了使用pygame绘制基本几何图形和绘制简单动画的方式
  • 绘画、动画其实都不难,重要的是画面的设计,只要有了连续的图片,就可以用数组的方式来实现连续动画
  • 对于一个规模越来越大的程序,想少出错、容易维护,就需要代码尽量规范、简洁、函数化

本课程至此就全部结束了。作为面对刚刚接触计算机软件编程的初学者课程,我们使用了15讲的篇幅,从Python的安装、命令行的互动计算开始,讲述了数学计算、程序逻辑控制、常用数据类型等基本Python编程的知识。接着又针对科技绘图、互联网编程、游戏编程等专业领域的应用做了讲解。时间所限,我们并没有能够特别深入对所有话题更进一步的学习。课程也没有对当前流行的面向对象编程做讲解,这些有待于学习者在对初级内容有了一段时间的熟悉和体验之后,继续深入学习。
希望各位同学能在当前学习的基础上,根据自己的爱好和自己的日常学习、生活需要。有选择的做进一步学习,让Python成为我们学习的好助手,生活的得力工具。
水平所限,课程内容难免疏漏、错误,敬请谅解并欢迎指正。


练习答案

请参考代码:mp3Player1.py

  • 连载正文结束,所有程序源码及练习答案将会整理后在下一期提供下载。