导读
Python的强大超出你的认知,Python的功能不止于可以做网络爬虫,数据分析,Python完全可以进行后端开发,AI,Python也可进行游戏开发,本文将会详细介绍Python使用pygame模块来开发一个名为“合金弹头”的游戏
请先阅读上篇:使用pygame开发游戏:合金弹头(4)
合理绘制地图
前面开发已经完成了游戏中主要要求:各种怪物和角色,只是角色跑动的效果较差,这其实只是一个视觉效果:由于游戏的背景地图总是静止的,因此玩家会感觉角色似乎并未跑动。
为了让角色的跑动效果更加真实,游戏需要根据玩家跑动的位移来改变背景地图,当游戏的背景地图动起来之后,玩家控制的角色就似乎在地图上“跑”起来了。
为了集中处理游戏的界面绘制,程序在ViewManager类中定义了一个draw_game(self, screen, mm, player)方法,该方法负责整个游戏场景。该方法的实现思路就是先绘制游戏地图,然后所有的怪物,最后绘制绘制游戏角色即可。下面是draw_game()方法的代码。
def draw_game(self, screen, mm, player):
''' 绘制游戏界面的方法,该方法先绘制游戏背景地图,
再绘制所有怪物,最后绘制游戏角色 '''
# 画地图
if self.map != None:
width = self.map.get_width() + player.shift()
# 绘制map图片,也就是绘制地图
screen.blit(self.map, (0, 0), (-player.shift(), 0, width, self.map.get_height()))
total_width = width
# 采用循环,保证地图前后可以拼接起来
while total_width < self.screen_width:
map_width = self.map.get_width()
draw_width = self.screen_width - total_width
if map_width < draw_width:
draw_width = map_width
screen.blit(self.map, (total_width, 0), (0, 0, draw_width,
self.map.get_height()))
total_width += draw_width
# 画角色
player.draw(screen)
# 画怪物
mm.draw_monster(screen, self)
上面方法中第一行screen.blit(...)代码使用screnn的blit()方法来绘制背景位图,第二行screen.blit(...)代码依然使用了blit()方法来绘制背景位图——这是因为当角色在地图上不断地向右移动时,随着地图不断地向左拖动,地图就会不能完全覆盖屏幕右边,此时需要再绘制一张背景位图,这样才可以拼成完成的地图——这样就形成了无限循环的游戏地图。
由于ViewManager已经提供了draw_game()方法来绘制游戏界面,因此game_functions程序的update_screen()方法只要调用ViewManager已经提供了draw_game()方法即可。因此将game_functions程序的update_screen()方法改为如下形式。
# 处理更新游戏界面的方法
def update_screen(screen, view_manager, mm, player):
# 随机生成怪物
mm.generate_monster(view_manager)
# 处理角色的逻辑
player.logic(screen)
# 如果游戏角色已死,判断玩家失败
if player.is_die():
print('游戏失败!')
# 检查所有怪物是否将要死亡
mm.check_monster(view_manager, player)
# 绘制背景图
# screen.blit(view_manager.map, (0, 0))
# 画角色
# player.draw(screen)
# 画怪物
# mm.draw_monster(screen, view_manager)
# 绘制游戏
view_manager.draw_game(screen, mm, player) #①
# 更新屏幕显示,放在最后一行
pygame.display.flip()
上面程序中3行被注释的代码是之前绘制游戏背景图片、绘制角色、绘制怪物的代码,现在把这行代码删掉(或注释掉),改为调用ViewManager的draw_game()方法绘制游戏界面即可,如上程序中①号代码所示。
此时再运行程序将会看到非常好的跑动效果。
增加音效
现在游戏已经运行起来了,但整个游戏是安静无声,这还不够好,游戏应该增加背景音效,还应该为发射子弹、爆炸、打中目标增加各种音效,增加音效的游戏会更逼真。
pygame提供了pygame.mixer模块来播放音效,该模块下主要包含了两种播放音效的方式:
-
使用pygame.mixer的Sound类:每个Sound对象管理一个音效,该对象通常用于播放短暂的音效,比如射击音效、爆炸音效等。
-
使用pygame.mixer.music子模块:该子模块通常用于播放游戏的背景音乐,该子模块提供了一个load()方法用于加载背景音乐,并提供了一个play()方法用于播放背景音乐。
为了给游戏增加背景音乐,修改metal_slug.py程序,在该程序中加载背景音乐、播放背景音乐即可。将metal_slug.py程序中run_game()方法改为如下形式。
def run_game():
# 初始化游戏
pygame.init()
# 初始化混音器模块
pygame.mixer.init() # ①
# 加载背景音乐
pygame.mixer.music.load('music/background.mp3') # ②
# 创建ViewManager对象
view_manager = ViewManager()
# 设置显示屏幕,返回Surface对象
screen = pygame.display.set_mode((view_manager.screen_width,
view_manager.screen_height))
# 设置标题
pygame.display.set_caption('合金弹头')
# 创建玩家角色
player = Player(view_manager, '孙悟空', MAX_HP)
while(True):
# 处理游戏事件
gf.check_events(screen, view_manager, player)
# 更新游戏屏幕
gf.update_screen(screen, view_manager, mm, player)
# 播放背景音乐
if pygame.mixer.music.get_busy() == False:
pygame.mixer.music.play()
上面程序中①号代码初始化pygame的混音器模块;②号代码调用pygame.mixer.music子模块的load()方法加载背景音乐;最后一行代码则调用pygame.mixer.music子模块的play()方法播放背景音乐。
接下来程序同样使用ViewManager来管理游戏所用的发射、爆炸等各种音效,程序在ViewManager的构造器中增加如下代码。
# 管理图片加载和图片绘制的工具类
class ViewManager:
# 加载所有游戏图片、声音的方法
def __init__ (self):
...
self.Y_JUMP_MAX = self.screen_height * 50 / 100
# 使用list列表管理所有的音效
self.sound_effect = [] #①
# load方法加载指定音频文件,并将被加载的音频添加到list列表中管理
self.sound_effect.append(pygame.mixer.Sound("music/shot.wav"))
self.sound_effect.append(pygame.mixer.Sound("music/bomb.wav"))
self.sound_effect.append(pygame.mixer.Sound("music/oh.wav"))
上面程序中①号代码创建了一个list列表,接下来程序将所有通过Sound加载的音效都保存到该list列表中,以后程序即可通过该list列表来访问这些音效。
接下来为Player发射子弹时添加音效,Player使用add_bullet()方法来发射子弹,因此程序应该在该方法最后添加如下一行即可。
# 发射子弹的方法
def add_bullet(self, view_manager):
...
self.left_shoot_time = MAX_LEFT_SHOOT_TIME
# 播放射击音效
view_manager.sound_effect[0].play() # ①
上面程序中①号代码即可控制Player在发射子弹时播放射击音效。
此外还需要控制怪物死亡时播放对应的音效:当炸弹和飞机爆炸时,应该播放爆炸特效,当枪兵死时,应该播放惨叫特效。因此程序需要修改monster_manager的check_monster()函数(该函数用于检测怪物是否将要死亡),当该函数内的代码检测到怪物将要死亡时,程序增加播放音效的代码。
修改后的check_monster()函数代码如下。
# 检查怪物是否将要死亡的函数
def check_monster(view_manager, player):
# 获取玩家发射的所有子弹
bullet_list = player.bullet_list
# 定义一个del_list列表,用于保存将要死亡的怪物
del_list = []
# 定义一个del_bullet_list列表,用于保存所有将要被删除的子弹
del_bullet_list = []
# 遍历所有怪物
for monster in monster_list.sprites():
# 如果怪物是炸弹
if monster.type == TYPE_BOMB:
# 角色被炸弹炸到
if player.is_hurt(monster.x, monster.end_x,
monster.start_y, monster.end_y):
# 将怪物设置为死亡状态
monster.is_die = True
# 播放爆炸音效
view_manager.sound_effect[1].play() # ①
# 将怪物(爆炸的炸弹)添加到del_list列表中
del_list.append(monster)
# 玩家控制的角色的生命值减10
player.hp = player.hp - 10
continue
# 对于其他类型的怪物,则需要遍历角色发射的所有子弹
# 只要任何一个子弹打中怪物,即可判断怪物即将死亡
for bullet in bullet_list.sprites():
if not bullet.is_effect:
continue
# 如果怪物被角色的子弹打到
if monster.is_hurt(bullet.x, bullet.y):
# 将子弹设为无效
bullet.is_effect = False
# 将怪物设为死亡状态
monster.is_die = True
# 如果怪物是飞机
if monster.type == TYPE_FLY:
# 播放爆炸音效
view_manager.sound_effect[1].play()
# 如果怪物是人
if monster.type == TYPE_MAN:
# 播放惨叫音效
view_manager.sound_effect[2].play()
# 将怪物(被子弹打中的怪物)添加到del_list列表中
del_list.append(monster)
# 将打中怪物的子弹添加到del_bullet_list列表中
del_bullet_list.append(bullet)
# 将del_bullet_list包含的所有子弹从bullet_list中删除
bullet_list.remove(del_bullet_list)
# 检查怪物子弹是否打到角色
monster.check_bullet(player)
# 将已死亡的怪物(保存在del_list列表中)添加到die_monster_list列表中
die_monster_list.add(del_list)
# 将已死亡的怪物(保存在del_list列表中)从monster_list中删除
monster_list.remove(del_list)
上面第①号代码之前,程序将代表炸弹的怪物的is_die设为True,这表明炸弹已死、即将爆炸,因此第①号代码播放了爆炸音效;程序第二段粗体字代码同样放在monster.is_die=True之后,这意味着程序先将代表飞机或枪兵(人)的怪物死亡状态,然后使用粗体字代码播放了对应的音效。
此时再次运行游戏将会听到游戏的背景音乐,当角色发射子弹、怪物被打死时都会产生相应的音效,此时游戏变得逼真多了。
现在游戏还剩一个小小的问题:游戏中玩家控制的角色居然是不死的,即使角色生命值变成了负数,玩家依然可以继续玩这个游戏,程序只是在控制台打印“游戏失败!”字样,这显然不是我们期望的效果,下面将开始解决这个问题。
增加游戏场景
当玩家控制的角色的生命值小于0时,此时应该显示游戏失败,本游戏虽然已经判断了游戏失败,但程序只是在控制台打印“游戏失败!”字样,这显然是不够的,此处考虑增加一个代表游戏失败的场景。
此外,正常游戏开始时,通常会显示游戏登录的场景,而不是直接开始游戏,因此本节将会为游戏增加游戏开始、游戏失败两个场景。
下面先修改game_functions.py程序,在该程序中定义三个代表不同场景的变量。
# 代表登录场景的常量
STAGE_LOGIN = 1
# 代表游戏场景的常量
STAGE_GAME = 2
# 代表失败场景的常量
STAGE_LOSE = 3
接下来该程序需要在check_events()函数中针对不同场景处理不同的事件:对于游戏登录和游戏失败的场景,游戏会在界面上显示按钮,因此程序主要负责处理游戏界面的鼠标点击事件。
在update_screen()函数中,程序则需要根据不同场景来绘制不同的界面。
下面是修改后的game_functions.py程序的代码。
import sys
import pygame
from player import *
# 代表登录场景的常量
STAGE_LOGIN = 1
# 代表游戏场景的常量
STAGE_GAME = 2
# 代表失败场景的常量
STAGE_LOSE = 3
def check_events(screen, view_manager, player):
''' 响应按键和鼠标事件 '''
for event in pygame.event.get():
# 处理游戏退出(只有登录界面和失败界面才可退出)
if event.type == pygame.QUIT and (view_manager.stage == STAGE_LOGIN \
or view_manager.stage == STAGE_LOSE):
sys.exit()
# 处理登录场景下的鼠标按下事件
if event.type == pygame.MOUSEBUTTONDOWN and view_manager.stage == STAGE_LOGIN:
mouse_x, mouse_y = pygame.mouse.get_pos()
if on_button(view_manager, mouse_x, mouse_y):
# 开始游戏
view_manager.stage = STAGE_GAME
# 处理失败场景下的鼠标按下事件
if event.type == pygame.MOUSEBUTTONDOWN and view_manager.stage == STAGE_LOSE:
mouse_x, mouse_y = pygame.mouse.get_pos()
if on_button(view_manager, mouse_x, mouse_y):
# 将角色生命值恢复到最大
player.hp = MAX_HP
# 进入游戏场景
view_manager.stage = STAGE_GAME
# 处理登录场景下的鼠标移动事件
if event.type == pygame.MOUSEMOTION and view_manager.stage == STAGE_LOGIN:
mouse_x, mouse_y = pygame.mouse.get_pos()
if on_button(view_manager, mouse_x, mouse_y):
# 如果鼠标在按钮上方移动,控制按钮绘制高亮图片
view_manager.start_image_index = 1
else:
view_manager.start_image_index = 0
pygame.display.flip()
# 处理游戏场景下按键被按下的事件
if event.type == pygame.KEYDOWN and view_manager.stage == STAGE_GAME:
if event.key == pygame.K_SPACE:
# 当角色的left_shoot_time为0时(上一枪发射结束),角色才能发射下一枪。
if player.left_shoot_time <= 0:
player.add_bullet(view_manager)
# 用户按下向上键,表示跳起来
if event.key == pygame.K_UP:
player.is_jump = True
# 用户按下向右键,表示向右移动
if event.key == pygame.K_RIGHT:
player.move = MOVE_RIGHT
# 用户按下向右键,表示向左移动
if event.key == pygame.K_LEFT:
player.move = MOVE_LEFT
# 处理游戏场景下按键被松开的事件
if event.type == pygame.KEYUP and view_manager.stage == STAGE_GAME:
# 用户松开向右键,表示向右站立
if event.key == pygame.K_RIGHT:
player.move = MOVE_STAND
# 用户松开向左键,表示向左站立
if event.key == pygame.K_LEFT:
player.move = MOVE_STAND
# 判断当前鼠标是否在界面的按钮上
def on_button(view_manager, mouse_x, mouse_y):
return view_manager.button_start_x < mouse_x < \
view_manager.button_start_x + view_manager.again_image.get_width()\
and view_manager.button_start_y < mouse_y < \
view_manager.button_start_y + view_manager.again_image.get_height()
# 处理更新游戏界面的方法
def update_screen(screen, view_manager, mm, player):
# 如果处于游戏登录场景
if view_manager.stage == STAGE_LOGIN:
view_manager.draw_login(screen)
# 如果当前处于游戏场景
elif view_manager.stage == STAGE_GAME:
# 随机生成怪物
mm.generate_monster(view_manager)
# 处理角色的逻辑
player.logic(screen)
# 如果游戏角色已死,判断玩家失败
if player.is_die():
view_manager.stage = STAGE_LOSE
# 检查所有怪物是否将要死亡
mm.check_monster(view_manager, player)
# 绘制游戏
view_manager.draw_game(screen, mm, player)
# 如果当前处于失败场景
elif view_manager.stage == STAGE_LOSE:
view_manager.draw_lose(screen)
# 更新屏幕显示,放在最后一行
pygame.display.flip()
从上面check_events()函数的粗体字代码来看,游戏在处理事件时对游戏场景进行了判断,这表明该程序会针对不同场景使用不同的事件处理。
程序的update_screen()函数同样对当前程序场景进行了判断:不同场景调用ViewManager的不同方法来绘制游戏界面。
-
登录场景:调用draw_login()方法绘制游戏界面。
-
游戏场景:调用draw_game()方法绘制游戏界面。
-
失败场景:调用draw_lose()方法绘制游戏界面。
接下来就需要为ViewManager增加draw_login()方法和draw_lose()方法,使用这两个方法来绘制登录场景和失败场景。
在增加这两个方法之前,程序应该在ViewManager的构造器中将游戏的初始场景设为登录场景(STAGE_LOGIN),还应该将在构造器中加载绘制登录场景和失败场景的图片。ViewManager类中修改后的构造器代码如下。
# 管理图片加载和图片绘制的工具类
class ViewManager:
# 加载所有游戏图片、声音的方法
def __init__ (self):
self.stage = STAGE_LOGIN
...
# 加载开始按钮的两张图片
self.start_bn_images = []
self.start_bn_images.append(pygame.image.load("images/start_n.gif"))
self.start_bn_images.append(pygame.image.load("images/start_s.gif"))
self.start_image_index = 0
# 加载“原地复活”按钮的图片
self.again_image = pygame.image.load("images/again.gif")
# 计算按钮的绘制位置
self.button_start_x = (self.screen_width - self.again_image.get_width()) // 2
self.button_start_y = (self.screen_height - self.again_image.get_height()) // 2
上面程序该处的构造器代码就是该版本程序新增的构造器代码,其中第一行粗体字代码增加了一个self.start_image_index变量,该变量用于控制开始按钮显示哪张图片(为了给开始按钮增加高亮效果,本程序为开始按钮准备了两张图片);程序中最后两行粗体字代码还计算了按钮的开始坐标,这个坐标将保证把按钮绘制在屏幕中间。
接下来为ViewManager类增加如下两个方法,分别用于绘制登录场景和失败场景。
# 绘制游戏登录界面的方法
def draw_login(self, screen):
screen.blit(self.map, (0, 0))
screen.blit(self.start_bn_images[self.start_image_index],
(self.button_start_x, self.button_start_y))
# 绘制游戏失败界面的方法
def draw_lose(self, screen):
screen.blit(self.map_back, (0, 0))
screen.blit(self.again_image, (self.button_start_x, self.button_start_y))
从上面代码可以看出,程序开始时游戏处于登录场景;当玩家单击登录场景上的“开始”按钮时,程序进入游戏场景;当玩家控制的角色的生命值小于0时,程序会进入游戏失败的场景。
再次运行metal_slug程序,将会看到程序启动时自动进入登录场景,如图1所示。
图1 游戏登录场景
当玩家控制的角色死亡之后,游戏将会自动进入如图2所示的游戏失败场景。
图2 游戏失败场景
在图2所示界面,如果玩家单击“原地复活”按钮,游戏会将角色生命值恢复成最大值,并再次进入游戏场景,玩家将可以继续游戏。
本文结束
另外本人还开设了个人公众号:JiandaoStudio ,会在公众号内定期发布行业信息,以及各类免费代码、书籍、大师课程资源。
扫码关注本人微信公众号,有惊喜奥!公众号每天定时发送精致文章!回复关键词可获得海量各类编程开发学习资料!
例如:想获得Python入门至精通学习资料,请回复关键词Python即可。