这是使用 Python 3 和 Pygame 模块创建视频游戏的系列文章的第 12 部分。之前的文章是
- 学习如何通过构建简单的骰子游戏来使用 Python 编程
- 使用 Pygame 模块使用 Python 构建游戏框架
- 如何向你的 Python 游戏添加玩家
- 使用 Pygame 移动你的游戏角色
- 没有反派的英雄算什么?如何向你的 Python 游戏添加一个
- 使用 Pygame 在 Python 游戏中放置平台
- 在你的 Python 游戏中模拟重力
- 向你的 Python 平台游戏添加跳跃
- 使你的 Python 游戏玩家能够向前和向后跑
- 在你的 Python 平台游戏中放置一些战利品
- 向你的 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
。
测量生存时间
接下来,就像 Player
和 Enemy
一样,你需要一个 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)
与你在设置部分中创建的火球不同,你使用 1
将 self.firing
设置为不可用。
最后,你必须更新和绘制你的可投掷对象。此顺序很重要,因此请将此代码放在你现有的 enemy.move
和 player_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
再次尝试你的游戏,并清除你世界中的一些坏人。

(Seth Kenlon,CC BY-SA 4.0)
作为奖励挑战,尝试在每次敌人被消灭时增加玩家的分数。
完整代码
#!/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)
3 条评论