专栏首页疯狂软件李刚使用pygame开发合金弹头(5)

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

导读

Python的强大超出你的认知,Python的功能不止于可以做网络爬虫,数据分析,Python完全可以进行后端开发,AI,Python也可进行游戏开发,本文将会详细介绍Python使用pygame模块来开发一个名为“合金弹头”的游戏

合理绘制地图

前面开发已经完成了游戏中主要要求:各种怪物和角色,只是角色跑动的效果较差,这其实只是一个视觉效果:由于游戏的背景地图总是静止的,因此玩家会感觉角色似乎并未跑动。

为了让角色的跑动效果更加真实,游戏需要根据玩家跑动的位移来改变背景地图,当游戏的背景地图动起来之后,玩家控制的角色就似乎在地图上“跑”起来了。

为了集中处理游戏的界面绘制,程序在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所示界面,如果玩家单击“原地复活”按钮,游戏会将角色生命值恢复成最大值,并再次进入游戏场景,玩家将可以继续游戏。

本文结束

本文选自李刚老师的《疯狂Python讲义》,欢迎阅读原书

本文分享自微信公众号 - 疯狂软件李刚(fkbooks),作者:疯狂软件李刚

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-05-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用pygame开发合金弹头(4)

    Python的强大超出你的认知,Python的功能不止于可以做网络爬虫,数据分析,Python完全可以进行后端开发,AI,Python也可进行游戏开发,本文将会...

    疯狂软件李刚
  • Python的排列组合函数

    上面程序用到了一个字符串的join()方法,该方法用于将元组的所有元素都连接成一个字符串。

    疯狂软件李刚
  • Java泛型的协变与逆变

    泛型是Java最基础的语法之一,众所周知:出于安全原因,泛型默认不能支持型变(否则会引入危险),因此Java提供了通配符上限和通配符下限来支持型变,其中通配符上...

    疯狂软件李刚
  • 微信小程序视图层之wxss

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    菲宇
  • 微信小程序前端样式WXSS书写

    WXSS(WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式。

    小小咸鱼YwY
  • 前端-彻底学会CSS布局-这是最全的

    其实,我们可能经常在聊组件化,咋地咋地。但是,回过头来思考一下,如果你看到一张设计稿的时候,连布局都不清不楚,谈何组件化呢。所以,我们需要在分清楚组件之前,先来...

    grain先森
  • 面试官问我Linux下常见网络命令

    今天继续来讲面试,已经出了将近十个美团java一面真题系列文章了,今天来讲一讲Linux命令相关内容,在全球超级计算机TOP500强操作系统排行榜中,Linux...

    用户7656790
  • 《2018腾讯移动游戏技术评审标准与实践案例》开放下载

    ? 进入2018年,中国移动游戏市场增速放缓,竞争愈发激烈;同时现象级手游层出不穷,手游精品化趋势更加明显,对移动游戏研发质量也提出了更高的要求。 在此背景下...

    WeTest质量开放平台团队
  • 《2018腾讯移动游戏技术评审标准与实践案例》开放下载

    原文链接:https://wetest.qq.com/lab/view/418.html

    WeTest质量开放平台团队
  • 币聪科技:游戏行业或许是通证经济发展的最快工具之一?

    目前有数以千计的加密货币和区块链项目竞相在各自的行业中设计产品,这些产品将具有实际用例和实现大规模采用的潜力。游戏能否成为这些重要技术融入社会和日常生活的手段?

    币聪财经

扫码关注云+社区

领取腾讯云代金券