使用pygame开发游戏:合金弹头(5)

导读

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()方法来绘制背景位图——这是因为当角色在地图上不断地向右移动时,随着地图不断地向左拖动,地图就会不能完全覆盖屏幕右边,此时需要再绘制一张背景位图,这样才可以拼成完成的地图——这样就形成了无限循环的游戏地图。

                                                                                 使用pygame开发游戏:合金弹头(5)_第1张图片

 

由于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()方法播放背景音乐。

使用pygame开发游戏:合金弹头(5)_第2张图片

 

接下来程序同样使用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所示。

使用pygame开发游戏:合金弹头(5)_第3张图片

                                                                                    图1  游戏登录场景

当玩家控制的角色死亡之后,游戏将会自动进入如图2所示的游戏失败场景。

使用pygame开发游戏:合金弹头(5)_第4张图片

                                                                                     图2  游戏失败场景

在图2所示界面,如果玩家单击“原地复活”按钮,游戏会将角色生命值恢复成最大值,并再次进入游戏场景,玩家将可以继续游戏。

 


                                                                                       本文结束


 

另外本人还开设了个人公众号:JiandaoStudio ,会在公众号内定期发布行业信息,以及各类免费代码、书籍、大师课程资源。

 

                                            

扫码关注本人微信公众号,有惊喜奥!公众号每天定时发送精致文章!回复关键词可获得海量各类编程开发学习资料!

例如:想获得Python入门至精通学习资料,请回复关键词Python即可。

 

你可能感兴趣的:(Python实践项目)