准备工作
1.准备python环境
2.准备pycharm,新建工程
3.安装pygame
Python环境的配置
配过多次python环境,久病也没成医,现配还是得查。还好开发项目之前python的环境已经配置完成了。
如果有一天,我又需要配一遍python环境,我会回来把过程记在这里。
新建工程
先下载pycharm
https://www.jetbrains.com/pycharm/
白嫖一个community版够用了,安装一路点next
新建工程,命名为Plane_war
Pygame的安装
新建一个main.py,输入import pygame报错,点小灯泡,然后让pycharm自动下载就好
总体设计
1.游戏的框架
2.什么是游戏循环——书写主游戏类和主函数
3.展示主界面
游戏框架
一个游戏由三部分组成,即初始化,游戏循环,退出
初始化完成的是游戏正式开始之前的准备工作,如导入资源,加载存档等
游戏循环意味着游戏正式开始,一般会有一个死循环,该循环以固定的帧率刷新屏幕,直到满足某个条件时,游戏退出
退出时的代码做一些释放资源,存储等操作
书写主游戏类
根据上面的说明,我们知道主游戏类至少具有:
初始化方法:初始化并创建一些资源
主方法:运行游戏循环,因此我们先写一个死循环占位
退出方法:退出游戏,不妨使用exit方法
因此,我们新建文件main_game.py
在其中定义Main_Game类:
注意:在使用pygame提供的模块前,应先执行pygame.init()方法来进行一些初始化操作,在退出游戏之前,也应该执行oygame.quit()方法
1 | import pygame |
接着,主函数main中,新建一个Main_Game对象并调用其start方法1
2
3
4
5import main_game
game = main_game.Main_Game()
game.start()
展示主界面
在pygame中,创建主界面的方法为:
pygame.display.set_mode(resolution(0, 0),flags=0,depth=0)->Surface
resolution属性制定主界面的宽和高,默认值为全屏,flags指定附加选项,depth指定颜色位数
我们需要接收创建出的Surface对象,之后的绘制工作全都在它上面进行
因此,不妨在Main_Game的init方法中,加入:
self.screen = pygame.display.set_mode()
这时再执行代码,就能看到黑色的框创建出来了
细化框架
1.分析游戏循环中应该执行那些操作
2.细化游戏循环的框架
3.设置帧率
4.实现退出游戏功能
游戏循环分析
我们知道,在游戏运行中,我们可以通过操作键盘和鼠标对游戏中的角色进行控制,因此我们需要一个事件处理函数
每当一次游戏循环运行时,游戏中的人物会移动,我们需要更新显示,因此我们需要一个显示更新函数
在Main_Game类中加入上述两个函数,并先使用pass占位1
2
3
4
5
6
7def __event_handler(self):
"""事件处理"""
pass
def __update_screen(self):
"""更新屏幕"""
pass
在游戏循环中,分别调用这两个函数:1
2
3
4
5
6
7def start(self):
"""游戏循环"""
while True:
self.__event_handler()
self.__update_screen()
设置帧率
帧率的设置需要定义设置时钟对象
要设置时钟对象,需要使用pygame中的time模块中的Clock
在Main_Game的init方法中,创建时钟对象:1
2
3
4
5
6
7def __init__(self):
"""游戏初始化"""
pygame.init()
# 创建主界面
self.screen = pygame.display.set_mode()
# 设置帧率
self.clock = pygame.time.Clock()
在start的游戏循环中,调用clock的tick方法,指定帧率为60
这里,为了避免直接使用整数,我们建立一个constant.py文件,在其中定义:
FLAME_PER_SECOND = 60
然后start方法改为:1
2
3
4
5
6
7
8def start(self):
"""游戏循环"""
while True:
self.clock.tick(constants.FLAME_PRE_SECOND)
self.__event_handler()
self.__update_screen()
实现退出事件监听
游戏的退出是事件的一种,我们在事件处理函数中,如果检测到退出事件,应调用game_over函数
想要得到发生的事件,需要使用pygame.event.get()函数来得到当前事件列表
event_handler函数改成:1
2
3
4
5def __event_handler(self):
"""事件处理"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
Main_Game.game_over()
现在,点击黑色框的×可以将游戏正常退出了
工具类
1.Rect类
2.Sprite
3.Sprite组
Rect类
pygame.Rect是pygame中描述矩形的类,其中主要属性有:
x,y分布表示左上点的位置横纵坐标,其中屏幕的最左上点坐标为(0,0)
left表示最左边x坐标,right类推
top表示顶部y坐标,bottom类推
center是一个二元组(centerx,centery),表示中心位置
size是一个二元组(width,height)表示矩形大小
创建Rect对象需要传入四元组(x, y, w, h)
Sprite
pygame.sprite.Sprite类,翻译过来是精灵。实际上表示游戏中可以独立活动的一个整体,比如一架飞机,或者一颗子弹。
使用Sprite的好处是,我们可以把一个独立活动的整体设置成一个对象,定义了其update方法之后,就可以让它自动在屏幕上,以我们设置好的方法移动,避免我们在每次循环中手动绘制游戏画面
主要方法和属性有:
image是一个图像
导入一个图像可以使用pygame.image.load(路径)方法
rect是该精灵的位置大小等信息
update方法:设置更新精灵位置的方式
kill方法:销毁该精灵,并在所有组中删除
精灵组
我们可以把精灵加入精灵组,这样我们就可以同时调用一组精灵的update方法,并方便我们针对不同种类的精灵进行不同的操作
精灵组位于pygame.sprite.Group
主要方法和属性:
add(精灵列表):向该组中添加精灵
update方法:调用该组中所有精灵的update方法
draw(Surface):把组中所有精灵的位置绘制在屏幕上
Sprites():返回组中精灵的列表
需要更新精灵的位置时,一般要三步:
使用精灵组的update方法,更新精灵的位置属性
使用精灵组的draw方法,绘制在屏幕上
调用pygame.display.update()方法来更新显示
精灵和精灵组
1.创建游戏精灵
2.在主游戏类中创建游戏精灵组
3.创建背景精灵
4.实现背景的更新
5.进一步完善
创建游戏精灵
经过上篇,我们知道,游戏精灵就是游戏中所有运动的对象,它继承自pygame.sprite.Sprite,而我们创建自己的游戏精灵,至少需要重写其init和update方法
在本游戏中,我们先创建一个基本游戏精灵,并令其它的精灵都继承自这个基本精灵。
新建一个类,命名为:Game_Sprite,继承自pygame.sprite.Sprite
基本精灵应该有一个图片类型的数据以绘制其图像,指定其初始位置,还应该有横向和纵向的速度,因此,我们的init函数重写为:
这里默认了游戏中所有类创建初始位置是(0,0),初始横向速度为0,纵向速度为1,即默认水平
1 | def __init__(self, image_file: str, init_pos=(0, 0), speed_x=0, speed_y=1): |
这样,我们的update函数就可以使用速度来更新精灵的位置了:
1 | def update(self, *args, **kwargs) -> None: |
创建游戏精灵组
在游戏初始化时,我们需要创建精灵,因此,我们先添加一个创建精灵组函数,统一创建我们想要的精灵组,然后在主类初始化的时候调用它
1 | def __create_sprites(self): |
主类的初始化函数修改为:
1 | def __init__(self): |
创建背景精灵
我们首先创建背景精灵,绘制背景图案
首先,在constants.py这个存放常数的文件里面添加背景图片的路径:
1 | BACKGROUND_FILE = "./images/background.png" |
新建一个类,命名为Background_Sprite,继承自我们的游戏精灵Game_Sprite,重写其init方法为调用父类的init方法并传入背景图片路径
1 | import pygame |
在主类的__create_sprites函数里,先为背景精灵创建一个背景精灵组:
1 | def __create_sprites(self): |
更新屏幕
尽管我们已经创建了背景精灵,但为了能够让它正常地运动起来,我们还需要调用精灵组的update方法,这里,我们在主类中实现一个更新屏幕的函数,在其中调用每个精灵组的update和draw方法以更新它们在屏幕中的位置:
1 | def __update_screen(self): |
在每个游戏循环的最后都加入这个更新屏幕方法
1 | def start(self): |
这样,我们就可以运行的时候看到下降的背景画面了
进一步完善
背景虽然下降了,但下降到屏幕外面就没了,我们显然不希望这样,我们希望背景能够一直存在,并且一直动态地下降,这样,我们就使用两张图片交替显示的方式,当一张图片完全移动到屏幕外面的时候,我们再重新把它放在屏幕的最上方
可以通过修改背景精灵的update方法来实现这样的功能,每次update时,先调用父类的update使其正常下移,然后判断图片的y坐标是否已经下降到屏幕外(y > 屏幕的高度),若是,则把y重新调到上面去
1 | def update(self, *args, **kwargs) -> None: |
写完这里,可以再执行一下测试能否实现上述的功能
之后就是两张图片如何交替移动的问题了,其实很简单,我们只需要把背景图片组中添加两个初始位置不同的图片就可以了,一张的初始位置在(0,0),另一张不妨就设置到第一张的上方
为此,我们修改一下主类的创建背景精灵函数:
1 | def __create_sprites(self): |
这样再执行,背景图片就可以非常连贯地交替下移了
敌机
1.创建敌机
2.定时器——敌机随机出现
创建敌机
首先,把敌机图片的路径写入到constants里面
1 | # 敌机图片路径 |
然后,创建敌机精灵类,命名为Enemy_Sprite
1 | import pygame |
敌机出现时总是在屏幕最上方,位置x随机,垂直速度speed_y为1~3随机,没有水平速度
这样,我们定义敌机的init方法为:
1 | def __init__(self): |
敌机的更新方式不需要大的改动,但有一点需要注意的是,当敌机已经飞出屏幕的时候,我们就不需要它了,所以,我们需要把这个敌机对象杀死
这就需要使用到我们的析构函数del和精灵的kill方法
析构函数和update:
1 | def update(self, *args, **kwargs) -> None: |
这时候可以在主类里面创建一个敌机,看看是不是可以正确行动
在create_sprites里面添加:
1 | # 创建敌机组 |
然后在update_screen里面添加:
1 | # 更新敌机 |
随机出现的敌机
我们需要使用pygame中的计时器pygame.time.set_timer()
使用方法是:传入两个参数,第一个为事件id,第二个为事件间隔时间,单位毫秒
事件id是自己定义的,设定计时器之后,每次计时器到达时间点事件id会被放入事件列表
可以使用pygame自带的事件id,pygame.USERENEVT,之后的事件可以在此基础上+1
在constants里面设定敌机出现事件的事件id和时间间隔:
1 | # 创建敌机事件id |
在主类的init方法里添加:
1 | # 创建敌机计时器 |
这样,事件处理列表中需要变为:
1 | def __event_handler(self): |
再把之前为了测试敌机的行为写的代码去掉,即create_sprites中创建敌机组时,直接创建一个空组,不再上来就为其添加一个敌机
运行,敌机可以随机创建了
主角
1.创建英雄
2.英雄发射子弹
创建英雄
首先添加英雄图片路径,在constant里加入:
1 | # 英雄图片路径 |
创建英雄精灵类Hero_Sprite,我们假设英雄初始位置在界面底部中间位置,初始无速度,定义其init方法:
1 | class Hero_Sprite(game_sprite.Game_Sprite): |
然后在主类create_sprites中添加英雄组,在英雄组中加入英雄:
1 | # 创建英雄组 |
同时在更新屏幕函数中添加英雄组的更新
1 | # 更新英雄 |
发射子弹
添加子弹图片路径
1 | # 子弹图片路径 |
子弹从英雄图片的正上方发射,默认竖直向上移动
所以,创建子弹时,我们需要传出英雄的位置参数
定义子弹类Bullet_Sprite,其初始化方法:
1 | class Bullet_Sprite(game_sprite.Game_Sprite): |
由于子弹飞出屏幕之后我们也需要销毁它,同样改写update:
1 | def update(self, *args, **kwargs) -> None: |
在create_sprite函数里面添加子弹组,并向里面添加一发子弹来测试效果,因为子弹的发射初始位置和英雄的位置有关,我们遍历英雄组,取英雄位置作为子弹的初始参数
1 | # 创建子弹组 |
在更新屏幕函数里面添加更新子弹:
1 | # 更新子弹 |
可以看到子弹的行为正常
接下来我们添加定时发射子弹,首先添加发射子弹事件id,并设置定时器
constants中:
1 | # 添加子弹事件id |
主类的init中添加:
1 | # 创建子弹计时器 |
把刚刚为了测试而写在create_sprite里面遍历英雄发射子弹的代码,移动到事件处理函数里:
1 | def __event_handler(self): |
这样,我们的英雄就会持续发出子弹了
主角操作
1.捕获键盘实现英雄的移动
2.碰撞检测来摧毁敌机
3.碰撞检测牺牲英雄
英雄移动
为了方便控制英雄,我们修改创建英雄方法,把创建出的英雄也作为主类的一个属性保存起来
1 | # 创建英雄组 |
我们使用pygame.key.get_pressed()方法,该方法可以检测当前被按下的键,并返回一个元组,元组中,凡是被按下的键对应的值都是true
我们在检测事件中加入:
1 | key_pressed = pygame.key.get_pressed() |
这样,英雄就可以在我们的控制下移动了,但有问题的是,此时英雄可以移出屏幕外,我们不希望这样,因此我们在英雄的update方法里面增加一个边界检测:
1 | def update(self, *args, **kwargs) -> None: |
这样,英雄的移动就做好了
摧毁敌机
使用碰撞检测pygame.sprite.groupcollide()来判断子弹和敌机是否碰撞。
传入四个参数,前两个是检测碰撞的组,后面接上两个True,表示检测到碰撞后清除碰撞在一起的对象
可以在事件监听函数中加入碰撞检测
1 | pygame.sprite.groupcollide(self.enemy_sprite_group, self.bullet_sprite_group, True, True) |
牺牲英雄
同样使用碰撞检测,只不过这次不使用英雄组,而是使用英雄对象,检测某个组和某个对象有碰撞的函数是pygame.sprite.spritecollide,参数是对象,组名,是否杀死,我们检测英雄对象和敌机组有没有碰撞,这个函数会返回敌机列表
1 | enemies = pygame.sprite.spritecollide(self.hero, self.enemy_sprite_group, True) |