使用 Pygame 在 Python 游戏中放置平台

在本系列构建 Python 游戏的第六部分中,创建一些平台供您的角色移动。
361 位读者喜欢此文。
Gaming

Opensource.com 提供

这是使用 Pygame 模块在 Python 3 中创建视频游戏的系列文章的第 6 部分。之前的文章是

  1. 通过构建一个简单的骰子游戏来学习如何用 Python 编程
  2. 使用 Pygame 模块用 Python 构建游戏框架
  3. 如何将玩家添加到您的 Python 游戏中
  4. 使用 Pygame 来移动您的游戏角色
  5. 没有反派的英雄算什么? 如何将一个添加到你的 Python 游戏中

平台游戏需要平台。

Pygame 中,平台本身就是精灵,就像您的可玩精灵一样。 这很重要,因为拥有作为对象的平台可以使您的玩家精灵更容易与它们互动。

创建平台有两个主要步骤。 首先,您必须编写对象的代码,然后必须映射出您希望对象出现的位置。

编写平台对象代码

要构建平台对象,您需要创建一个名为 Platform 的类。 它是一个精灵,就像您的 Player 精灵 一样,具有许多相同的属性。

您的 Platform 类需要知道大量信息,包括您想要的平台类型、它应该出现在游戏世界中的位置以及它应该包含的图像。 很多信息可能还不存在,这取决于您对游戏的规划程度,但这没关系。 正如您在 移动文章 结束时才告诉您的 Player 精灵移动速度一样,您不必预先告诉 Platform 所有内容。

在脚本的 objects 部分,创建一个新类

# x location, y location, img width, img height, img file
class Platform(pygame.sprite.Sprite):
    def __init__(self, xloc, yloc, imgw, imgh, img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images', img)).convert()
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc

调用时,此类会在某个 X 和 Y 位置、某个宽度和高度,并使用某个图像文件作为纹理,在屏幕上创建一个对象。 这与玩家或敌人绘制在屏幕上的方式非常相似。 您可能从 Player 和 Enemy 类中认出了这个相同的代码结构。

平台类型

下一步是映射出所有平台需要出现的位置。

平铺方法

实现平台游戏世界有几种不同的方法。 在最初的横向卷轴游戏中,例如《马里奥兄弟》和《索尼克》,这项技术是使用“平铺”,这意味着有一些块代表地面和各种平台,这些块被使用和重用来制作一个关卡。 您只有 8 到 12 种不同类型的块,并且您将它们在屏幕上排列以创建地面、浮动平台以及游戏所需的任何其他内容。 有些人发现这是制作游戏的更简单方法,因为您只需制作(或下载)一小套关卡资源即可创建许多不同的关卡。 但是,该代码需要更多的数学知识。

Supertux, a tile-based video game

SuperTux,一款基于平铺的视频游戏。

手绘方法

另一种方法是将每个资产都作为一个完整的图像来制作。 如果您喜欢为您的游戏世界创建资产,这是一个花时间在图形应用程序中构建游戏世界的每个部分的绝佳借口。 这种方法需要的数学知识较少,因为所有平台都是完整的对象,并且您告诉 Python 将它们放置在屏幕上的位置。

每种方法都有优点和缺点,并且您必须使用的代码根据您选择的方法而略有不同。 我将介绍这两种方法,因此您可以在您的项目中使用其中一种,或者甚至混合使用。

关卡映射

映射您的游戏世界是关卡设计和游戏编程的重要组成部分。 它确实涉及数学,但没有什么太难的,而且 Python 擅长数学,因此它可以提供一些帮助。

您可能会发现先在纸上设计很有帮助。 拿一张纸,画一个框来代表您的游戏窗口。 在框中绘制平台,并标记每个平台的 X 和 Y 坐标,以及其预期宽度和高度。 框中的实际位置不必完全准确,只要您保持数字的真实性即可。 例如,如果您的屏幕宽 720 像素,那么您无法在一个屏幕上容纳八个每个 100 像素的平台。

当然,并非您游戏中的所有平台都必须适合一个屏幕大小的框,因为您的游戏会随着玩家的行走而滚动。 因此,请继续在第一个屏幕的右侧绘制您的游戏世界,直到关卡结束。

如果您喜欢更高的精度,可以使用方格纸。 这在设计带有平铺的游戏时特别有用,因为每个网格正方形可以代表一个平铺。

Mapping out tile placement

坐标

您可能在学校里学过笛卡尔坐标系。 您所学的知识适用于 Pygame,但 Pygame 中您的游戏世界的坐标将 0,0 放置在屏幕的左上角,而不是中间,这可能与您在几何课程中使用的不同。

Example of coordinates in Pygame

Pygame 中坐标的示例。

X 轴从最左边的 0 开始,并无限向右增加。 Y 轴从屏幕顶部的 0 开始,并向下延伸。

图像尺寸

如果您不知道您的玩家、敌人和平台有多大,那么映射游戏世界就没有意义。 您可以在图形程序中找到您的平台或平铺的尺寸。 例如,在 Krita 中,单击图像菜单,然后选择属性。 您可以在属性窗口的顶部找到尺寸。

或者,您可以创建一个简单的 Python 脚本来告诉您图像的尺寸。 为此,您必须安装一个名为 Pillow 的 Python 模块,该模块提供了 Python 图像库 (PIL)。 将 Pillow 添加到您项目的 requirements.txt 文件中

pygame~=1.9.6
Pillow

在 PyCharm 中创建一个新的 Python 文件,并将其命名为 identify。 将此代码键入其中

#!/usr/bin/env python3

# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.

from PIL import Image
import os.path
import sys

if len(sys.argv) > 1:
    print(sys.argv[1])
else:
    sys.exit('Syntax: identify.py [filename]')

pic = sys.argv[1]
img = Image.open(pic)
X   = img.size[0]
Y   = img.size[1]

print(X, Y)

单击 PyCharm 窗口底部的终端选项卡,以在您的虚拟环境中打开一个终端。 现在您可以将 Pillow 模块安装到您的环境中

(venv) pip install -r requirements.txt
Requirement already satisfied: pygame~=1.9.6 [...]
Installed Pillow [...]

安装完成后,从您的游戏项目目录中运行您的脚本

(venv) python ./identify.py images/ground.png
(1080, 97)

在本例中,地面平台的图像大小为 1080 像素宽,97 像素高。

平台块

如果您选择单独绘制每个资产,您必须创建多个平台以及您想要插入到游戏世界中的任何其他元素,每个元素都在其自己的文件中。 换句话说,您应该每个资产一个文件,如下所示

One image file per object

每个对象一个图像文件。

您可以根据需要多次重用每个平台,只需确保每个文件仅包含一个平台即可。 您不能使用包含所有内容的文件,如下所示

Your level cannot be one image file

您的关卡不能是一个图像文件。

您可能希望您的游戏在您完成时看起来像那样,但是如果您在一个大文件中创建您的关卡,则无法区分平台和背景,因此请在它们自己的文件中绘制您的对象,或者从一个大文件中裁剪它们并保存单独的副本。

注意: 与您的其他资产一样,您可以使用 GIMPKritaMyPaintInkscape 来创建您的游戏资产。

平台在每个关卡开始时出现在屏幕上,因此您必须在您的 Level 类中添加一个 platform 函数。 这里的特殊情况是地面平台,它非常重要,可以被视为其自身的平台组。 通过将地面视为其自身的一种特殊平台,您可以选择它是滚动还是在其他平台漂浮在其顶部时保持静止。 这取决于你。

将这两个函数添加到您的 Level 类中

def ground(lvl,x,y,w,h):
    ground_list = pygame.sprite.Group()
    if lvl == 1:
        ground = Platform(x,y,w,h,'block-ground.png')
        ground_list.add(ground)

    if lvl == 2:
        print("Level " + str(lvl) )

    return ground_list

def platform( lvl ):
    plat_list = pygame.sprite.Group()
    if lvl == 1:
        plat = Platform(200, worldy-97-128, 285,67,'block-big.png')
        plat_list.add(plat)
        plat = Platform(500, worldy-97-320, 197,54,'block-small.png')
        plat_list.add(plat)
    if lvl == 2:
        print("Level " + str(lvl) )
        
    return plat_list

ground 函数需要一个 X 和 Y 位置,以便 Pygame 知道在哪里放置地面平台。 它还需要平台的宽度和高度,以便 Pygame 知道地面在每个方向延伸多远。 该函数使用您的 Platform 类在屏幕上生成一个对象,然后将该对象添加到 ground_list 组。

platform 函数本质上是相同的,只是需要列出的平台更多。 在此示例中,只有两个,但您可以根据需要添加任意多个。 输入一个平台后,您必须先将其添加到 plat_list,然后再列出另一个。 如果您不将平台添加到组中,则它不会出现在您的游戏中。

提示: 考虑到您的游戏世界顶部为 0 可能会很困难,因为现实世界中发生的情况恰恰相反; 当计算您的身高时,您不是从天空往下测量,而是从脚到头顶测量。

如果您更容易从“地面”向上构建您的游戏世界,那么将 Y 轴值表示为负数可能会有所帮助。 例如,您知道您的游戏世界的底部是 worldy 的值。 因此,worldy 减去地面的高度(在此示例中为 97)是您的玩家通常站立的位置。 如果您的角色高 64 像素,则地面减去 128 正好是您的玩家的两倍高。 实际上,放置在 128 像素处的平台相对于您的玩家来说大约是两层楼高。 -320 处的平台是另外三层楼。 诸如此类。

正如您现在可能已经知道的那样,如果您不使用它们,您的任何类和函数都毫无价值。 将此代码添加到您的设置部分

ground_list = Level.ground(1, 0, worldy-97, 1080, 97)
plat_list = Level.platform(1)

并将这些行添加到您的主循环(同样,第一行仅用于上下文)

enemy_list.draw(world)  # refresh enemies
ground_list.draw(world)  # refresh ground
plat_list.draw(world)  # refresh platforms

平铺平台

平铺游戏世界被认为更容易制作,因为您只需预先绘制一些块,就可以反复使用它们来创建游戏中的每个平台。 有一些具有 知识共享许可 的平铺集供您在 kenney.nlOpenGameArt.org 等网站上使用。 来自 kenney.nl 的 simplified-platformer-pack 是 64 像素的正方形,因此这是本文中使用的平铺的尺寸。 如果您下载或创建了尺寸不同的平铺,请根据需要调整代码。

Platform 类与前面章节中提供的类相同。

然而,Level 类中的 groundplatform 必须使用循环来计算创建每个平台所需的方块数量。

如果你打算在游戏世界中有一个坚实的地面,那么地面很简单。 你只需将地面瓦片“克隆”到整个窗口。 例如,你可以创建一个 X 和 Y 值列表来指示每个瓦片应该放置的位置,然后使用循环来获取每个值并绘制一个瓦片。 这只是一个例子,所以不要将其添加到你的代码中

# Do not add this to your code
gloc = [0,656,64,656,128,656,192,656,256,656,320,656,384,656]

但是,如果你仔细观察,你会发现所有的 Y 值总是相同的(具体来说是 656),并且 X 值以 64 的增量稳步增加,这是瓦片的大小。 这种重复正是计算机擅长的,所以你可以使用一点数学逻辑来让计算机为你完成所有的计算

将此添加到你的脚本的 setup 部分

gloc = []
tx   = 64
ty   = 64

i=0
while i <= (worldx/tx)+tx:
    gloc.append(i*tx)
    i=i+1

ground_list = Level.ground( 1,gloc,tx,ty )

有了这段代码,无论你的窗口大小如何,Python 都会将游戏世界的宽度除以瓦片的宽度,并创建一个数组,列出每个 X 值。 这不会计算 Y 值,但无论如何,在平坦的地面上 Y 值永远不会改变。

要在函数中使用该数组,请使用 while 循环,该循环查看每个条目并在适当的位置添加一个地面瓦片。 将此函数添加到你的 Level

def ground(lvl,gloc,tx,ty):
    ground_list = pygame.sprite.Group()
    i=0
    if lvl == 1:
        while i < len(gloc):
            ground = Platform(gloc[i],worldy-ty,tx,ty,'tile-ground.png')
            ground_list.add(ground)
            i=i+1

    if lvl == 2:
        print("Level " + str(lvl) )

    return ground_list

除了 while 循环之外,这与前一节中提供的块状平台游戏的 ground 函数几乎相同。

对于移动平台,原理是相似的,但你可以使用一些技巧来让你的生活更轻松。

你可以通过平台的起始像素(它的 X 值)、距地面的高度(它的 Y 值)以及要绘制的瓦片数量来定义平台,而不是按像素映射每个平台。 这样,你就不必担心每个平台的宽度和高度。

这个技巧的逻辑稍微复杂一些,所以请仔细复制这段代码。 一个 while 循环位于另一个 while 循环内部,因为此函数必须查看每个数组条目中的所有三个值才能成功构建一个完整的平台。 在此示例中,只有三个平台被定义为 ploc.append 语句,但你的游戏可能需要更多,因此定义你需要的那样多的平台。 当然,有些平台暂时不会出现,因为它们离屏幕太远了,但是一旦你实现了滚动,它们就会进入视野。

def platform(lvl,tx,ty):
    plat_list = pygame.sprite.Group()
    ploc = []
    i=0
    if lvl == 1:
        ploc.append((200,worldy-ty-128,3))
        ploc.append((300,worldy-ty-256,3))
        ploc.append((500,worldy-ty-128,4))
        while i < len(ploc):
            j=0
            while j <= ploc[i][2]:
                plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'tile.png')
                plat_list.add(plat)
                j=j+1
            print('run' + str(i) + str(ploc[i]))
            i=i+1
            
    if lvl == 2:
        print("Level " + str(lvl) )

    return plat_list

当然,这只是创建了一个用于计算每个级别的平台的函数。 你的代码尚未调用该函数。

在程序的 setup 部分,添加以下行

plat_list = Level.platform(1, tx, ty)

要使平台出现在你的游戏世界中,它们必须位于你的主循环中。 如果你还没有这样做,请将以下行添加到你的主循环中(同样,第一行只是为了提供上下文)

        enemy_list.draw(world)  # refresh enemies
        ground_list.draw(world) # refresh ground
        plat_list.draw(world)   # refresh platforms

启动你的游戏,并根据需要调整平台的放置位置。 不要担心看不到在屏幕外生成的平台; 你很快就会解决这个问题。

Platforms at last

应用你所知道的

我还没有演示如何在你的游戏世界中放置你的敌人,但是应用你到目前为止所学到的知识,将敌人精灵放置在平台上或地面上。

暂时不要放置你的英雄精灵。 这必须由重力(或至少是它的模拟)来管理,你将在接下来的两篇文章中学习它。

现在,这是到目前为止的代码

#!/usr/bin/env python3
# by Seth Kenlon

# GPLv3
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://gnu.ac.cn/licenses/>.

import pygame
import sys
import os

'''
Variables
'''

worldx = 960
worldy = 720
fps = 40
ani = 4
world = pygame.display.set_mode([worldx, worldy])

BLUE = (25, 25, 200)
BLACK = (23, 23, 23)
WHITE = (254, 254, 254)
ALPHA = (0, 255, 0)

'''
Objects
'''

# x location, y location, img width, img height, img file
class Platform(pygame.sprite.Sprite):
    def __init__(self, xloc, yloc, imgw, imgh, img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images', img)).convert()
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc

class Player(pygame.sprite.Sprite):
    """
    Spawn a player
    """

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.images = []
        for i in range(1, 5):
            img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect = self.image.get_rect()

    def control(self, x, y):
        """
        control player movement
        """
        self.movex += x
        self.movey += y

    def update(self):
        """
        Update sprite position
        """

        self.rect.x = self.rect.x + self.movex
        self.rect.y = self.rect.y + self.movey

        # moving left
        if self.movex < 0:
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = pygame.transform.flip(self.images[self.frame // ani], True, False)

        # moving right
        if self.movex > 0:
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = self.images[self.frame // ani]

        hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in hit_list:
            self.health -= 1
            print(self.health)


class Enemy(pygame.sprite.Sprite):
    """
    Spawn an enemy
    """

    def __init__(self, x, y, img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images', img))
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

    def move(self):
        """
        enemy movement
        """
        distance = 80
        speed = 8

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance * 2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1


class Level:
    def ground(lvl, gloc, tx, ty):
        ground_list = pygame.sprite.Group()
        i = 0
        if lvl == 1:
            while i < len(gloc):
                ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png')
                ground_list.add(ground)
                i = i + 1

        if lvl == 2:
            print("Level " + str(lvl))

        return ground_list

    def bad(lvl, eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0], eloc[1], 'enemy.png')
            enemy_list = pygame.sprite.Group()
            enemy_list.add(enemy)
        if lvl == 2:
            print("Level " + str(lvl))

        return enemy_list

    # x location, y location, img width, img height, img file
    def platform(lvl, tx, ty):
        plat_list = pygame.sprite.Group()
        ploc = []
        i = 0
        if lvl == 1:
            ploc.append((200, worldy - ty - 128, 3))
            ploc.append((300, worldy - ty - 256, 3))
            ploc.append((500, worldy - ty - 128, 4))
            while i < len(ploc):
                j = 0
                while j <= ploc[i][2]:
                    plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png')
                    plat_list.add(plat)
                    j = j + 1
                print('run' + str(i) + str(ploc[i]))
                i = i + 1

        if lvl == 2:
            print("Level " + str(lvl))

        return plat_list


'''
Setup
'''

backdrop = pygame.image.load(os.path.join('images', 'stage.png'))
clock = pygame.time.Clock()
pygame.init()
backdropbox = world.get_rect()
main = True

player = Player()  # spawn player
player.rect.x = 0  # go to x
player.rect.y = 30  # go to y
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10

eloc = []
eloc = [300, 0]
enemy_list = Level.bad(1, eloc)

gloc = []
tx = 64
ty = 64

i = 0
while i <= (worldx / tx) + tx:
    gloc.append(i * tx)
    i = i + 1

ground_list = Level.ground(1, gloc, tx, ty)
plat_list = Level.platform(1, tx, ty)


'''
Main Loop
'''

while main:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            try:
                sys.exit()
            finally:
                main = False

        if event.type == pygame.KEYDOWN:
            if event.key == ord('q'):
                pygame.quit()
                try:
                    sys.exit()
                finally:
                    main = False
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(-steps, 0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(steps, 0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                print('jump')

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps, 0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps, 0)

    world.blit(backdrop, backdropbox)
    player.update()
    player_list.draw(world)
    enemy_list.draw(world)
    ground_list.draw(world)
    plat_list.draw(world)
    for e in enemy_list:
        e.move()
    pygame.display.flip()
    clock.tick(fps)
标签
Seth Kenlon
Seth Kenlon 是一名 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。 他曾在电影和计算机行业工作,而且通常是同时进行。
User profile image.
Jess Weichler 是一位数字艺术家,她使用开源软件和硬件在 CyanideCupcake.com 上以数字方式和物理世界中创作作品。

6 条评论

哇,信息太棒了,我喜欢 Python,你认为成为一名熟练的 Python 程序员需要多长时间?

好问题。 我希望其他人也参与评论,但这是我的初步三个想法

0. 练习。实践才能出真知。 要学习编程,请开始编程。 如果你需要,可以重新发明轮子; 我的一些第一个程序是为一些愚蠢的事情(批量重命名文件、生成图像缩略图等)而编写的,我可以在其他地方找到这些程序,但我选择重新创建它们以供自己学习。

1. 开源。 这是答案:它可以让你从他人的代码中学习,并利用其他人构建的东西(例如 Pygame 或 Arcade,甚至 Python 本身)。

2. 知道如何解析文本。 看起来很简单,但至少 50% 的编程归结为知道如何比较和操作文本。

3. 无畏。 立即开始。 不要被比你“更好”的程序员吓倒。

哦,还有一个额外奖励:继续阅读我的文章和 opensource.com 上的其他精彩编程文章 ;-)

回复 Ismail K

Seth,很棒的文章!

在我的业余时间里,我一直在阅读、学习、实现(重复)一个滚动平台游戏,你的文章很值得一读。 我期待阅读更多你的文章。 制作游戏真的令人满意,像你这样分享方法的人正在做很棒的工作。 感谢分享。

Jason

我的荣幸,Jason。 我发现制作游戏是学习编程的一种有趣方式,而且这些概念出奇地通用。 虽然今天用 Python 创建游戏的每个人注定不会找到一份制作视频游戏的工作,但例如,将一组活动项目抽象成一个列表,然后在每个循环中更新该列表的想法可以广泛应用于任何编程练习(这只是一个任意的例子)。

回复 Jason S (未验证)

如何创建像 Sonic Hedgehog 这样的倾斜平台? SuperTux 没有倾斜平台。

Creative Commons License本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© 2025 open-source.net.cn. All rights reserved.