帮助!如何移动图像?

作者

皮特·辛纳斯

联系方式

pete@shinners.org

许多编程和图形新手都很难弄清楚如何让图像在屏幕上移动。如果不理解所有的概念,可能会非常令人困惑。你不是第一个被困在这里的人,我会尽我所能一步一步地处理。我们甚至会尝试以保持动画效率的方法来结束。

请注意,在本文中,我们不会教您如何使用python编程,而是向您介绍一些使用pyGame的基础知识。

只有屏幕上的像素

PYGAME有一个显示界面。这基本上是一个在屏幕上可见的图像,该图像由像素组成。更改这些像素的主要方法是调用Blit()函数。这会将像素从一个图像复制到另一个图像。

这是第一件要理解的事情。当你在屏幕上乱画一幅图像时,你只是改变了屏幕上像素的颜色。像素不会添加或移动,我们只是更改屏幕上已有像素的颜色。你在屏幕上乱画的这些图像也是PYGAME中的表面,但它们绝不会连接到显示表面。当它们被显示在屏幕上时,它们会被复制到显示屏上,但您仍然拥有原始图像的唯一副本。

用这个简短的描述。也许您已经了解了“移动”图像需要什么。我们实际上什么都不动。我们只需将图像定位到一个新的位置。但在我们将图像绘制到新位置之前,我们需要“擦除”旧图像。否则,该图像将在屏幕上的两个位置可见。通过快速擦除图像并将其重新绘制到新的位置,我们实现了运动的“错觉”。

在本教程的其余部分中,我们将把这个过程分解成更简单的步骤。甚至解释了让多个图像在屏幕上移动的最佳方式。你可能已经有疑问了。比如,我们如何在将图像绘制到新位置之前将其“擦除”?也许你还是完全迷路了?好吧,希望本教程的其余部分能够为您理清问题。

让我们倒退一步

也许像素和图像的概念对你来说还有点陌生?好消息是,在接下来的几节中,我们将使用代码来实现我们想要的所有功能,只是不使用像素。我们将创建一个包含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]

如果您立即开始使用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                         #a new blank screen
>>> 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想象成 “分配” 像素。与上面的屏幕列表中的设置值非常相似,blotting会指定图像中像素的颜色。

其他图形库将使用该单词 比特 ,或者只是 blt ,但他们谈论的是同一件事。它基本上就是将内存从一个地方复制到另一个地方。实际上,它比直接复制内存要高级一些,因为它需要处理像像素格式、剪辑和扫描线间距这样的事情。高级闪光器还可以处理透明度和其他特殊效果。

从名单到屏幕

将我们在上面看到的代码作为示例,并使它们与pyGame一起工作是非常简单的。我们将假装加载了一些漂亮的图形,并将它们命名为“terrain 1”、“terrain 2”和“Hero”。在我们将数字分配给列表之前,我们现在将图形blit放到屏幕上。另一个重大变化是,我们现在需要一个二维坐标,而不是使用位置作为单一索引(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)坐标传递。这表示右侧的像素数,以及放置图像时向下的像素数。曲面的左上角是坐标(0,0)。向右移动一点将是(10,0),然后向下移动同样将是(10,10)。在进行blit时,位置参数表示源的左上角应该放置在目标上的什么位置。

PYGAME附带了一个方便的容器来存放这些坐标,它是一个RECT。矩形基本上表示这些坐标中的一个矩形区域。它有一个最大的边角和一个尺寸。RECT提供了许多方便的方法来帮助您移动和定位它们。在接下来的例子中,我们将用Rect来表示我们的对象的位置。

我还知道,pyGame中的许多函数都需要使用rect参数。所有这些函数还可以接受4个元素(左、上、宽、高)的简单元组。您并不总是需要使用这些RECT对象,但您将主要希望使用。此外,blit()函数可以接受RECT作为其位置参数,它只使用RECT的左上角作为实际位置。

更改背景

在我们之前的所有部分中,我们都将背景存储为不同类型的地面的列表。这是一个很好的方式来创建一个基于瓷砖的游戏,但我们希望滚动流畅。为简单起见,我们将背景更改为覆盖整个屏幕的单个图像。这样,当我们想要“擦除”我们的对象(在重新绘制它们之前)时,我们只需要将被擦除的背景的一部分斑点到屏幕上。

通过将可选的第三个rect参数传递给Blit,我们告诉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()这样的神秘函数到底是什么?我们还需要一种方法来获得简单的用户输入,并循环超过100帧。我们将以这里的示例为例,并将其转换为一个会让妈妈感到自豪的面向对象的创建。

首先,神秘的功能

有关这些类型的函数的完整信息可以在其他教程和参考中找到。图像模块有一个Load()函数,它将执行我们想要的操作。加载图像的行应该是这样的。**

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

我们可以看到这非常简单,Load函数只接受一个文件名,并返回一个新的Surface和加载的图像。加载后,我们调用Surface方法Convert()。Convert返回图像的新Surface,但现在已转换为与我们的显示相同的像素格式。由于屏幕上的图像将是相同的格式,因此它们将非常快地进行blit。如果我们不进行转换,则blit()函数会更慢,因为它必须在执行过程中从一种类型的像素转换为另一种类型的像素。

您可能还注意到,Load()和Convert()都返回新曲面。这意味着我们实际上是在每条线上创建两个曲面。在其他编程语言中,这会导致内存泄漏(这不是一件好事)。幸运的是,Python足够聪明,能够处理这一问题,而pyGame将适当地清理我们最终不使用的Surface。

我们在上面的示例中看到的另一个神秘函数是creatcreen()。在PYGAME中,为图形创建新窗口很简单。创建640x480曲面的代码如下所示。通过不传递其他参数,pyGame将为我们挑选最佳的颜色深度和像素格式。**

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

处理一些输入

我们迫切需要更改主循环以查找任何用户输入(例如当用户关闭窗口时)。我们需要在程序中添加“事件处理”。所有图形程序都使用这种基于事件的设计。该程序会从计算机中获取“按下键盘”或“移动鼠标”之类的事件。然后程序对不同的事件做出响应。下面是代码应该是什么样子的。我们将继续循环,直到用户要求我们停止,而不是循环100帧。**

>>> while True:
...     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):                    #create 10 objects</i>
...     o = GameObject(player, x*40, x)
...     objects.append(o)
>>> while True:
...     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中也有其他教程和示例介绍了这些问题。所以,当你准备好继续学习的时候,继续阅读。:-)

最后,如果你有任何关于这方面的问题,你可以随时来到电子游戏的邮件列表或聊天室。手头总有人可以帮你做这类生意。

最后,玩得开心,这就是游戏的目的!




Edit on GitHub