为你的 Python 游戏添加计分功能

在本 Python Pygame 模块编程系列的第十一篇也是最后一篇文章中,当你的游戏玩家收集战利品或受到伤害时,显示他们的得分。
162 位读者喜欢这个。
connecting yellow dots in a maze

Opensource.com

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

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

如果你一直关注这个系列,你已经学习了使用 Python 创建视频游戏所需的所有基本语法和模式。但是,它仍然缺少一个重要的组成部分。这个组成部分不仅对用 Python 编程游戏很重要;无论你探索哪个计算分支,这都是你必须掌握的东西:通过阅读语言或库的文档来学习新的编程技巧。

幸运的是,你正在阅读这篇文章的事实表明你对文档感到自在。为了使你的平台游戏更完善的实际目的,在本文中,你将在游戏屏幕上添加得分和生命值显示。但是,本课的半公开议程是教你如何找出库提供了什么,以及如何使用新功能。

在 Pygame 中显示得分

现在你有了玩家可以收集的战利品,完全有理由保持得分,以便你的玩家看到他们收集了多少战利品。你还可以跟踪玩家的生命值,以便当他们击中敌人之一时,会产生后果。

你已经有变量来跟踪得分和生命值,但这都发生在后台。本文教你如何在游戏过程中在游戏屏幕上以你选择的字体显示这些统计信息。

阅读文档

大多数 Python 模块都有文档,即使没有文档的模块也可以通过 Python 的 Help 函数进行最少的文档记录。Pygame 的主页链接到其文档。然而,Pygame 是一个大型模块,有很多文档,而且它的文档的编写风格与 Opensource.com 上的文章风格并不完全相同(友好、启发性和有帮助)。它们是技术文档,它们列出了模块中可用的每个类和函数,每个类和函数期望的输入类型等等。如果你不习惯参考代码组件的描述,这可能会让人感到不知所措。

在研究库的文档之前,首先要做的是考虑你试图实现什么。在本例中,你希望在屏幕上显示玩家的得分和生命值。

一旦你确定了你期望的结果,就要考虑它需要哪些组件。你可以从变量和函数的角度来考虑,或者,如果这对你来说还不自然,你可以泛泛地思考。你可能认识到,显示得分需要一些文本,你希望 Pygame 在屏幕上绘制这些文本。如果你仔细思考,你可能会意识到它与在屏幕上渲染玩家或战利品或平台并没有太大区别。

从技术上讲,你可以使用数字图形并让 Pygame 显示这些图形。这不是实现你的目标的最简单方法,但如果这是你唯一知道的方法,那么这是一种有效的方法。但是,如果你参考 Pygame 的文档,你会看到列出的模块之一是 font,这是 Pygame 将文本打印到屏幕上的方法,就像打字一样简单。

解读技术文档

font 文档页面以 pygame.font.init() 开头,它将其列为用于初始化字体模块的函数。它由 pygame.init() 自动调用,你已经在你的代码中调用了它。再一次,你已经达到了技术上足够好的程度。虽然你还不知道如何,但你知道你可以使用 pygame.font 函数在屏幕上打印文本。

但是,如果你继续阅读,你会发现还有一种更好的打印字体的方法。pygame.freetype 模块在文档中这样描述

pygame.freetype 模块是用于加载和渲染字体的 pygame.fontpygame 模块的替代品。它具有原始模块的所有功能,以及许多新功能。

pygame.freetype 文档页面的下方,有一些示例代码

import pygame
import pygame.freetype

你的代码已经导入了 Pygame,但是修改你的 import 语句以包含 Freetype 模块

import pygame
import sys
import os
import pygame.freetype

在 Pygame 中使用字体

从字体模块的描述中可以清楚地看出,Pygame 使用字体(无论是你提供的字体还是 Pygame 内置的默认字体)在屏幕上渲染文本。滚动浏览 pygame.freetype 文档以查找 pygame.freetype.Font 函数

pygame.freetype.Font
Create a new Font instance from a supported font file.

Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font

pygame.freetype.Font.name
  Proper font name.

pygame.freetype.Font.path
  Font file path

pygame.freetype.Font.size
  The default point size used in rendering

这描述了如何在 Pygame 中构造字体“对象”。将屏幕上的简单对象视为多个代码属性的组合可能对你来说并不自然,但这与你构建英雄和敌人精灵的方式非常相似。你需要一个字体文件而不是图像文件。一旦你有了字体文件,你就可以使用 pygame.freetype.Font 函数在你的代码中创建一个字体对象,然后使用该对象在屏幕上渲染文本。

资源管理

因为世界上并非每个人都在他们的计算机上安装了完全相同的字体,所以将你选择的字体与你的游戏捆绑在一起非常重要。要捆绑字体,首先在你的游戏文件夹中创建一个新目录,与你为图像创建的目录并排。将其命名为 fonts

即使你的计算机附带了几种字体,但赠送这些字体是不合法的。这看起来很奇怪,但法律就是这样运作的。如果你想随游戏一起发布字体,你必须找到一个允许你随游戏一起赠送字体的开源或知识共享字体。

专门提供免费和合法字体的网站包括

当你找到你喜欢的字体时,下载它。解压缩 ZIP 或 TAR 文件,并将 .ttf.otf 文件移动到你的游戏项目目录中的 fonts 文件夹中。

你不是在你的计算机上安装字体。你只是将其放在你的游戏的 fonts 文件夹中,以便 Pygame 可以使用它。如果你愿意,你可以在你的计算机上安装字体,但这没有必要。重要的是将其放在你的游戏目录中,以便 Pygame 可以将其“描绘”到屏幕上。

如果字体文件的名称复杂,带有空格或特殊字符,只需重命名它即可。文件名是完全任意的,它越简单,你越容易将其键入你的代码中。

在 Pygame 中使用字体

现在告诉 Pygame 你的字体。从文档中,你知道当你至少向 pygame.freetype.Font 提供字体文件的路径时,你将获得一个字体对象作为返回值(文档明确指出所有剩余属性都是可选的)

Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font

创建一个名为 myfont 的新变量,用作游戏中的字体,并将 Font 函数的结果放入该变量中。此示例使用 amazdoom.ttf 字体,但你可以使用你想要的任何字体。将此代码放在你的设置部分

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)

在 Pygame 中显示文本

现在你已经创建了一个字体对象,你需要一个函数来将你想要的文本绘制到屏幕上。这与你用于绘制游戏中的背景和平台的原理相同。

首先,创建一个函数,并使用 myfont 对象创建一些文本,将颜色设置为某个 RGB 值。这必须是一个全局函数;它不属于任何特定的类。将其放在你的代码的 objects 部分,但将其保留为独立函数

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

当然,你现在知道,如果不在主循环中,你的游戏中就不会发生任何事情,所以在文件底部附近添加对你的 stats 函数的调用

    stats(player.score,player.health) # draw text

尝试你的游戏。如果你完全按照本文中的示例代码进行操作,那么现在当你尝试启动游戏时,你会收到一个错误。

解读错误

错误对程序员很重要。当你的代码中出现错误时,理解原因的最佳方法之一是阅读错误输出。不幸的是,Python 的交流方式与人类不同。虽然它确实有相对友好的错误,但你仍然必须解读你所看到的内容。

在本例中,启动游戏会产生此输出

Traceback (most recent call last):
  File "/home/tux/PycharmProjects/game_001/main.py", line 41, in <module>
    font_size = tx
NameError: name 'tx' is not defined

Python 断言变量 tx 未定义。你知道这不是真的,因为你现在已经在多个地方使用了 tx,并且它按预期工作。

但是 Python 也引用了一个行号。这是导致 Python 停止执行代码的行。它不一定是包含错误的行。

有了这些知识,你可以查看你的代码,试图理解哪里出了问题。

第 41 行尝试将字体大小设置为 tx 的值。但是,从第 41 行向上反向阅读文件,你可能会注意到 tx(和 ty)未列出。实际上,txty 被随意地放置在你的设置部分中,因为当时,将它们与其他重要的图块信息放在一起似乎很容易且合乎逻辑。

txty 行从你的设置部分移动到第 41 行之上的某一行可以修复错误。

当你在 Python 中遇到错误时,请注意它提供的提示,然后仔细阅读你的源代码。即使对于经验丰富的程序员来说,找到错误也可能需要时间,但是你对 Python 的理解越深入,它就变得越容易。

运行游戏

当玩家收集战利品时,得分会增加。当玩家被敌人击中时,生命值会下降。成功!

Score

但是,有一个问题。当玩家被敌人击中时,生命值会大幅下降,这不公平。你刚刚发现了一个非致命错误。非致命错误是应用程序中的那些小问题,它们不会阻止应用程序启动甚至正常工作(大部分情况下),但它们要么没有意义,要么会惹恼用户。以下是如何修复此错误。

修复生命值计数器

当前生命值系统的问题在于,每当敌人接触玩家时,都会从 Pygame 时钟的每个刻度中减去生命值。这意味着一个移动缓慢的敌人可以在一次遭遇中将玩家的生命值降至 -200,这不公平。当然,你可以只给你的玩家 10,000 的起始生命值,而不用担心;这会奏效,并且可能没有人会介意。但有一种更好的方法。

目前,你的代码检测到玩家和敌人何时碰撞。修复生命值问题的解决方案是检测两个独立的事件:玩家和敌人何时碰撞,以及一旦它们碰撞后,何时停止碰撞。

首先,在你的 Player 类中,创建一个变量来表示玩家和敌人何时碰撞

        self.frame = 0
        self.health = 10
        self.damage = 0

在你的 Player 类的 update 函数中,删除此代码块

        for enemy in enemy_hit_list:
            self.health -= 1
            #print(self.health)

并在其位置,只要玩家当前未被击中,就检查碰撞

        if self.damage == 0:
            for enemy in enemy_hit_list:
                if not self.rect.contains(enemy):
                    self.damage = self.rect.colliderect(enemy)

你可能会看到你删除的代码块和你刚刚添加的代码块之间的相似之处。它们都在做相同的工作,但新代码更复杂。最重要的是,新代码仅在玩家当前未被击中时运行。这意味着此代码在玩家和敌人碰撞时运行一次,而不是像以前那样在碰撞发生期间持续运行。

新代码使用了两个新的 Pygame 函数。self.rect.contains 函数检查敌人当前是否在玩家的边界框内,而 self.rect.colliderect 在为真时将你的新 self.damage 变量设置为 1,无论它为真多少次。

现在,即使被敌人击中三秒钟,在 Pygame 看来仍然像一次击中。

我通过阅读 Pygame 的文档发现了这些函数。你不必一次阅读所有文档,也不必阅读每个函数的每个单词。但是,重要的是花时间阅读你正在使用的新库或模块的文档;否则,你很有可能重新发明轮子。不要花一个下午试图拼凑一个解决方案来解决你正在使用的框架已经解决的问题。阅读文档,找到函数,并从他人的工作中受益!

最后,添加另一个代码块来检测玩家和敌人何时不再接触。然后并且只有在那时,才从玩家那里减去一点生命值。

        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

请注意,此新代码仅在玩家被击中时触发。这意味着当你的玩家在你的游戏世界中奔跑、探索或收集战利品时,此代码不会运行。它仅在 self.damage 变量被激活时运行。

当代码运行时,它使用 self.rect.collidelist 来查看玩家是否仍然在你的敌人列表中接触敌人(当检测到没有碰撞时,collidelist 返回负一)。一旦它不再接触敌人,就该偿还 self.damage 债务了:通过将其设置回零来停用 self.damage 变量并减去一点生命值。

现在尝试你的游戏。

Health

现在你有一种让玩家知道他们的得分和生命值的方法,你可以让某些事件在你的玩家达到某些里程碑时发生。例如,可能有一个特殊的战利品物品可以恢复一些生命值。并且可能生命值达到零的玩家必须从关卡的开头重新开始。

你可以在你的代码中检查这些事件并相应地操作你的游戏世界。

升级

你已经知道如何做很多事情了。现在是时候提升你的技能了。浏览文档以获取新技巧,并在你自己的项目中尝试它们。编程是你培养的一项技能,所以不要止步于这个项目。发明另一个游戏,或一个有用的应用程序,或者只是使用 Python 来尝试疯狂的想法。你使用得越多,你就越习惯它,最终它将成为第二天性。

继续努力,保持开源!

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

#!/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)

# 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.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)
        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


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

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)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps, 0)

    # 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)
    enemy_list.draw(world)
    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。

5 条评论

感谢您分享关于 Python 游戏的精彩文章,我喜欢它 :)

这篇文章很棒。当一切都自动化时,Python 将成为下一个重要的东西。我可以将这篇文章分享到我的社交媒体上吗?

你当然可以!如果你还没有尝试过开源社交媒体平台 Mastodon,你应该试一试!

回复 作者 Sansa Stark

感谢这篇精彩的文章

我用它为我一直在做的 Python 项目添加反馈,它工作得很好。

太棒了

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