接下来就使用面向对象的设计方式来设计一下飞机大战的主游戏类.
先回顾一下刚刚完成的快速入门案例,如果从一个游戏主程序的职责来看一个游戏的主程序需要做哪两件事情,一个游戏的主程序需要做 游戏的初始化 以及 游戏循环.
在游戏初始化做一些准备工作,当准备工作就绪之后,就可以进入游戏循环正式启动游戏.
那现在就结合一下下面这张示意图,
梳理一下在游戏初始化以及游戏循环都要做哪些事情,通过对这两部分工作的共同梳理, 来明确一下,接下来要设计的飞机大战这个游戏类中需要封装哪些个属性以及方法.
目的明确之后, 先看一下游戏初始化,在游戏初始化中首先应该创建一个游戏的窗口,因为后续所有精灵的图像都需要绘制在游戏窗口中,所以啊需要在飞机大战游戏中封装一个屏幕的属性,这个是游戏窗口.
那接下来在游戏初始化还需要来创建一个游戏的时钟,因为只要有了时钟对象之后,才能够在游戏循环中,通过时钟调用tick 方法来设置一下刷新帧率.
所以呢,同样需要在飞机大战的游戏中封装一个时钟属性,一个窗口, 一个时钟,除此之外,还需要根据游戏的需求创建不同的精灵以及精灵组.
现在来观察一下游戏初始化主要在做什么事情,游戏初始化主要就是在定义飞机大战游戏中涉及到的属性,屏幕, 时钟以及精灵和精灵组.
那现在考虑一下,既然是定义属性,就可以借助初始化方法, 一次性的把游戏初始化中需要定义的属性全部完成.
可以利用初始化方法完成之前的游戏初始化要做的所有事情,同时呢,因为现在还不能够明确在飞机大战这个游戏中需要创建多少个精灵以及精灵组,所以可以封装一个私有方法.
以__开头的方法是私有方法,封装一个创建精灵__create_sprites 的私有方法, 在初始化方法内部来调用一下这个私有方法,由__create_sprites这个私有方法,专门负责精灵组以及精灵的创建.
如果这样设计的话,就意味着初始化方法一旦完成,后续再需要添加精灵组也好,添加精灵也好,只需要修改一下__create_sprites这个私有方法,而不再需要对初始化方法内部的代码作任何的改动.
这个就是由初始化方法完成游戏的初始化动作.
那接下来再看一下游戏循环,之前提到过, 当游戏循环内部的代码开始执行,就意味着有游戏的正式启动,所以呢,可以在飞机大战的游戏类中封装一个start_game的方法, 开始游戏,在这个方法内部来开启整个游戏循环, 开启了游戏循环就意味着游戏的开始.
那现在简单梳理一下游戏循环中要做的事情,第1步要设置刷新帧率,所谓设置刷新帧率啊,就是让时钟对象调用下tick 方法,通过tick 方法,就能够控制到游戏循环内部的代码执行的频率,然后呢要做一下事件监听,通过事件监听就能够捕获到用户对游戏做了哪些操作,捕获到对应的操作,才能够做出相应的反应.
这个是事件监听要做的事情,那接下来再看碰撞检测,这是一个陌生的名词, 那什么叫做碰撞检测呢?回顾一下,在飞机大战这个游戏中,英雄发射的子弹一旦碰撞到敌机要把敌机销毁.
那同时敌人的飞机碰撞的英雄会怎样?英雄会牺牲, 整个游戏结束,这个就是碰撞检测.
有关如何实现碰撞检测,在后续的案例演练中,会详细给讲解,现在先有一个印象,在游戏循环中需要做一下碰撞检测,只有通过碰撞检测才知道英雄的得分,以及知道英雄是否牺牲.
那接下来再来看一下游戏循环中要做的其他事情, 当事件监听, 碰撞检测做完,就可以让精灵组调用一下update的方法,update 方法会通知精灵组内部所有的精灵更新位置, 当更新位置完成,再让精灵组调用绘制方法,一次性把精灵组中所有的精灵绘制在屏幕的不同位置.
当所有绘制工作完成,不要忘记还需要更新一下屏幕的显示,这个就是游戏循环中要做的事情.
已经提到过,在飞机大战这个游戏类中,封装一个start_game的方法,由start_game的这个方法来开启游戏循环.
但是呢,在游戏循环中需要做的事情还是很多的,设置刷新帧率, 事件监听等等.
如果要把这些操作代码全部写在一个循环体内部,会让一个游戏循环的代码变得非常非常长.
那怎么样解决这个问题呢?就可以再定义一些私有方法,定义一个__event_handler(self) 这个方法, 从字面上来看,这个私有方法是专门做事件监听的.
再定一个__check_collide(self)的方法,碰撞检测,让这个私有方法专门负责碰撞检测的工作,然后呢再定一个__update_sprites(self)方法,由这个方法专门负责更新和绘制精灵组,同时呢,再定一个私有方法__game_over(),__game_over()是游戏结束.
之前在快速演练中, 回顾一下, 当监听到用户点击关闭按钮,编写过两句非常固定的代码,pygame点quit, 卸载所有的模块,然后调用exit函数直接终止程序.
现在在游戏循环中要做碰撞检测这个动作,当英雄牺牲了, 游戏也意味着同样需要结束,那么为了避免相同的代码重复的编写,所以啊在设计类时就可以提前封装一个__game_over()的方法,这样呢,无论用户是点击关闭按钮还是英雄挂掉了,都可以直接调用__game_over()这个方法.
到这里,就把之前完成了快速入门案例中游戏初始化部分的代码以及游戏循环部分的代码梳理了一下.
通过对两部分工作的共同梳理, 明确了一下飞机大战这个主游戏内容,需要封装的属性以及方法.
一句话讲要封装一个屏幕属性, 用于精灵的绘制,要封装一个时钟属性,用于设置刷新帧率,然后要封装游戏中所有需要使用到的精灵和精灵组,同时利用初始化方法完成游戏的初始化所有工作, 利用start_game方法完成游戏循环所做的所有工作.
接下来先确定一下在飞机大战这个游戏中需要包含哪些python 文件,并且明确一下每个Python文件各自的职责.
其实在飞机大战这个游戏中,只需要开发两个python 文件就够了.
第1个python 文件,叫做plane_main, 主要的意思,在这个文件中,把上一小节分析的主游戏类做一个封装,然后呢,使用这个游戏类创建一个飞机大战的游戏对象,并且通过start_game方法来启动一下飞机大战的游戏,
这样就是主程序的职责,一句话讲, 封装主游戏类并启动游戏.
那另外一个python 文件呢,看plane_sprites, 飞机精灵,在之前案例呢, 已经建立过这个文件,并且在飞机精灵模块中封装了一个游戏精灵的子类.
既然这个模块已经存在,那么就在飞机大战这个游戏中,让飞机精灵这个模块继续发挥它的力量,把游戏中所有需要使用的精灵子类,全部封装到这个模块中,由飞机精灵这个模块统一向主程序提供所有需要使用的工具.
这个就是飞机大战这个游戏中需要使用的两个python 文件.
一个主程序, 负责封装主游戏内启动游戏,另外一个飞机精灵模块, 负责提供所有需要使用的工具.
明确了文件的职责之后,在这一小节, 就针对主文件plane_main.py 做一个准备工作,在主文件中把需要使用的模块做一个导入,并且把上一小节分析的主游戏类,做个简单的代码搭建.
目的明确之后, 首先点击右键,找到new, 创建一个Python file,
先来创建一个主程序,给它起个名字,plane_main, 回车,
主程序建立完成,先点击右键选择执行,
把主程序设置成可执行的.
右上方的工具栏已经变化了,
下一次再 右键+run 就会运行主程序.
那现在把光标放在主程序中, 先来导入一下pygame 这个模块,然后呢再使用from关键字。把plane_sprites 这个模块中提供的所有工具全部导入,一次性导入所有的工具.
那现在再使用class关键字来定一个PlayGame()的类, 让飞机大战的主游戏类,继承自object这个基类,然后呢增加一个文档注释, 飞机大战主游戏.
文档注释添加完成, 来看一下这一张类图,按照之前的一个小节分析,需要在飞机大战游戏的初始化方法中完成游戏的初始化动作,同时还需要封装一个start_game的方法,在start_game方法内部来开启游戏的循环.
那现在就让回到Pycharm 上, 首先使用def 关键字来找到初始化方法,然后呢,使用print函数做个输出,写下 游戏初始化,初始化方法先准备一下,再使用def 关键字来定义的start_game的方法,在这个方法内部,同样使用print函数做个输出, 来写一下游戏开始.
一个简单的飞机大战主游戏类定义完成,接下来可以考虑创建对象, 开始游戏.
但是现在有一个问题,在之前学习模块的时候,重点提示过,每一个独立的python 文件都应该可以被当做模块被导入,那么怎么样能够做到,当前的主程序同样也可以被当做模块导入呢,可以使用if判断, 判断一下__name这个内置属性,那现在要分享一个Pycharm 的小技巧,现在光标在第15行, 敲一个main, 敲完main 之后, Pycharm 会给一个智能提示,
现在摁一下回车,按下回车之后,会自动帮增加对__name 这个内置属性的判断.
因此就可以把光标放在if语句的下方来编写,在主程序中希望直接执行的代码,那现先增加一个注释, 创建游戏对象,然后呢,再来启动游戏,两个步骤明确之后.
就先给游戏对象起个名字叫做game,然后呢使用刚刚定义的PlaneGame,来创建一个对象, 对象创建完成,就让game这个对象调用一下start_game这个方法, 代码编写完成,运行一下程序.
控制台输出了游戏初始化, 游戏开始.
通过这两个输出已经知道初始化方法被调用了,启动游戏方法同样也被调用了,那现在让回忆一下,在这一小节啊,先明确了一下,在飞机大战这个游戏中需要使用的两个文件,一个是主程序,一个是提供的工具的飞机精灵模块.
主程序中封装一下主游戏类,并且创建游戏对象, 启动游戏.
飞机精灵这个模块就要封装一下游戏中需要使用的所有精灵子类,向主程序提供所有需要使用的工具.
明确了两个文件的职责之后,在这一小节还针对主程序做了一个基础的准备,在主程序中准备了一个简单的主游戏类,并且完成了创建对象和启动游戏的工作.
接下来就参照这张类图,在飞机游戏的初始化方法中完成游戏的初始化动作.
先来明确一下游戏初始化需要做的事情,第1步设置一下游戏的窗口,然后呢,创建一个游戏的时钟对象,紧接着要调用一个私有方法,让这个私有方法完成精灵和精灵组的创建工作,友情提示下,在这一小节,只是准备一个空的私有方法,至于要创建哪些精灵和精灵组,稍后再讲.
目的明确之后, 把光标放在初始化方法内部,先增加几个单行注释,进一步来明确一下游戏初始化要做的事情。
第1步,要创建游戏的窗口,然后呢,再来创建游戏的时钟, 窗口和时钟创建完成,第3步就要调用私有方法,让私有方法来完成一下精灵和精灵组的创建.
三个步骤明确下来,既然要调用私有方法,
就先使用def 关键字来定一个私有方法,注意啊,私有方法需要以两个下划线开头,那现在给方法起个名字__create_sprites, 方法名起完, 就直接使用pass 做一个占位,因为在这一小节只需要准备一个空方法就可以.
那现在就把光标要放在12行,创建一下游戏的窗口,现在就使用self点,先给游戏窗口起个名字screen ,名字起完.
在pygame 中怎么样设置游戏的窗口,可以通过display模块提供的set_mode 方法就可以设置游戏窗口.
set_mode 方法,第1个参数要接收一个元组,元组的第1个值是屏幕的宽度,第2个值是屏幕的高度,游戏的窗口设置完成.
再来创建一下游戏的时钟, 同样使用self点给时钟属性起个名字clock.
然后要创建游戏的时钟应该怎么做呀,可以使用pygame 提供的time模块, time 模块中提供了一个clock类, 利用clock 类就可以创建出一个时钟对象.
现在窗口有了, 时钟有了,紧接着就调用一下私有方法,让这个私有方法去执行精灵和精灵组的创建动作.
那现在就使用self点找到__create_sprites这个方法,由这个私有方法来完成精灵和精灵组的创建.
现在游戏的初始化代码写完了,运行一下程序,看看游戏的窗口能不能出现在屏幕上.
一个黑窗口一闪而逝,代码写到这里,游戏初始化的代码,已经在飞机游戏的初始化方法中基本完成了.
那在结束视频之前,要重点强调一个问题, 在设置游戏窗口时,游戏窗口的大小使用固定的数值,但是要友情提示一下,这种方式编写代码虽然不会有任何的问题,但是在正式开发程序时要有一个印象,就是如果要设置固定的数值,最好不要在代码中把固定的数值直接写死,
这种方式并不是好的方法,那应该如何解决呢?
接下来介绍一个概念叫做常量,所谓常量,就是定义之后不能变化的量就叫做常量,那么之前习惯的变量呢,就是定义之后可以随便修改变化的量就叫做变量, 常量就是不能变化的量.
在开发时什么时候会使用常量呢?在开发的时候,有的时候可能会需要使用一些固定的数值,譬如屏幕的高度是700,如果出现这种情况,实际上是建议不要直接使用700这个数字,为什么会有这种建议呢?
看一下大钻石的软件,之前曾经做过一个案例,回顾一下,当英雄的飞机飞到屏幕的顶部,让英雄重新回到了屏幕底部,要实现这个效果, 是怎么处理的,只是把英雄飞机的y值设置成了屏幕高度700,
那现在试想一下,假设游戏需求发生了变化,屏幕的高度不再是700,而是600,一旦需求变化,就意味着需要把已经开发完成的代码打开,找到所有出现700的位置,把700修改成600,游戏才能够修改完成.
那现在再试想一下,假设需求又变化了, 高度不是600, 而是650,又意味着需要再次打开程序把所有出现600的位置,统一修改成650, 才能完成对游戏的修改,这个是一个非常繁琐的事情.
那应该怎么样解决这个问题呢?再回忆下,如果在开发时需要使用一些固定的数值,就可以把这些固定的数值先定义成常量,然后呢,在代码中使用常量而不使用固定的数值, 这个就是解决办法.
那开发时应该怎么样定义常量呢?怎么定义常量的方式和定义变量的方式啊,语法是完全一模一样的,只使用赋值语句就可以定义一个常量了,但是呢,在定义常量时给常量命名需要注意,在给常量命名的时候,所有字母都应该大写,单词与单词之间先使用下划线连接.
为什么有这种命名约定呢?在Python这门语言中,本质上并没有真正意义的常量。因为Python是一个纯动态的语言,Python中并没有真正意义上的常量,所以在开发时如果要定义常量,都是通过命名的约定来定义的,也就是其他的程序员一看到所有字母都是大写,这个应该是一个常量,所以呢,在开发时就不会轻易修改了,这个就是常量的定义方式.
讲到这里,先介绍了一下常量的应用场景以及定义方式.
那现在回到代码检查一下已经完成的程序,现在在初始化方法中,创建游戏窗口时, 指定游戏窗口的大小, 使用了480和700两个固定的数值,而按照刚刚介绍,在开发时最好不要这样指定,而应该定一个常量,那常量应该怎么定义呢?
要切换一下文件, 切换到为主程序提供工具的飞机精灵这个模块,
然后呢,把光标放在第2行,现在增加一个注释,要定一个屏幕大小的常量,
那现在就先给常量起个名字,每个字母都是大写,然后加一个下划线, 再写个RECT,SCREEN_RECT, 然后呢,使用pygame提供的Rect 类来创建一个矩形对象,现在写一个x是0,y是0, 宽是480,高是700,一个屏幕大小的常量定义完成.
那有了常量之后,就可以对主程序的代码进行一个改造.
但是在改造主程序代码之前,先来运行一下程序,看看现在运行时窗口的大小是怎样的,现在,一个黑窗口一闪而逝,不能够看清楚这个窗口.
那么就找到启动游戏这个方法,在启动游戏中增加一个无限循环,因为启动游戏本身就是要封装游戏循环的,
现在再运行一下程序,看一个480×700的黑窗口出现了.
现在是使用固定的数值来指定的窗口大小,那现在把程序停止一下,
来使用一下刚刚定义的SCREEN_RECT 这个常量来设置一下窗口的大小,敲一个SCREEN_RECT,但是注意SCREEN_RECT是一个什么类型, SCREEN_RECT是一个矩形对象,而set_mode 这个方法接收的第1个参数是一个元组,怎么样从一个矩形中拿到元组,只需要找到size属性,矩形对象的size属性就是一个元组,那现在就选中size属性,
然后再运行一下程序,程序启动了, 仍然是宽度480, 高度700, 跟之前使用固定数值指定的屏幕大小是完全一样的.
那现在再切换一下窗口,
假设游戏的开发需求变化,窗口不再是480×700,而是800×600,只需要把屏幕大小的常量做一个修改,
现在再运行一下程序,看看游戏的窗口有没有发生变化,看, 一个800×600的窗口就出现了,这个就是使用常量的好处.
那现在就停止一下程序,然后把窗口的宽度恢复一下,把窗口的高度也恢复一下.
在这一小节就介绍了一个开发中的概念叫做常量,一句话讲, 在编写代码时, 如果需要使用一些固定的数值,建议把这些固定的数值啊先定义成常量.
在代码中使用常量,而不要直接使用固定的数值,这个就是常量的应用场景.
同时在定义常量时为了跟变量加以区分,在给常量命名时,需要把所有的字母都是用大写,单词与单词之间使用下划线连接,
同时呢,在这一小节还对之前完成的代码做了一个改造, 之前是使用固定数值指定的窗口大小,而在这一小节定义了一个屏幕大小的常量,通过这个常量来指定了游戏窗口的大小.
接下来就参照这张类图, 把start_game, 也就是开启游戏这个方法实现一下,
在开启游戏这个方法中,首先要建立一个游戏循环.
现在游戏循环已经有了,然后再在游戏循环中顺序执行5件事情,分别是设置刷新帧率, 事件监听等等,那么在这一小节啊,就先把光标放在游戏循环内部,以单行注释的方式先来逐一明确一下, 在游戏循环中需要做的5件事情.
第1件事情需要设置刷新帧率,
第2件事情呢,需要做事件监听.
当事件监听完成之后,第三件事情, 需要做一下碰撞检测,
碰撞检测完成之后,第四件事情, 就需要让精灵组做一下更新和绘制精灵组的动作.
当所有的精灵绘制完成,第五件事情是更新显示,只有更新了显示, 才能够在屏幕上看到最终绘制的结果.
现在使用单行注释的方式明确了一下游戏循环中要做的5件事情。
那现在再看一下类图,按照之前的分析,为了简化start_game这个方法内部的代码,之前分析过要来封装4个私有方法,
在4个私有方法分别执行一下事件监听, 碰撞检测, 更新精灵组以及游戏结束的事情.
那现在就在Pycharm中,先来定义4个空的私有方法,使用def 关键字,先来定义一个事件监听的方法, 在这里,给方法起个名字,__event_handler,方法名写完, 就使用pass 做个占位.
事件监听的方法有了,紧接着再定义一个碰撞检测的方法,同样使用def关键字,同样输入两个下划线,然后写下__check_collide,这个是碰撞检测的意思,那现在仍然使用pass做一个占位, 碰撞检测的方法准备完成.
再准备一个更新精灵组的方法,尽量使用def 关键字,同样先输入两个下划线,现在给方法起个名字__update_sprites, 方法名写完, 同样使用pass 做一个占位.
最后一个封装的方法是__game_over的方法.方法名写完,在__game_over 这个方法中代码是非常固定的,那现在就先使用print 函数做个输出, 写下游戏结束.
游戏结束之后,应该先让pygame调用一下quit 方法,卸载所有的模块,然后呢,然后调用一下系统的exit 函数, 通过exit 函数就可以直接终止当前正在执行的程序.
4个私有方法准备完成,但是_game_over这个方法有个灰灰的虚线,并且提示_game_over应该是一个静态方法.
_game_over方法中并没有使用到对象的属性, 也没有使用到类属性, 因此_game_over这个方法可以把它定义成一个静态方法.
要定义静态方法,这个方法就不需要接收self 参数了.
现在把self 删除,
然后注意, 要定义静态方法,需要在方法的上方增加一个修饰符,找到@staticmethod,现在一个静态方法定义完成.
4个私有方法,全部准备到位.
现在就把光标放在游戏循环内部,来把游戏循环中需要做的5件事情逐一填写一下,第1步设置刷新帧率,要设置刷新帧率,就使用之前创建的时钟对象来调用一下tick 方法,并且指定一下刷新帧率,在这里先写一个60.
然后第2件事情要调用事件监听,那么就使用self来调用一下刚刚封装的__event_handler 这个方法,第3件事情要做碰撞检测,就使用self点来调用一下刚刚封装的碰撞检测这个方法,那第4件事情,应该使用self来调用一下刚刚封装的更新精灵这个方法.
到了更新显示了,怎么样让pygame更新屏幕的显示?
可以使用pygame的display模块,提供的update 方法,update 方法就可以更新显示了.
现在代码写完, 先来运行一下程序,看看增加了这些代码之后, 程序能不能正常执行,现在走,一个黑窗口又出现了,并没有提示任何的错误信息.
那现在点一下叉叉, 点叉并不能关闭窗口,因为现在代码中还没有针对事件监听做任何的处理.
那现在就把光标定位在事件监听这个方法内部,
来回顾一下之前学习过的事件监听处理,来监听一下用户点击了关闭按钮,目的明确之后,怎么样获得当前这一时刻所有的用户事件,可以通过pygame找到event模块,并且调用一下get方法.
get 方法会返回当前这一时刻发生的所有事件列表。
既然是一个事件列表,要想监听事件应该使用一个for循环.
那现在就在前面增加一个for ,然后呢定一个变量event ,event 这个变量是在循环体内部需要使用的变量,然后在末尾增加一个重要的冒号, for 循环准备完成.
先来增加一个注释,因为现在要监听的是用户点击退出按钮,那现在就编写一下注释,判断是否退出游戏,注释写完, 既然要判断, 就使用if,怎么样判断事件的类型,可以event变量, 敲一个点, 要判断事件类型,就敲一个type,然后用 == 判断一下是否是pygame定义的退出事件,如果是, 就可以调用一下退出方法.
刚刚已经封装了一个_game_over的静态方法, 在这里怎么样调用静态方法,可以用类名的方式来调用静态方法. 那现在写一下PlaneGame,然后呢调用一下_game_over这个静态方法, 代码改造完.
再来运行一下程序,看看能否监听到用户点击退出按钮,程序又启动了,
现在点击一下叉叉, 游戏结束已经输出,这就说明 事件监听方法是正确的.
现在再检查一下代码,现在在游戏循环中, 指定刷新帧率时用了一个固定数字,按照之前介绍的开发套路,碰到固定数字, 应该定义成一个常量.
那既然要定义常量,就切换一下文件,
切换到专门为主程序提供工具的飞机精灵这个文件,现在先增加一个注释,要定义的常量呢,是刷新的帧率,然后给常量起个名字,每一帧对应的英文单词是FRAME,然后写个下划线, PER_SEC, 每秒更新的帧数,FRAME_PER_SEC, 现在指定一个60的数字.
现在一个刷新的帧率常量已经定义完成,
就再切换回主程序,
把之前使用的固定的数值替换成刚刚定义的常量.
现在选中FRAME_PER_SEC,
再运行一下程序, 黑窗口又出现了,
点击一下叉叉, 游戏结束.
代码演练到这里,就针对之前分析的类图,把开始游戏这个方法做了一个实现, 并且在这一小节中回顾了一下, 事件监听的代码怎么写,以及游戏退出的代码怎么实现,并且呢,也回顾了一下怎么样用display这个模块提供了update 方法来更新一下屏幕的显示,最后呢还复习了一下定义常量来替换程序中固定的数值.