向你的 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本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.