向您的 Python 游戏中添加投掷机制

四处奔跑躲避敌人是一回事。反击又是另一回事。在本关于在 Pygame 中创建平台游戏的系列文章的第 12 篇中学习如何操作。
65 位读者喜欢这个。
Gaming on a grid with penguin pawns

Opensource.com

这是关于使用 Python 3Pygame 模块创建视频游戏的正在进行的系列文章的第 12 部分。之前的文章是

  1. 学习如何通过构建简单的骰子游戏来用 Python 编程
  2. 使用 Pygame 模块使用 Python 构建游戏框架
  3. 如何向您的 Python 游戏中添加玩家
  4. 使用 Pygame 在游戏中移动角色
  5. 没有反派的英雄算什么?如何向您的 Python 游戏中添加一个
  6. 使用 Pygame 在 Python 游戏中放置平台
  7. 在您的 Python 游戏中模拟重力
  8. 向您的 Python 平台游戏添加跳跃功能
  9. 使您的 Python 游戏玩家能够向前和向后跑
  10. 在您的 Python 平台游戏中放置一些战利品
  11. 向您的 Python 游戏添加计分功能

我之前的文章本应是本系列的最后一篇文章,它鼓励您为这款游戏编写自己的补充内容。你们中的许多人这样做了!我收到了电子邮件,询问关于我尚未涵盖的常见机制的帮助:战斗。毕竟,跳跃以避开坏人是一回事,但有时让他们消失是非常令人满意的。在视频游戏中,向敌人投掷东西是很常见的,无论是火球、箭、闪电还是任何其他适合游戏的东西。

与您到目前为止在本系列平台游戏中编程的任何内容不同,可投掷物品具有生命周期。一旦你扔出一个物体,它应该行进一段距离然后消失。如果它是箭或类似的东西,它可能会在穿过屏幕边缘时消失。如果是火球或闪电,它可能会在一段时间后熄灭。

这意味着每次生成可投掷物品时,也必须生成其寿命的独特度量。为了介绍这个概念,本文演示了如何一次只投掷一件物品。(换句话说,一次只能存在一件可投掷物品。)一方面,这是一个游戏限制,但另一方面,它本身就是一种游戏机制。你的玩家不能一次投掷 50 个火球,因为你一次只允许一个,因此对于你的玩家来说,何时释放火球以试图击中敌人成为一个挑战。在幕后,这也使您的代码保持简单。

如果您想一次启用更多可投掷物品,请在完成本教程后挑战自己,并在此基础上构建您获得的知识。

创建可投掷类

如果您按照本系列的其他文章进行操作,您应该熟悉在屏幕上生成新对象时的基本 __init__ 函数。这与您用于生成 玩家敌人 的函数相同。这是一个 __init__ 函数,用于生成可投掷对象

class Throwable(pygame.sprite.Sprite):
    """
    Spawn a throwable object
    """
    def __init__(self, x, y, img, throw):
        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.firing = throw

此函数与您的 Player 类或 Enemy__init__ 函数的主要区别在于它具有 self.firing 变量。此变量跟踪可投掷对象当前是否在屏幕上处于活动状态,因此有理由认为,当创建可投掷对象时,该变量设置为 1

测量生命周期

接下来,就像 PlayerEnemy 一样,您需要一个 update 函数,以便可投掷对象在被抛向空中的敌人后自行移动。

确定可投掷对象寿命的最简单方法是检测它何时离开屏幕。您需要监控哪个屏幕边缘取决于您的可投掷对象的物理特性。

  • 如果您的玩家投掷的东西沿水平轴快速移动,例如弩箭或箭或非常快的魔法力,那么您需要监控游戏屏幕的水平限制。这由 worldx 定义。
  • 如果您的玩家投掷的东西垂直或水平和垂直移动,那么您必须监控游戏屏幕的垂直限制。这由 worldy 定义。

此示例假定您的可投掷对象向前移动一点并最终落到地面。但是,该对象不会从地面弹起,而是继续掉落到屏幕外。您可以尝试不同的设置,看看哪个最适合您的游戏

    def update(self,worldy):
        '''
        throw physics
        '''
        if self.rect.y < worldy: #vertical axis
            self.rect.x  += 15 #how fast it moves forward
            self.rect.y  += 5  #how fast it falls
        else:
            self.kill()     #remove throwable object
            self.firing = 0 #free up firing slot

要使您的可投掷对象移动得更快,请增加 self.rect 值的动量。

如果可投掷对象在屏幕外,则该对象将被销毁,从而释放它占用的 RAM。此外,self.firing 将设置回 0,以允许您的玩家再次射击。

设置您的可投掷对象

就像您的玩家和敌人一样,您必须在设置部分创建一个精灵组来保存可投掷对象。

此外,您必须创建一个非活动的可投掷对象才能开始游戏。如果游戏开始时没有可投掷对象,则玩家第一次尝试投掷武器时将会失败。

此示例假定您的玩家开始时使用火球作为武器,因此可投掷对象的每个实例都由 fire 变量指定。在以后的关卡中,随着玩家获得新技能,您可以引入一个新变量,使用不同的图像,但利用相同的 Throwable 类。

在此代码块中,前两行已在您的代码中,因此请勿重新键入它们

player_list = pygame.sprite.Group() #context
player_list.add(player)             #context
fire = Throwable(player.rect.x,player.rect.y,'fire.png',0)
firepower = pygame.sprite.Group()

请注意,可投掷物品从与玩家相同的位置开始。这使其看起来像是可投掷物品来自玩家。第一次生成火球时,使用 0,以便 self.firing 显示为可用。

在主循环中进行投掷

未出现在主循环中的代码将不会在游戏中使用,因此您需要在主循环中添加一些内容,以使您的可投掷对象进入您的游戏世界。

首先,添加玩家控件。目前,您没有火力触发器。键盘上的按键有两种状态:按键可以按下,也可以弹起。对于移动,您同时使用两者:按下开始玩家移动,松开按键(按键弹起)停止玩家。发射只需要一个信号。使用哪个按键事件(按键按下还是按键释放)来触发可投掷对象取决于个人喜好。

在此代码块中,前两行用于上下文

            if event.key == pygame.K_UP or event.key == ord('w'):
                player.jump(platform_list)
            if event.key == pygame.K_SPACE:
                if not fire.firing:
                    fire = Throwable(player.rect.x,player.rect.y,'fire.png',1)
                    firepower.add(fire)

与您在设置部分中创建的火球不同,您使用 1self.firing 设置为不可用。

最后,您必须更新和绘制您的可投掷对象。此顺序很重要,因此请将此代码放在您现有的 enemy.moveplayer_list.draw 行之间

    enemy.move()  # context

    if fire.firing:
        fire.update(worldy)
        firepower.draw(world)
    player_list.draw(screen)  # context
    enemy_list.draw(screen)   # context

请注意,只有当 self.firing 变量设置为 1 时,才会执行这些更新。如果设置为 0,则 fire.firing 不为真,并且会跳过更新。如果您尝试执行这些更新,无论如何,您的游戏都会崩溃,因为将没有 fire 对象可以更新或绘制。

启动您的游戏并尝试投掷您的武器。

检测碰撞

如果您玩了具有新投掷机制的游戏,您可能会注意到您可以投掷物体,但它对您的敌人没有任何影响。

原因是您的敌人不会检查碰撞。敌人可能会被您的可投掷对象击中,但永远不会知道。

您已经在 Player 类中完成了碰撞检测,这非常相似。在您的 Enemy 类中,添加一个新的 update 函数

    def update(self,firepower, enemy_list):
        """
        detect firepower collision
        """
        fire_hit_list = pygame.sprite.spritecollide(self,firepower,False)
        for fire in fire_hit_list:
            enemy_list.remove(self)

代码很简单。每个敌方对象都会检查它是否被 firepower 精灵组击中。如果是,则敌人将从敌人组中移除并消失。

要将该函数集成到您的游戏中,请在主循环中的新发射块中调用该函数

    if fire.firing:                             # context
        fire.update(worldy)                    # context
        firepower.draw(screen)                  # context
        enemy_list.update(firepower,enemy_list) # update enemy

您现在可以尝试您的游戏,并且大多数内容都按预期工作。但是,仍然存在一个问题,那就是投掷方向。

更改投掷机制方向

目前,您英雄的火球仅向右移动。这是因为 Throwable 类的 update 函数将像素添加到火球的位置,并且在 Pygame 中,X 轴上的较大数字表示向屏幕右侧移动。当您的英雄转向另一侧时,您可能希望它向左投掷火球。

至此,您至少在技术上知道如何实现这一点。但是,最简单的解决方案是在对您来说可能是新方式的方式中使用变量。通常,您可以“设置标志”(有时也称为“翻转位”)来指示您的英雄所面对的方向。完成此操作后,您可以检查该变量以了解火球是需要向左还是向右移动。

首先,在您的 Player 类中创建一个新变量,以表示您的英雄所面对的方向。因为我的英雄自然地面向右侧,所以我将其视为默认值

        self.score = 0
        self.facing_right = True  # add this
        self.is_jumping = True

当此变量为 True 时,您的英雄精灵面向右侧。每次玩家更改英雄的方向时都必须重新设置它,因此请在主循环中的相关 keyup 事件中执行此操作

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps, 0)
                player.facing_right = False  # add this line
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps, 0)
                player.facing_right = True  # add this line

最后,更改 Throwable 类的 update 函数以检查英雄是否面向右侧,并根据需要从火球的位置添加或减去像素

        if self.rect.y < worldy:
            if player.facing_right:
                self.rect.x += 15
            else:
                self.rect.x -= 15
            self.rect.y += 5

再次尝试您的游戏,清除您世界中的一些坏人。

作为奖励挑战,尝试在每次敌人被击败时增加玩家的分数。

完整代码

#!/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 pygame.freetype
import sys
import os

'''
Variables
'''

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

BLUE = (80, 80, 155)
BLACK = (23, 23, 23)
WHITE = (254, 254, 254)
ALPHA = (0, 255, 0)

tx = 64
ty = 64

font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts", "amazdoom.ttf")
font_size = tx
pygame.freetype.init()
myfont = pygame.freetype.Font(font_path, font_size)


'''
Objects
'''

def stats(score, health):
    myfont.render_to(world, (4, 4), "Score:"+str(score), BLUE, None, size=64)
    myfont.render_to(world, (4, 72), "Health:"+str(health), BLUE, None, size=64)


class Throwable(pygame.sprite.Sprite):
    """
    Spawn a throwable object
    """
    def __init__(self, x, y, img, throw):
        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.firing = throw

    def update(self, worldy):
        '''
        throw physics
        '''
        if self.rect.y < worldy:
            if player.facing_right:
                self.rect.x += 15
            else:
                self.rect.x -= 15
            self.rect.y += 5
        else:
            self.kill()
            self.firing = 0


# 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.damage = 0
        self.score = 0
        self.facing_right = True
        self.is_jumping = True
        self.is_falling = True
        self.images = []
        for i in range(1, 5):
            img = pygame.image.load(os.path.join('images', 'walk' + 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)
        if self.damage == 0:
            for enemy in enemy_hit_list:
                if not self.rect.contains(enemy):
                    self.damage = self.rect.colliderect(enemy)
        if self.damage == 1:
            idx = self.rect.collidelist(enemy_hit_list)
            if idx == -1:
                self.damage = 0   # set damage back to 0
                self.health -= 1  # subtract 1 hp

        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

        loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False)
        for loot in loot_hit_list:
            loot_list.remove(loot)
            self.score += 1
            print(self.score)

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)

        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

    def update(self, firepower, enemy_list):
        """
        detect firepower collision
        """
        fire_hit_list = pygame.sprite.spritecollide(self, firepower, False)
        for fire in fire_hit_list:
            enemy_list.remove(self)


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

    def loot(lvl):
        if lvl == 1:
            loot_list = pygame.sprite.Group()
            loot = Platform(tx*5, ty*5, tx, ty, 'loot_1.png')
            loot_list.add(loot)

        if lvl == 2:
            print(lvl)

        return loot_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
fire = Throwable(player.rect.x, player.rect.y, 'fire.png', 0)
firepower = pygame.sprite.Group()

eloc = []
eloc = [300, worldy-ty-80]
enemy_list = Level.bad(1, eloc)
gloc = []

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)
enemy_list = Level.bad( 1, eloc )
loot_list = Level.loot(1)


'''
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)
                player.facing_right = False
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps, 0)
                player.facing_right = True
            if event.key == pygame.K_SPACE:
                if not fire.firing:
                    fire = Throwable(player.rect.x, player.rect.y, 'fire.png', 1)
                    firepower.add(fire)

    # scroll the world forward
    if player.rect.x >= forwardx:
        scroll = player.rect.x - forwardx
        player.rect.x = forwardx
        for p in plat_list:
            p.rect.x -= scroll
        for e in enemy_list:
            e.rect.x -= scroll
        for l in loot_list:
            l.rect.x -= scroll

    # scroll the world backward
    if player.rect.x <= backwardx:
        scroll = backwardx - player.rect.x
        player.rect.x = backwardx
        for p in plat_list:
            p.rect.x += scroll
        for e in enemy_list:
            e.rect.x += scroll
        for l in loot_list:
            l.rect.x += scroll

    world.blit(backdrop, backdropbox)
    player.update()
    player.gravity()
    player_list.draw(world)
    if fire.firing:
        fire.update(worldy)
        firepower.draw(world)
    enemy_list.draw(world)
    enemy_list.update(firepower, enemy_list)
    loot_list.draw(world)
    ground_list.draw(world)
    plat_list.draw(world)
    for e in enemy_list:
        e.move()
    stats(player.score, player.health)
    pygame.display.flip()
    clock.tick(fps)

 

接下来阅读什么
标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,通常同时从事这两个行业。
User profile image.
Jess Weichler 是一位数字艺术家,使用开源软件和硬件在 CyanideCupcake.com 上创作数字和物理世界的作品。

3 条评论

令人难以置信的 Seth 和 Jess,谢谢你们。

感谢您继续您的教程。我想要更多关于如何根据 player.score 更改关卡、如何通过按某个键重置和重启游戏等主题的剧集。

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