Python游戏开发之精灵和精灵组

Python
347
0
0
2023-05-19
标签   Python游戏
目录
  • 1. 基本概念
  • 2. 自定义精灵子类需求分析
  • 3. 派生精灵子类代码实现
  • 4. 创建敌机并且实现敌机动画
  • 总结

1. 基本概念

接下来介绍两个pygame中提供的高级类, 精灵和精灵组.

在介绍这两个类之前, 先来共同回顾一下到目前为止掌握的游戏开发套路.

在游戏初始化,需要加载一下游戏中所有的图像, 然后呢,在游戏循环中,需要针对每张图像来编写代码、修改图像的位置,并且需要用screen对象来调用一下blit 方法,把所有变化位置的图像重新做一个绘制.

那现在假设开发的游戏,需要处理100张图像,意味着游戏循环内部的代码就会变得非常的繁琐.

因为需要在游戏循环中针对每张图像的位置变化,编写代码,然后再让screen对象调用blit 方法依次绘制每个图像,这样的代码呢,就显得太繁琐了.

那怎么样有效的解决这个问题呢?pygame就提供了两个高级类,一个是精灵,一个是精灵组.

精灵这个词汇听起来有些奇怪,可以把精灵理解成一个对象,在一个精灵对象中有两个重要的属性,一个是精灵要显示的图像数据,一个是精灵要把这个图像显示在屏幕上的位置.

一句话讲,包含了图像数据和显示位置的对象,就可以把它叫做精灵,同时呢,在精灵类中啊,还提供了一个非常重要的方法update,

在开发时, 可以根据不同的游戏角色派生出不同的游戏子类, 然后在每个子类中根据各自的需求分别重写各自的update 方法, 在update 方法中, 专门来处理一下当前这个游戏角色位置变化的代码, 这个就是update方法的作用. 

之所以要派生子类, 是因为不同的游戏角色在屏幕上的运动方式是不一样的,.

一个精灵类, 提供了一个image属性, 提供了一个rect 属性,并且提供了一个重要的update方法.

这个就是精灵类最重要的两个属性和一个方法.

那接下来看一下精灵组,从字面上来看,精灵组是一个包含了多个精灵的对象,在创建精灵组的时候可以使用多值参数的方式,一次性把精灵组中包含的所有精灵传入到精灵组内部.

当有了精灵组之后,当创建了一个精灵组之后,在编写游戏循环代码时,只需要在游戏循环中,让精灵组调用两个方法,第一个方法update,第二个方法draw.

这两个方法有什么作用呢?当让精灵组 调用update方法时,精灵组,就会让组中所有的精灵,各自调用各自的update方法,精灵组调用一次update 方法,精灵组中所有的精灵就会各自调用各自的update方法.

各自都用各自的update方法,就可以根据游戏的需求,各自修改各自不同的位置,当所有的精灵更新完位置之后,再让精灵组调一下draw 方法,同时呢,把屏幕对象当作参数传递给 draw 方法, draw 方法会把精灵组中所有精灵的image绘制到屏幕上每一个精灵对应的rect 位置。

在精灵中包含有两个重要的属性,一个是精灵要显示的图像数据,一个是精灵在屏幕上的位置,而在游戏循环中,让精灵组调用一下draw 方法,就可以一次性的把精灵组所有的精灵图像,各自绘制到不同的位置上.

这个就是使用精灵组之后,游戏循环的代码能够得到大大的改善.

但是务必要注意哦,当让精灵组调用了draw 方法之后,如果希望在屏幕上看到最终的绘制结果,同样是需要调用一下display模块儿提供的update的方法,因为啊,只有调用了update的方法,才能够在屏幕上看到最终的绘制结果。

简单介绍了一下pygame中提供的两个高级类,一个是精灵,一个是精灵组.

在精灵类中封装了两个重要的属性,一个是精灵显示的图像数据image ,一个是精灵在屏幕上对应的显示位置rect ,同时,每个精灵都各自有自己不同的update 方法,在开发时可以派生一个精灵的子类,根据游戏角色不同,重写update的方法,在update 方法内部,针对当前这个精灵角色编写代码、修改精灵的位置就可以,当精灵建立完成,就可以建立一个精灵组,一个精灵组是可以包含多个精灵的,当有了精灵组之后再编写游戏循环的代码,只需要让精灵组调用update的方法,update 方法就可以让所有的精灵同时更新位置,然后呢,再调一下draw 方法,draw 方法就会把更新位置之后的精灵绘制到屏幕对应的位置,这个就是精灵和精灵组的作用.

2. 自定义精灵子类需求分析

接下来就做一下派生精灵子类的演练,先共同来明确一下派生精灵子类这个演练的需求与步骤,第一步啊,要在项目中建立一个plane_sprites.py 的python 文件.

从这一小节开始,就要正式的进入到面向对象程序开发了,而在使用面向对象程序开发时,给每个模块起名都需要有一个意识,就是起一个稍微正式一些的名字,而在这一小节派生出来的精灵子类会用在后续的飞机大战实战中, 所以啊,给他起一个稍微正式一些的名字,飞机精灵,然后呢,在这个模块中建立一个Game_Sprites类,让这个游戏精灵类继承自pygame提供的精灵类pygame.sprites.Sprites .

在这里提示一下,在使用pygame时,第一个点后面通常是模块的名称,而第二个点后面才是类的名称,回顾一下,在给类起名时,首字母应该大写,所以啊,pygame提供的精灵类准确的名称是点儿sprite点儿Sprite.

第一个sprite是模块的名称,第二个Sprite才是类的名称.

现在要重点强调一个话题,在使用面向对象开发时,如果某一个类的父类不是object这个基类,也就是开发的这个类,不是继承自object基类,那么,在这个类的初始化方法中, 一定要先调用一下父类的初始化方法,现在要开发的游戏精灵类是继承自pygame的精灵类,那么试想一下,在这个父类的初始化方法中,是不是很有可能已经提前封装了一部分代码,那么,如果在子类的初始化方法中不调用父类的初始化方法,就不能够享受到父类中原本封装的代码.

所以啊,为了保证父类原本封装的初始化代码能够正常的被执行,在开发时一定要记住,如果开发的子类不是继承自object这个基类,那么,在初始化方法中一定都要主动调用一下父类的初始化方法.

来看一下游戏精灵类的需求,在游戏精灵类中啊,封装三个属性,分别是image, rect 和speed ,speed这个单词翻译过来是速度的意思,现在要做的是飞机大战的游戏.

飞机大战意味着屏幕上的每一个精灵都各自拥有不同的飞行速度,所以在这一小节的演练中,就在游戏精灵这个类中封装一个速度的属性.

要给对象定义属性应该在初始化方法中定义,那现在再强调一下,这一小节要编写的精灵子类是继承自pygame的精灵类的,所以啊,必须要在初始化方法中主动调用一下父类的初始化方法,只有这样才能够保证父类已经封装好的初始化代码能够被正常的执行,那现在来看一下初始化方法的参数.

第一个参数是image_name,第二个参数是speed ,有了image_name这个参数,就可以在初始化方法中通过image.load来加载出图像,当图像加载出来之后,怎么样指定图像的rect 属性呢?在这里分享的小技巧,在pygame的image对象,提供了一个get_rect()方法,

让图像调用一下这个方法就会返回一个矩形对象, 矩形对象的x, y都是0,但是矩形对象的宽和高就是刚刚加载出来的图像的宽和高.

通过get_rect() 这个方法,就能够在初始化方法内部非常方便的指令一下图像精灵的初始位置,然后呢,再使用speed 这个参数来指定一下游戏精灵的初始速度,如果在调用时不指定速度,就使用默认的1 来设置一下游戏精灵的速度.

这个就是游戏精灵中,要封装的三个属性,图像,位置以及速度,那接下来再来看一下update这个方法,在这一小节演练中,Update这个方法,让图像精灵的y值跟速度进行相加,要修改y值会让游戏精灵在屏幕的垂直方向进行运动,这个就是接下来要派生的精灵子类的需求.

一句话讲封装三个属性,图像, 位置以及速度, 重写一个update的方法,在update方法中,让游戏精灵在屏幕上做垂直方向的运动.

3. 派生精灵子类代码实现

接下来就参照这张类图共同实现一下游戏精灵的子类开发,已经提前准备好了plane_sprites 这个模块文件.

在这一小节要开发的游戏精灵是继承自pygame的精灵子类,所以啊,应该先在模块中使用import关键字导入一下pygame这个模块儿, 导入之后,就使用class关键字来给类起个名字,GameSprite,然后在小括号中指令一下游戏精灵的父类,写下pygame.sprite,

 第一个sprite是模块的名称,需要再敲一个点儿,然后输入大写的Sprite , 大写的Sprite才是类的名称.

 那现在就在类名下方增加一个文档注释, 写一下飞机大战游戏精灵, 文档注释增加完成, 再来看一下类图,

 在游戏精灵类GameSprite中, 需要封装三个属性, 既然要定义对象的属性, 就应该先实现一下初始化方法.

 在初始化方法的参数中, 需要传入图像的名称image_name, 以及精灵的初始速度speed.

先使用def 关键字找到__init__ 这个初始化方法, 然后呢, 然后增加两个形参. 初始化方法准备完成, 先敲一个pass 占位, 现在一个简单的初始化方法就准备完成了.

pycharm 以深黄色的背景提示初始化方法没有调用父类的初始化方法,之前提到过, 当在开发时某一个子类的父类不是object基类,一定要在初始化方法中主动调用一下父类的初始化方法.

那现在就增加两个注释, 第一步呢,应该调用父类的初始化方法,调用完成之后,再来定义对象的属性,两个步骤写完, 要想调用父类的方法,应该有一个特殊的对象,可以让super这个对象来调用一下父类的初始化方法.

当父类的初始化方法调用完成, 就可以在下方定义一下游戏精灵的三个属性, 分别是图形, 位置和速度这三个属性.

先使用self点 来定义第一个属性image, 要想从一个图像文件中加载数据,可以使用pygame点image,然后调用load 方法,把传入的image_name 当做参数,传递给这个方法,这样就可以把指定名称的图像加载到图像属性中了.

图像属性定义完成,紧接着定义一个rect 属性,这个rect 属性, 默认大小,如果要是图像的大小, 可以让image 调用一下get_rect 方法,get_rect()方法返回的大小就是刚刚加载出来的图像大小,同时x和y都是零.

现在第2个属性定义完成,再来定义第3个属性速度,

self.speed 就直接把形参的速度传递过来,现在三个属性定义完成.

就把pass这个占位符删除一下,

三个属性定义完成,再来看一下类图, 在这一小节中,还需要重写一下父类的update方法,在update方法中,让游戏的精灵在垂直方向上运动,那现在就使用def 关键字, 然后输入一个update小括号.

update方法找到之后, 游戏精灵需要在垂直方向上移动,那现在增加一个注释,在屏幕的垂直方向上移动,要在屏幕的垂直方向上移动,就可以修改一下self.rect属性的y 属性,让y属性来加上当前的速度属性,这样相加之后意味着在创建对象时指定的速度不同,那么游戏精灵在屏幕上移动的速度也会不同.

代码演进到这里,就实现了一下游戏精灵这个子类的代码,让游戏精灵继承自pygame 的精灵类,同时在初始化方法中,先调用了一下父类的初始化方法,然后呢,按照类图的需求依次定义了三个属性,图像, 位置以及速度,并且重写了一下父类的update方法,在update的方法中,对rect 的y值做了一个修改,让y值加上速度,这样呢就能够实现让游戏精灵按照垂直方向进行移动了.

再强调一下,因为在开发子类的时候,如果子类的父类不是object这个基类, 一定要记住,在初始化方法中需要主动的调用父类的初始化方法,因为不主动调用父类的初始化方法就没有办法享受到父类中已经封装好的初始化代码了.

 

4. 创建敌机并且实现敌机动画

接下来就使用刚刚派生的游戏精灵这个类来创建一个低级精灵对象,并且实现一下敌机的动画效果。在开始动手之前,先让来明确一下这一小节的演练步骤.

首先使用from这个关键字,把plane_sprites这个模块导入一下,然后呢,在游戏初始化位置来创建一个敌机的精灵对象,并且创建一个敌机的精灵组对象.

当精灵对象和精灵组对象创建完成,就在游戏循环中,让精灵组对象分别调用一下update方法和draw 方法.

在开始演练之前,再强调一下精灵和精灵组这两个对象的职责,精灵对象是负责封装精灵要显示的图像以及精灵在屏幕上的位置,并且封装一下精灵的运动速度, 同时精灵这个对象还要提供一个update方法,在update方法中根据游戏的需求来更改自己的位置变化, 这个是精灵对象的职责.

那么再看一下精灵组对象的职责,精灵组对象可以包含多个精灵对象,当精灵组对象建立完成,就可以在游戏循环中让精灵组对象来调用update 方法和draw 方法,update 方法会让精灵组中所有的精灵各自调用各自的update方法.

调用之后,精灵组中所有的精灵位置都会发生变化, 当所有精灵的位置调整完毕,就让精灵组再调用一下draw 方法,调用draw 方法之后就会把精灵组中所有的精灵全部绘制在screen这个屏幕对象上, 这个就是精灵和精灵组的职责.

明确了演练步骤,并且强调了一下精灵和精灵组的职责之后,现在就做一下代码演练,因为现在看到的代码是之前一个小节完成的监听退出事件的代码,先运行一下程序验证一下现在的代码执行效果.

游戏启动了,

现在点击叉叉,可以退出游戏.

这个是之前一个小节完成的代码.

那在这一小结中,就在之前一个小节完成的代码基础上, 来实现一下敌机精灵的创建,并且实现一下敌机的动画效果.

首先把光标放在第2行使用from这个关键字,从plane_sprites这个模块来导入一下所有的内容,

导入之后, 在开发时使用import 导入模块和from 导入模块有什么区别呀?使用import 导入模块,在使用这个模块时,必须要使用模块点的方式来使用.

而使用from来导入模块,在使用时可以直接使用模块提供的工具,而不再需要输入模块的名称.

现在飞机精灵的模块导入之后,就把代码向下方滚动,来找到游戏循环,为了看清楚这一小节完成的代码,在游戏循环上方多增加几个空行.

现在先增加一个单行注释来明确一下这一小节要演练的重点. 在游戏初始化位置应该创建敌机的精灵,创建完精灵之后, 还需要创建敌机的精灵组,精灵要在初始化位置创建, 精灵组同样也要在初始化位置创建.

那现在就把光标放在31行,先给敌机的精灵起个名字叫做enemy ,然后使用上一小节派生的游戏精灵类来创建一个精灵对象.

在创建精灵对象时, 第1个参数需要指定图像的名称,那现在先写一个点表示当前目录,然后写一个image,那要加载哪一张图像呢?就把images这个目录展开,敌机的图像是enemy1,那么就在image 后面写上enemy1.png ,图像指定完成,一个敌机的精灵也创建完成.

那现在就来创建一下敌机的精灵组,同样先给精灵组起个名字enemy_group,然后呢,使用pygame提供的sprite模块中提供的Group类来创建一个精灵组,

在创建精灵组的时候,可以把多个精灵以多值参数的方式传递给精灵组, 那么就把刚刚创建的敌机精灵传递给这个精灵组,现在敌机的精灵组也创建完成了.

那现在有了精灵,也有了精灵组, 但现在运行程序不能够看到敌机的画面.

因为要看到敌机的画面,需要在游戏循环中让精灵组调用draw 方法.

来先运行验证一下,游戏启动了,但是只有英雄的飞机孤孤单单,并没有敌机的身影,那现在先停止一下程序.

现在已经在游戏初始化位置创建出来了敌机的精灵, 创建出来了敌机的精灵组,那接下来应该把代码移动到游戏循环内部,在游戏循环中让敌机的精灵组来调用两个方法,那现在就找到update这个方法,然后多增加几个空行,在这里先增加一个注释,让精灵组调用两个方法.

让精灵组需要调用哪两个方法,第1个方法是update,第2个方法是draw.

update 方法可以让精灵组中所有的精灵对象都执行一下update 方法,而draw 方法呢,会把精灵组中所有精灵的图像绘制在屏幕上,那现在就使用enemy_group来调用一下update方法,enemy_group.update(), 调用完成,再让enemy_group来调用一下draw 方法, enemy_group.draw(screen).

在调用draw 方法的时候,需要把屏幕对象传递给方法,因为精灵组需要知道把精灵组内部的精灵绘制到谁的身上,现在就选中draw 方法,并且把之前创建好的screen 对象传递给这个方法, 又增加了两行代码,运行一下程序,看看这一次能不能看到敌机的身影.

程序启动了, 一个敌人的小飞机,从上向下再运动了,现在英雄已经不再孤单了.

代码写到这里, 创建了一个精灵,创建了一个精灵组,就已经实现了敌机的精灵动画.

那么先来看一下游戏循环内部的代码,在游戏循环中,只是让精灵组调用了一下update方法.

对比一下之前的英雄飞机,之前的英雄飞机,需要在游戏循环中修改飞机的位置,并且判断飞机的位置,

而有了精灵组之后,直接让精灵组调用一个update 方法,调用之后精灵组就能够让所有的精灵更新位置.

写一下注释,让组中的所有精灵更新位置,这个是update方法的好处.

那么再来看一下draw 方法,之前在绘制图像时都是让screen 对象调用blit 方法,然后要指定一下,把这个图像绘制到哪里,

而使用精灵组只需要调用一下draw 方法,并且把屏幕对象传递给draw 方法, 精灵组就会把内部的所有精灵全部绘制在屏幕上.

这个就是draw 方法的好处,再写下注释,在screen上绘制所有的精灵,这就是使用精灵和精灵组在开发游戏时能够大大简化游戏循环内部的代码.

那现在再看一下之前创建精灵和精灵组的代码,之前创建了一个敌机的精灵,把敌机的精灵添加到了精灵组中,添加之后, 现在运行程序, 屏幕上就有了一个敌人的飞机.

精灵组中可以包含多少个精灵?精灵组中可以包含多个精灵.

那既然可以包含多个精灵,就对这个代码进行一个扩展,再来定一个敌机的对象,给他起个名字叫做enemy1,然后同样使用GameSprite()来创建一个敌人的飞机,再指定一下图像位置,image下的enemy1.png,指定完成,在创建游戏精灵时,还可以指定一下精灵的速度.

那现在把第2架飞机的速度值乘2,这样呢,可以跟第1架敌机加以区分,现在创建了两架敌机的精灵,就可以把第2架敌机同样也添加到敌机的精灵组中.

现在就以多值参数的方式再添加一个敌机, 代码改造完成.

现在运行一下程序,看一下屏幕上是有几个敌机.

游戏启动了,两个敌人的小飞机从上向下持续飞行,并且飞行的速度各不相同.

再来对比一下现在的代码,通过一个简单的改造再创建一个敌机的精灵,把敌机的精灵添加到精灵组中, 不需要在对游戏循环的代码进行任何的修改, 轻轻松松就可以在游戏中又增加了一个敌机的对象.

在这一小节,就使用之前一个小节也派生出来的游戏精灵为创建了两架敌机,并且实现了一下敌机的动画效果.

在这一小节中,重点是要体会一下精灵和精灵组的职责,精灵这个对象负责封装图像, 位置以及速度并且提供一个update的方法,update 方法,会根据游戏的需求修改精灵自己应该更新的位置,这个是精灵的职责,而精灵组负责包含多个精灵对象, 精灵组只需要在游戏循环中,分别调用一下update方法和draw 方法就可以了,update 方法可以更新组中所有精灵的位置,而draw 方法呢会把组中所有的精灵全部绘制在屏幕对象上.