如何移动图像?
许多刚接触编程和图形的人都不知道如何让图像在屏幕上移动。如果不理解所有的概念,可能会非常混乱。你不是第一个被困在这里的人,我会尽力一步一步来。我们甚至会尝试以保持你的动画高效的方法结束。
请注意,在本文中我们不会教您使用python编程,只是向您介绍一些使用pygame的基础知识。
只是屏幕上的像素
Pygame有一个显示Surface。这基本上是一个在屏幕上可见的图像,图像由像素组成。改变这些像素的主要方法是调用blit()函数。这将像素从一个图像复制到另一个图像。
这是要理解的第一件事。当你在屏幕上分割图像时,你只是改变了屏幕上像素的颜色。像素不会被添加或移动,我们只是改变屏幕上像素的颜色。在pygame中,你将这些图像位到屏幕上也是Surface,但它们没有连接到显示Surface。当它们被传送到屏幕上时,它们被复制到显示器上,但你仍然拥有原始文件的唯一副本。
用这个简单的描述。也许您已经理解了“移动”图像需要什么。我们实际上没有移动任何东西。我们简单地把图像分割到一个新位置。但是在我们在新位置绘制图像之前,我们需要“擦除”旧的图像。否则,图像将在屏幕上的两个地方可见。通过快速擦除图像并在一个新的地方重新绘制,我们实现了运动的“错觉”。
在本教程的其余部分中,我们将把这个过程分解为更简单的步骤。甚至解释了让多个图像在屏幕上移动的最佳方法。你可能已经有问题了。比如,在把图像绘制到一个新位置之前,我们如何“擦除”图像?也许你还是完全迷路了?希望本教程的其余部分可以为您澄清这些问题。
让我们后退一步
也许像素和图像的概念对你来说还是有点陌生?好的,好消息是,在接下来的几节中,我们将使用代码来做我们想做的一切,它只是不使用像素。我们将创建一个由6个数字组成的小python列表,想象它代表一些我们可以在屏幕上看到的奇妙图形。这可能会让人惊讶,它与我们稍后将用真实图形所做的非常接近。
让我们从创建屏幕列表开始,用1s和2s的美丽风景填充它。
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]
如果您使用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]
哎呦。现在我们可以看到两个英雄。一个在老位置,一个在新位置。这就是为什么我们需要先“抹去”英雄原来的位置,然后再把他画到新的位置上。要删除他,我们需要将列表中的值修改回英雄出现之前的值。这意味着我们需要在英雄替换它们之前跟踪屏幕上的值。有几种方法可以做到这一点,但最简单的通常是保留一个屏幕背景的单独副本。这意味着我们需要对我们的小游戏做出一些改变。
创建一个背景
我们要做的是创建一个单独的列表,称之为background。我们将创建背景,使它看起来像我们的原始屏幕所做的,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]
这看起来像是很多额外的工作。我们和上次试图让他动起来时没什么两样。但这次我们有了转移他所需的额外信息。
英雄行动(取2次)
这一次可以很容易地移动英雄。首先,我们要把英雄从他原来的位置上抹去。为此,我们将正确的值从背景复制到屏幕上。然后我们将在屏幕上的新位置上绘制角色
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看作是“分配”像素。就像在上面的屏幕列表中设置值一样,blitering为图像中的像素分配颜色。
其他图形库将使用单词bitblt,或只是blt,但它们谈论的是相同的事情。它基本上就是把记忆从一个地方复制到另一个地方。实际上,它比直接复制内存要高级一些,因为它需要处理像素格式、剪切和扫描线倾斜等问题。高级的碎片也可以处理像透明度和其他特殊效果。
从列表到屏幕
将我们在上面的示例中看到的代码用于pygame非常简单。我们假装已经加载了一些漂亮的图像,并将它们命名为“terrain1”,“terrain2”和“hero”。以前我们给列表分配数字,现在我们将图形位元到屏幕上。另一个大的变化是,不再使用位置作为单一索引(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))
嗯,这些代码看起来应该非常熟悉,希望更重要的是;上面的代码应该有点意义。希望我在列表中设置简单值的例子显示了在屏幕上设置像素(使用blit)的相似性。唯一需要额外工作的部分是将玩家位置转换成屏幕上的坐标。现在我们只使用粗糙的(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)。当分段时,position参数表示源的顶角应该放在目标上的位置。
Pygame为这些坐标提供了一个方便的容器,它是一个矩形,这个矩形基本上代表了这些坐标中的一个矩形区域。它有左上角和一个尺寸。Rect提供了许多方便的方法来帮助你移动和定位它们。在下一个例子中,我们将用矩形表示对象的位置。
还要知道,pygame中的许多函数都需要Rect参数。所有这些函数都可以接受由4个元素组成的简单元组(left, top, width, height)。您并不总是需要使用这些Rect对象,但您主要希望这样做。此外,blit()函数可以接受一个矩形作为它的位置参数,它只是使用矩形的最上面的角落作为真正的位置。
改变背景
在之前的所有章节中,我们都将背景存储为不同类型的地面列表。这是创造基于砖块的游戏的好方法,但我们想要的是平滑滚动。为了简单一点,我们将把背景变成覆盖整个屏幕的单一图像。这样,当我们想要“擦除”我们的对象时(在重新绘制它们之前),我们只需要将被擦除的背景部分块移到屏幕上。
通过向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) #延迟0.1s
好了。这就是在屏幕上平滑地动画一个对象所需要的所有代码。我们甚至可以使用一个漂亮的背景字符。采用这种方式制作背景的另一个好处是,玩家的图像可以有透明或剪切部分,它仍然可以正确地绘制在背景之上(这是免费的奖励)。
我们还在上面的循环结束时调用pygame.time.delay()。这会降低程序的运行速度,否则它可能会运行得非常快,您可能看不到它。
好,接下来呢?
好了,我们知道了。希望本文已经完成了它所承诺的所有内容。但是,现在的代码还没有为下一款畅销游戏做好准备。我们如何轻松地拥有多个移动对象?像load_player_image()这样神秘的函数到底是什么?我们还需要一种方法来获得简单的用户输入,并循环超过100帧。我们将以这里的例子为例,把它变成一个面向对象的创建,这会让妈妈感到骄傲。
第一个神秘的函数
关于这些函数类型的完整信息可以在其他教程和参考资料中找到。pygame。Image模块有一个load()函数,可以做我们想做的事情。加载图像的行应该变成这样。
player = pygame.image.load('player.bmp').convert()
background = pygame.image.load('liquid.bmp').convert()
我们可以看到,这非常简单,加载函数只是接受一个文件名,并返回一个带有加载图像的新Surface。加载后,我们调用Surface方法convert()。Convert返回图像的一个新的Surface,但是现在转换为与显示相同的像素格式。由于这些图像在屏幕上的格式是相同的,它们将很快被分割。如果我们不进行转换,blit()函数就会比较慢,因为它必须在转换过程中从一种像素转换到另一种像素。
您可能还注意到load()和convert()都返回了新的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 = 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): #创建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 examples目录中找到。它是名为moveit.py的示例。看一看代码,玩玩它,运行它,学习它。
你可能想要处理的事情可能是拥有不止一种类型的对象。当您不想再显示对象时,找到一种干净地“删除”对象的方法。同时更新display.update()调用,以传递屏幕上已更改区域的列表。
pygame中还有其他一些教程和示例也涉及到这些问题。所以当你准备继续学习的时候,继续阅读。: -)
最后,如果您有任何问题,可以随时访问pygame邮件列表或聊天室。你身边总有人能帮你解决这类问题。
最后,获得乐趣,这就是游戏的意义所在!