1.游戏框架搭建
介绍pygame开发图像界面游戏的几个要素,并且把贪吃蛇游戏的整体框架搭建完成
本节知识点包括:
pygame的初始化和退出
游戏主窗口
游戏循环和游戏时钟
主窗口背景颜色
绘制文本
pygame的坐标系
游戏事件监听
绘制图形
定时器事件
1.5绘制文本
pygame的font模块中专门提供了一个SysFont类,可以创建系统字体对象,从而能够实现在游戏窗口中绘制文字内容。
要想在游戏窗中绘制文本内容,需要执行以下3个步骤:
创建字体对象
用字体渲染指定的文本内容,并生成一张图像 (Surface 对象)
将生成的图像绘制在游戏主窗口的指定位置
以上三个步骤的示意图如下:
1.5.1创建字体对象
SysFont的初始化方法的语法如下:
SysFont(name, size, bold=False, italic=False) -> Font
其中:
参数
name系统字体的名称
注意:可以设置的字体与操作系统有关,通过pygame . font . get_ fonts() 可以获取当前系统的所有可用字体列表
print(pygame.font.get_ fonts())
size 字体大小
bold 是否粗体,默认为否
italic 是否斜体,默认为否
返回
Font 对象
代码实现
贪吃蛇游戏中的文字一共有2种颜色,分别是:
分数文字颜色一浅灰色
游戏暂停和结束时的提示文字颜色一深灰色
为了方便后续代码的编写,我们首先在game_items.py 模块的顶部先定义两个字体颜色常量,代码如下:
SCORE_TEXT_COLOR = (192, 192, 192) # 分数文字颜色
TIP_TEXT_COLOR = (64, 64, 64) # 提示文字颜色
在game_items.py 中定义Lablel类,并且实现初始化方法,代码如下
class Label(object):
"""文字标签类"""
def __init__(self, size =48, is_score=True):
"""初始化标签信息
:param size 文本的字体大小
:param is_score 是否显示得分的对象
"""
self.font = pygame.font.SysFont('simhei', size)
self.is_score = is_score
1.5.2渲染文本内容
使用创建的字体对象调用render 方法,渲染生成一张图像(Surface 对象), 语法如下:
render(text, antialias, color, background=None) -> Surface
其中:
参数
text 文字内容
antialias 是否抗锯齿,抗锯齿效果会让绘制的文字看起来更加平滑
color 文字颜色
background 背景颜色,默认为None
返回
Surface 对象,可以理解为一张刚好包含文字内容的图像
Surface对象调用get_ rect 方法,可以获得图像大小
1.5.3绘制渲染结果
pygame为Surface对象提供了一个blit 方法,可以在一个 Surface对象中绘制另外一个Surface对象的内容。语法如下:
blit(source, dest, area=None, special_ flags = 0) -> Rect
其中:
参数
source 要绘制的图像(Surface 对象)
dest目标位置
暂时传入(0, 0),有关内容在后续小节介绍
返回
Rect 对象,绘制结果对应的矩形区域,有关内容在后续小节介绍
代码实现
在game_items.py中的Label类中定义draw方法,并接收游戏主窗口作为参数,代码如下:
def draw(self, window, text):
"""绘制当前对象的内容
:param window: 游戏主窗口
:param text: 要显示的文本内容
"""
# 使用字体渲染文本内容
color = SCORE_TEXT_COLOR if self.is_score else TIP_TEXT_COLOR
text_surface = self.font.render(text, True, color)
# 在游戏窗口中绘制渲染结果
window.blit(text_surface, (0, 0))
在game.py中的start方法中的while循环中加入测试分数的代码
# 绘制得分
self.score += 1
self.score_label.draw(self.main_window, "Denfen:%d" % self.score)
1.5.4测试分数文本的绘制
Label类准备告一段落后,接下来就对Game类进行一个扩展,先在屏幕的左上角位置显示游戏的分数
首先,在game.py中的Game类的初始化方法中,定义两个属性,代码如下:
class Game(object):
"""游戏类"""
def __init__(self):
self.main_window = pygame.display.set_mode((640, 480))
pygame.display.set_caption("贪吃蛇")
self.score_label = Label() # 得分的标签
self.score = 0
然后,在game.py中的Game类的start方法的游戏循环内部,增加代码修改部分并绘制在游戏主窗口,代码如下:
while True:
# 设置窗口背景颜色
self.main_window.fill(BACKGROUND_COLOR)
# 绘制得分
self.score += 1
self.score_label.draw(self.main_window, "Denfen:%d" % self.score)
# 刷新窗口内容
pygame.display.update()
# 设置刷新率
clock.tick(60)
1.5.5阶段性小结
绘制文本内容的3个步骤:
pygame.font.SysFont("simhei", size) 创建字体对象
self.font.render(self.text, True, color) 使用文字及颜色渲染内容
window.blit(text_surface, (0, 0)) 在window中绘制渲染结果
pygame的绘制动作类似于绘制油画——也就是后绘制的内容会叠加在之前绘制内容上
调用完所有绘制方法,一定要调用pygame.display.update()方法,否则无法在屏幕上看到绘制结果
1.6 pygame坐标系
1.6.1pygame的坐标系
坐标系的用处就是以坐标原点为参照,准确地描述出屏幕上的任意位置或者区域。pygame的坐标系定义如下:
坐标原点在游戏窗口的左上角
x轴沿水平方向向右,逐渐增加
y轴沿垂直方向向下,逐渐增加
如下图所示:
现在,我们回顾一下在上一小节Label类的draw方法中的最后一句代码:
# 绘制文本内容到窗口
window.blit(text_surface, (0, 0))
我们给blit方法传递的第二个参数(0, 0),就是指定在游戏窗口的什么位置来绘制文字
1.6.2矩形区域
在游戏中,所有可见的元素都是以矩形区域来描述位置区域的,一个矩形区域包含四个要素: (x, y) (width,height)
pygame专门提供了一个类Rect于描述矩形区域。Rect 的初始化方法的语法如下:
Rect(x, y, width, height) -> Rect
知道了矩形区域这个概念之后,接下来我们就做一个实战演练,将得分文字绘制在游戏窗口的左下角。
要实现这一目标,一共需要3个步骤:
利用get _rect获得渲染完成的文本图像(Surface 对象)的矩形区域
通过计算设置文本图像的显示位置
利用Rect提供的属性设置文本图像的显示位置
获得文本图像的矩形区域和位置
修改Label类的draw方法代码如下:
def draw(self, window, text):
"""绘制当前对象的内容"""
# 渲染字体
color = SCORE_TEXT_COLOR if self.is_score else TIP_TEXT_COLOR
text_surface = self.font.render(text, True, color)
# 获取文本的矩形
text_rect = text_surface.get_rect()
# print("文字的矩形:", text_rect)
# 获取窗口的矩形
window_rect = window.get_rect()
# 修改显示的坐标
text_rect.y = window_rect.height - text_rect.height
# 绘制文本内容到窗口
window.blit(text_surface, text_rect)
利用Rect的属性设置显示位置
为了方便程序员对矩形区域进行操作,pygame提供了一系列方便的属性。
下表以矩形区域rect = Rect(10, 80, 168, 50)
序号 | 属性 | 说明 | 示例 |
1 | x、left | 水平方向和Y轴的距离 | rect.x = 10、rect.left = 10 |
2 | y、top | 垂直方向和X轴的距离 | rect.y = 80、rect.top = 80 |
3 | width、w | 宽度 | rect.width = 168、 rect.w = 168 |
4 | height、h | 高度 | rect.height = 50、rect.h = 50 |
5 | right | 右侧 = x +w | rect.right =178 |
6 | bottom | 底部 = y + h | rect.bottom = 130 |
7 | size | 尺寸(w, h) | rect.size = (168, 50) |
8 | topleft | (x, y) | rect.topleft = (10, 130) |
9 | bottomleft | (x, bottom) | rect.bottomleft = (10, 130) |
10 | topright | (right, y) | rect.topright = (178, 80) |
11 | bottomright | (right, bottom) | rect.bottomright = (178, 130) |
12 | centerx | 中心点x = x + 0.5 * w | rect.centerx = 94 |
13 | centery | 中心点y = y + 0.5 * h | rect.centery = 105 |
14 | center | (centerx, centery) | rect.center = (94, 105) |
15 | midtop | (centerx, y) | rect.midtop = (94, 80) |
16 | midleft | (x, centery) | rect.midleft = (10, 105) |
17 | midbottom | (centerx, bottom) | rect.midbottom = (94, 130) |
18 | midright | (right, centery) | rect.midright = (178, 105) |
属性示意图如下:
介绍了Rect的属性之后,我们使用属性来修改一下Label类的draw方法
def draw(self, window, text):
"""绘制当前对象的内容"""
# 渲染字体
color = SCORE_TEXT_COLOR if self.is_score else TIP_TEXT_COLOR
text_surface = self.font.render(text, True, color)
# 获取文本的矩形
text_rect = text_surface.get_rect()
# 获取窗口的矩形
window_rect = window.get_rect()
# 修改显示的坐标
text_rect.bottomleft = window_rect.bottomleft
# 绘制文本内容到窗口
window.blit(text_surface, text_rect)
1.6.3阶段性小结
pygame的坐标原点在窗口的左上角,x水平向右递增,y垂直向下递增
利用Rect对象可以准确地指定游戏窗口上的某一块矩形区域
合理利用Rect提供的属性,在设置矩形区域时会更加方便
1.7游戏事件监听
之前学习了:
游戏循环的主要目的是保证游戏不会立即退出
在游戏循环中,需要做的3件事情,分别是:
绘制游戏元素
每一次执行游戏循环内的代码,游戏窗口中的所有内容都会被绘制
更新显示
只有调用了pygame.diaplay.update()方法,才能在游戏窗口中看到最终的绘制结果
设置刷新帧率
达到流畅的动画效果
降低CPU的负荷
在游戏循环中要需要做的最后一件事 -- 事件监听
1.7.1事件和监听概念
首先,我们来明确2个概念:事件和监听
事件就是游戏启动后,用户针对游戏所做的操作,例如:点击关闭按钮,点击鼠标,按下键盘等
监听就是在游戏循环中,判断用户在当前这一时刻,所做的具体操作
提示:只有监听到用户具体的操作,才能有针对性的做出响应
明确了事件监听的概念后,我们来看一下游戏循环内部的工作示意图:
通过示意图,可以很直观地看到,只有:
监听到退出事件,才能实现用户点击关闭按钮时终止游戏的效果
监听到键盘按键事件
才能在用户按下方向键后,改变贪吃蛇的运动方向
才能在用户按下空格后
如果本轮游戏没有结束,则暂停或者继续游戏
如果本轮游戏已经结束,则开启新一轮游戏
1.7.2 pygame监听事件
pygame专门提供了一个event模块用于处理游戏事件,通过pygame.event.get()方法可以获得当前这一时刻发生的所有事件列表
提示:同一时刻,可能会发生很多事件!例如,在 用户按下方向键修改贪吃蛇方向的同时, 定时器事件被触发了,此时还需要让 贪吃蛇向前移动
首先演练一下对退出事件的监听和处理。找到game.py中的Game类的start方法,对代码调整如下:
def start(self):
"""启动并控制游戏"""
clock = pygame.time.Clock() # 游戏时钟
while True:
# 事件监听
for event in pygame.event.get(): # 遍历同一时刻发生的事件列表
if event.type == pygame.QUIT: # 判断退出事件
print("点击了关闭按钮")
return
# 设置窗口背景颜色
self.main_window.fill(BACKGROUND_COLOR)
然后,再来监听一下用户按键事件,就是按下ESC键时同样能够直接退出游戏。对game.py中的Game类的start方法的代码调整如下:
def start(self):
"""启动并控制游戏"""
clock = pygame.time.Clock()
while True:
# 事件监听
for event in pygame.event.get(): # 遍历同一时刻发生的事件列表
if event.type == pygame.QUIT: # 判断退出事件
print("点击了关闭按钮")
return
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
print("按下了ESC键")
return
# 设置窗口背景颜色
self.main_window.fill(BACKGROUND_COLOR)
1.7.3游戏状态切换和提示
按照贪吃蛇的游戏规则描述,空格键可以改变游戏的状态:
一局游戏结束后,按下空格键可以重新开启一局新游戏
游戏过程中按下空格键,可以暂停游戏;再次按下空格键,可以继续游戏
要实现游戏状态的切换,我们分两步来完成:
创建提示标签及游戏状态标记,游戏状态不同显示不同的提示信息
监听空格键按键,并根据当前游戏状态标记,修改游戏状态
提示标签和状态标记
修改game.py中Game类的初始化方法,创建提示标签和游戏状态标记,代码如下:
def __init__(self):
self.main_window = pygame.display.set_mode((640, 480))
pygame.display.set_caption("贪吃蛇")
self.score_label = Label() # 得分的标签
self.score = 0
self.tip_label = Label(24, False) # 暂停和游戏结束的提示标签
self.is_game_over = False # 游戏是否结束的标记,如果为True, 则说明游戏已经结束
self.is_pause = False # 游戏是否暂停的标记,如果为True,则说明游戏已经被暂停
修改game.py中的Game类中的start方法中的游戏循环中的代码,在游戏暂停和游戏结束时,显示不同的信息,并且只有在既不是暂停也没有结束时,才修改游戏得分,代码如下:
# 设置窗口背景颜色
self.main_window.fill(BACKGROUND_COLOR)
# 绘制得分
self.score_label.draw(self.main_window, "DeFen: %d" % self.score)
# 绘制暂停、游戏结束的标签
if self.is_game_over:
self.tip_label.draw(self.main_window, "Youxi Jiesu anKonggeJian KaiQi xinyouxi")
elif self.is_pause:
self.tip_label.draw(self.main_window, "Youxi Zanting KonggejianJixu...")
else:
self.score += 1
# 刷新窗口内容
pygame.display.update()
修改game_items.py中的Label类中的draw方法,将提示标签的文字显示在游戏窗口的中间位置,代码如下:
def draw(self, window, text):
"""绘制当前对象的内容"""
# 渲染字体
color = SCORE_TEXT_COLOR if self.is_score else TIP_TEXT_COLOR
text_surface = self.font.render(text, True, color)
# 获取文本的矩形
text_rect = text_surface.get_rect()
# 获取窗口的矩形
window_rect = window.get_rect()
# 修改显示的坐标
if self.is_score:
# 游戏得分,显示在窗口左下角
text_rect.bottomleft = window_rect.bottomleft
else:
# 提示信息,暂停,游戏结束,显示在窗口中间
text_rect.center = window_rect.center
# 绘制文本内容到窗口
window.blit(text_surface, text_rect)
监听空格键按键
修改game.py中的Game类中的start方法中的游戏循环中的代码,并根据当前游戏状态标记,修改游戏状态
def start(self):
"""启动并控制游戏"""
clock = pygame.time.Clock()
while True:
# 事件监听
for event in pygame.event.get():
if event.type == pygame.QUIT:
# print("点击了关闭按钮")
return
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
# print("按下了ESC键")
return
elif event.key == pygame.K_SPACE:
if self.is_game_over:
self.reset_game()
else:
self.is_pause = not self.is_pause
# 设置窗口背景颜色
self.main_window.fill(BACKGROUND_COLOR)
1.7.4阶段性小结
游戏循环中需要做4件事情
事件监听
绘制游戏元素
更新显示
设置刷新帧率
在同一时刻,可能会发生多个事件,因此应该使用for来遍历事件列表
使用event.type可以判断事件的类型,如:退出事件,按键事件等
如果是按键事件,使用event.key可以判断具体的按键
1.8绘制图形
pygame的draw模块中专门提供了一系列方法,可以方便地绘制图形。
常用的绘制图形方法如下:
序号 | 语法 | 说明 |
1 | rect(Surface, color, Rect, width=0) -> Rect | 绘制矩形,width > 0则绘制边框 |
2 | ellipse(Surface, color, Rect, width=0) -> Rect | 绘制与Rect内切的椭圆,width同上 |
3 | line(Surface, color, start_ pos, end_pos, width=1) ->Rect | 绘制从start_pos到end _pos的线条 |
4 | lines(Surface,color, closed, pointlist, width=1) ->Rect | 绘制多个线条,closed 设置首尾相连 |
在这一小节,我们就利用draw模块提供的ellipse方法实现食物的绘制工作。
要实现这一目标,一共需要3个步骤:
在屏幕的左上角绘制一个圆形表示食物
让食物随机出现
增加食物出场动画
1.8.1简单绘制食物
首先,在game_items.py模块中定义屏幕矩形区域和小格子大小的全局常量,之所以要定义全局常量是因为:
定义屏幕矩形区域是为了方便后续计算食物的位置以及蛇身体的位置
在贪吃蛇游戏中,我们可以把屏幕矩形区域划分成若干个小格子
每一个小格子中,可以显示一个食物或者一节蛇的身体
定义全局常量的代码如下:
在game_items.py的顶部定义全局变量:
# 全局变量的定义
SCREEN_RECT = (0, 0, 640, 480) # 窗口的大小
CELL_SIZE = 20 # 每一个格子的宽高
在game_items.py模块的末尾,定义Food类,并实现如下代码:
class Food(object):
"""食物类"""
def __init__(self):
"""初始化食物"""
self.color = (255, 0, 0) # 颜色初始为 红色
self.score = 10 # 默认得分为10分
self.rect = (0, 0, CELL_SIZE, CELL_SIZE) # 初始的显示位置
def draw(self, window):
"""使用当前食物的矩形,绘制实心圆形"""
pygame.draw.ellipse(window, self.color, self.rect)
1.8.2随机食物位置
在game_items.py的顶部导入random模块,以方便使用随机数,代码如下:
import random
在game_items.py模块的Food类中定义random_rect方法,随机确定游戏窗口的任一小格子设置食物出现的位置,实现如下代码:
class Food(object):
"""食物类"""
def __init__(self):
"""初始化食物"""
self.color = (255, 0, 0) # 颜色初始为 红色
self.score = 10 # 默认得分为10分
self.rect = (0, 0, CELL_SIZE, CELL_SIZE) # 初始的显示位置
# 初始化食物随机分配一个位置
self.random_rect()
def draw(self, window):
"""使用当前食物的矩形,绘制实心圆形"""
pygame.draw.ellipse(window, self.color, self.rect)
def random_rect(self):
"""随机确定绘制食物的位置"""
# 计算可用的行数和列数
col = SCREEN_RECT.w / CELL_SIZE - 1
row = SCREEN_RECT.h / CELL_SIZE - 1
# 随机分配一个行和列,并计算行和列的x,y值
x = random.randint(0, col) * CELL_SIZE # 屏幕上小格子的列数
y = random.randint(0, row) * CELL_SIZE # 屏幕上小格子的行数
# 重新生成绘制食物的矩形
self.rect = pygame.Rect(x, y, CELL_SIZE, CELL_SIZE)
修改game.py中Game类的reset_game方法,在游戏复位时,重新设置食物的位置,代码如下:
def reset_game(self):
"""重置游戏参数"""
self.score = 0
self.is_game_over = False
self.is_pause = False
1.8.3食物出现动画
通过之前小节的学习,我们已经知道,游戏循环体中的代码执行频率是每秒60帧,这样做的目的之一是为了达到非常连续、高品质的动画效果。
在实际开发中,我们只需要让连续帧绘制的结果相差不大,这样就能够在视觉上产生连续的动画效果。如下图所示:
pygame在Rect类中提供了2个方法inflate和inflate_ip,可以非常方便地实现食物由小变大的动画效果,语法如下:
inflate(x, y) -> Rect
inflate_ip(x, y) -> None
inflate和inflate _ip方法都可以以矩形区域的中心点为中心,向四周扩大或缩小,其中:
x表示水平方向缩放的像素,正数放大,负数缩小
y表示垂直方向缩放的像素,正数放大,负数缩小
注意: x和y必须要是偶数,例如传入-2,表示左边减少-个像素, 右边同样减少1个像素。
inflate和inflate_ ip方法的区别是:
inflate返回一个新的Rect对象,不会修改调用Rect对象的属性
inflate_ip不返回新的Rect对象,会修改调用Rect对象的属性
现在,我们就利用inflate方法来实现一下食物在出现时,由小变大的动画效果
首先,在game_items.py的Food类中的random_rect方法中,设置食物大小的初始矩形区域,代码如下:
def random_rect(self):
"""随机确定绘制食物的位置"""
# 计算可用的行数和列数
col = SCREEN_RECT.w / CELL_SIZE - 1
row = SCREEN_RECT.h / CELL_SIZE - 1
# 随机分配一个行和列,并计算行和列的x,y值
x = random.randint(0, col) * CELL_SIZE
y = random.randint(0, row) * CELL_SIZE
# 重新生成绘制食物的矩形
self.rect = pygame.Rect(x, y, CELL_SIZE, CELL_SIZE)
self.rect.inflate_ip(-CELL_SIZE, -CELL_SIZE) # 把创建好的矩形大小修改为0
然后,在game_items.py的Food类中修改draw方法,在绘制食物前,判断一下食物的矩形区域宽度,如果还没有和小格子宽度一致,则向四周放大,代码如下:
def draw(self, window):
"""使用当前食物的矩形,绘制实心圆形"""
if self.rect.w < CELL_SIZE: # 只要显示的矩形还小于单元格大小,就继续放大
self.rect.inflate_ip(2, 2) # 向四周各自放大1个像素
pygame.draw.ellipse(window, self.color, self.rect)
1.8.4阶段性小节
draw模块提供了一系列方法,可以方便地绘制图形:
rect绘制矩形
ellipse绘制椭圆
定义SCREEN_RECT和CELL_SIZE常量可以方便计算食物的出现位置
inflate和iflate_ip方法可以方便地缩放矩形,参数务必是偶数
在游戏循环中,每一帧绘制的结果相差不大,在视觉上就会产生一个连续播放的动画效果
1.9定时器事件
按照贪吃蛇的游戏规则描述,如果食物出现的30秒内,贪吃蛇没有吃到食物,那么:
没有被吃到的食物从屏幕上消失
在游戏窗口的其他任一随机位置再次出现新的食物
现在如果暂时不考虑贪吃蛇吃食物的情况,要实现游戏规则的需求,我们只需要每隔30秒让食物对象调用一下game_items.py的 Food类的random_rect方法即可。
这种每隔一段时间,复执行一个固定动作的场景,我们可以使用定时器事件来实现。pygame 的time模块中提供的set_ timer方法,就是专门]用来定义定时器事件的。
要定义定时器事件,一共需要 3个步骤:
定义定时器事件代号常量
使用set _timer 方法设置定时器事件
在游戏循环中监听定时器事件
1.9.1设置定时器事件
首先,在game_items.py模块中定义定时器事件常量,代码如下
FOOD_UPDATE_EVENT = pygame.USEREVENT # 食物更新标志
提示:
事件代号是一个整数,初始值可以使用pygame.USEREVENT
如果要定义多个事件代号,可以从pygame.USEREVENT开始顺序递增
然后,在game.items.py中的Food类中的random_rect的末尾,设置定时器事件,代码如下:
def random_rect(self):
"""随机确定绘制食物的位置"""
# 计算可用的行数和列数
col = SCREEN_RECT.w / CELL_SIZE - 1
row = SCREEN_RECT.h / CELL_SIZE - 1
# 随机分配一个行和列,并计算行和列的x,y值
x = random.randint(0, col) * CELL_SIZE
y = random.randint(0, row) * CELL_SIZE
# 重新生成绘制食物的矩形
self.rect = pygame.Rect(x, y, CELL_SIZE, CELL_SIZE)
self.rect.inflate_ip(-CELL_SIZE, -CELL_SIZE) # 把创建好的矩形大小修改为0
# 设置定时,时间到了之后要重新设置食物位置
pygame.time.set_timer(FOOD_UPDATE_EVENT, 30000)
1.9.2监听定时器事件
在game.py中Game类中的start方法中的游戏循环中,增加更新食物事件监听,代码如下:
def start(self):
"""启动并控制游戏"""
clock = pygame.time.Clock()
while True:
# 事件监听
for event in pygame.event.get():
if event.type == pygame.QUIT:
# print("点击了关闭按钮")
return
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
# print("按下了ESC键")
return
elif event.key == pygame.K_SPACE:
if self.is_game_over:
self.reset_game()
else:
self.is_pause = not self.is_pause
if not self.is_pause and not self.is_game_over:
# 只有当游戏没有暂停也没有结束才需要处理
if event.type == FOOD_UPDATE_EVENT:
# 更新食物位置
self.food.random_rect()
内容总结于:https://space.bilibili.com/441640380