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

导读

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

在开发游戏之前,首先需要从程序员的角度来分析游戏界面,并逐步实现游戏界面上的各种组件。

游戏界面分析

看图1的游戏界面:

                                                                         图1  合金弹头

对于图1所示的游戏界面,从普通玩家的角度来看,游戏界面上有受玩家控制移动、跳跃、发射子弹的角色,还有不断发射子弹的敌人,地上有炸弹,天空中有正在爆炸的飞机……乍看上去给人一种眼花缭乱的感觉。

                                                           

如果从程序员的角度来看,游戏界面大致可包含如下组件。

  • 游戏背景:只是一张静止图片。

  • 角色:可以站立、走动、跳跃、射击。

  • 怪物:代表游戏界面上所有的敌人,包括拿枪的敌人、地上的炸弹、天空中的飞机……虽然这些怪物的图片不同、发射的子弹不同,攻击力也可能不同,但这些只是实例与实例之间的差异,因此程序只要为怪物定义一个类即可。

                                                          

  • 子弹:不管是角色发射的子弹还是怪物发射的子弹,都可归纳为子弹类。虽然不同子弹的图片不同,攻击力不同,但这些只是实例与实例之间的差异,因此程序只要为子弹定义一个类即可。

从上面的介绍不难看出,开发这款游戏,主要就是实现上面的角色、怪物和子弹3个类。

实现怪物类

由于不同怪物之间会存在如下差异,因此需要为怪物类定义相应的实例变量来记录这些差异。

  • 怪物的类型。

  • 代表怪物位置的X、Y坐标。

  • 标识怪物是否已经死亡的旗标。

  • 绘制怪物图片左上角的X、Y坐标。

  • 绘制怪物图片右下角的X、Y坐标。

  • 怪物发射的所有子弹(有的怪物不会发射子弹)。

  • 怪物未死亡时所有的动画帧图片和怪物死亡时所有的动画帧图片

提示

本程序并未把怪物的所有动画帧图片直接保存在怪物实例中。本程序将会专门使用一个工具类来保存所有角色、怪物的所有动画帧图片。

为了让游戏界面上的角色、怪物都能“动起来”,程序的实现思路是这样的:通过pygame的定时器控制角色、怪物不断地更换新的动画帧图片——因此,程序需要为怪物增加一个成员变量来记录当前游戏界面正在绘制怪物动画的第几帧,而pygame只要不断地调用怪物的绘制方法即可——实际上,该绘制方法每次只绘制一张静态图片(这张静态图片是怪物动画的其中一帧)。

下面是怪物类的构造器代码,该构造器代码负责初始化怪物类的成员变量。

import pygame
import sys
from random import randint
from pygame.sprite import Sprite
from pygame.sprite import Group
from bullet import *
# 控制怪物动画的速度
COMMON_SPEED_THRESHOLD = 10
MAN_SPEED_THRESHOLD = 8
# 定义代表怪物类型的常量(如果程序需要增加更多的怪物,则只需在此处添加常量即可)
TYPE_BOMB = 1
TYPE_FLY = 2
TYPE_MAN = 3​
class Monster(Sprite):
    def __init__ (self, view_manager, tp=TYPE_BOMB):
        super().__init__()
        # 定义怪物的类型
        self.type = tp
        # 定义怪物的X、Y坐标的属性
        self.x = 0
        self.y = 0
        # 定义怪物是否已经死亡的旗标
        self.is_die = False
        # 绘制怪物图片左上角的X坐标
        self.start_x = 0
        # 绘制怪物图片左上角的Y坐标
        self.start_y = 0
        # 绘制怪物图片右下角的X坐标
        self.end_x = 0
        # 绘制怪物图片右下角的Y坐标
        self.end_y = 0
        # 该变量用于控制动画刷新的速度
        self.draw_count = 0
        # 定义当前正在绘制怪物动画的第几帧的变量
        self.draw_index = 0
        # 用于记录死亡的动画只绘制一次,不需要重复绘制
        # 每当怪物死亡时,该变量都会被初始化为等于死亡动画的总帧数
        # 当怪物的死亡动画帧播放完成后,该变量的值变为0
        self.die_max_draw_count = sys.maxsize
        # 定义怪物发射的子弹
        self.bullet_list = Group()
        # -------下面代码根据怪物类型来初始化怪物的X、Y坐标------
        # 如果怪物是炸弹(TYPE_BOMB)或敌人(TYPE_MAN)
        # 怪物的Y坐标与玩家控制的角色的Y坐标相同
        if self.type == TYPE_BOMB or self.type == TYPE_MAN:
            self.y = view_manager.Y_DEFALUT
        # 如果怪物是飞机,则根据屏幕高度随机生成怪物的Y坐标
        elif self.type == TYPE_FLY:
            self.y = view_manager.screen_height * 50 / 100 - randint(0, 99)
        # 随机计算怪物的X坐标。
        self.x = view_manager.screen_width + randint(0, 
            view_manager.screen_width >> 1) - (view_manager.screen_width >> 2)
    ...

上面的成员变量即可记录该怪物实例的各种状态。实际上,如果以后程序要升级,比如为怪物增加更多的特征,如怪物可以拿不同的武器,怪物可以穿不同的衣服,怪物可以具有不同的攻击力……则都可考虑将这些定义成怪物的成员变量。

从上面的代码可以看到,怪物类的构造器可传入一个tp参数,该参数用于告诉系统,该怪物是哪种类型。当前程序支持定义3种怪物,这3种怪物由代码中的3个常量来代表。

  • TYPE_BOMB:代表炸弹的怪物。

  • TYPE_FLY代表飞机的怪物。

  • TYPE_MAN代表人的怪物。

从最后面几行计算怪物X、Y坐标的代码可以看出,程序在创建怪物实例时,不仅负责初始化怪物的type成员变量,而且还会根据怪物类型来设置怪物的X、Y坐标。

  • 如果怪物是炸弹和拿枪的敌人(都在地面上),那么它们的Y坐标与角色默认的Y坐标(在地面上)相同。如果怪物是飞机,那么怪物的Y坐标是随机计算的。

  • 不管是什么怪物,它的X坐标都是随机计算的。

                                   

前面介绍了绘制怪物动画的思路:程序将由pygame控制不断地绘制怪物动画的下一帧,但实际上每次绘制的只是怪物动画的某一帧。下面是绘制怪物的方法。

   # 绘制怪物的方法
    def draw(self, screen, view_manager):
        # 如果怪物是炸弹,则绘制炸弹
        if self.type == TYPE_BOMB:
            # 死亡的怪物使用死亡的图片,活着的怪物使用活着的图片
            self.draw_anim(screen, view_manager, view_manager.bomb2_images 
                if self.is_die else view_manager.bomb_images)
        # 如果怪物是飞机,则绘制飞机
        elif self.type == TYPE_FLY:
            self.draw_anim(screen, view_manager, view_manager.fly_die_images 
                if self.is_die else view_manager.fly_images)
        # 如果怪物是人,则绘制人
        elif self.type == TYPE_MAN:
            self.draw_anim(screen, view_manager, view_manager.man_die_images 
                if self.is_die else view_manager.man_images)
        else:
            pass
    # 根据怪物的动画帧图片来绘制怪物动画
    def draw_anim(self, screen, view_manager, bitmap_arr):
        # 如果怪物已经死亡,且没有播放过死亡动画
        #(self.die_max_draw_count等于初始值,表明未播放过死亡动画)
        if self.is_die and self.die_max_draw_count == sys.maxsize:
            # 将die_max_draw_count设置为与死亡动画的总帧数相等
            self.die_max_draw_count = len(bitmap_arr)    # ⑤
        self.draw_index %= len(bitmap_arr)
        # 获取当前绘制的动画帧对应的位图
        bitmap = bitmap_arr[self.draw_index]  # ①
        if bitmap == None:
            return
        draw_x = self.x
        # 对绘制怪物动画帧位图的X坐标进行微调
        if self.is_die:
            if type == TYPE_BOMB:
                draw_x = self.x - 50
            elif type == TYPE_MAN:
                draw_x = self.x + 50
        # 对绘制怪物动画帧位图的Y坐标进行微调
        draw_y = self.y - bitmap.get_height()
        # 绘制怪物动画帧的位图
        screen.blit(bitmap, (draw_x, draw_y))
        self.start_x = draw_x
        self.start_y = draw_y
        self.end_x = self.start_x + bitmap.get_width()
        self.end_y = self.start_y + bitmap.get_height()
        self.draw_count += 1
        # 控制人、飞机发射子弹的速度
        if self.draw_count >= (COMMON_SPEED_THRESHOLD if type == TYPE_MAN
                else MAN_SPEED_THRESHOLD):  # ③
            # 如果怪物是人,则只在第3帧才发射子弹
            if self.type == TYPE_MAN and self.draw_index == 2:
                self.add_bullet()
            # 如果怪物是飞机,则只在最后一帧才发射子弹
            if self.type == TYPE_FLY and self.draw_index == len(bitmap_arr) - 1:
                self.add_bullet()
            self.draw_index += 1   # ②
            self.draw_count = 0    # ④
        # 每播放死亡动画的一帧,self.die_max_draw_count就减1
        # 当self.die_max_draw_count等于0时,表明死亡动画播放完成
        if self.is_die:
            self.die_max_draw_count -= 1   # ⑥
        # 绘制子弹
        self.draw_bullets(screen, view_manager)

上面代码包含两个方法,draw(self, screen, view_manager)方法只是简单地对怪物类型进行判断,并针对不同类型的怪物使用不同的怪物动画。

draw(self, screen, view_manager)方法总是调用draw_anim(self, screen, view_manager, bitmap_arr)方法来绘制怪物,在调用时会根据怪物类型、怪物是否死亡传入不同的图片数组——每个图片数组就代表一组动画帧的所有图片。

draw_anim()方法中的①号代码根据self.draw_index来获取当前帧对应的图片,而程序执行draw_anim()方法时,②号代码可以控制self.draw_index加1,这样即可保证下次调用draw_anim()方法时就会绘制动画的下一帧。

draw_anim()方法还涉及一个self.draw_count变量,这个变量是控制动画刷新速度的计数器——程序在③号代码处进行了控制:只有当self.draw_count的值大于10(对于其他类型的怪物,该值为8)时才会调用self.draw_index += 1,这意味着当怪物类型是TYPE_MAN时,draw_anim()方法至少被调用8次才会将self.draw_index加1(即绘制下一帧位图);

当怪物是其他类型时,draw_anim()方法至少被调用6次才会将self.draw_index加1(即绘制下一帧位图)——这是因为使用pygame控制动画刷新的频率是固定的,如果不加任何控制,游戏界面上的所有怪物“动”的速度就会是一样的,而且都动得非常快。

为了解决这个问题,程序就需要使用self.draw_count来控制不同怪物pygame每刷新几次才更新一次动画帧。

对于上面的代码来说,如果怪物类型是TYPE_MAN,则只有当self.draw_count的值大于10时才会更新一次动画帧,这意味着只有当pygame每刷新10次时才会更新一次动画帧;如果是其他类型的怪物,那么只有当self.draw_count的值大于6时才会更新一次动画帧,这意味着只有当pygame每刷新6次时才会更新一次动画帧。

提示

如果游戏中还有更多类型的怪物,且这些怪物的动画帧具有不同的更新速度,那么程序还需要进行更细致的判断

draw_anim()方法还涉及一个self.die_max_draw_count变量,这个变量用于控制怪物的死亡动画只会被绘制一次——在怪物临死之前,程序都必须播放怪物的死亡动画,该动画播放完成后,就应该从地图上删除该怪物。

当怪物已经死亡(is_die为真)且还未绘制死亡动画的任何帧时(self.die_max_draw_count等于初始值),程序在⑤号代码处将self.die_max_draw_count设置为与死亡动画的总帧数相等,程序每次调用draw_anim()方法时,⑥号代码都会把self.die_max_draw_count减1。

当self.die_max_draw_count变为0时,表明该怪物的死亡动画的所有帧都绘制完成,接下来程序即可将该怪物从地图上删除了——在后面的monster_manager.py中将会看到,当self.die_max_draw_count为0时,程序将怪物从地图上删除的代码。

Monster还包含了start_x、start_y、end_x、end_y四个变量,这些变量就代表怪物当前帧所覆盖的矩形区域。因此,如果程序需要判断该怪物是否被子弹打中,那么只要子弹出现在该矩形区域内,即可判断出怪物被子弹打中。

下面是判断怪物是否被子弹打中的方法。

 # 判断怪物是否被子弹打中的方法
    def is_hurt(self, x, y): 
        return self.start_x < x < self.end_x and self.start_y < y < self.end_y

                         

接下来实现怪物发射子弹的方法。
 

    # 根据怪物类型获取子弹类型,不同怪物发射不同的子弹
    # return 0代表这种怪物不发射子弹
    def bullet_type(self):
        if self.type == TYPE_BOMB:
            return 0
        elif self.type == TYPE_FLY:
            return BULLET_TYPE_3
        elif self.type == TYPE_MAN:
            return BULLET_TYPE_2
        else:
            return 0
    # 定义发射子弹的方法
    def add_bullet(self):
        # 如果没有子弹
        if self.bullet_type() <= 0:
            return
        # 计算子弹的X、Y坐标
        draw_x = self.x
        draw_y = self.y - 60
        # 如果怪物是飞机,则重新计算飞机发射的子弹的Y坐标
        if self.type == TYPE_FLY:
            draw_y = self.y - 30
        # 创建子弹对象
        bullet = Bullet(self.bullet_type(), draw_x, draw_y, player.DIR_LEFT)
        # 将子弹对象添加到该怪物发射的子弹Group中
        self.bullet_list.add(bullet)

怪物发射子弹的方法是add_bullet(),该方法需要调用bullet_type(self)方法来判断该怪物所发射的子弹类型(不同怪物可能需要发射不同的子弹)。如果bullet_type(self)方法返回0,则代表这种怪物不发射子弹。

一旦确定怪物发射子弹的类型,程序就可根据不同怪物计算子弹的初始X、Y坐标——基本上,保持子弹的X、Y坐标与怪物当前的X、Y坐标相同,再进行适当微调即可。程序最后的两行字代码创建了一个Bullet对象(子弹实例),并将新的Bullet对象添加到self.bullet_list中。

             

当怪物发射子弹之后,程序还需要绘制该怪物的所有子弹。下面是绘制怪物发射的所有子弹的方法。
 

# 更新所有子弹的位置:将所有子弹的X坐标减少shift距离(子弹左移)
    def update_shift(self, shift):
        self.x -= shift
        for bullet in self.bullet_list:
            if bullet != None:
                bullet.x -= shift
    # 绘制子弹的方法
    def draw_bullets(self, screen, view_manager) :
        # 遍历该怪物发射的所有子弹
        for bullet in self.bullet_list.copy():
             # 如果子弹已经越过屏幕
            if bullet.x <= 0 or bullet.x > view_manager.screen_width:
                # 删除已经移出屏幕的子弹
                self.bullet_list.remove(bullet)  # ⑦
        # 绘制所有子弹
        for bullet in self.bullet_list.sprites():
            # 获取子弹对应的位图
            bitmap = bullet.bitmap(view_manager)
            if bitmap == None:
                continue
            # 子弹移动
            bullet.move()
            # 绘制子弹位图
            screen.blit(bitmap, (bullet.x, bullet.y))

上面程序中的update_shift(self, shift)方法负责将怪物发射的所有子弹全部左移shift距离,这是因为界面上的角色会不断地向右移动,产生一个shift偏移,所以程序就需要将怪物(包括其所有子弹)全部左移shift距离,这样才会产生逼真的效果。

上面程序中的⑦号代码负责将越过屏幕的子弹删除。

接下来,程序采用循环遍历该怪物发射的所有子弹。先获取子弹对应的位图,然后调用子弹的move()方法控制子弹移动。上面方法中的最后一行代码负责绘制子弹位图

-----------------------------------------------------------------未完待续--------------------------------------------------------------------------------------------

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

                                            

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

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

猜你喜欢

转载自blog.csdn.net/weixin_41213648/article/details/90746506