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

导读

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

请先阅读上篇:使用pygame开发游戏:合金弹头(3)

游戏的角色类(也就是受玩家控制的那个人)和怪物类其实差不多,它们具有很多相似的地方,因此它们在类实现上有很多相似之处。不过由于角色需要受玩家控制,它的动作比较多,因此程序需要额外为角色定义一个成员变量,用于记录该角色正在执行的动作,并且需要将角色的头部、腿部分开进行处理。


                                                        开发“角色”类

本游戏将会采用迭代方式进行开发,因此本节打算开发metal_slug_v2版,该版本的游戏需要实现角色类,因此程序使用player.py文件定义Player类。下面是Player类的构造器。

import pygame
import sys
from random import randint
from pygame.sprite import Sprite
from pygame.sprite import Group
import pygame.font
from bullet import *
import monster_manager as mm
# 定义角色的最高生命值
MAX_HP = 50
# 定义控制角色动作的常量
# 此处只控制该角色包含站立、跑、跳等动作
ACTION_STAND_RIGHT = 1
ACTION_STAND_LEFT = 2
ACTION_RUN_RIGHT = 3
ACTION_RUN_LEFT = 4
ACTION_JUMP_RIGHT = 5
ACTION_JUMP_LEFT = 6
# 定义角色向右移动的常量
DIR_RIGHT = 1
# 定义角色向左移动的常量
DIR_LEFT = 2
# 定义控制角色移动的常量
# 此处控制该角色只包含站立、向右移动、向左移动三种移动方式
MOVE_STAND = 0
MOVE_RIGHT = 1
MOVE_LEFT = 2
MAX_LEFT_SHOOT_TIME = 6
class Player(Sprite):
    def __init__(self, view_manager, name, hp):
        super().__init__()
        self.name = name # 保存角色名字的成员变量
        self.hp = hp # 保存角色生命值的成员变量
        self.view_manager = view_manager
        # 保存角色所使用枪的类型(以后可考虑让角色能更换不同的枪)
        self.gun = 0
        # 保存角色当前动作的成员变量(默认向右站立)
        self.action = ACTION_STAND_RIGHT
        # 代表角色X坐标的属性
        self._x = -1
        # 代表角色Y坐标的属性
        self.y = -1
        # 保存角色射出的所有子弹
        self.bullet_list = Group()
        # 保存角色移动方式的成员变量
        self.move = MOVE_STAND
        # 控制射击状态的保留计数器
        # 每当用户发射一枪时,left_shoot_time会被设为MAX_LEFT_SHOOT_TIME,然后递减
        # 只有当left_shoot_time变为0时,用户才能发射下一枪
        self.left_shoot_time = 0
        # 保存角色是否跳动的属性
        self._is_jump = False
        # 保存角色是否跳到最高处的成员变量
        self.is_jump_max = False
        # 控制跳到最高处的停留时间
        self.jump_stop_count = 0
        # 当前正在绘制角色脚部动画的第几帧
        self.index_leg = 0
        # 当前正在绘制角色头部动画的第几帧
        self.index_head = 0
        # 当前绘制头部图片的X坐标
        self.current_head_draw_x = 0
        # 当前绘制头部图片的Y坐标
        self.current_head_draw_y = 0
        # 当前正在画的脚部动画帧的图片
        self.current_leg_bitmap = None
        # 当前正在画的头部动画帧的图片
        self.current_head_bitmap = None
        # 该变量控制用于控制动画刷新的速度
        self.draw_count = 0
        # 加载中文字体
        self.font = pygame.font.Font('images/msyh.ttf', 20)
    ...

上面构造器中定义的大量的成员变量正是角色类与怪物类的差别所在,由于角色有名字、生命值(hp)、动作、移动方式这些特殊的状态,因此程序为角色定义了name、hp、action、move这些成员变量。

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

 

上面程序还为Player类定义了一个self.left_shoot_time变量,该变量的作用有两个。

  • 当角色的self.left_shoot_time不为0时,表明角色当前正处于射击状态,因此此时角色的头部动画必须使用射击的动画帧。

  • 当角色的self.left_shoot_time不为0时,表明角色当前正处于射击状态,因此角色不能立即发射下一枪——必须等到self.left_shoot_time为0时,角色才能发射下一枪。这意味着:即使用户按下 “射击”按钮,也必须等到角色上一枪发射完成才会发射下一枪。

上面程序中的最后6行粗体字代码是绘制角色位图相关的成员变量,从这些成员变量可以看出,程序把角色按头部、腿部分开处理,因此程序需要为头部、腿部分开定义相应的成员变量。

为了计算角色的方向(程序需要根据角色的方向来绘制角色),程序为Player类提供了如下方法。

# 计算该角色当前方向:action成员变量为奇数代表向右
    def get_dir(self):
        return DIR_RIGHT if self.action % 2 == 1 else DIR_LEFT

从上面代码可以看出,程序可根据角色的self.action来计算角色的方向,只要self.action变量的值为奇数,即可判断该角色的方向为向右。

由于程序对Player的self._x变量赋值时需要进行逻辑控制,因此程序应该提供setter方法来控制对self._x的赋值、getter方法来访问self._x的值,并使用property为self._x定义x属性。也就是在Player类中增加如下代码。
 

 def get_x(self):
        return self._x
    def set_x(self, x_val):
            self._x = x_val % (self.view_manager.map.get_width() +
                self.view_manager.X_DEFAULT)
            # 如果角色移动到屏幕最左边
            if self._x < self.view_manager.X_DEFAULT:
                self._x = self.view_manager.X_DEFAULT
    x = property(get_x, set_x)

Player的self._is_jump在赋值时也需要进行额外的控制,因此程序也需要按以上方式为self._is_jump定义is_jump属性。在Player类中增加如下代码。

 def get_is_jump(self):
        return self._is_jump
    def set_is_jump(self, jump_val):
        self._is_jump = jump_val
        self.jump_stop_count = 6
    is_jump = property(get_is_jump, set_is_jump)

在介绍Monster类时已经提出,为了更好地在屏幕上绘制Monster对象以及所有子弹,程序需要根据角色在游戏界面上的位移来进行偏移,因此程序需要为Player方法来计算角色在游戏界面上的位移。下面是Player类中计算位移的方法。

 # 返回该角色在游戏界面上的位移
    def shift(self):
        if self.x <= 0 or self.y <= 0:
            self.init_position()
        return self.view_manager.X_DEFAULT - self.x

从上面代码可以看出,程序计算角色位移的方法很简单,只要用角色的初始X坐标减去角色当前X坐标即可。

游戏绘制角色、绘制角色动画的方法,与绘制怪物、绘制怪物动画的方法基本相似,只是程序需要分开绘制角色头部、腿部,读者可参考光盘代码来理解绘制角色、绘制角色动画的方法。

为了在游戏界面左上角绘制角色的名字、头像、生命值,Player类提供了如下方法。

# 绘制左上角的角色、名字、生命值的方法
    def draw_head(self, screen):
        if self.view_manager.head == None:
            return
        # 对图片执行镜像(第二个参数控制水平镜像,第三个参数控制垂直镜像)
        head_mirror = pygame.transform.flip(self.view_manager.head, True, False)
        # 画头像
        screen.blit(head_mirror, (0, 0))
        # 将名字渲染成图像
        name_image = self.font.render(self.name, True, (230, 23, 23))
        # 画名字
        screen.blit(name_image, (self.view_manager.head.get_width(), 10))
        # 将生命值渲染成图像
        hp_image = self.font.render("HP:" + str(self.hp), True, (230, 23, 23))
        # 画生命值
        screen.blit(hp_image, (self.view_manager.head.get_width(), 30))

上面方法的实现非常简单,第一行粗体字用于将头像位图进行水平镜像,接下来程序会将变换后的位图绘制在程序界面上。第二行粗体字代码将角色名字渲染成图像,接下来程序即可将该图片绘制在程序界面上;第二行粗体字代码将角色生命值渲染成图像,接下来程序即可将该图片绘制在程序界面上。

角色是否被子弹打中的方法与怪物是否被子弹打中的方法基本相似:只要判断子弹出现在角色图片覆盖的区域中,即可判断子弹打中了角色。

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

 

与怪物类相似的是,Player类同样也需要提供绘制子弹的方法,该方法负责绘制该角色发射的所有子弹,而且在绘制子弹之前,应该先判断子弹是否已越过屏幕边界,如果子弹越过屏幕边界,就应该将其清除。由于绘制子弹的方法与Monster类中绘制子弹的方法大致相似,此处不再赘述。

由于角色发射子弹是受玩家单击按钮控制的,但本游戏的设定是角色发射子弹之后,必须等待一定时间才能发射下一发子弹,因此程序为Player定义了一个self.left_shoot_time计数器,只要该计数器不等于0,角色就处于发射子弹的状态,角色不能发射下一发子弹。

下面是发射子弹的方法代码。
 

 # 发射子弹的方法
    def add_bullet(self, view_manager):
        # 计算子弹的初始X坐标
        bullet_x = self.view_manager.X_DEFAULT + 50 if self.get_dir() \
            == DIR_RIGHT else self.view_manager.X_DEFAULT - 50
        # 创建子弹对象
        bullet = Bullet(BULLET_TYPE_1, bullet_x, self.y - 60, self.get_dir())
        # 将子弹添加到用户发射的子弹Group中
        self.bullet_list.add(bullet)
        # 发射子弹时,将self.left_shoot_time设置为射击状态最大值
        self.left_shoot_time = MAX_LEFT_SHOOT_TIME
    # 画子弹
    def draw_bullet(self, screen):
        delete_list = []
        # 遍历角色发射的所有子弹
        for bullet in self.bullet_list.sprites():
            # 将所有越界的子弹收集到delete_list列表中
            if bullet.x < 0 or bullet.x > self.view_manager.screen_width:
                delete_list.append(bullet)
        # 清除所有越界的子弹
        self.bullet_list.remove(delete_list)
        # 遍历用户发射的所有子弹
        for bullet in self.bullet_list.sprites():
            # 获取子弹对应的位图
            bitmap = bullet.bitmap(self.view_manager)
            # 子弹移动
            bullet.move()
            # 画子弹,根据子弹方向判断是否需要翻转图片
            if bullet.dir == DIR_LEFT:
                # 对图片执行镜像(第二个参数控制水平镜像,第三个参数控制垂直镜像)
                bitmap_mirror = pygame.transform.flip(bitmap, True, False)
                screen.blit(bitmap_mirror, (bullet.x, bullet.y))
            else:
                screen.blit(bitmap, (bullet.x, bullet.y))

正如从上面粗体字代码所看到的,程序每次发射子弹时都会将self.left_shoot_time设为最大值,而self.left_shoot_time会随着动画帧的绘制不断地自减,只有当self.left_shoot_time为0时才可判断角色已结束射击状态。这样后面程序控制角色发射子弹时,也需要先判断self.left_shoot_time的值:只有当self.left_shoot_time的值小于、等于0时(角色不处于发射状态),角色才可以发射子弹。

由于玩家还可以控制界面上的角色移动、跳动,因此程序还需要实现角色移动、角色移动与跳跃之间的关系。程序为Player提供了如下两个方法。

   # 处理角色移动的方法
    def move_position(self, screen):
        if self.move == MOVE_RIGHT:
            # 更新怪物的位置
            mm.update_posistion(screen, self.view_manager, self, 6)
            # 更新角色位置
            self.x += 6
            if not self.is_jump:
                # 不跳的时候,需要设置动作
                self.action = ACTION_RUN_RIGHT
        elif self.move == MOVE_LEFT:
            if self.x - 6 < self.view_manager.X_DEFAULT:
                # 更新怪物的位置
                mm.update_posistion(screen, self.view_manager, self, \
                    -(self.x - self.view_manager.X_DEFAULT))
            else:
                # 更新怪物的位置
                mm.update_posistion(screen, self.view_manager, self, -6)
            # 更新角色位置
            self.x -= 6
            if not self.is_jump:
                # 不跳的时候,需要设置动作
                self.action = ACTION_RUN_LEFT
        elif self.action != ACTION_JUMP_RIGHT and self.action != ACTION_JUMP_LEFT:
            if not self.is_jump:
                # 不跳的时候,需要设置动作
                self.action = ACTION_STAND_RIGHT
​
    # 处理角色移动与跳的逻辑关系
    def logic(self, screen):
        if not self.is_jump:
            self.move_position(screen)
            return
        # 如果还没有跳到最高点
        if not self.is_jump_max:
            self.action = ACTION_JUMP_RIGHT if self.get_dir() == \
                DIR_RIGHT else ACTION_JUMP_LEFT
            # 更新Y坐标
            self.y -= 8
            # 设置子弹在Y方向上具有向上的加速度
            self.set_bullet_y_accelate(-2)
            # 已经达到最高点
            if self.y <= self.view_manager.Y_JUMP_MAX:
                self.is_jump_max = True
        else:
            self.jump_stop_count -= 1
            # 如果在最高点停留次数已经使用完
            if self.jump_stop_count <= 0:
                # 更新Y坐标
                self.y += 8
                # 设置子弹在Y方向上具有向下的加速度
                self.set_bullet_y_accelate(2)
                # 已经掉落到最低点
                if self.y >= self.view_manager.Y_DEFALUT:
                    # 恢复Y坐标
                    self.y = self.view_manager.Y_DEFALUT
                    self.is_jump = False
                    self.is_jump_max = False
                    self.action = ACTION_STAND_RIGHT
                else:
                    # 未掉落到最低点,继续使用跳的动作
                    self.action = ACTION_JUMP_RIGHT if self.get_dir() == \
                    DIR_RIGHT else ACTION_JUMP_LEFT
        # 控制角色移动
        self.move_position(screen)

Player类同样提供了draw()和draw_anim()方法,分别用于绘制角色和绘制角色的动画帧,由于这两个方法与Monster对应的方法大致相似,故此处不再给出介绍。Player类中还包含以下简单方法:

  • is_die(self):判断角色是否死亡。

  • init_position(self):初始化角色初始坐标的方法。

  • update_bullet_shift(self, shift):更新角色所发射子弹位置的方法。

  • set_bullet_y_accelate(self, accelate):计算角色所发射子弹的垂直方向加速度的方法。

上面4个方法的代码比较简单,读者可参考Player.py源程序。


                                                  将“角色”添加进来

为了将角色添加进来,程序先为Monster类增加一个方法,该方法用于判断怪物的子弹是否打中角色,如果打中角色,则删除该子弹。下面是该方法的代码。
 

# 判断子弹是否与玩家控制的角色碰撞(判断子弹是否打中角色)
    def check_bullet(self, player):
        # 遍历所有子弹
        for bullet in self.bullet_list.copy():
            if bullet == None or not bullet.is_effect:
                continue
            # 如果玩家控制的角色被子弹打到
            if player.is_hurt(bullet.x, bullet.x, bullet.y, bullet.y):
                # 子弹设为无效
                bullet.isEffect = False
                # 将玩家的生命值减5
                player.hp = player.hp - 5
                # 删除已经击中玩家控制的角色的子弹
                self.bullet_list.remove(bullet)

接下来需要为monster_manager程序中update_posistion(screen, view_manager, player, shift)函数的结尾处增加一行代码(需要为原方法增加一个player形参),这行代码用于更新玩家的子弹的位置。此外,还需要为monster_manager程序额外增加一个检查怪物是否将要死亡的函数:check_monster(),该函数可用于检查界面上的怪物是否将要死亡,将要死亡的怪物将会从monster_list中删除,并添加到die_monster_list中,然后程序将会负责绘制它们的死亡动画。

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

 

下面为monster_manager程序的update_posistion(screen, view_manager, player, shift)函数增加一行代码,并增加一个check_monster()函数。

# 更新怪物与子弹的坐标的函数
def update_posistion(screen, view_manager, player, shift):
  ...
    # 更新玩家控制的角色的子弹坐标
    player.update_bullet_shift(shift)
​
# 检查怪物是否将要死亡的函数
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
                # 将怪物(爆炸的炸弹)添加到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
                # 将怪物(被子弹打中的怪物)添加到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)

上面程序中第一行粗体字代码就是为update_posistion()函数额外增加的一行。

程序中check_monster()函数的判断逻辑非常简单,程序把怪物分为两类进行处理。

  • 如果怪物是地上的炸弹,只要炸弹炸到角色,炸弹也就即将死亡。上面程序中第二行粗体字代码处理了怪物是炸弹的情形。

  • 对于其他类型的怪物,程序则需要遍历角色发射的子弹,只要任意一颗子弹打中了怪物,即可判断怪物即将死亡。上面程序中第三行粗体字代码正是遍历玩家所发射的子弹的循环代码。

为了将角色添加到游戏中,程序需要在metal_slug主程序中创建Player对象,并将Player对象传给check_events()、update_screen()函数。修改后的metal_slug程序中run_game()函数的代码。
 

def run_game():
    # 初始化游戏
    pygame.init()
    # 创建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)

此时需要修改game_function程序中两个函数,其中check_events()函数要处理更多的按键事件:游戏要根据用户按键来激发相应的处理;update_screen()函数则需要增加对Player对象的处理、并在界面上绘制Player对象。下面是修改后的game_functions程序的代码。

import sys
import pygame
from player import *

def check_events(screen, view_manager, player):
    ''' 响应按键和鼠标事件 '''
    for event in pygame.event.get():
        # 处理游戏退出
        if event.type == pygame.QUIT:
            sys.exit()
        # 处理按键被按下的事件
        if event.type == pygame.KEYDOWN:
            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:
            # 用户松开向右键,表示向右站立
            if event.key == pygame.K_RIGHT:
                player.move = MOVE_STAND
            # 用户松开向左键,表示向左站立
            if event.key == pygame.K_LEFT:
                player.move = MOVE_STAND
​
# 处理更新游戏界面的方法
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)  
    # 更新屏幕显示,放在最后一行
    pygame.display.flip()

上面程序中check_events()函数增加了大量事件处理代码,用于处理用户的按键事件,这样用户即可通过按键来控制游戏角色跑动、跳动、发射子弹;update_screen()方法中的粗体字代码就是新增的代码,这些代码用于处理Player对象,判断Player对象是否已经死亡、绘制Player对象。

再次运行metal_slug程序,此时将可在界面上看到用户控制的游戏角色,用户可通过箭头键控制角色跑动、跳动,可通过空格键控制角色射击。此时可看到如图1所示的游戏界面。

 

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

                                                                                 图1  加入角色后的效果

此时游戏中的角色可以接受用户控制,游戏角色可以跳动、发射子弹、子弹也能打死怪物,怪物的子弹也能击中角色,但在“跑动”的效果很差:看上去好像只是怪物在移动,角色并没有动,这是下一步将要解决的问题。

                                                                                         未完待续


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

 

                                            

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

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

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