一、模块
1. 利用 pip 安装 pygame 模块
Windows系统下的安装参考如下文章:
https://blog.csdn.net/qq_38721302/article/details/83243632
注:应在PyCharm的系统解释器的 Scripts 目录安装了 pygame 再新建工程
Linux系统下的安装:
安装pygame
sudo pip3 install pygame
验证安装(aliens是一个内置小游戏)
python3 -m pygame.examples.aliens
2. 图片素材下载
图片素材直接在下面百度云链接下载即可:
https://pan.baidu.com/s/1pMM0beb
二、pygame 模块初识
1. 游戏的初始化和退出
方法 | 含义 |
---|---|
pygame.init() | 导入并初始化所有pygame模块,使用其他模块之前,必须先调用init方法 |
pygame.quit() | 卸载所有pygame模块,在游戏结束之前调用 |
2. pygame 中的游戏坐标系
原点在左上角 O(0,0)
x轴水平方向向右,逐渐增加
y轴竖直方向向下,逐渐增加
在游戏中,所有可见的元素都是以上述矩形区域来描述位置的
在 游戏窗口(坐标系)中,要描述一个矩形区域应该有四要素:(x,y) 和 (width,height),其中 (x,y) 描述的是矩形区域左上角(原点)所在的坐标,而 (width,height) 则控制矩形区域的大小
在pygame中提供了一个类 pygame.Rect,其用于描述矩形区域:
矩形区域对象名 = pygame.Rect(x, y, width, height)
创建一个矩形区域的对象
其中:x, y, width, height 均属于对象属性,可以通过对象名来访问
另有一个 size 的元组属性:它是一个元组——(width, height),可以直接访问到 width 和 height,可以作为参数值来传递到 pygame.display.set_mode(…) 中,之后会讲到
例:在坐标系原点创建一个width=100,height=140的 plane 对象
import pygame
pygame.init()
plane = pygame.Rect(0, 0, 100, 140)
print("飞机所在位置:(%d,%d),飞机的大小:(%d,%d)" % (plane.x, plane.y, plane.width, plane.height))
pygame.quit()
第6行代码更改为下述代码也可以:
print("飞机所在位置:(%d,%d)" % (plane.x, plane.y), "飞机的大小:(%d,%d)" % plane.size)
3. 创建游戏主窗口
在pygame中提供了一个模块 pygame.display,其用于创建、管理游戏主窗口:
方法 | 含义 |
---|---|
pygame.display.set_mode() | 初始化游戏显示窗口 |
pygame.display.update | 刷新显示游戏窗口 |
游戏主窗口对象名 = pygame.display.set_mode(resulution=(0, 0), flags=0, depth=0)
创建一个游戏显示窗口的对象
其中:resoluthon, flags, depth均属于对象属性
resolution 是一个元组,需要传递一个元组,它指定屏幕的宽和高,缺省时默认值为整个屏幕的大小,这个元组可以是 pygame.Rect(…) 函数返回的矩形区域对象的 size 属性 ——它是个元组
flags 指定屏幕的附加选项,例如是否全屏等,默认不需要传递
depth 表示颜色的位数,默认自动匹配
返回值 游戏的主窗口,游戏的元素都需要被绘制到游戏的主窗口上
例:创建一个 (480,700) 的游戏主窗口
import pygame
import time
"""窗口大小:(480,700)"""
window = pygame.display.set_mode((480, 700))
"""系统休眠5秒"""
time.sleep(5)
pygame.quit()
4. 游戏主窗口上图像的绘制
在游戏中,能够看到的游戏元素大多数是图像,图像文件初始是保存在磁盘上的,如果需要使用,第一步就需要把图像加载到内存中,一般要在屏幕上看到某一个图像的内容,需要安装下述三个步骤来进行:
使用 pygame.image.load() 加载图像的数据
使用 游戏主窗口 对象来调用 blit 方法将图像绘制到指定的位置
使用 pygame.display.update() 方法更新整个屏幕的显示
图片对象名 = pygame.image.load(图片所在路径)
把图像打开并把数据加载到内存中,其中路径可以是图片相对源代码所在 .py文件 的 相对路径 或 绝对路径,返回图片对象
游戏主窗口对象名.blit(图片对象名, 矩形区域对象或(x, y)坐标)
在游戏主窗口中绘制图像
矩形区域对象 图片对象在游戏主窗口中矩形区域原点对应的位置,一般所设置的这个矩形区域与图片的大小一致,以此来让图片对应这片矩形区域——图片负责动画的实现,矩形区域负责游戏的逻辑控制
(x, y) 坐标元组,即图片对象在游戏主窗口的位置pygame.display.update()
刷新显示游戏主窗口
提示:要想在屏幕上看到绘制的结果,就一定要调用 pygame.display.update() 方法
例:我们尝试将飞机大战的背景图像 background.png 文件加载到游戏主窗口上
import pygame
import time
pygame.init()
"""创建游戏主窗口"""
window = pygame.display.set_mode((480, 700))
"""加载图片到内存"""
background = pygame.image.load("./游戏素材/background.png")
"""在游戏主窗口中显示图片"""
window.blit(background, (0, 0))
"""刷新显示"""
pygame.display.update()
"""系统休眠5秒"""
time.sleep(5)
pygame.quit()
提示:我们可以通过 blit() 方法将游戏主窗口的所有的图像布置完成后,再使用 pygame.display.update() 刷新游戏主窗口即可
5. 阶段小结(一):英雄飞机显示
需求:把英雄飞机 me1.png 或 me2.png 文件显示在游戏主窗口的背景上
import pygame
import time
pygame.init()
"""创建游戏主窗口"""
window = pygame.display.set_mode((480, 700))
"""加载需要的图片"""
background = pygame.image.load("./游戏素材/background.png")
me1 = pygame.image.load("./游戏素材/me1.png")
"""绘制背景和飞机"""
window.blit(background, (0, 0))
window.blit(me1, (189, 574))
"""刷新显示"""
pygame.display.update()
time.sleep(5)
pygame.quit()
我们使用 PyCharm 或 PS 打开图片,发现英雄飞机图片的背景是一系列灰白相间的小格子,这些灰白相间的小格子代表英雄飞机是透明背景:
6. 扩展
本文只针对 pygame 模块中一些简单的功能进行介绍,实际上 pygame 还拥有很多强大的功能,若有兴趣学习的可以参考下述文章:
pygame官网:https://www.pygame.org/news
pygame官网文档:https://www.pygame.org/docs/
三、游戏主功能
1. 动画实现原理——帧 Frame
我们知道,视频是一帧一帧地播放的,其实质是由一帧一帧变化的图像的动态刷新显示来呈现运动的效果,其利用了肉眼的视觉暂留原理,因此我们可以利用计算机对多张图片的快速刷新显示,即可达到动画的效果
一般在电脑上每秒绘制60次,就能够达到非常连续高品质的动画效果,每次绘制的结果被称为 帧 Frame
每秒播放图像的次数被称为 每秒传输帧数 FPS(Frames Per Second)),这就是游戏中的 fps
2. 游戏循环
通常,游戏循环意味着游戏的正式开始
游戏循环的作用:
保证游戏不会直接退出
变化图像位置——达到动画效果
每隔1/60秒移动一下所有图像的位置
调用 pygame.display.update() 刷新显示
检测用户的交互——键盘、鼠标等外设
3. 游戏时钟
在Python中,while True: 循环的速度可达每秒钟数十万次,而我们对图像帧率的要求不需要这么高,pygame 中有一个时钟类 pygame.time.Clock 可以非常方便地设置游戏主窗口的绘制速度——刷新帧率
时钟对象名 = pygame.time.Clock()
创建一个时钟对象
时钟对象名.tick(帧)
可以设置时钟的帧率,相当于在调用 tick() 方法的位置处暂停 1/帧 秒的时间,以此来实现固定帧率的刷新
例:
import pygame
pygame.init()
clock = pygame.time.Clock()
i = 0
while True:
clock.tick(1)
print(i)
i += 1
pygame.quit()
这里的0、1、2、3、4是每秒钟增加一个的
4. 英雄飞机的简单动画显示
注:由于 blit() 方法可以传递 坐标元组 或者 矩形区域对象,因此在创建了一个矩形区域对象后,我们就可以直接用矩形区域对象来作为动画播放时图片的位置,让图片位置与矩形区域位置一一对应
例:利用 Rect 的矩形区域在y轴上的移动来实现飞机的移动
import pygame
pygame.init()
"""创建游戏主窗口"""
window = pygame.display.set_mode((480, 700))
"""加载需要的图片"""
background = pygame.image.load("./游戏素材/background.png")
me1 = pygame.image.load("./游戏素材/me1.png")
"""创建一个矩形区域对象,设置初始位置"""
hero_plane = pygame.Rect(189, 574, 102, 126)
"""设定一个时钟"""
clock = pygame.time.Clock()
"""绘制背景"""
window.blit(background, (0, 0))
while True:
window.blit(me1, hero_plane)
"""刷新显示"""
pygame.display.update()
hero_plane.y -= 50
clock.tick(1)
pygame.quit()
5. 英雄飞机的正确动画显示
注:由于在同一张背景下绘制的图片不会消失,因此如果想要在一张背景下实现英雄飞机的动画式的变化,可以在游戏主窗口中再绘制一张背景,把原来的图像覆盖掉,再绘制英雄飞机图片,这样就可以实现英雄飞机的动画式变化
以上述代码为基础,只需把window.blit(background, (0, 0))
放在while True:
中即可实现
while True:
"""绘制背景"""
window.blit(background, (0, 0))
window.blit(me1, hero_plane)
"""刷新显示"""
pygame.display.update()
hero_plane.y -= 50
clock.tick(1)
注:这里飞机会飞出屏幕,而我们只需给矩形区域的位置做简单的判断即可实现飞机周而复始的运动;另外,我们调整一下帧率和飞机的移动距离,实现动画的连贯播放
if hero_plane.y + hero_plane.height <= 0:
hero_plane.y = 700
else:
hero_plane.y -= 1
clock.tick(240)
6. 英雄飞机的动作动画显示
利用两张或多张不同的图片在同一位置的显示,来完成英雄飞机的动作——喷气
例:利用 me1.png 和 me2.png 的不同来刷新显示英雄飞机的变化
注:由于在同一个背景下绘制的图像不会消失,因此要想在背景下显示不同的影响,则需要绘制新的背景覆盖原背景
import pygame
pygame.init()
"""创建游戏主窗口"""
window = pygame.display.set_mode((480, 700))
"""加载需要的图片"""
background = pygame.image.load("./游戏素材/background.png")
me1 = pygame.image.load("./游戏素材/me1.png")
me2 = pygame.image.load("./游戏素材/me2.png")
"""设定一个时钟"""
clock = pygame.time.Clock()
while True:
"""绘制背景和飞机"""
window.blit(background, (0, 0))
window.blit(me2, (189, 574))
"""刷新显示"""
pygame.display.update()
clock.tick(5)
"""重新绘制背景,以覆盖原来的图像"""
window.blit(background, (0, 0))
window.blit(me1, (189, 574))
"""刷新显示"""
pygame.display.update()
clock.tick(5)
7. 在游戏循环中监听事件
事件 event,即游戏启动后,用户针对游戏所做的交互操作,如:按下键盘,点击鼠标
Python中提供了一个 pygame.event.get() 方法可以获得用户当前所做的动作的事件列表,用户可以在同一时间做很多事件,该方法的返回值即为游戏主窗口上发生的事件列表
例如在上面的例子中添加如下代码可以监听事件:
event_list = pygame.event.get()
if len(event_list) > 0:
print(event_list)
例:监听用户 × 退出 游戏界面
"""游戏循环"""
while True:
"""设置屏幕刷新帧率"""
clock.tick(60)
"""事件监听"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
print("退出游戏...")
pygame.quit()
"""直接退出游戏"""
exit()
提示:这段代码非常的固定,几乎所有的pygame游戏都大同小异
四、pygame高级类:精灵与精灵组
1. 精灵与精灵组简介
在上述的开发中,图像加载、位置变化、绘制图像都需要我们自己编写代码来分别进行处理,而为了简化这些开发步骤,pygame提供了两个类:
pygame.sprite.Sprite——用于创建 【 具有图像数据属性:image属性 和精灵位置属性:rect属性 的对象——精灵 】 的类
pygame.sprite.Group——精灵组是包含了多个精灵对象的类
2. 派生精灵子类
pygame.sprite.Sprite 和 pygame.sprite.Group 自带的方法如下图所示:
精灵规定两个固有实例属性:
self.image属性 为图像,一般通过 pygame.image.load(图片路径) 来加载获得
self.rect属性 为在游戏主窗口上的位置,一般通过 self.image.get_rect() 获得
pygame.image.load(图片路径) 方法中,图片路径可以是绝对路径,也可以是相对路径,用于将图片加载到内存中,返回一个图像对象,一般应将图片放在当前工程文件下
self.image.get_rect() 方法会返回同 pygame.Rect(0, 0, 图像宽, 图像高) 类似的对象,注意:所在坐标系的位置为(0, 0),大小为 self.image 的大小,坐标位置可以通过其返回值对象的属性 x 来对横坐标进行调整
精灵和精灵组的原生内置方法都是基于 image 和 rect 这两个名字来调用的属性的,因此,如果不按这两个名字来赋予属性,那么精灵和精灵组的内置方法将不可用,精灵就变成具空壳(重要)
精灵需要派生子类,这是因为原生精灵的 __init__() 方法没有定义 self.image 和 self.rect——当然,这个肯定是留给开发者来写的,精灵用什么图像,大小如何,自然是由开发者来决定
我们需要通过派生类来重写 __init__() 方法,来传递 self.image 和 self.rect 需要的参数,不过在此之前我们需要用 super().init() 调用一下精灵的初始化方法,因为里面有定义其他内容,否则直接重写方法会覆盖掉(重要)
image 是精灵图像的图片, rect一般通过self.image.get_rect() 方法获得,只要我们得到了 image,那么 rect 也自然得到
其次,精灵规定了一个固有方法:update(),但没有内容,需要在派生类中重写,这个方法用来写精灵的运动,通过 self.rect.x 和 self.rect.y 来设计
在创建完精灵后,我们需要把同一派生类的精灵归入一个精灵组,通过:
精灵组名 = pygame.sprite.Group(精灵1, 精灵2…精灵N) 来归入同一个精灵组
当然,如果想要向已定义的精灵组中增加精灵,可以调用 精灵组名.add(精灵列表) 方法
当一个精灵阵亡了——即不需要再绘制在屏幕上了,则需要该精灵移出精灵组,调用精灵的 kill() 方法可以把精灵移出其所属的精灵组,同时该精灵的内存会被释放
对于同一个精灵组的精灵来说,精灵组名.update() 方法可以让属于该精灵组的所有精灵调用精灵的 update() 方法,即调用该方法后,该精灵组的所有精灵发生 update() 规定的运动,这也是为什么精灵里有一个固有方法 update() 的原因,是因为精灵组的 update() 方法是基于精灵的 update() 方法来写的(重要)
对于同一个精灵组的精灵来说, draw(Surface) 方法可以让属于该精灵组的所有精灵绘制在游戏主窗口 Surface 上(重要)
draw(Surface) 中 Surface 传递的是游戏主窗口对象
图中 screen 指的是游戏主窗口
调用 random 模块可以实现随机创建不同类型的敌机;随机设置敌机不同的出场位置;随机设置敌机不同的出场速度
创建游戏精灵类——派生于精灵类:
新建 plane_sprites.py 文件,定义游戏精灵类 GameSprite 继承自 pygame.sprite.Sprite
属性:
image 精灵的图像,使用 image_name 为图片名通过 pygame.image.load(…) 来加载
rect 精灵大小,默认使用图像大小
speed 精灵移动速度,默认为1
方法:
__init__ 初始化上述属性值
update 每次更新屏幕时在游戏循环内调用,让精灵的 self.rect.y += self.speed——向下移动
注:
如果一个类的父类不是 object
在重写初始化方法 __init__ 时,一定要先 super().__init__(…) 调用一下原父类方法,防止原父类方法的内容被覆盖(比如:定义的 GameSprite 继承自 pygame 中 sprite 模块的 Sprite 类)
这样才能保证父类中实现的 __init__ 代码能够被正常执行
plane_sprites.py 文件的 代码:
import pygame
class GameSprite(pygame.sprite.Sprite):
def __init__(self, image_name, speed=1):
"""调用父类的初始化方法"""
super().__init__()
"""定义属性"""
self.image = pygame.image.load(image_name)
self.rect = self.image.get_rect()
self.speed = speed
def update(self):
"""精灵在游戏主窗口垂直方向下移动"""
self.rect.y += self.speed
3. 使用精灵和精灵组创建敌机
敌机是游戏中会动的对象,可以作为精灵
需求:
使用上述派生的精灵类 Gamesprite 创建敌机精灵类,并实现敌机动画
即使用 plane_sprites.py 文件中定义的类来进行创建
步骤:
使用 from 导入 plane_sprites 模块
from 导入的模块可以直接使用
import 导入的模块需要通过 模块名. 来使用
在游戏初始化创建精灵对象和精灵组
在游戏循环中让精灵组分别调用 update() 和 draw(screen) 方法
职责:
(1) 精灵:
封装图像 image、位置 rect 和速度 speed
提供 update() 方法,根据游戏需求,更新位置 rect
(2) 精灵组:
包含多个精灵对象
update() 方法,让精灵组中的所有精灵调用 update() 方法更新位置
draw(screen) 方法,在 screen 上绘制精灵组中的所有精灵
import pygame
from plane_sprites import *
pygame.init()
"""创建游戏主窗口"""
window = pygame.display.set_mode((480, 700))
"""加载需要的图片"""
background = pygame.image.load("./游戏素材/background.png")
"""创建敌机精灵和精灵组"""
enemy_plane1 = GameSprite("./游戏素材/enemy1.png", speed=3)
enemy_plane2 = GameSprite("./游戏素材/enemy2.png", speed=2)
enemy_plane3 = GameSprite("./游戏素材/enemy3_n1.png")
enemy_group = pygame.sprite.Group(enemy_plane1, enemy_plane2, enemy_plane3)
"""设定一个时钟"""
clock = pygame.time.Clock()
while True:
"""绘制背景"""
window.blit(background, (0, 0))
"""绘制所有敌机精灵"""
enemy_group.draw(window)
enemy_group.update()
"""刷新显示"""
pygame.display.update()
"""设置帧率"""
clock.tick(60)
pygame.quit()
注:image 中的 get_rect() 方法默认返回 pygame.Rect(0, 0, 图像宽, 图像高) 的对象,其在坐标系中的位置为(0, 0),但其横坐标可以通过其返回值对象的属性 x 来进行调节
五、游戏框架的搭建
目标:使用面向对象设计飞机大战游戏类
1. 文件分配
根据需求,我们实际上只需创建2个 .py 文件即可,plane_main.py 文件作为飞机大战游戏的主程序文件, plane_sprites.py 文件作为各个精灵类的定义文件
plane_main.py :
封装主游戏类
创建游戏对象
启动游戏
plane_sprites.py:
封装游戏中所有需要使用的精灵子类
提供游戏的相关工具
英雄飞机直接创建,敌机使用精灵与精灵组来创建,需求如下图所示:
plane_sprites.py 文件代码:
import pygame
# 屏幕大小的常量
SCREEN_RECT = pygame.Rect(0, 0, 480, 700)
class GameSprite(pygame.sprite.Sprite):
"""游戏主类——继承自pygame.sprite.Sprite"""
def __init__(self, image_name, speed=1):
# 调用父类的初始化方法
super().__init__()
# 定义属性
self.image = pygame.image.load(image_name)
self.rect = self.image.get_rect()
self.speed = speed
def update(self):
# 在屏幕的垂直方向x向下移动
self.rect.y += self.speed
注:为了代码的可更改性(需求)与可阅读性——后续修改代码时只需修改常量即可,我们定义了矩形区域对象常量:
SCREEN_RECT = pygame.Rect(0, 0, 480, 700)
——该矩形区域对象用于代表游戏主窗口,因此我们后续将通过:
pygame.display.set_mode(SCREEN_RECT.size) 来创建游戏主窗口
plane_main.py 文件代码:
from plane_sprites import *
class PlaneGame(object):
"""主游戏类"""
def __init__(self):
print("游戏正在初始化...")
# 创建游戏主窗口
self.screen = pygame.display.set_mode(SCREEN_RECT.size)
# 创建游戏时钟
self.clock = pygame.time.Clock()
# 调用私有方法,创建精灵和精灵组
self.__create_sprites()
def __create_sprites(self):
"""用于创建精灵和精灵组"""
pass
def start_game(self):
"""启动游戏"""
# 游戏主循环
while True:
# 设置刷新帧率为60
self.clock.tick(120)
# 事件监听
self.__event_handler()
# 碰撞检测
self.__check_collide()
# 位置更新
self.__update_sprites()
# 游戏主窗口刷新显示
pygame.display.update()
def __check_collide(self):
"""碰撞检测"""
pass
def __event_handler(self):
"""事件监听"""
pass
def __update_sprites(self):
"""位置更新"""
self.background_group.update()
self.background_group.draw(self.screen)
@staticmethod
def __game_over():
# 结束游戏
print("游戏结束...")
pygame.quit()
exit()
if __name__ == '__main__':
# 初始化pygame
pygame.init()
# 创建游戏对象
game = PlaneGame()
# 启动游戏
game.start_game()
2. 游戏方式——背景的轮换滚动
虽然在 三、游戏主功能 5.英雄飞机的正确动画显示 中,我们讨论了游戏飞机向上移动的问题
但实际上,在许多跑酷类游戏的开发套路中,我们使用背景图像的运动来代替英雄的运动,那么英雄只需停留在屏幕的某一位置(可以左右移动),即可在视觉上产生英雄飞机正在向前移动的错觉
思路构图如图所示:
创建两个背景图像精灵,之所以使用精灵,是因为背景图像也像敌机精灵一样移动
开始时,背景图像1和游戏主窗口完全重合,背景图像2在屏幕的正上方
两背景图像将一起向下移动:self.rect.y += self.speed
当任意背景精灵的 rect.y >= 屏幕的深度 时说明当前背景精灵已经移动到屏幕下方,那么我们需要将 移动到屏幕下方的这张背景图像 设置到 游戏主窗口的正上方,即:rect.y = -rect.height
实现上述功能即可实现图像的连续滚动。在原 plane_sprites.py 文件中,我们只定义了敌机精灵的向下移动,而屏幕精灵也可以使用这个向下移动方法,但需要增加屏幕图像的轮换滚动,就需要在原 GameSprite 类中派生一个子类,再重写 update() 方法(这个方法扩展了对图像位置的判断);其次,我们还需对方法 __init__(…) 进行扩展,即在创建背景图像精灵时,判断创建的是不是背景图像2,如果是背景图像2,则应该设置其初始位置所在坐标应为游戏主窗口的正上方
plane_sprites.py 文件新增代码——BackGround 类:
class BackGround(GameSprite):
"""背景图像类,继承自GameSprite"""
def __init__(self, is_alt=False):
# 调用父类方法创建精灵对象
super().__init__("./游戏素材/background.png")
# 判断是否为背景图像2,若是则改变初始坐标位置
if is_alt:
self.rect.y = -SCREEN_RECT.height
def update(self):
# 调用父类方法——向下移动
super().update()
if self.rect.y >= SCREEN_RECT.height:
self.rect.y = -SCREEN_RECT.height
3. 飞机大战核心框架
根据上述思想所得——类的继承关系:
补全部分pass代码和其他部分代码后的代码——主框架:
plane_sprites.py 文件代码:
import pygame
# 屏幕大小的常量
SCREEN_RECT = pygame.Rect(0, 0, 480, 700)
class GameSprite(pygame.sprite.Sprite):
"""游戏主类——继承自pygame.sprite.Sprite"""
def __init__(self, image_name, speed=1):
# 调用父类的初始化方法
super().__init__()
# 定义属性
self.image = pygame.image.load(image_name)
self.rect = self.image.get_rect()
self.speed = speed
def update(self):
# 在屏幕的垂直方向x向下移动
self.rect.y += self.speed
class BackGround(GameSprite):
"""背景类,继承自GameSprite"""
def __init__(self, is_alt=False):
# 调用父类方法创建精灵对象
super().__init__("./游戏素材/background.png")
# 判断是否为背景图像2,若是则改变初始坐标位置
if is_alt:
self.rect.y = -SCREEN_RECT.height
def update(self):
# 调用父类方法——向下移动
super().update()
if self.rect.y >= SCREEN_RECT.height:
self.rect.y = -SCREEN_RECT.height
plane_main.py 文件代码:
from plane_sprites import *
class PlaneGame(object):
"""主游戏类"""
def __init__(self):
print("游戏正在初始化...")
# 创建游戏主窗口
self.screen = pygame.display.set_mode(SCREEN_RECT.size)
# 创建游戏时钟
self.clock = pygame.time.Clock()
# 调用私有方法,创建精灵和精灵组
self.__create_sprites()
def __create_sprites(self):
# 创建背景精灵
background1 = BackGround()
background2 = BackGround(is_alt=True)
# 创建背景精灵组
self.background_group = pygame.sprite.Group(background1, background2)
def start_game(self):
"""启动游戏"""
# 游戏主循环
while True:
# 设置刷新帧率为60
self.clock.tick(120)
# 事件监听
self.__event_handler()
# 碰撞检测
self.__check_collide()
# 位置更新
self.__update_sprites()
# 游戏主窗口刷新显示
pygame.display.update()
def __check_collide(self):
"""碰撞检测"""
pass
def __event_handler(self):
"""事件监听"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
PlaneGame.__game_over()
def __update_sprites(self):
"""位置更新"""
self.background_group.update()
self.background_group.draw(self.screen)
@staticmethod
def __game_over():
# 结束游戏
print("游戏结束...")
pygame.quit()
exit()
if __name__ == '__main__':
# 初始化pygame
pygame.init()
# 创建游戏对象
game = PlaneGame()
# 启动游戏
game.start_game()
4. 敌机定时与随机出场设计
约定:
游戏启动后,每隔1秒会出现一架敌机——定时器
每驾敌机像屏幕下方飞行,飞行速度各不相同——随机
每架敌机出现的水平位置也不尽相同——随机
当敌机从游戏主窗口下方飞出时,不会再回到屏幕中——删除精灵对象
定时器:
在pygame中可以使用 pygame.time.set_time() 来添加定时器,所谓定时器,就是中断——即每隔固定的一个时间段就发出一次信号
pygame.time.set_time(eventid, millisecond)
添加定时器,没有返回值
eventid 事件代号,需要基于常量 pygame.USEREVENT来指定,USEREVENT 是一个整数,再增加的事件可以使用 USEREVERT + 1 指定,以此类推
millisecond 单位:毫秒,即定时器的触发时间间隔
提示:设置定时器后,定时器每隔一段时间就会发出一个事件——eventid,而这个事件是可以被pygame.event.get() 监听到的,因此,我们只需在 for event in pygame.event.get() 中设定当监听到 事件eventid 需要做的事情即可
pygame的定时器使用套路十分固定:
定义定时器常量——eventid
在初始化方法中,调用 set_timer 方法设置定时器事件
在游戏主循环中,监听定时器事件
在plane_sprites.py 文件的顶部定义:
把 pygame.USEREVENT 记作 CREATE_ENEMY_EVENT事件
# 创建敌机的定时器常量
CREATE_ENEMY_EVENT = pygame.USEREVENT
在plane_main.py 文件的 PlaneGame 类的 __init__() 中增加 定时创建敌机事件 的设置:
class PlaneGame(object):
"""主游戏类"""
def __init__(self):
print("游戏正在初始化...")
# 创建游戏主窗口
self.screen = pygame.display.set_mode(SCREEN_RECT.size)
# 创建游戏时钟
self.clock = pygame.time.Clock()
# 调用私有方法,创建精灵和精灵组
self.__create_sprites()
# 设置定时器事件——每隔1秒创建敌机
pygame.time.set_timer(CREATE_ENEMY_EVENT, 1000)
在plane_main.py 文件的 __event_handler() 方法中监听该事件:
def __event_handler(self):
"""事件监听"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
PlaneGame.__game_over()
elif event.type == CREATE_ENEMY_EVENT:
pass
设计 Enemy 类:
由于每驾敌机在游戏主窗口中向下方飞行,飞行速度各不相同,且每架敌机出现的水平位置也不尽相同;此外,当敌机飞出屏幕后,应该把对应的精灵从精灵组中删除,要实现这些特有的功能,就要在原 GameSprite 类中派生一个子类 enemy,扩展功能:
因此新的架构图如图所示:
为了实现随机化,我们需要导入 random 模块
在 __init__(…) 方法中,我们首先抽取一个 1~3 的随机数,三个随机数分别代表创建三种不同类型的敌机——调用父类方法 super.__init__(…),分别传入 ./游戏素材/enemy1.png
./游戏素材/enemy2.png、./游戏素材/enemy3_n1 来对应创建不同的敌机
random.randrange([start], stop[, step]):从 [start, stop) 的区间上,间隔 step 大小来抽取随机数,如:random.randrange(10, 30, 2),相当于从 [10, 12, 14,…,26,28] 中抽取随机数
self.rect.x = random.randrange(0, (SCREEN_RECT.width - self.rect.width), self.rect.width):其作用是在游戏主窗口中(位置不能越界),以敌机精灵的图片大小为间隔地选择位置
self.speed = random.randint(1, 3) 则是随机抽取出场速度
self.rect.y = -self.rect.height 最后应记得设置敌机的出场位置为游戏主窗口的正上方
提示:self.rect 中有一个属性:self.rect.bottom,实际上就是精灵图像的底部y轴的位置,即也可以设置 self.rect.bottom = 0,即为 self.rect.y = -self.rect.height 的意思
在 update() 方法中调用父类方法,并增加敌机精灵飞出游戏主窗口的判断,当敌机精灵飞出游戏主窗口时,需要将其从精灵组中移出,并释放内存,调用 self.kill() 方法即可,该方法会将精灵从其所属精灵组中移除的同时释放内存
在plane_sprites.py 文件中新增代码——Enemy 类:
import random
class Enemy(GameSprite):
"""敌机精灵类,继承自GameSprite"""
def __init__(self):
# 随机抽取敌机
number = random.randint(1, 3)
if number == 1:
# 调用父类方法创建精灵对象
super().__init__("./游戏素材/enemy1.png")
# 随机抽取出场位置
elif number == 2:
# 调用父类方法创建精灵对象
super().__init__("./游戏素材/enemy2.png")
elif number == 3:
# 调用父类方法创建精灵对象
super().__init__("./游戏素材/enemy3_n1.png")
# 随机抽取出场位置
self.rect.x = random.randrange(0, (SCREEN_RECT.width - self.rect.width), self.rect.width)
# 随机抽取出场速度
self.speed = random.randint(1, 3)
# 初始位置应该在游戏主窗口的上方
self.rect.y = -self.rect.height
def update(self):
# 调用父类方法——向下移动
super().update()
# 判断是否飞出屏幕,是则移出精灵组释放内存
if self.rect.y >= SCREEN_RECT.height:
self.kill()
首先在 __create_sprites(self) 中创建敌机精灵组,用于管理敌机精灵
在plane_main.py 文件的 PlaneGame 类的 __create_sprites(self) 中创建敌机精灵组:
def __create_sprites(self):
# 创建背景精灵
background1 = BackGround()
background2 = BackGround(True)
# 创建背景精灵组
self.background_group = pygame.sprite.Group(background1, background2)
# 创建敌机精灵组
self.enemy_group = pygame.sprite.Group()
每当事件监听方法监听到 CREATE_ENEMY_EVENT 时,说明要创建敌机精灵了,我们调用 Enemy() 创建敌机精灵对象,由于敌机精灵组已经创建,只需通过 self.enemy_group.add() 让新建的敌机精灵加入敌机精灵组即可
在plane_main.py 文件的 __event_handler() 方法中监听的定时器事件,将pass改为创建敌机,该事件就是用来间隔地创建敌机精灵的:
def __event_handler(self):
"""事件监听"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
PlaneGame.__game_over()
elif event.type == CREATE_ENEMY_EVENT:
# 创建敌机精灵同时加入精灵组
self.enemy_group.add(Enemy())
别忘了在 __update_sprites() 方法中让敌机精灵组调用 update() 更改位置 和 self.enemy_group.draw(self.screen) 刷新显示
在 plane_main.py 文件的 __update_sprites() 方法中的代码如下:
def __update_sprites(self):
"""位置更新"""
self.background_group.update()
self.background_group.draw(self.screen)
self.enemy_group.update()
self.enemy_group.draw(self.screen)
5. 英雄飞机和子弹发射设计
英雄飞机类——Hero类 需求:
游戏开始后,游戏飞机应出现在游戏主窗口的正下方中间的位置,可以设置
self.rect.bottom = SCREEN_RECT.height
英雄飞机每隔 0.5 秒发射一次子弹,每次三连发
英雄飞机需要通过键盘上的 ← 或 → 按键来左右移动,不用上下运动,可以通过 pygame.key.get_pressed() 来实现键盘的交互
子弹类——Bullet类 需求:
子弹从英雄飞机的正上方发射沿直线向上方飞行
同样,飞出屏幕后,需要从精灵组中移除
英雄飞机——设计 Hero 类:
调用父类方法 super().__init__("./游戏素材/me1.png") ,传入英雄飞机图片
由于英雄飞机在竖直方向不动,因此要设置 self.speed = 0
设置其初始位置在游戏主窗口的正下方中央
定义 fire() 方法,用于发射子弹
重写 update() 方法,由于英雄飞机不需要在竖直方向移动,因此不需要调用父类的 update() 方法,而需要重写为水平方向的移动,但初始速度 self.speed 需要设置为0,否则英雄飞机会因为精灵组 update() 的调用而导致英雄飞机自己动了,而只有当键盘按下 左/右 移动的按键时,我们才更改 self.speed 的值,使得英雄飞机会因为精灵组 update() 的调用而产生移动,当按键没有按下时,我们只需让 self.speed 的值恢复为 0 即可
于是,由上述条件可知,因为我们后续要访问英雄飞机的 speed 属性,因此我们要把英雄飞机精灵创建成 PlaneGame类 的实例属性,才能在 __create_sprites 方法的外部使用到英雄飞机对象(重要)
同时,要在 update() 方法中进行边界限制,防止英雄飞机越出游戏主窗口的范围
提示:self.rect 中有一个属性:self.rect.centerx,实际上就是精灵图像中心的横坐标的位置,可以设置 self.rect.centerx = SCREEN_RECT.centerx,即让精灵图像属于游戏主窗口的中心位置。同理,还有 self.rect.centery 图像中心的纵坐标属性…
提示:self.rect 中有一个属性:self.rect.right,实际上就是精灵图像右边界的坐标;代码中 elif self.rect.right > SCREEN_RECT.width: 就是判断图像的右边界是否越界。同理,还有 self.rect.left 左边界属性…
在plane_sprites.py 文件的中新增代码——Hero类,定义 fire() 英雄飞机发射子弹的方法——先用pass跳过:
class Hero(GameSprite):
"""英雄飞机类,继承自GameSprite"""
def __init__(self):
# 设置速度为0
super().__init__("./游戏素材/me1.png", speed=0)
# 位于游戏主窗口的中央
self.rect.centerx = SCREEN_RECT.centerx
self.rect.bottom = SCREEN_RECT.height - 10
def update(self):
# 英雄飞机在水平方向移动且不能移出边界
if self.rect.x < 0:
self.rect.x = 0
elif self.rect.right > SCREEN_RECT.width:
self.rect.right = SCREEN_RECT.width
else:
self.rect.x += self.speed
def fire(self):
"""英雄飞机发射子弹"""
pass
在plane_main.py 文件的 __create_sprites 方法中增加英雄飞机对象的定义和英雄飞机精灵组的定义:
def __create_sprites(self):
# 创建背景精灵
background1 = BackGround()
background2 = BackGround(True)
# 创建英雄飞机精灵——作为属性
self.hero_plane = Hero()
# 创建背景精灵组
self.background_group = pygame.sprite.Group(background1, background2)
# 创建游戏飞机精灵组
self.hero_group = pygame.sprite.Group(self.hero_plane)
# 创建敌机精灵组
self.enemy_group = pygame.sprite.Group()
在plane_main.py 文件的 __update_sprites 方法中调用 update() 和 draw() 方法:
def __update_sprites(self):
"""位置更新"""
self.background_group.update()
self.background_group.draw(self.screen)
self.enemy_group.update()
self.enemy_group.draw(self.screen)
self.hero_group.update()
self.hero_group.draw(self.screen)
在pygame中针对键盘按键的捕获:
首先使用 pygame.key.get_pressed() 返回所有按键的元组,然后通过键盘常量——下标,判断元组中某一个按键是否被按下——如果被按下,对应的数值为1,否则为0
键盘常量实际上是 pygame 内部定义的常量,实际上是一个整型数:
pygame中的键盘常量 | 含义 |
---|---|
K_RIGHT | 键盘的 → 按键 |
K_LEFT | 键盘的←按键 |
K_UP | 键盘的 ↑ 按键 |
K_DOWN | 键盘的 ↓ 按键 |
K_a | 键盘的 a 按键 |
… | … |
由于键盘常量实际上是在 pygame 内部定义一个整型数常量,因此可以直接使用 键盘常量 作为下标来在按键元组中找到对应的按键,当元组中某一按键对应的值为1时,则表示已按下
在事件监听中通过 pygame.key.get_pressed() 获得按键元组,keys_pressed[pygame.K_LEFT] 和 keys_pressed[pygame.K_RIGHT] 即为访问元组中键盘按键 ←按键 和 →按键 的状态
通过按键元组中的值的0、1来判断按键的状态,记住,当 K_RIGHT 和 K_LEFT 都没有按下时,需要设置:self.hero_plane.speed = 0,防止因为精灵组 update() 的调用使得英雄飞机自己移动了
这里也体现了为什么要把英雄飞机精灵定义成实例属性,因为要在 PlaneGame 中 __create_sprites 的外部通过 self.hero_plane.speed 才能调用英雄飞机的速度属性
在plane_main.py 文件的 __event_handler() 事件监听方法中增加键盘按键判断:
def __event_handler(self):
"""事件监听"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
PlaneGame.__game_over()
elif event.type == CREATE_ENEMY_EVENT:
# 创建敌机精灵同时加入精灵组
self.enemy_group.add(Enemy())
# 按键判断
keys_pressed = pygame.key.get_pressed()
if keys_pressed[pygame.K_RIGHT]:
self.hero_plane.speed = 1
if keys_pressed[pygame.K_LEFT]:
self.hero_plane.speed = -1
else:
# 当没有按下左右方向键时,速度应该设置为0
self.hero_plane.speed = 0
发射子弹:英雄飞机每隔 0.5 秒发射一次子弹,每次三连发,由其特性可知,可以使用定时器来实现这个功能
还是那套固定的定时器使用套路:
定义定时器常量——eventid
在初始化方法中,调用 set_timer 方法设置定时器事件
在游戏主循环中,监听定时器事件
在plane_sprites.py 文件的顶部定义:
把 pygame.USEREVENT + 1 记作 HERO_FIRE_EVENT 事件
# 英雄飞机发射子弹的事件
HERO_FIRE_EVENT = pygame.USEREVENT + 1
在plane_main.py 文件的 PlaneGame 类的 __init__() 中增加 英雄飞机定时发射子弹事件 的设置:
class PlaneGame(object):
"""主游戏类"""
def __init__(self):
print("游戏正在初始化...")
# 创建游戏主窗口
self.screen = pygame.display.set_mode(SCREEN_RECT.size)
# 创建游戏时钟
self.clock = pygame.time.Clock()
# 调用私有方法,创建精灵和精灵组
self.__create_sprites()
# 设置定时器事件——每隔1秒创建敌机
pygame.time.set_timer(CREATE_ENEMY_EVENT, 1000)
# 设置定时器事件——每隔0.5秒发射一次子弹
在plane_main.py 文件的 __event_handler() 方法中监听该事件——执行 fire() 发射子弹方法:
def __evnet_handler(self):
"""事件监听"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
PlaneGame.__game_over()
elif event.type == CREATE_ENEMY_EVENT:
# 创建敌机精灵同时加入精灵组
self.enemy_group.add(Enemy())
elif event.type == HERO_FIRE_EVENT:
self.hero_plane.fire()
# 按键判断
keys_pressed = pygame.key.get_pressed()
if keys_pressed[pygame.K_RIGHT]:
self.hero_plane.speed = 2
elif keys_pressed[pygame.K_LEFT]:
self.hero_plane.speed = -2
else:
# 当没有按下左右方向键时,速度应该设置为0
self.hero_plane.speed = 0
子弹——设计 Bullet 类:
子弹向上移动,因此 self.rect.y 应该逐渐变小:self.rect.y -= speed,结合背景精灵的移动,具有更快的相对移动速度
游戏每隔 0.5 秒发射一次子弹,每次三连发,使用定时器来实现这个功能,这个设置跟敌机的定时出场设计十分类似,变为了子弹定时出场
以下操作与敌机 Enemy 类的设计十分相识:
在plane_sprites.py 文件中新增代码——Bullet 类:
class Bullet(GameSprite):
"""子弹类,继承自GameSprite"""
def __init__(self):
super().__init__("./游戏素材/bullet1.png", speed=-2)
def update(self):
# 调用父类方法——向下移动
super().update()
# 判断子弹是否飞出屏幕,是则释放
if self.rect.bottom <= 0:
self.kill()
在plane_sprites.py 文件的 Hero 类的 __init__(self) 中创建子弹精灵组:
为什么要在Hero 类中创建子弹精灵组,而不在 __create_sprites(self) 中创建? 因为英雄飞机想要开火需要调用 fire() 方法,而事件 HERO_FIRE_EVENT 的事件监听是游戏中调用 fire() 来实现开火,因此实现子弹发射的动作应该是在 fire() 方法里,而发射子弹即创建子弹精灵,并加入精灵组,因此精灵组应该在 Hero 类的 __init__(self) 中创建(重要)
class Hero(GameSprite):
"""英雄飞机类,继承自GameSprite"""
def __init__(self):
# 设置速度为0
super().__init__("./游戏素材/me1.png", speed=0)
# 位于游戏主窗口的中央
self.rect.centerx = SCREEN_RECT.centerx
self.rect.bottom = SCREEN_RECT.height - 10
# 创建子弹精灵组
self.bullet_group = pygame.sprite.Group()
在 plane_main.py 文件的 __update_sprites() 方法中的代码如下:
注意:bullet_group 精灵组是在 hero_plane 内部的 fire() 创建的,需要通过 hero_plane 来调用
def __update_sprites(self):
"""位置更新"""
self.background_group.update()
self.background_group.draw(self.screen)
self.enemy_group.update()
self.enemy_group.draw(self.screen)
self.hero_group.update()
self.hero_group.draw(self.screen)
self.hero_plane.bullet_group.update()
self.hero_plane.bullet_group.draw(self.screen)
在plane_sprites.py 文件的 Hero 类的 fire() 中,实现子弹的发射和三连发功能:
需要在此创建精灵子弹精灵,并通过 self.bullet_group.add(…) 加入精灵组
i = 0、1、2 时,分别对应不同高度子弹精灵的创建
bullet.rect.y = self.rect.y - 2 * i * bullet.rect.height 是根据 i 的值来调整子弹的高度
2 * i * bullet.rect.height 中 2 的目的是加宽子弹之间的间距,增强辨析度
def fire(self):
"""英雄飞机发射子弹"""
for i in (0, 1, 2):
# 创建子弹精灵
bullet = Bullet()
# 设定子弹精灵的位置,应该与英雄飞机的正上方中央发射
bullet.rect.y = self.rect.y - 2 * i * bullet.rect.height
bullet.rect.centerx = self.rect.centerx
# 子弹精灵加入精灵组
self.bullet_group.add(bullet)
今天就分享的到这里了,需要完整源码的可以后台私信666获取的