Pygame学习笔记(三):如何移动图像?

学习资料:《How Do I Move An Image?

很多刚开始学习图形化编程的人,都很难弄明白怎样才能让一个图像在屏幕上移动。如果不理解所有相关的概念,它会很令人费解。

屏幕上的像素点

Pygame有一个display Surface,它是一个基本的在屏幕上可见的图像,这个图像是由像素构成。改变这些像素点的主要方式,是调用blit()函数,它将像素点从一个图像拷贝到另一个图像上。

这是首先要理解的。当你把一个图像blit到屏幕上时,你只是简单地改变了屏幕上某些像素点的颜色。像素点不会被增加或移动,我们只是改变屏幕上已经存在的像素点的颜色。这些你blit到屏幕上的图像,也都是Pygame中的Surface对象,但它们与display Surface没有联系。当图像被blit到屏幕上时,它们被复制到了display中,但此时你仍然保有一份它们原来的拷贝。

通过简短的描述,也许你已经知道了要“移动”一个图像需要些什么。实际上,我们根本没有移动任何东西,仅仅是将图像blit到了一个新的位置。但是,在新的位置画出图像之前,我们必须“擦除”旧图像。否则,图像将会出现在屏幕上的两个位置。通过快速擦除图像,并在新的位置重新绘制它,我们得到了移动的“错觉”。

接下来,我们将整个过程分解为几个简单的步骤。还会提到使多个图像在屏幕上移动的最佳实践。

退一步来看

让我们用实际的代码进一步说明上面的概念。先创建一个包含6个数字的Python列表,并把它想象成一个屏幕。你会发现,这与稍后基于图形的例子惊人地相似。

下面,我们开始创建一个“屏幕”列表,并填满“地形”1和2。

>>> screen = [1, 1, 2, 2, 2, 1]
>>> print screen
[1, 1, 2, 2, 2, 1]

现在背景已经创建完毕,我们来创建英雄(用数字8表示)。

>>> screen[3] = 8
>>> print screen
[1, 1, 2, 8, 2, 1]

现在,整个屏幕上的元素还是静态的。

让英雄移动

在移动角色之前,我们需要记录它的位置。

>>> playerpos = 3
>>> screen[playerpos] = 8
>>> print screen
[1, 1, 2, 8, 2, 1]

现在可以很容易地移动他了。让我们试试直接改变playerpos的值,然后把英雄重新绘制到屏幕上。

>>> playerpos = playerpos - 1
>>> screen[playerpos] = 8
>>> print screen
[1, 1, 8, 8, 2, 1]

额,现在我们看到了两个英雄,一个在旧位置,一个在新位置。这就是为什么我们在重绘英雄到新位置之前,需要先“擦除”旧英雄的原因。要实现“擦除”效果,我们最好保存一份背景的拷贝。让我们对这个小游戏做一点修改。

创建一个地图

我们要创建一个单独的列表,作为我们的背景。然后将背景的值拷贝到屏幕列表中,最后在屏幕上画出英雄。

>>> background = [1, 1, 2, 2, 2, 1]
>>> screen = [0] * 6
>>> for i in range(6):
...     screen[i] = background[i]
>>> print screen
[1, 1, 2, 2, 2, 1]
>>> playerpos = 3
>>> screen[playerpos] = 8
>>> print screen
[1, 1, 2, 8, 2, 1]

现在我们已经做好了移动英雄的准备。

让英雄移动(v2)

在这个版本中,我们先把英雄从旧位置擦除,然后再把他绘制到新的位置。

>>> print screen
[1, 1, 2, 8, 2, 1]
>>> screen[playerpos] = background[playerpos]
>>> playerpos = playerpos - 1
>>> screen[playerpos] = 8
>>> print screen
[1, 1, 8, 2, 2, 1]

可以了,现在英雄向左侧移动了一个位置。我们可以用同样的方法,让英雄再向左移动一个位置。

>>> screen[playerpos] = background[playerpos]
>>> playerpos = playerpos - 1
>>> screen[playerpos] = 8
>>> print screen
[1, 8, 2, 2, 2, 1]

很棒吧!这并不是你期待看到的动画,但是,只要做一些细微的改动,我们就可以在屏幕上用真正的图形把它绘制出来。

定义:“blit”

接下来,我们会把之前的程序改为图形版本。显示图形时,我们会频繁使用blit这个术语。可以认为它是给像素点赋值。

BLIT: Basically, blit means to copy graphics from one image to another. A more formal definition is to copy an array of data to a bitmapped array destination. You can think of blit as just "assigning" pixels. Much like setting values in our screen-list above, blitting assigns the color of pixels in our image.

有的图形库使用bitblt或blt这样的词汇,意思和这里是一样的。

从列表到屏幕

把上面的例子在Pygame中实现是很简单的。假设我们已经加载了一些图形,命名为“terrain1”、“terrain2”和“hero”。之前我们是把数字赋值到列表中,现在我们把图形blit到屏幕上。还有一个很大的不同点,不再使用单个索引来表示位置,而是使用一个二维坐标系。假定游戏中每一个图形宽度为10个像素点。

>>> background = [terrain1, terrain1, terrain2, terrain2, terrain2, terrain1]
>>> screen = create_graphics_screen()
>>> for i in range(6):
...    screen.blit(background[i], (i*10, 0))
>>> playerpos = 3
>>> screen.blit(playerimage, (playerpos*10, 0))

这段代码看上去是不是很熟悉?让我们将玩家移动一个位置。

>>> screen.blit(background[playerpos], (playerpos*10, 0))
>>> playerpos = playerpos - 1
>>> screen.blit(playerimage, (playerpos*10, 0))

这段代码中,我们在屏幕上显示出了一个背景,画出了角色,并且对角色进行移动。在此基础上,还能做些什么吗?其实现在的实现看上去有些奇怪。首先,我们要找一个更加干净的方式来表示背景和玩家的位置;其次,我们要让移动变得平滑一些,就像真正的动画一样。

屏幕坐标系

向屏幕上放置一个物体时,我们必须告诉blit()函数把图像放到哪里。在Pygame中,我们总是通过(X, Y)坐标来传递位置信息,分别表示放置图像时向右、向下移动的像素点个数。Surface左上角的坐标是(0, 0)。当blit的时候,位置参数代表源图像左上角在目标图像上的位置。

Pygame带有一个很方便的坐标容器,Rect。Rect表示坐标系中的一块矩形区域,它包含左上角和尺寸信息。Rect提供了一些方法,可以方便地进行移动。接下来的例子中,我们使用Rect来表示物体的位置。

Pygame中的很多函数都接受Rect参数,所有这些元素也同样接受包含4个元素的元组(left, top, width, height)。blit函数也接受Rect类型作为位置参数,这种情况下,仅仅使用了Rect的左上角作为真正的位置参数。

改变背景

在本文之前的部分,我们使用包含不同地形的列表来表示背景。这种方式比较适合战棋类游戏(tile-base game),但并不能实现我们想要的平滑效果。我们将采用一个更简单的方法,即改为用单独一张图片覆盖整个屏幕来创建背景。这样,当我们想要“擦除”物体时,只需要把背景中的相关区域blit到屏幕上即可。

通过向blit传递一个可选的第三个Rect类型参数,我们可以让blit只使用源图像的子区域。

还要注意,当我们完成了向屏幕的绘制操作后,要调用pygame.display.update()来显示我们在屏幕上绘制的所有东西。

平滑移动

要让某个东西看起来平滑移动,我们只要每次把它移动几个像素点。下面是让物体在屏幕上平滑移动的代码。

>>> screen = create_screen()
>>> player = load_player_image()
>>> background = load_background_image()
>>> screen.blit(background, (0, 0))       #draw the background
>>> position = player.get_rect()
>>> screen.blit(player, position)         #draw the player
>>> pygame.display.update()               #and show it all
>>> for x in range(100):                  #animate 100 frames
...    screen.blit(background, position, position) #erase
...    position = position.move(2, 0)     #move player
...    screen.blit(player, position)      #draw new player
...    pygame.display.update()            #and show it all
...    pygame.time.delay(100)             #stop the program for 1/10 second

在循环的最后,我们调用了pygame.time.delay(),这让程序慢了下来。如果不这样做,程序可能运行得很快,以至于你根本看不到它。

接下来干嘛?

到目前为止,我们已经达成了本文一开始的目标。但是,现在的代码离一个真正的游戏还是有相当的差距。我们如何轻易创建多个移动中的物体?像load_player_image()这样神秘的函数里面到底是什么?我们也需要一个方式来获取简单的用户输入,并且循环很多次。我们还要把现在这个例子变成面向对象风格。

首先,看看这些神秘的函数

关于这类函数的详细信息可以在其他tutor或参考文档中找到。pygame.image模块有一个load()函数来实现我们的想要的功能,如下所示:

>>> player = pygame.image.load('player.bmp').convert()
>>> background = pygame.image.load('liquid.bmp').convert()

load函数接受一个文件名,返回一个加载了图像的Surface对象。加载完成之后,我们调用了Surface类的convert方法。convert函数也返回一个该图像的新Surface对象,但图像被转换为与我们的显示(display)相同的像素点格式。由于图像与屏幕有着同样的格式,blit时会非常快。如果我们没有进行转换,blit函数会更慢,因为它运行时必须把一种格式的像素点转换为另一种格式。

你可能注意到了,load()和convert()函数都返回了一个新的Surface。这意味着在上面的每一行代码中,我们实际上都创建了两个Surface对象。在其他的编程语言中,这会导致内存泄漏。幸运的是,Python足够聪明,Pygame会恰当地清理掉我们不再使用的Surface对象。

之前看到的另一个神秘的函数是create_screen()。在Pygame中,创建一个新的图形窗口是很简单的,下面的代码可以创建一个640*480的Surface对象。如果不传入其他参数,Pygame会替我们选择最佳的颜色深度和像素格式。

>>> screen = pygame.display.set_mode((640, 480))

处理输入

我们需要向程序中加入“事件处理”。

>>> while 1:
...    for event in pygame.event.get():
...        if event.type == QUIT:
...            sys.exit()
...    move_and_draw_all_game_objects()

如果用户点击窗口的关闭按钮,就退出程序。

移动多个图像

假设我们想让10个不同的图像在屏幕上移动。使用Python中的类是一个很好的方法。我们将创建一个代表游戏对象的类。这个对象有一个函数来移动自己,然后我们就可以创建任意多个对象了。

>>> class GameObject:
...    def __init__(self, image, height, speed):
...        self.speed = speed
...        self.image = image
...        self.pos = image.get_rect().move(0, height)
...    def move(self):
...        self.pos = self.pos.move(0, self.speed)
...        if self.pos.right > 600:
...            self.pos.left = 0

在我们的类中有两个函数,init函数创建了对象,它放置了对象并设定了速度。move方法将对象移动一步,如果移动得太远,则让对象回到最左边。

组合在一起

把刚才讲到的东西组合在一起,就得到了一个完整的程序:

>>> screen = pygame.display.set_mode((640, 480))
>>> player = pygame.image.load('player.bmp').convert()
>>> background = pygame.image.load('background.bmp').convert()
>>> screen.blit(background, (0, 0))
>>> objects = []
>>> for x in range(10): 		#create 10 objects
...    o = GameObject(player, x*40, x)
...    objects.append(o)
>>> while 1:
...    for event in pygame.event.get():
...        if event.type in (QUIT, KEYDOWN):
...            sys.exit()
...    for o in objects:
...        screen.blit(background, o.pos, o.pos)
...    for o in objects:
...        o.move()
...        screen.blit(o.image, o.pos)
...    pygame.display.update()
...    pygame.time.delay(100)

这个例子的最终可运行版本,可以在Pygame的例程中找到,名字叫做“moveit.py”。找来玩一玩。

这里有Pygame的论坛和邮件列表的信息。

最后

Lastly, have fun, that's what games are for!