这是关于使用 Python 3 和 Pygame 模块创建视频游戏的系列文章的第 11 部分。之前的文章有:
- 通过构建一个简单的掷骰子游戏来学习如何用 Python 编程
- 使用 Pygame 模块用 Python 构建游戏框架
- 如何为你的 Python 游戏添加玩家
- 使用 Pygame 移动你的游戏角色
- 没有反派的英雄算什么? 如何为你的 Python 游戏添加一个
- 为你的游戏添加平台
- 在你的 Python 游戏中模拟重力
- 为你的 Python 平台游戏添加跳跃功能
- 让你的 Python 游戏玩家能够向前和向后跑
- 使用 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)没有列出。事实上,tx 和 ty 被随意地放置在你的设置部分,因为当时,将它们与其他重要的图块信息放在一起似乎很容易也很合乎逻辑。
将 tx 和 ty 行从你的设置部分移动到第 41 行之上的某行即可修复错误。
当你在 Python 中遇到错误时,请注意它提供的提示,然后仔细阅读你的源代码。 即使对于经验丰富的程序员来说,找到错误也可能需要时间,但你越了解 Python,它就变得越容易。
运行游戏
当玩家收集战利品时,得分会增加。当玩家被敌人击中时,生命值会下降。 成功!

但是,有一个问题。当玩家被敌人击中时,生命值下降得太快了,这不公平。你刚刚发现了一个非致命的错误。非致命的错误是应用程序中的一些小问题,它们不会阻止应用程序启动甚至(大部分)正常工作,但它们要么没有意义,要么让用户感到恼火。以下是如何解决这个问题。
修复生命值计数器
当前生命值系统的问题在于,每当敌人接触玩家时,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 变量并减去一点生命值。
现在试试你的游戏。

现在你有了一种让你的玩家知道他们的分数和生命值的方法,你可以让某些事件在你的玩家达到某些里程碑时发生。 例如,可能有一种特殊的战利品物品可以恢复一些生命值。 也许生命值降至零的玩家必须从关卡的开头重新开始。
你可以在你的代码中检查这些事件并相应地操纵你的游戏世界。
升级
你已经知道如何做很多事情了。 现在是时候提升你的技能了。去浏览文档,寻找新的技巧,并在你自己的项目上尝试它们。编程是一项你需要培养的技能,所以不要止步于此项目。发明另一个游戏,或者一个有用的应用程序,或者只是使用 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)
5 条评论