原文链接:https://www.pygame.org/docs/tut/MoveIt.html
作者:Pete Shinners
很多刚接触编程和图像处理的人都要经过一个很艰难的阶段才能搞明白图像是怎么在屏幕上移动的。在没有理解所有的概念之前,这确实会让人感到困惑不解。你不是第一个被难住的人,我会尽力一步一步地给你讲清楚。我们最后甚至会一起学习那些让你的动画变得高效的方法。
请注意,这篇文章不是教你用Python编程的,只是给你介绍一些pygame的基础知识。
不过是屏幕上的像素点罢了
Pygame有一个展示平面。它基本上就是一个可以在屏幕上看见的图像,这个图像是由像素点组成的。你改变这些像素点的主要方法就是调用blit()函数。这个函数会把像素点从一个图像上面拷贝到另一个图像上面。
这是你需要理解的第一件事情。当你把一个图像传输到屏幕上面,你只是在简单地改变屏幕上像素点的颜色。像素点没有被增加,也没有被移动,我们仅仅是改变那些已经存在于屏幕上的像素点的颜色。这些你传输到屏幕上的图像也是pygame里的Surface对象,但是它们跟展示平面没有关系。当它们被传输到屏幕上,它们就被拷贝到展示平面上了,但是你仍然有一个原始图像的独立副本。
看了这个简短的描述,或许你已经能够理解“移动”图像需要做什么了。我们实际上不需要移动任何东西。我们只是简单地把图像传输到一个新的位置上去。但是,在我们到一个新位置绘制图像之前,我们需要“擦除”旧的图像。否则,在屏幕上的两个地方都能看到那个图像。通过快速地擦除图像以及在新位置重绘图像,我们就获得了一种移动的”错觉“。
在这份指南的余下部分,我们会把这个过程分成更简单的步骤。我甚至还会介绍在屏幕上移动多个图像的最佳方法。你可能已经有疑问了。比方说,我们在新位置绘制图像之前怎么“擦除”原有的图像?或许你依然不知所云?希望这份指南的余下部分能够为你理清思路。
让我们往回走一步
可能“像素点”和“图像”的概念对你来说也有点陌生?好消息是,在下面的几个部分,我们只使用代码就能完成所有的工作了,用不着像素点。我们会创建一个只有6个数字的小Python列表,然后把它想象成我们能在屏幕上看到的一副神奇的图画。事实上这个列表和我们稍后要处理的真实图形惊人地相似。
所以,让我们着手创建我们的屏幕列表吧,我们就用由1和2组成的漂亮景色来填充它。
>>>screen = [1,1,2,2,2,1]
>>>print(screen) # 原文这里是没有括号的,但是我想现在用Python3的人比较多吧
[1,1,2,2,2,1]
现在我们已经创建好了我们的背景。它还不是那么让人振奋,除非我们在屏幕上也画个玩家。我们会创建一个高大威猛的英雄,他长得像数字8似的。我们把他放在地图的中间,看看现在什么样。
>>>screen[3] = 8
>>>print(screen)
[1,1,2,8,2,1]
如果你刚开始用pygame做图形化编程,这可能就是你能做出来的东西了。你在屏幕上画出了一些很漂亮的东西,但是它们还不能移动。现在我们的屏幕就是一串数字,或许这样能更容易地看出怎么移动玩家?
让英雄动起来!
在我们移动人物之前,我们需要记录他的位置。在上一部分,当我们绘制他的时候,我们就是随便给他挑选了一个位置。这次我们做得更正式一些。
>>>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]
哎呀!现在我们看到了两个英雄。一个在原来的位置,还有一个在新的位置。这就是我们在新位置绘制英雄之前要把原来位置的英雄“擦除”的原因。为了擦掉他,我们需要把列表里的数值改为英雄出现之前的样子。那就意味着我们需要记录英雄取代屏幕上的值之前的那些数值。有几种不同的实现方法,但是最简单的通常是保留一份屏幕背景的独立副本。这意味着,我们得修改一下我们的小游戏。
创建一张地图
我们要做的就是创建一个独立的列表,这个列表就是我们所说的背景。我们会创建这个背景,它看起来就像我们一开始的屏幕那样,里面都是1和2.然后,我们把背景里的每个元素都复制到屏幕上。然后我们就可以把英雄画到屏幕上了。
>>>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]
看起来好像做了很多额外的工作。跟我们上一次试着移动英雄之前相比,我们没有什么进展。但是这次我们有了恰当移动英雄所需的额外信息。
让英雄动起来(第二次)
这次移动英雄会很容易。首先我们会把英雄从原来的位置擦除。我们通过把背景里面正确的值拷贝到屏幕上来完成这个工作。然后我们会把英雄绘制到屏幕上的新位置。
>>>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:基本上,blit的意思就是把图形从一个图像上复制到另一个上面。一个更加正式的定义是,把一个数据的数组复制到一个位图数组的目的地上去。你可以把blit理解为给像素点“赋值”。很像我们上面给屏幕列表设置数值,blit就是给我们图像里的像素点赋上颜色值。
其他的图形库会使用bitblt,或者就是blt,但是它们说的都是一回事儿。他本质上就是把内存从一个地方拷贝到另一个地方。事实上,它不止是直接拷贝内存,因为它还要处理像素格式,剪切,扫描等事务。高级的传输器还会处理透明度和其他的特殊效果。
从列表到屏幕
以我们上面看到的那些代码作为例子,并让它们跟pygame结合起来是非常简单的。假装我们已经加载了一些漂亮的图像,并且给它们命名为"terrain1","terrain2"和"hero"【译者注:terrain的意思是地形】. 不同于我们之前往列表里添加数字,我们现在要把图像传输到屏幕上。另一个大的变化是,我们不再用索引来表示位置(0到5),现在我们需要一个二维的坐标。我们假定我们游戏里的每个图形都是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))
# 译者吐槽:上面不是说好了英雄的图片命名为hero吗,这里怎么就变成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).往右移动一点点会是(10,0), 然后再往下移动同样的距离就得到了(10,10)。传输的时候,位置参数表示图片源的左上角应该被放置到目的地的什么位置上。
Pygame有一个非常便捷的包裹这些坐标的容器,它就是Rect。Rect本质上表示在这些坐标点上的矩形区域。它有一个左上角,还有一个尺寸。Rect有很多便捷的方法,这些方法能帮助你移动和放置它们。在我们下面的例子里,我们就会用Rect来表示我们那些对象的位置。
你也要知道,pygame里的很多函数都需要Rect类型的参数。所有这些函数也可以接受一个简单的包含4个元素(左,上,宽度,高度)的元组作为参数。你并没有被要求总是使用Rect对象,但是你大体上会想使用它们的, blit()函数可以接受一个Rect作为它的位置参数, 它简单地使用Rect对象的左上角作为真正的位置。
改变背景
在我们前面的所有部分,我们都是把背景保存为一个包含不同地形的列表。如果我们只是想创建一个棋盘类的游戏,这个办法没什么问题,但是我们现在想让游戏顺畅地滚动起来。简单起见,我们将会把背景改成一张覆盖了整个屏幕的图片。这样,当我们想要“擦除”我们的对象(在重绘它们之前),我们只需要把擦掉的背景部分传输到屏幕上。
通过给blit函数传递一个可选的第三个Rect参数,我们告诉blit仅仅使用源图片的某一部分。下面我们擦除玩家图像的时候你就会看到这个。
也要注意,现在当我们完成在屏幕上的绘制,我们调用pygame.display.update(),这个函数就会把我们画在屏幕上的东西都显示出来。
流畅的移动
为了让一个东西流畅地移动,我们一次只让它移动几个像素点的距离。下面就是让一个对象在屏幕上流畅地从一边移动到另一边的代码。基于我们现在已经知道的知识,这些代码看上去非常简单。
>>>screen = create_screen()
>>>player = load_player_image()
>>>background = load_background_image()
>>>screen.blit(background,(0,0)) # 绘制背景图片
>>>position = player.get_rect()
>>>screen.blit(player, position) # 绘制玩家
>>>pygame.display.update() # 显示全部的图像
>>>for x in range(100): # 100帧动画
screen.blit(background,position,position) # 擦除玩家原来的图像
position = position.move(2,0) # 移动玩家
screen.blit(player, position) # 在新的位置绘制玩家
pygame.display.update() # 显示全部的图像
pygame.time.delay(100) # 让程序暂停十分之一秒
搞定。这就是让一个对象在屏幕上流畅移动所需的全部代码。我们甚至可以使用一个漂亮的背景人物。这样绘制背景的另一个好处是,玩家的图片可以有透明度或者剪掉的部分,这样的图片依然可以正确地绘制在背景上面(这是一个福利)。
我们还在上面的循环结尾调用了pygame.time.delay(). 这把我们的程序运行速度降低了一点点,否则它可能运行得太快,你可能就看不到了。
所以,接下来呢?
上面我们已经得到我们想要的了。希望这篇文章已经把一开始承诺做的事情都做到了。但是,目前的代码还没有为下一个最畅销的游戏做好准备。我们怎么样才能很容易地移动多个对象呢?那些神秘的函数——像load_player_image()之类的——到底是什么?我们还需要简单的方法来获取用户输入,以及循环多于100帧。我们会用我们已有的代码做例子,然后把它转化为让你的母亲都为你感到骄傲的面向对象的创造。
首先,那些神秘的函数
这些类型的函数的完整信息可以在其他指南以及使用参考里面找到。pygame.image模块有一个load()函数,它就能做到我们想要做的事情。
加载图片的代码应该变成下面这样:
>>>player = pygame.image.load('player.bmp').convert()
>>>background = pygame.image.load('liquid.bmp').convert()
我们可以看到这些代码非常简单,load函数只需要一个文件名,然后返回了一个带有被加载的图像的新Surface对象。加载图像之后我们调用了Surface对象的convert()方法。Convert给我们返回了一个该图像的新Surface对象,但是现在转化为了我们显示图像时候用到的同样的像素格式。因为屏幕上的图像都是同样的格式,它们会传输得非常快。如果我们没有转化,blit()函数就会慢一点,因为它在传输的时候需要把一种像素格式转化为另一种。
你可能还注意到了load()和convert()都会返回新的Surface对象。这意味着,在每一行代码里我们都创建了两个Surface对象。在别的编程语言里面,这会导致内存溢出(不是好事儿)。幸运的是Python足够智能,可以很好地处理这种情况,pygame会在我们不再使用某些Surface对象的时候把它们清理掉。
在上面的例子,我们看到的另一个神秘的函数是create_screen(). 在pygame里,创建一个新的图形窗口非常简单。创建一个640x480的平面的代码在下面给出了。在不传递其他参数的情况下,pygame会给我们选择最佳的颜色深度和像素格式。
>>>screen = pygame.display.set_mode((640,480))
处理一些输入
我们肯定需要更改主循环来寻找用户的输入,(比如用户关闭窗口)。我们需要给我们的程序加上“事件处理”。所有的图形程序都是用了这个基于事件的设计。程序会从电脑获取像“键盘按下”、“鼠标移动”之类的事件。然后程序会对不同的事件作出响应。代码应该看起来像下面这样。不同于之前的循环100帧,我们会一直循环,直到用户让我们停止。
>>>while 1:
for event in pygame.event.get():
if event.type in (QUIT, KEYDOWN):
sys.exit()
move_and_draw_all_game_objects()
这些代码做的事情很简单,首先开启一个无限循环,然后检查有没有来自用户的事件。如果用户按下了键盘上的一个按键或者点击了窗口上的关闭按钮,我们就退出程序。在我们检测了所有的事件之后,我们移动并绘制我们的游戏对象。(在它们移动之前我们也要先把它们擦除)
移动多个图像
这是我们真正要改变周围的东西的部分。假设我们想让10个不同的图像都在屏幕上面移动。一个好的处理方法是使用Python的类。我们会创建一个代表我们的游戏对象的类。这个对象有一个移动它自己的函数(方法),然后我们可以想创建几个对象就创建几个。那些绘制和移动对象的函数需要以每次只移动一帧(或者说一步)的方式工作。下面就是创建我们这个类的Python代码。
>>>class GameObject:
def __init__(self, image, height, speed):
self.speed = speed
self.image = iamge
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
# 译者吐槽:图像一直都在沿着y轴方向运动,但是最后对边缘的处理
# 却在判断它是否走到了右边界 这个代码是有问题的
所以我们的类里面有两个函数。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): #创建10个对象
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)
就是这样。这就是我们在屏幕上让10个对象动起来需要的代码。唯一需要解释的点可能就是我们清除所有的对象和绘制所有的对象用到的两个循环。为了恰当地完成我们的工作,我们需要在绘制对象之前把它们全都擦掉。在我们这个例子里可能不那么要紧,但是当不同的对象重叠的时候,像这样使用两个循环是非常必要的。
从现在开始就全靠你自己了
所以你接下来该学习什么呢?首先,自己再好好研究一下这个例子。这个例子的完整可运行版本可以在Pygame例子目录里找到。它的名字是moveit.py。看看那些代码,玩儿一玩儿,运行一下,学习学习。
你可能想要做的事情是拥有不止一种对象。当你不想再展示那些对象的时候,找一个方法干净地“删除”它们。更新display.update()的调用,传递给它一个屏幕上发生变化的区域列表。
pygame里有其他的教程和例子覆盖了这些问题。所以当你准备好继续学习了,就接着读吧。:-)
最后,如果你对这些东西有问题,随时欢迎你加入pygame的邮件列表或者聊天室。总有一些人可以帮助你解决这类问题。
最后,玩儿得开心点儿,这正是游戏的目的!