学习资料:《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!