为你的 Python 平台游戏添加跳跃功能

学习如何在用 Python 的 Pygame 模块编程视频游戏的本期文章中,通过跳跃来对抗重力。
131 位读者喜欢这篇文章。
5 arcade-style games for Linux

Cicada Strange 在 Flickr 上,CC BY-SA 2.0

在本系列的上一篇文章中,你模拟了重力,但现在你需要让你的玩家有一种方法通过跳跃来对抗重力。

跳跃是对重力的暂时缓解。在短暂的时刻,你向上跳跃,而不是像重力拉你一样向下坠落。但是一旦你到达跳跃的顶峰,重力再次发挥作用,把你拉回地面。

在代码中,这转化为变量。首先,你必须为玩家精灵建立变量,以便 Python 可以跟踪精灵是否正在跳跃。一旦玩家精灵正在跳跃,那么重力将再次应用于玩家精灵,将其拉回到最近的物体。

设置跳跃状态变量

你必须向你的 Player 类添加两个新变量

  • 一个用于跟踪你的玩家是否正在跳跃,这取决于你的玩家精灵是否站在坚实的地面上
  • 一个用于将玩家拉回地面

将这些变量添加到你的 Player 类中。在以下代码中,注释上方的行用于提供上下文,因此只需添加最后两行

        self.frame = 0
        self.health = 10
        # jump code below
        self.is_jumping = True
        self.is_falling = False

这些新值称为布尔值,这是一个术语(以数学家乔治·布尔的名字命名),意思是真或假。在编程中,这是一种特殊的数据类型,指示变量是“开”还是“关”。在本例中,英雄精灵可以是正在坠落或未坠落,并且可以是正在跳跃或未跳跃。

第一个变量 (is_jumping) 设置为 True,因为我正在天空中生成英雄,并且需要它立即坠落到地面,就像它在半空中跳跃一样。这有点违反直觉,因为英雄实际上并没有跳跃。英雄只是刚刚生成。这在理论上是对这个布尔值的滥用,并且承认具有实际上反映现实的 True 和 False 语句的代码更“干净”。但是,我发现让重力帮助英雄找到地面比必须为每个级别硬编码生成位置更容易。它也唤起了经典的平台游戏,并给玩家一种“跳入”游戏世界的感觉。换句话说,这是一个服务于程序的小小的初始谎言,所以将其设置为 True

另一个变量 (is_falling) 也设置为 True,因为英雄确实需要下降到地面。

条件重力

在现实世界中,跳跃是一种对抗重力的行为。但在你的游戏中,只有当英雄精灵没有站在坚实的地面上时,才需要“开启”重力。当你一直开启重力(在 Pygame 中)时,你可能会在你的英雄精灵上获得弹跳效果,因为重力不断试图迫使英雄向下,而与地面的碰撞会产生抵抗。并非所有游戏引擎都需要与重力进行如此多的交互,但 Pygame 并非专门为平台游戏而设计(例如,你可以编写一个俯视视角的游戏),因此重力不是由引擎管理的。

你的代码只是在你的游戏世界中模拟重力。英雄精灵在看起来坠落时实际上并没有坠落,它是由你的 gravity 函数移动的。为了允许你的英雄精灵对抗重力并跳跃,或者与坚固的物体(如地面和浮动平台)碰撞,你必须修改你的 gravity 函数,使其仅在英雄跳跃时激活。此代码替换了你为上一篇文章编写的整个 gravity 函数

    def gravity(self):
        if self.is_jumping:
            self.movey += 3.2

这会导致你的英雄精灵直接穿过屏幕底部坠落,但是你可以通过对地面进行一些碰撞检测来解决这个问题。

编程坚实的地面

在上一篇文章中,实现了一个快速的技巧来防止英雄精灵穿过屏幕底部坠落。它将英雄保持在屏幕上,但仅通过在屏幕底部创建一个隐形墙。使用对象作为对象更简洁,而且在平台游戏中允许玩家因跳跃时机不佳而从世界中坠落也很常见。

在你的 Player 类的 update 函数中,添加此代码

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.movey = 0
            self.rect.bottom = g.rect.top
            self.is_jumping = False  # stop jumping

        # fall off the world
        if self.rect.y > worldy:
            self.health -=1
            print(self.health)
            self.rect.x = tx
            self.rect.y = ty

此代码块检查地面精灵和英雄精灵之间发生的碰撞。这与你检测敌人击中你的英雄时使用的原理相同。

在发生碰撞时,它使用 Pygame 提供的内置信息来查找英雄精灵的底部 (self.rect.bottom),并将其位置设置为地面精灵的顶部 (p.rect.top)。这提供了英雄精灵“站立”在地面的错觉,并防止其穿过地面坠落。

它还将 self.is_falling 设置为 0,以便程序知道英雄没有在半空中跳跃。此外,它将 self.movey 设置为 0,以便英雄不受重力拉动(这是游戏物理学的一个怪癖,一旦精灵着陆,你就不需要继续将精灵拉向地球)。

末尾的 if 语句检测玩家是否下降到地面以下的水平;如果是这样,它会扣除健康点数作为惩罚,然后将英雄精灵重新生成回屏幕的左上角(使用 txty 的值,即图块的大小,作为快速简便的起始值。)这假设你希望你的玩家因从世界中坠落而失去健康点数并重新生成。这不是绝对必要的;这只是平台游戏中的一个常见约定。

在 Pygame 中跳跃

跳跃的代码发生在几个地方。首先,创建一个 jump 函数来“翻转” is_jumpingis_falling

    def jump(self):
        if self.is_jumping is False:
            self.is_falling = False
            self.is_jumping = True

从跳跃动作中实际起飞发生在你的 Player 类的 update 函数中

        if self.is_jumping and self.is_falling is False:
            self.is_falling = True
            self.movey -= 33  # how high to jump

此代码仅在 is_jumping 变量为 True 而 is_falling 变量为 False 时执行。当满足这些条件时,英雄精灵的 Y 位置调整为“空中” 33 像素。它是 33,因为 Pygame 中 Y 轴上的数字越小,表示它越靠近屏幕顶部。这实际上是一个跳跃。你可以调整像素数以获得更低或更高的跳跃。此子句还将 is_falling 设置为 True,这可以防止注册另一个跳跃。如果你将其设置为 False,则跳跃动作将自身叠加,将你的英雄发射到太空,这很有趣,但不适合游戏玩法。

调用 jump 函数

问题是你的主循环中没有任何内容在调用 jump 函数。你早先为其制作了一个占位符按键,但现在,所有跳跃键所做的只是将 jump 打印到终端。

在你的主循环中,将向上箭头的操作从打印调试语句更改为调用 jump 函数。

            if event.key == pygame.K_UP or event.key == ord('w'):
                player.jump()

如果你更喜欢使用空格键进行跳跃,请将键设置为 pygame.K_SPACE 而不是 pygame.K_UP。或者,你可以同时使用两者(作为单独的 if 语句),以便玩家可以选择。

降落在平台上

到目前为止,你已经为玩家精灵撞击地面时定义了一个反重力条件,但是游戏代码将平台和地面保持在单独的列表中。(与本文中做出的许多选择一样,这并非绝对必要,你可以尝试将地面视为另一个平台。)为了使玩家精灵能够站在平台上,你必须检测玩家精灵和平台精灵之间的碰撞,并阻止重力“拉动”它向下。

将此代码放入你的 update 函数中

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.is_jumping = False  # stop jumping
            self.movey = 0

            # approach from below
            if self.rect.bottom <= p.rect.bottom:
               self.rect.bottom = p.rect.top
            else:
               self.movey += 3.2

此代码扫描平台列表,查找与你的英雄精灵的任何碰撞。如果检测到碰撞,则将 is_jumping 设置为 False,并取消精灵 Y 位置的任何移动。

平台悬挂在空中,这意味着玩家可以通过从上方或下方接近平台来与之互动。平台如何对你的英雄精灵做出反应取决于你,但阻止精灵从下方访问平台并不少见。第二个代码块中的代码将平台视为一种天花板或凉棚,这样英雄可以跳到平台上,只要它跳得比平台的顶部高,但在精灵试图从下方跳跃时会阻碍精灵

if 语句的第一个子句检测英雄精灵的底部是否小于(在屏幕上更高)平台。如果是,则英雄“降落”在平台上,因为英雄精灵底部的值等于平台精灵的顶部。否则,英雄精灵的 Y 位置会增加,导致其“掉落”远离平台。

坠落

如果你现在尝试你的游戏,你会发现跳跃大部分情况下按预期工作,但坠落不一致。例如,在你的英雄跳到平台上之后,它无法从平台上走下来坠落到地面。它只是停留在空中,好像下方仍然有一个平台。但是,你可以使英雄离平台。

造成这种情况的原因是重力的实现方式。与平台碰撞会“关闭”重力,因此英雄精灵不会穿过平台坠落。问题是,当英雄从平台边缘走下来时,没有任何东西会重新开启重力。

你可以通过在英雄精灵的移动过程中激活重力来强制重力重新激活。编辑你的 Player 类的 update 函数中的移动代码,添加一个语句以在移动期间激活重力。你需要添加的两行已注释

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

        if self.movex > 0:
            self.is_jumping = True  # turn gravity on
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = self.images[self.frame // ani]

这会激活重力足够长的时间,以在平台碰撞检查失败时使英雄精灵坠落到地面。

现在尝试你的游戏。一切都按预期工作,但尝试更改一些变量以查看可能发生的情况。

在下一篇文章中,你将使你的世界滚动起来。

这是到目前为止的所有代码

#!/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.is_jumping = True
        self.is_falling = True
        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 gravity(self):
        if self.is_jumping:
            self.movey += 3.2

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

    def jump(self):
        if self.is_jumping is False:
            self.is_falling = False
            self.is_jumping = True

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

        # moving left
        if self.movex < 0:
            self.is_jumping = True
            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.is_jumping = True
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = self.images[self.frame // ani]

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

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.movey = 0
            self.rect.bottom = g.rect.top
            self.is_jumping = False  # stop jumping

        # fall off the world
        if self.rect.y > worldy:
            self.health -=1
            print(self.health)
            self.rect.x = tx
            self.rect.y = ty

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.is_jumping = False  # stop jumping
            self.movey = 0
            if self.rect.bottom <= p.rect.bottom:
               self.rect.bottom = p.rect.top
            else:
               self.movey += 3.2

        if self.is_jumping and self.is_falling is False:
            self.is_falling = True
            self.movey -= 33  # how high to jump

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

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((550, 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'):
                player.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.gravity()
    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)

这是关于使用 Pygame 模块在 Python 3 中创建视频游戏的持续系列的第 8 篇文章。之前的文章包括:

  1. 学习如何通过构建一个简单的骰子游戏来用 Python 编程
  2. 使用 Pygame 模块用 Python 构建游戏框架
  3. 如何向你的 Python 游戏添加玩家
  4. 使用 Pygame 移动你的游戏角色
  5. 没有反派的英雄算什么?如何向你的 Python 游戏添加一个
  6. 向你的游戏添加平台
  7. 在你的 Python 游戏中模拟重力
接下来阅读什么
标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,而且经常同时从事这两个行业。
User profile image.
Jess Weichler 是一位数字艺术家,使用开源软件和硬件在 CyanideCupcake.com 上以数字方式和物理世界中创作作品。

1 条评论

我喜欢这篇文章。它非常有用且富有洞察力。这是学习 python 的最佳方式!!

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.