基于Python的飞机大战游戏设计
2019-06-03瞿苏
瞿 苏
(江苏旅游职业学院, 江苏 扬州 225000)
Python是一种开源的简单易学的高级语言,应用场景涉及到Web应用开发、操作系统管理、科学计算、游戏等领域。飞机大战是一款飞行射击类游戏,游戏方法是玩家操作飞机与敌机在空中战斗。本文主要介绍用Python软件实现飞机大战游戏。
游戏的整个界面是一张背景图片,游戏中用到的其他角色同样都对应着相应的图片,这些图片需要借助Pygame模块搭载窗口以显示[1]。在窗口中,对象显示的位置通过坐标标注。其中,窗口的左上角坐标为(0,0),x轴向右延伸坐标数值增大,y轴向下延伸坐标数值增大。所有的游戏元素都参考这个坐标系,对象在窗口的移动就是坐标位置的变化。
1 飞机大战游戏总体设计
1.1 总体设计原则
主要设计原则如下:(1)简单性。在实现软件的功能的同时,尽量让软件操作简单易懂;(2)针对性。基于Python软件,实现飞机大战游戏的各种要求;(3)一致性。类型、变量和其他元素的命名规则保持一致;完成同样的功能应该尽量使用同样的元素;界面元素的外观风格、摆放位置在同一个界面和不同界面之间是一致的。
1.2 系统总体设计
系统中只有玩家一种用户,不必进行身份验证等操作。玩家点击应用图标直接进入开始界面。整个游戏的操作包括:显示玩家飞机、控制移动方向;显示玩家发射子弹(子弹移动);显示敌人飞机,控制敌人飞机移动、敌人飞机发射子弹。
1.3 准备工作
1.3.1 添加pygame模块
pygame是一套用来开发游戏的Python模块,该模块允许在Python程序中创建功能丰富的游戏和多媒体程序。PyCharm作为一款开发Python的编辑器,它不仅可以帮助开发人员提高开发效率,而且包含了像pygame这样功能丰富的第三方模块。
在PyCharm中添加pygame模块,在程序中导入pygame模块进行测试,编译器编译通过,就证明成功导入了模块。
1.3.2 搭建游戏界面
飞机大战游戏的整个场景都需要一个窗口作为载体,展示游戏中的画面。为了让整个游戏的角色和背景的风格统一,准备了一张背景图片。在开发中,导入pygame模块,就能直接调用模块中的方法。
1.3.3 检查键盘的输入
当敌人飞机发射子弹的时候,玩家飞机需要使用键盘适时地调整位置,以躲避子弹的攻击。在Python程序中,移动鼠标、敲击键盘等人机交互的动作属于事件,它交由pygame的event模块进行控制。event模块采用列表形式存储的事件,可以通过get函数来获取。如果要获取键盘和鼠标产生的事件,就使用for循环遍历事件列表,取出每个事件与event.type(事件的类型)进行对比。如果event.type的值为Quit,说明用户使用鼠标点击了窗口右上角的关闭按钮,此时就要退出程序;如果event.type的值为Keydown,说明用户使用了键盘,此时就要明确按下的是哪个键。
在while循环中,已经显示了游戏的背景图片。这时,在程序中需要检测是否有事件发生,比如按下键盘等。如果没有事件发生,就执行更新操作,如果有事件发生,就先处理键盘事件以后再更新。
2 飞机大战游戏功能实现
2.1 显示玩家飞机,控制移动方向
在窗口中要显示玩家飞机,可以根据玩家飞机图片的名称创建图像,再把这些图像显示到屏幕上设定的位置。玩家飞机左右移动功能,可以通过改变坐标x的值实现,飞机向左移动减小坐标x的值,反之则增大x的值。
新建一个Python File,取名为“plane”.在plane.py文件中,导入pygame模块,之后定义一个表示玩家飞机的类HeroPlane。
程序设计中,定义为display、move-left和move-right三种方法。其中,display用于在默认的位置显示玩家飞机;moveleft用于让飞机向左移动;moveright用于让飞机向右移动。在start函数中创建飞机对象,并且显示到窗口中。在while true语句中,根据玩家按下的按键来调用相应的方法,以控制飞机移动的方向。
当按“←”键或者“A”键时,控制玩家的飞机向左移动,当按“→” 或者“D”键时,控制玩家的飞机向右移动。
2.2 玩家飞机发射子弹
2.2.1 显示子弹
当按空格键时,代表玩家飞机要发射一枚子弹,此时需要在玩家飞机的头部位置生成一颗子弹对象。飞机左右移动到任意位置,只要按空格键,子弹生成的初始位置永远会位于玩家飞机的头部。
新建一个Python File,取名“bullet”。在bullet.py文件中,导入模块,之后定义一个表示子弹的类。代码如下:
class Bullet(object):
def-init-(self,x,y,screen):
self.x=x+40
self.y=y-20
self.screen=screen
self.iamge=pygame.image.load(“./feiji/bullet-3.gif”).convert()
def.display(self):
self.screen.blit(self.iamge,(self.x,self.y))
按空格键发射子弹,代码如下:
heroPlane.launch-bullet()
运行程序,按下空格键以后,玩家飞机的头部显示了待发射的子弹。此时,无论飞机移动到哪个位置,生成的新的子弹永远会位于其顶部。
2.2.2 子弹移动
每执行一次while循环,就会调用一次display方法,让子弹再次显示到屏幕上。由于屏幕刷新的速度特别快,肉眼是无法捕捉到的。利用程序的这个特点,每刷新一次屏幕,就让子弹显示的位置上移几个单位,从而形成向上发射子弹的动画效果。
如果无限制地往列表中添加子弹对象,终究会耗尽设备的内存,所以,一旦子弹离开屏幕可视范围时,就把子弹对象从列表中删除。
在Bullet类中添加一个judge方法,用于反馈子弹是否发射到屏幕以外的情况,若子弹图像的y值小于0,则表示子弹移出了屏幕,返回True;反之则返回False。
在HeroPlane类的display方法中,定义一个存放待删除子弹对象的列表。从列表中取出每个带删除的子弹对象进行判断,如果子弹对象已经发射到屏幕的外面, 就添加到刚定义的列表中,然后清空列表中所有被删除的子弹对象。具体代码如下:
def display(self):
self.screen.blit(self.image,(self.x,self.y))
need-del-list=[]
for item in self.bullet-list:
if item.judge():
need-del-list.append(item)
for del-item in need-del-list:
self.bullet-list.remove(del-item)
for bullet in self.bullet-list:
bullet.display()
bullet.move()
2.3 敌人飞机
2.3.1 显示敌人飞机
跟玩家飞机类似,在plane.py文件中定义一个表示敌人飞机的EnemyPlane类。敌人飞机应该有默认的位置、呈现图像的窗口、存放子弹的列表这些属性。然后来到main函数的while循环语句中,在创建HeroPlane类对象的后面,创建表示敌人飞机的对象。在显示玩家飞机的代码后面,调用display方法来显示敌人飞机。
2.3.2 控制敌人飞机移动
当程序启动以后,敌人的飞机开始在窗口的顶部做直线运动,直到碰到屏幕的边缘后向反方向做直线运行。可以为敌人飞机类添加一个移动的方法,由于敌人的飞机是不停地运动的,所以放到while语句中最为合适[2]。
飞机碰到左侧的屏幕边缘时,移动方向变化为向右移动,飞机碰到右侧的屏幕边缘时,移动方向变为向左移动。因此,在EnemyPlane类的-int-()方法中增加一个direction属性,用于记录飞机的初始运动方向:
self. direction=“right”
然后定义一个move方法,根据飞机移动的方向改变x对应的坐标值,再根据x的值限定敌人飞机移动的范围,只要超过屏幕的宽度范围,就改变飞机运动的方向。
在main.py文件的while语句中,调用display()方法显示敌人飞机,调用move方法实现敌人飞机一直左右移动的效果。
运行程序,敌人飞机在屏幕顶部移动的速度非常快。产生这种情况,主要因为屏幕刷新的速度太快,导致敌人飞机移动的速率过快,并且占用了程序过多的内存,因此,需要使用time模块来降低程序执行的效率。
2.3.3 敌人飞机发射子弹
敌人飞机发射子弹的功能与玩家飞机发射子弹的功能基本一样,不同的是子弹反射的方向及发射的个数。为此,在bullet.py文件中,新建一个表示敌人飞机发射的子弹的EnemyBullet类,直接复制Bullet类的代码到EnemyBullet类中,然后再对发射子弹的功能代码进行局部调整。在main.py文件的循环语句中,调用move方法的后,调用敌人发射子弹的方法为:
Enemy-plane.launch-bullet()
运行程序,由于敌人飞机发射子弹的速度太快了,使得子弹形成了一条斜线。所以,在EnenmyPlane类的launch-bullet(发射子弹)方法中,设置发射子弹的数量是随机的,这样可以降低子弹发射的频率,代码如下:
def launch-bullet(self):
number=random.randint(1,100)
if number==88:
new-bullet=EnemyBullet(self.x,self.y,self.screen)
self.bullet-list.append(new-bullet)
上述方法实现了敌人飞机发射子弹的功能,首先使用函数获取了从1到100的随机整数,然后使用if语句判断随机数的值是否与88相等,只有这两个值相等,才会创建要发射的子弹对象,通过这种随机数的方式,使得敌人发射子弹的数量变为原来的百分之一。由于使用了random模块的函数,所以导入 random模块[3],代码为:
Import random
3 优化程序代码
在前面的程序代码中,表示敌人飞机的类(EnemyPlane)和表示玩家飞机的类(HeroPlane)中有很多功能相似的代码,除此之外,表示子弹的两个类中有着很多重复的代码,这些重复的代码不仅使程序显得过于臃肿,而且没有清晰的结构。可以利用继承的技巧[4],对所有类的代码进行优化,以明确程序的结构。
3.1 抽取子弹基类
玩家飞机发射的子弹和敌人飞机发射的子弹这两大类的功能几乎相同,出现了很多重复的代码,使得整个程序的结构过于臃肿。可以定义这两个类的公共类PublicBullet[5],在这个类中既能抽取出它们共同拥有的功能,又能区分它们的不同情况。
定义一个表示子弹的公共类PublicBullet,把表示玩家飞机子弹类的代码复制到PublicBullet类中,再根据两个子弹类不同的地方进行调整。
由于PublicBullet类已经合并了EnemyBullet类和Bullet类的功能,并对它们各自不用的地方进行了处理,所以创建EnemyBullet类和Bullet类对象时,可以替换为创建PublicBullet类对象:
New-bullet=PublicBullet(self.x,self.y,self.screen,“enemy”)
同样在HeroPlane类的launch-bullet方法中,把创建Bullet类对象的代码改为创建PublicBullet类的对象:
New-bullet=PublicBullet(self.x,self.y,self.screen,“hero”)
运行程序,界面依然显示了跟以前一样的效果,这证明代码抽取成功。EnemyBullet类和Bullet类现在对程序没有起任何作用,可以直接删除。
3.2 抽取飞机基类
进一步抽取EnemyPlane和HeroPlane两个类中功能相似的代码。由于这两个类中实现的功能较多,如果依然使用抽取公共类的技巧,就需要频繁地使用if-else语句来区分不同飞机的情况。因此,需要把EnemyPlane和HeroPlane类中相同的功能放到基类中,再让这两个类继承基类后单独调整。
通过比较EnemyPlane和HeroPlane类中的代码,发现 EnemyPlane 类中的-int-、display和 sheBullet 方法与HeroPlane类中的-int-、display和 sheBullet方法功能非常相似。为此,需要把这三个方法的代码提取到基类中,子类EnemyPlane和HeroPlane 继承基类以后,会拥有这三个方法,可以根据自己的要求重写这些方法。
3.2.1-init-()方法
通过比较两个飞机类的代码发现,EnemyPlane类的-init-()方法比HeroPlane类的-init-()方法中多一个属性,所以需要把HeroPlane类的-init-()方法粘贴到新创建的基类Plane中,具体如下:
Class Plane(object):
def-init-(self,screen):
self.x=230
self.y=600
self.screen=screen
self.image-name=”./feiji/hero.gif”
self.image=pygame.image.load(self.image-name).convert()
self.bullet-list=[]
让HeroPlane类继承Plane类,这样HeroPlane类就拥有了从父类Plane继承来的-int-()方法。此时,可以删除类HeroPlane中-int-()方法。同样让EnemyPlane类继承自Plane类,Plane类就拥有了从父类继承而来的-int-()方法。不过,EnemyPlane类需要增加一个表示方向的属性,为此需要重写父类的-int-()方法,具体如下:
def-init-(self,screen):
super().-int-(screen)
self.direction=”right”
3.2.2 display方法
通过比较EnemyPlane类和HeroPlane类的display方法发现,它们的功能是一样的。因此,可以在Plane类中沿用EnemyPlane类的display方法代码。
由于EnemyPlane类和HeroPlane类都已经继承了Plane类,所以就拥有了display方法,而且它们的功能没有任何变化,直接删除它们每个类中的display方法就行了[6]。
3.2.3 launch-bullet方法
为了能区分是哪架飞机要发射子弹,在创建飞机对象构造方法的参数列表中,增加一个表示飞机名称的字符串。虽然这样能区分飞机的名称,但是方法参数中直接使用字符串扩展性不是很好。因此,在Plane类的-init-方法中,添加表示飞机名称的name属性,使得EnemyPlane类 和HeroPlane类创建对象时就有了名字。
在EnemyPlane类重写的方法中,增加name参数。在类的方法中,把创建PublicBullet类对象的参数列表中的最后一个参数改为self.name。改完以后,把整个launch-bullet方法剪切到Plane类中。代码如下:
def launch-bullet(self):
new-bullet=PublicBullet(self.x,self.y,self.screen,self.name)
self.bullet-list.append(new-bullet)
EnemyPlane类中的launch-bullet方法,与从父类直接继承的launch-bullet方法在功能上存在着一些差异。为此,EnemyPlane类需要重写从父类继承的launch-bullet方法。在Plane类的-int-()方法中,由于图片素材的名称、x值和y值是变化的,每个子类需要单独进行调整,所以在Plane类中,把这三个属性移动到子类HeroPlane重写的-int-()方法中。在EnemyPlane类的-int-()方法中,同样增加这三个属性的设置。
图1 程序中所有类的继承结构
再次运行程序,发现跟以前运行的场景一样,这表示抽取成功。此时,程序中几个类的结构,如图1所示。
4 结论
本文围绕面向对象的编程思想,开发了飞机大战游戏的部分功能,包括搭建游戏界面,创建玩家飞机和敌人飞机类,飞机发射子弹等,并且利用继承的技巧优化了代码。模块以对象划分,有界面模块、Hero模块、子弹模块、Enemy模块、游戏控制模块;难点在于多架战机同时出动情况下,判断发生碰撞的算法设计。整个软件按照预期目标大致实现了飞机大战游戏的功能。整个游戏还存在一些不足。比如,怎么动态及时地显示积分数,怎么能够让各种敌机在不同的时间点以不同速度飞行且不发生碰撞;还有如何在多人联网下共同作战,及时分享自己的战绩等,都需要进一步研究。