使用 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 类需要知道很多关于你想要的平台类型、它应该在游戏世界中出现的位置以及它应该包含什么图像的信息。很多信息甚至可能还不存在,这取决于你计划了多少游戏,但这没关系。正如你在 Movement 文章 的结尾才告诉你的 Player 精灵移动速度一样,你不必预先告诉 Platform 所有内容。

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

# 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 类中识别出相同的代码结构。

平台类型

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

平铺方法

有几种不同的方法来实现平台游戏世界。在最初的横向卷轴游戏中,例如 Mario Super Bros. 和 Sonic the Hedgehog,技术是使用“平铺”,这意味着有一些块来代表地面和各种平台,并且这些块被使用和重用来制作关卡。你只有 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 的增量稳步增加,这是平铺的大小。这种重复性正是计算机擅长的,因此你可以使用一点数学逻辑让计算机为你完成所有计算

将其添加到你的脚本的设置部分

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

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

在你的程序的设置部分,添加此行

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 国际许可协议获得许可。
© . All rights reserved.