计算机编程的基本要素:输入和输出、变量、判断、循环、列表、函数、对象和模块。下面几章我们讲如何在屏幕上画图,比如直线、形状、颜色,还有动画。
图形
要让图形(或声音)在计算机上起作用,这可能有点复杂。这涉及操作系统和显卡,还需要大量底层代码(目前我们还不想考虑这些代码)。所以我们将使用一个名为 Pygame 的 Python 模块来提供帮助,让问题更简单一些。
Pygame
开始绘制图形时首先需要建立一个窗口。
import pygame
pygame.init()
screen = pygame.display.set_mode([640, 480])
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
Pygame 有一个事件循环(event loop)不断检查用户在做什么,比如按键、移动鼠标或关闭窗口。Pygame 程序需要有个事件循环一直运行。可以使用while循环。它可以随着程序的运行一直运行下去。[640, 480] 是窗口的大小,表示 640 像素宽、480 像素高。
像素
像素(pixel)这个词是“图像元素”(picture element)的简写。这表示屏幕上或图像中的一点。如果在一个图像浏览器中查看图片,充分放大(让图像非常大),就可以看到单个的像素。
一般的计算机屏幕可能有 768 行像素,每行有 1024 个像素。我们就会说这个屏幕“分辨率是 1024×768”。有些屏幕的像素更多,有些可能比这要少。
画图
下面就在这里面画一些图形。
import pygame
pygame.init()
screen = pygame.display.set_mode([640, 480])
screen.fill([255, 255, 255])
pygame.draw.circle(screen, [255, 0, 0], [100, 100], 30, 0)
pygame.display.flip()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
对于 Pygame 窗口中显示的所有内容,Pygame 中的显示对象(我们的显示对象名为 screen,都会有这些内容的两个副本。这样做的原因是,开始动画时,我们希望让动画尽可能流畅,速度尽可能快。所以不必在每次对图形做一个小小的修改时都更新显示,可以做很多修改后再“翻转”(flip)到图形的新版本。这样就会一次显示所有这些修改,而不是一个接一个地出现。这样一来,我们就不会显示出只画了一半的圆(或外星人,
或者其他任何东西)。
可以把这两个副本当作一个“当前屏”和一个“下一屏”。当前屏就是我们现在看到的,下一屏是完成“翻转”之后看到的。做完“下一屏”上的所有修改后,再翻转到下一屏,就能看到这些改变。pygame.draw.circle()
函数会画一个圆。必须告诉它以下 5 件事:
- 在哪个表面(surface)画这个圆。(在这里,要在第 3 行定义的表面 上画圆,名为 screen,这就是显
示表面。)
- 用什么颜色来画。(在这里要用红色,对应的值为 [255, 0, 0]。) 在什么位置画。(在这里要位于 [100, 100],这表示从左上角向下 100 像素再
- 向右 100 像素的位置)。
- 圆的大小。(这里是 30,这是圆的半径,也就是圆心到外围边界的距离,单位是像素。)
- 线宽。(如果 width = 0,圆是完全填充的,这里就采用了完全填充。)
画布
在实际生活中如果我让你画一幅画,你可能会先问“我在哪儿画?”在 Pygame 中,我们要在一个表面上画图。显示表面就是我们在屏幕上看到的表面(screen)。不过 Pygame 程序可以有多个表面,可以把图像从一个表面复制到另一个表面。还可以对表面做一些处理,比如旋转表面或者调整它们的大小(让它们更大或更小)。
显示表面有两个副本。按软件术语来讲,我们说显示表面是双缓冲的(double-buffered)。正是因为这个原因,我们不会在屏幕上看到只画了一半的形状和图像。我们会在缓冲区里画圆、外星人或者任何东西,然后“翻转”显示表面,来显示已经完全绘制的图像。
颜色
Pygame 中使用的颜色系统是很多计算机语言和程序中通用的系统,称为 RGB。这里的 R、G 和 B 分别代表红、绿和蓝。
通过结合或混合光的三原色(红、绿和蓝)可以得到任何颜色。计算机上也采用了同样的做法。每个颜色(红、绿和蓝)对应一个从 0 到 255 的数。由一个包含 3 个整数的列表来给出颜色,每个数的范围在 0 到 255 之间。如果所有数都是 0,就没有任何颜色,这就是全黑,所以会得到黑色。如果三个数都是 255,会将 3 种颜色以最大亮度混合在一起,这就是白色。如果颜色是 [255, 0, 0],这就是纯红色,没有绿色和蓝色。纯绿就是 [0, 255, 0],纯蓝是 [0, 0, 255]。如果所有 3 个数都一样,比如 [150, 150,150],你会得到某种灰度。数字越小,灰度就越深,数字越大,灰度就越浅。
Pygame 提供了一个命名颜色列表,如果你不想使用 [R, G, B] 记法,就可以使用这些命名颜色。定义好的颜色名有 600 多个。如果你想使用这些颜色名,必须在程序最前面增加下面这行代码,然后,使用某个命名颜色时,可以这样做:
from pygame.color import THECOLORS
pygame.draw.circle(screen, THECOLORS["red"],[100,100], 30, 0)
位置
如果想在屏幕上画个东西或者放上一个东西,需要指定这个东西应当放在屏幕上的哪个位置。这里有两个数:一个对应 x 轴(水平方向),还有一个对应 y 轴(垂直方向)。在 Pygame 中,这两个数从窗口左上角的 [0, 0] 坐标开始。
看到类似 [320, 240] 的一对数时,要知道第一个数表示水平方向,也就是相对于左边界的距离。第二个数表示垂直方向,也就是相对于顶边的距离。在数学和编程中,字母 x 通常用来表示水平距离,y 常用来表示垂直距离。
我们建立了一个 640 像素宽、480 像素高的窗口。如果希望在窗口正中间画圆,需要在 [320, 240] 上绘制。这个位置离左边界 320 像 素,离上边界 240 像素。
试一试在窗口中间画圆。
大小
使用 Pygame 的 draw 函数画形状时,必须指定形状的尺寸。对于圆来说,只有一个尺寸,也就是半径。而像矩形之类的形状,则必须指定长和宽。
Pygame 有一种特殊的对象,名为 rect(这是“rectangle”(矩形)的简写),用来定义矩形区域。rect 要使用左上角坐标、宽和高来定义: Rect(left, top, width, height)
矩形的位置和大小可以是一个简单的数字列表(或元组),也可以是一个Pygame的Rect对象。
import pygame
pygame.init()
screen = pygame.display.set_mode([640, 480])
screen.fill([255, 255, 255])
pygame.draw.circle(screen, [255, 0, 0], [100, 100], 30, 0)
rect = [250, 150, 300, 200]
#rect = pygame.Rect(250, 150, 300, 200)
pygame.draw.rect(screen, [255, 0, 0], rect, 0)
pygame.display.flip()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
注意这里只向 pygame.draw.rect 传递了 4 个参数,因为 rect 用一个参数就表示了位置和大小。在 pygame.draw.circle 中,位置和大小分别由两个不同的参数表示,所以需要传递 5 个参数。
线宽
画形状时最后需要指定的一点是线的粗细。在之前的例子中,使用的线宽都是 0,这会填充整个形状。如果使用不同的线宽,会看到形状的轮廓。
rect = pygame.Rect(250, 150, 300, 200)
pygame.draw.rect(screen, [255, 0, 0], rect, 2)
“机器之声”
来看一段“有趣”的代码:
import pygame
import sys
import random
pygame.init()
screen = pygame.display.set_mode([640,480])
screen.fill([255, 255, 255])
for i in range (100):
width = random.randint(0, 250)
height = random.randint(0, 100)
top = random.randint(0, 400)
left = random.randint(0, 500)
pygame.draw.rect(screen, [0,0,0], [left, top, width, height], 1)
pygame.display.flip()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
它会随机画 100 个大小不等、位置不同的矩形。增加一些颜色,将线宽也设为随机:
key = random.choice(list(THECOLORS.keys()))
color = THECOLORS[key]
line_width = random.randint(1, 3)
pygame.draw.rect(screen, color, [left, top, width, height], line_width)
点和线
如果我们并不想画一个圆或矩形这样的平面图形,而是希望画单个的点或像素。比如,我们要创建数学程序,想画一条正弦曲线。可以利用单个的点来画这样一条曲线。一种方法是画很小的圆或矩形(圆或矩形的大小只有一个或两个像素)。
import pygame, sys
import math
pygame.init()
screen = pygame.display.set_mode([640,480])
screen.fill([255, 255, 255])
for x in range(0, 640):
y = int(math.sin(x/640.0 * 4 * math.pi) * 200 + 240)
pygame.draw.rect(screen, [0,0,0], [x, y, 1,1], 1)
pygame.display.flip()
running = True
while running :
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame .quit ()
每个点都是宽和高分别为 1 像素的矩形。
注意:我们使用的线宽为 1,而不是 0。如果使用线宽 0,什么都不会显示,因为这样一个矩形没有“中间部分”可 以填充。
连接点和线
你可能会注意到,这个正弦曲线并不是连续的,中间的点之间存在空格。这是因为,在正弦曲线比较陡的部分,我们必须上移(或下移) 3 个像素而向右只移动 1 个像素。而且由于我们画的是单个的点,而不是线,所以没有什么来填充它们之间的间隔。
Pygame
有一个画线的方法,另外还有一种方法可以在一系列点之间画线(类似于“连接多个点”)。这个方法是 pygame.draw.lines()
,它需要下面这 5 个参数:
- 画线的表面(surface)。
- 颜色(color)。
- 是否要画一条线将最后一个点与第一个点相连接,使形状闭合(closed)。我们不希望正弦曲线闭合,所以对我们来说,这个参数是 False。
- 要连接的点的列表(list)。
- 线宽(width)。
import pygame, sys
import math
pygame.init()
screen = pygame.display.set_mode([640,480])
screen.fill([255, 255, 255])
points = []
for x in range(0, 640):
y = int(math.sin(x/640.0 * 4 * math.pi) * 200 + 240)
points.append([x, y])
#pygame.draw.rect(screen, [0,0,0], [x, y, 1,1], 1)
pygame.draw.lines(screen, [0, 0, 0], False, points, 1)
pygame.display.flip()
running = True
while running :
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame .quit ()
可以使用 draw.lines()
函数和一个点列表来创建图形。
import pygame, sys
pygame.init()
dots = [[221, 432], [225, 331], [133, 342], [141, 310],
[51, 230], [74, 217], [58, 153], [114, 164],
[123, 135], [176, 190], [159, 77], [193, 93],
[230, 28], [267, 93], [301, 77], [284, 190],
[327, 135], [336, 164], [402, 153], [386, 217],
[409, 230], [319, 310], [327, 342], [233, 331],
[237, 432]]
screen = pygame.display.set_mode([480,480])
screen.fill([255, 255, 255])
pygame.draw.lines(screen, [255,0,0],True, dots, 2)
pygame.display.flip()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
单个点
如果我们只想改变一个像素的颜色,可以不使用 draw
函数,而是利用 Surface.set_at() 方法访问一个表面上的单个像素。你要指出希望设置哪个像素,以及要设置成什么颜色:
screen.set_at([x, y], [0, 0, 0])
图片
在屏幕上画形状、线和单个像素只是制作图形的一种方式。有时我们还想用从别处得来的图片、可能是数码照片、从网上下载的图片或者在图像编辑程序中创建的图片。在 Pygame 中,使用图像最简单的方法就是利用 image 函数。
import pygame, sys
pygame.init()
screen = pygame.display.set_mode([640,480])
screen.fill([255, 255, 255])
my_ball = pygame.image.load("beach_ball.png")
screen.blit(my_ball, [50, 50])
pygame.display.flip()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
pygame.image.load()
函数从硬盘加载一个图像,并创建一个名为 my_ball
的对象。my_ball
对象是一个表面。不过我们看不到这个表面,它只在内存中。我们唯一能看到的表面是显示表面,名为 screen(这在第 4 行创建)。第 7 行把 my_ball
表面复制到 screen
表面上。通过 display.flip()
调用使它可见。
动画
利用计算机图形做动画时,移动一个东西要完成两个步骤:
- 在新的位置上画出图形。
- 把原来的图形擦掉。
声音
为了让程序更有趣、更好玩,视频游戏和很多其他程序都使用了声音。
声音既可以作为输入,也可以作为输出。作为输入,需要把一个麦克风或其他音源连接到计算机,程序
会把声音记录下来,或者对它做其他处理(可能通过互联网发送)。不过声音作为输出更为常见。
制造声音
程序产生声音有两种基本方式。程序可以生成或合成声音—这是指制造不同音高和音量的声波来从头创建声音。或者程序也可以播放一段录制的声音。这可以是 CD 上的一段音乐、一个 MP3 声音文件,或者其他类型的声音文件。
播放声音
播放声音时,要从硬盘(或从 CD,或者有时从互联网)得到一个声音文件,把它转换成可以在计算机的扬声器或耳机上听到的声音。计算机上可以使用多种不同类型的声音文件。以下是比较常见的类型:
- 波形文件:文件名以
.wav
结尾,如 hello.wav。 - MP3 文件:文件名以
.mp3
结尾,如 mySong.mp3。 - WMA(Windows 媒体音频,Windows Media Audio)文件:文件名以
.wma
结尾,如 someSong.wma。 - Ogg Vorbis 文件:文件名以
.ogg
结尾,如 yourSong.ogg。
控制音量
可以使用音量控制开关来控制计算机上的声音音量。
播放背景音乐
背景音乐是指玩游戏时在背景播放的音乐。
重复音乐
果要使用一首歌作为游戏的背景音乐,你可能希望只要程序在运行,音乐就一直继续下去。