Python游戏项目: 外星人入侵

项目总结

  1. 尽量遵守PEP8编码规范,使代码更加整洁易读;

  2. 重点理解内容:

    • 坐标系统: pygame中的坐标系统原点位于屏幕的左上角,向右是x轴的正向,向下是y轴的正向。所有对象的位置都是相对于原点的。
    • Surface对象:在pygame中,surface是一个代表图像的对象, 它是所有图像(如飞船,外星人,子弹)和图形(如绘制文本)的载体。在项目中使用了pygame.image.load('image.bmp')pygame.font.SysFont.render(str, antialias, str_color, backgroud_color)来获取相关的surface。surface的返回值形如:<Surface(宽度x高度x颜色深度 渲染方式)>,比如飞船为:<Surface(48x48x32 SW)>
    • Rect对象:Rect是Pygame中用于表示矩形区域的对象,它包含了矩形的位置和大小信息。每个surface对象都会有一个对应的rect对象,通过使用rect可以改变surface的位置,以及判断是否碰撞。在项目中使用了pygame.image.load('image.bmp').get_rect来获取surface的rect对象。rect的返回值形如<rect(x坐标, y坐标, 宽度, 高度)>, 比如飞船的rect为:<rect(0, 0, 48, 48)>
    • 编组:在pygame中,编组是一个特殊的列表,它可以存储游戏中所有的精灵(sprite)。比如可以管理所有的子弹或者外星人。当对编组调用方法时,pygame会自动对编组中的每个精灵调用该方法,比如子类重写的update()可以一次性更新所有精灵的相关状态,父类的draw()可以一次性绘制组中的所有元素(draw()期望的属性名分别是iamge和rect,故属性名不能随意修改)。在项目中使用了pygame.sprite.Group()来创建编组对象。
    • 绘制空白矩形:项目中的子弹,开始按钮都是通过创建矩形再绘制出来的,代码基本步骤为:首先使用pygame.Rect(x坐标, y坐标, 宽度, 高度)创建一个矩形(rect对象), 然后使用位置属性(如center, right, top)修改坐标,最后使用pygame.draw.rect(surface, color, rect)将矩形以指定color绘制在surface的rect位置上。
    • 将字符串绘制到屏幕:项目中的分数、等级、飞船数、按钮框上的提示,都需要绘制字符串,代码基本步骤为:首先使用font = pygame.font.SysFont(fontname, size)创建字体对象,然后使用str_image = font.render(str, antialias, str_color, backgroud_color)将字str渲染为一个surface图形对象, 再使用get_rect()获取surface()的rect对象,使用位置属性(如center, right, top)修改坐标,最后使用blit(surface, rect)将图像绘制在rect对象指定的位置上。对于飞船数,由于使用了编组进行管理,因此使用的是飞船Ship类的父类Sprite的draw()对每个精灵(飞船)进行绘制。
    • 外星人移动:外星人在屏幕上的移动是按照一定的方向和速度进行的。它们首先持续向右移动,当碰到屏幕的右边缘时开始向下移动指定像素位,并改变方向持续向左移动。同理,当移动到屏幕左边缘时,它们也会向下移动并改变方向开始向右移动。
    • 碰撞处理:外星人与飞船或子弹的碰撞处理:使用pygame.sprite.spritecollideany(sprite, group)来检测飞船和外星人编组中的每个精灵之间的碰撞,使用pygame.sprite.groupcollide(groupa, groupa, dokilla, dokillb)来检测子弹编组中的任意精灵和外星人编组中的任意精灵是否发生碰撞。

项目模块

alien_invasion.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import sys  # 可使用sys.exit()方法退出程序
import pygame # 包含游戏开发所需的功能
from time import sleep # 可使用sleep()函数暂停运行指定时间

from settings import Settings # 设置类
from ship import Ship # 飞船类
from bullet import Bullet # 子弹类
from alien import Alien # 外星人类
from game_stats import Gamestats # 游戏状态类
from button import Button # 游戏按钮类
from scoreboard import Scoreboard # 计分类


class AlienInvasion:
"""管理游戏资源和行文的类"""
def __init__(self):
pygame.init() # 初始化pygame库
self.settings = Settings() # 关联设置类
self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height)) # 创建一个显示窗口(实参来自Settings类)
# 创建全屏窗口
# self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
# self.settings.screen_width = self.screen.get_rect().width # 修改Settings类中的原值
# self.settings.screen_height = self.screen.get_rect().height
pygame.display.set_caption("Alien Invation") # 添加窗口标题
self.clock = pygame.time.Clock() # 创建一个Clock实例, 之后可在主循环调用其tick()方法来保持帧率,而不用重复创建此对象
self.ship = Ship(self) # 关联Ship类并将自身的所有属性传递过去
self.stats = Gamestats(self) # 存储游戏统计信息的实例
self.bullets = pygame.sprite.Group() # 存储子弹的编组(group)
self.aliens = pygame.sprite.Group() # 存储外星人的编组(group)
self._create_fleet() # 调用创建外星舰队的方法
self.game_active = False # 游戏启动后默认处于非活动状态
self.play_button = Button(self, 'Play') # 创建Button类的实例并传入按钮文本
self.sb = Scoreboard(self) # 存储游戏分数的实例

def run_game(self):
"""开始游戏的主循环"""
while True:
self._check_events() # 检查事件并做出响应(退出程序、修改移动飞船标志、空格创建子弹)

if self.game_active:
self.ship.update() # 根据方向键事件更新飞船的位置(飞船只有一个无需使用编组)
self._update_bullets() # 更新子弹位置和删除越界子弹以及判断子弹是否击中外星人并且是否消灭光
self._update_aliens() # 更新外星人位置以及判断是否处于屏幕左右和下边缘

self._update_screen() # 更新屏幕(屏幕上色、绘制子弹和飞船、刷新屏幕内容)
self.clock.tick(60) # 确保循环每秒恰好运行60次(维持60帧)

def _check_events(self):
"""响应按键和鼠标事件"""
# print(pygame.event.get()) # 打印事件列表
for event in pygame.event.get(): # 创建事件循环来响应用户的输入和其它事件
# print(event) # 打印事件
if event.type == pygame.QUIT:
sys.exit() # 如果检测到pygame.QUIT事件, 就终止程序

elif event.type == pygame.KEYDOWN: # 检查按下键盘按键事件
self._check_keydown_events(event)

elif event.type == pygame.KEYUP: # 检查松开键盘按键事件
self._check_keyup_events(event)

elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos() # 返回单击鼠标时光标的x坐标和y坐标的元组
self._check_play_button(mouse_pos)

def _check_play_button(self, mouse_pos):
"""在玩家单击Play按钮时开始新游戏"""
button_cliked = self.play_button.rect.collidepoint(mouse_pos) # 单击鼠标时的坐标位置若和按钮的rect重叠,则返回True
if button_cliked and not self.game_active: # 当单击了开始按钮以及游戏为未活动状态时执行以下代码
# 重置游戏的统计信息
self.settings.initialize_dynamic_settings() # 重置动态设置
self.stats.reset_stats() # 重置飞船数量,分数归零
self.sb.prep_score() # 分数归零后紧跟着渲染分数图像
self.sb.prep_level() # 渲染等级图像
self.sb.prep_ships() # 渲染可用飞船数目图像
self.game_active = True # 开始游戏主循环

# 清空外星人编组和子弹编组
self.bullets.empty()
self.aliens.empty()

# 创建一个新的外星舰队,并将飞船在屏幕底部的中央
self._create_fleet()
self.ship.center_ship()

# 游戏进行时隐藏光标
pygame.mouse.set_visible(False)

def _check_keydown_events(self, event):
"""响应按下"""
if event.key == pygame.K_q: # 按下q键
sys.exit()
elif event.key == pygame.K_RIGHT: # 按下右方向键
self.ship.moving_right = True
elif event.key == pygame.K_LEFT: # 按下左方向键
self.ship.moving_left = True
elif event.key == pygame.K_SPACE: # 按下空格键
self._fire_bullet() # 创建子弹并将其加入编组

def _check_keyup_events(self, event):
"""响应释放"""
if event.key == pygame.K_RIGHT: # 松开右键
self.ship.moving_right = False
elif event.key == pygame.K_LEFT: # 松开左键
self.ship.moving_left = False
# 无需响应空格释放事件, 因为不需要连发

def _fire_bullet(self):
"""检测到按下空格事件后会创建一颗子弹, 并将其加入编组bullets"""
if len(self.bullets) < self.settings.bullet_allowed: # 检查未消失的子弹数量是否小于设定值
new_bullet = Bullet(self) # 创建一个Bullet对象(Bullet对象是Sprite类的子类)
# print(new_bullet) # 打印屏幕上的子弹数量
self.bullets.add(new_bullet) # 将子弹对象加入了bullets编组(add方法只接受Sprite实例(没有任何属性)或其子类(有自定义属性)的对象!)

def _update_bullets(self):
"""移动子弹的位置并删除已消失的子弹以及判断子弹是否击中外星人并且是否消灭光"""
# 更新子弹位置
self.bullets.update() # 调用Group类的update方法,但Group类的update方法又会进一步调用Group内所有sprite(这里是Bullet类的实例)的update方法来更新子弹的位置

# 删除已消失(飞出屏幕)的子弹
for bullet in self.bullets.copy(): # 遍历编组的副本, 防止迭代器出错
if bullet.rect.bottom <= 0: # 判断矩形底部的Y坐标是否小于窗口的Y坐标(是否完全飞出屏幕); midbottom[1]也是Y坐标
self.bullets.remove(bullet) # 删除符合条件的原始编组的元素(即Bullet实例)
# print(len(self.bullets)) # 打印还未超出屏幕边界的元素(Bullet实例)数量

self._check_bullet_alien_collisions() # 处理子弹和外星人碰撞、全消灭再生成新的外星人

def _check_bullet_alien_collisions(self):
"""检查是否有子弹击中了外星人, 如果是, 就删除相应的子弹和外星人"""
collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, False, True) # 返回一个字典,键为参数1,值为参数2;碰撞后参数一不删除,即子弹能穿透外星人

if collisions:
for aliens in collisions.values(): # collisions字典的每个值都是一个列表
self.stats.score += self.settings.alien_points * len(aliens) # 增加分数
self.sb.prep_score() # 渲染分数图像(还未绘制到屏幕)
self.sb.check_high_score() # 检查是否创造了更高分

if not self.aliens: # 检查外星人编组是否为空, 空即为False, 符合条件
# 删除现有子弹并创建一个新的外星舰队
self.bullets.empty() # 使用empty()删除bullets编组中余下的所有精灵(子弹)
self._create_fleet() # 再创建一屏幕的外星人
self.settings.increase_speed() # 加快游戏节奏

# 提高等级
self.stats.level += 1 # 等级+1
self.sb.prep_level() # 渲染等级图像(还未绘制到屏幕)

def _create_fleet(self):
"""创建一个外星舰队(一屏幕外星人)"""
# 创建一个外星人, 再不断添加, 直到没有空间添加外星人为止, 外星人的间距为外星人的宽度和外星人的高度
alien = Alien(self) # 该实例用于提供起始坐标
alien_width, alien_height = alien.rect.size # 获取外星人的宽度和高度, 用于内外层循环时重设值

current_x, current_y = alien_width, alien_height # 下一行外星人的水平位置
while current_y < (self.settings.screen_height - 2 * alien_height): # 判断已创建的外星人的总高度是否超出限制
while current_x < (self.settings.screen_width - 2 * alien_width): # 判断已创建的外星人的总宽度是否超出限制
self._create_alien(current_x, current_y) # 循环调用创建一个外星人的方法直到创建出一排外星人
current_x += 2 * alien_width # 更新条件(一个外星人占自身2倍大小的宽度)

current_x = alien.rect.x # 重设初始宽
current_y += alien.rect.y # 重设叠加后的高

def _create_alien(self, x_position, y_position):
"""创建一个外星人并将其加入外星舰队"""
new_alien = Alien(self) # 创建一个外星人实例
new_alien.x = x_position # 记住每个外星人的left坐标(这个x属性给alien.py的update()方法来更新外星人的水平位置)
# print(new_alien.x) # 输出每个外星人的left坐标(即x坐标)
new_alien.rect.x = x_position # 设置此外星人的水平位置(x坐标)
new_alien.rect.y = y_position # 设置此外星人的垂直位置(y坐标)
self.aliens.add(new_alien) # 将外星人添加到用于存储外星人的编组中去

def _update_aliens(self):
"""检查是否有外星人位于左右屏幕边缘以及是否发生碰撞; 更新外星舰队中所有外星人的位置"""
self._check_fleet_edges() # 检测外星人是否触边并采取措施
self.aliens.update() # 调用alien类中重写的update()并根据fleet_direction来向右或向左移动外星人

# 检测外星人和飞船之间的碰撞
if pygame.sprite.spritecollideany(self.ship, self.aliens): # 检测飞船实例和外星人编组的每个元素之间的碰撞
self._ship_hit() # 响应外星人和飞船的碰撞,根据可用飞船数目判断是否停止主循环

self._check_aliens_bottom() # 检查外星人与屏幕下边缘的碰撞

def _check_fleet_edges(self):
"""在有外星人到达边缘时采取相应的措施"""
for alien in self.aliens.sprites(): # 遍历到第七个外星人时可以检测到右边缘是否触边; 遍历到第一个外星人可以检测到左边缘是否触边
if alien.check_edges(): # 如果触边(返回True), 则将所有外星人向下移动
self._change_fleet_direction() # 将所有外星人向下移动
break # 退出后续循环, 原因是检测到首个超出边缘的外星人时就会将所有外星人向下移动指定的高度

def _change_fleet_direction(self):
"""将所有外星人向下移动, 并改变他们的方向"""
for alien in self.aliens.sprites(): # 遍历每个精灵(外星人), 将其向下移动
alien.rect.y += self.settings.fleet_drop_speed # 将外星人向下移动
self.settings.fleet_direction *= -1 # -1 表示向左移动, 每次运行此代码都会改变其值

def _check_aliens_bottom(self):
"""检查是否有外星人到达了屏幕下边缘并做出处理;当飞船处于上方外星人被消灭的列时,其他列就有可能被外星人触底"""
for alian in self.aliens.sprites(): # 遍历每个精灵(外星人), 判断是否触底
if alian.rect.bottom >= self.settings.screen_height: # 如果触底
self._ship_hit() # 清空并重新创建外星人、将飞船复位
break # 只要任意一个外星人到了下边缘就可以停止检测了

def _ship_hit(self):
"""响应外星人和飞船或者底部的碰撞, 并将飞船数量减1"""
if self.stats.ships_left > 0: # 判断可用飞船的数量
self.stats.ships_left -= 1 # 将可用飞船数量减1
self.sb.prep_ships() # 绘制新的飞船数量
print(f"飞船已战毁1艘,当前还剩下{self.stats.ships_left}艘!")
self.bullets.empty() # 清空外星人列表
self.aliens.empty() # 清空子弹列表

self._create_fleet() # 重新创建一屏幕的外星人
self.ship.center_ship() # 将飞船复位(屏幕中底位置)

sleep(0.3) # 暂停0.5秒, 让玩家感知变化
else:
self.game_active = False # 飞船数量小于等于0,将游戏运行状态设置为False, 使游戏暂停
print("飞船已全部战毁!\n请重新开始游戏或退出。")
pygame.mouse.set_visible(True) # 游戏结束时重新显示光标

def _update_screen(self): # 屏幕上色、绘制子弹和飞船、刷新屏幕内容
"""更新屏幕上的图像, 并切换到新屏幕"""
self.screen.fill(self.settings.bg_color) # 填充窗口的背景色
for bullet in self.bullets.sprites(): # 遍历子弹编组中的每个精灵
bullet.draw_bullet() # 对每个精灵调用它的draw_bullet()方法在指定位置画出一个特定颜色、特定大小的矩形来表示子弹
self.ship.blitme() # 将飞船绘制到屏幕上
self.aliens.draw(self.screen) # pygame会对aliens编组中的每个精灵调用Group父类的draw方法将其绘制在屏幕上(依据image和rect属性)
self.sb.show_score() # 绘制分数(当前分数,最高分数,等级), 图层在外星人之上
if not self.game_active: # 如果游戏未活动,则绘制开始按钮;此代码块必须处于其他绘制函数之后才能置顶显示开始按钮
self.play_button.draw_button() # 将开始按钮绘制到屏幕的指定的区域
pygame.display.flip() # 刷新屏幕,使新内容呈现


if __name__ == '__main__':
# 创建游戏实例对象并运行游戏
ai = AlienInvasion()
ai.run_game()

# 注意: 运行游戏时关闭傻逼中文输入法, 否则q键将无法退出

ship.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import sys
import pygame
from pygame.sprite import Sprite # 创建的飞船编组用于绘制可用飞船数目


class Ship(Sprite):
"""管理飞船(ship)的类"""
def __init__(self, ai_game): # 传入参数的是Alien_Invasion类本身, 也就相当于创建了实例属性:ai_game = Alien_Invation
"""初始化飞船并设置其初始位置"""
super().__init__() # 继承父类Sprite的主方法中的属性,下面的image和rect属性会覆盖掉继承下来的的同名属性,用于Draw()绘制到屏幕上
self.screen = ai_game.screen # 获取主程序的screen属性,然后赋值给当前类的self.screen属性
self.screen_rect = ai_game.screen.get_rect() # 获取screen属性的rect对象
self.settings = ai_game.settings # 获取主程序的settings属性

# 加载飞船图像并获取其外接矩形
try:
self.image = pygame.image.load('images/ship.png') # 返回一个表示飞船的surface
except FileNotFoundError:
print("未找到飞船图像文件!")
sys.exit()
self.rect = self.image.get_rect() # 获取飞船图像(元素)的矩形属性(即在屏幕上的位置)

# 每艘新飞船都放置在屏幕底部的中央
self.rect.midbottom = self.screen_rect.midbottom # 设置飞船的矩形属性(self.rect)的midbottom(中间底部)值等于屏幕的矩形属性(self.screen_rect)的midbottom值
# 在飞船的属性x中存储一个浮点数
self.x = float(self.rect.x) # 存储x坐标浮点数的中间值

# 飞船移动的标志
self.moving_right = False
self.moving_left = False

def update(self):
"""根据移动标志和活动范围调整飞船的位置"""
if self.moving_right and self.rect.right < self.screen_rect.right: # 或者 < 1200
self.x += self.settings.ship_speed
if self.moving_left and self.rect.left > self.screen_rect.left: # 或者 > 0
self.x -= self.settings.ship_speed

self.rect.x = self.x # 只保留x坐标整数部分

def center_ship(self): # 通过方法修改属性的值(p147), 而不是再创建一个实例, 因为飞船只要一个
self.rect.midbottom = self.screen_rect.midbottom # 对齐屏幕的中底坐标
self.x = float(self.rect.x) # self.rect.x是通过self.x来更新的,因此必须再重置self.x, 否则飞船复位将异常

def blitme(self):
"""在指定位置绘制飞船"""
self.screen.blit(self.image, self.rect) # 将图像绘制到self.rect指定的位置

billet.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import pygame
from pygame.sprite import Sprite # 导入PyGame库中的sprite模块中的Sprite类


class Bullet(Sprite): # 继承Sprite类, 以使用Sprite类提供的通用功能
def __init__(self, ai_game):
"""在飞船的当前位置创建一个子弹对象"""
super().__init__() # 支持调用父类的主方法
self.screen = ai_game.screen # 引用AlienInvasion类中的screen属性
self.settings = ai_game.settings # 引用AlienInvasion类中的settings属性
self.color = self.settings.bullet_color # # 引用AlienInvasion类中的settings属性指向的Settings类中的bullet_color属性

# 在原点(0, 0)处创建一个表示子弹的矩形(无填充),再设置正确的位置
self.rect = pygame.Rect(0, 0, self.settings.bullet_width, self.settings.bullet_height) # 返回一个Rect实例,代表子弹的坐标
self.rect.midtop = ai_game.ship.rect.midtop # 对齐位置

# 存储用浮点数表示的子弹位置
self.y = float(self.rect.y) # 中间变量

def update(self): # 重写(覆盖)Sprite中的方法
"""向上移动子弹"""
self.y -= self.settings.bullet_speed # 更新子弹的准确位置
self.rect.y = self.y # 更新表示子弹的rect.y的位置(只存储整数值)

def draw_bullet(self):
"""在屏幕上绘制子弹"""
pygame.draw.rect(self.screen, self.color, self.rect) # self.screen表示要在哪个屏幕上绘制,self.color表示矩形的颜色,self.rect表示矩形的位置和大小信息

alien.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import pygame
from pygame.sprite import Sprite


class Alien(Sprite):
"""表示单个外星人的类"""

def __init__(self, ai_game):
"""初始化外星人并设置其起始位置"""
super().__init__()
self.screen = ai_game.screen
self.settings = ai_game.settings

# 加载外星人图像并设置其rect属性
try:
self.image = pygame.image.load("images\\alien.png") # <Surface(48x48x32 SW)>
except FileNotFoundError:
print("未找到外星人图像文件!")
self.rect = self.image.get_rect() # <rect(0, 0, 48, 48)>
# 注意: 这里的rect和image实例的名称不可更改,因为父类的draw()期望每个Sprite对象都有一个image属性和一个rect属性

# 每个外星人最初都出现在屏幕的左上角附近
self.rect.x = self.rect.width # 48
self.rect.y = self.rect.height # 48

# 存储外星人的精确水平位置
self.x = float(self.rect.x) # 每一个外星人都有该属性(该属性被主程序的_create_alien()方法更改), 用来更新水平位置, 以及存储浮点值坐标

def check_edges(self):
"""如果外星人位于屏幕边缘,就返回True"""
screen_rect = self.screen.get_rect()
# return (self.rect.right > screen_rect.right) or (self.rect.left < screen_rect.left)
if self.rect.right > screen_rect.right: # 判断外星人是否接触到屏幕右边缘
return True
elif self.rect.left < screen_rect.left: # 判断外星人是否接触到屏幕左边缘
return True

def update(self):
"""向右或向左移动外星人"""
self.x += self.settings.alien_speed * self.settings.fleet_direction # 更新水平位置
self.rect.x = self.x # 只保留整数

button.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import pygame.font


class Button:
"""该类用于创建游戏按钮"""
def __init__(self, ai_game, msg):
self.screen = ai_game.screen
self.screen_rect = self.screen.get_rect()

# 设置按钮的尺寸和其他属性
self.width, self.height = 200, 50
self.button_color = (72, 209, 204) # 按钮浅绿色
self.text_color = (255, 255, 255) # 文本纯白色
self.font = pygame.font.SysFont(None, 48) # 指定字体和字号

# 创建按钮的rect对象,并使其居中
self.rect = pygame.Rect(0, 0, self.width, self.height) # 和绘制子弹类似
self.rect.center = self.screen_rect.center

# 按钮的标签只需创建一次
self._prep_msg(msg)

def _prep_msg(self, msg):
"""将msg渲染为图形,并使其在按钮上居中"""
self.msg_image = self.font.render(msg, True, self.text_color, self.button_color) # 将文本存储为图像surface,True表示开启反锯齿,使文本的边缘更平滑
self.msg_image_rect = self.msg_image.get_rect() # 获取文本图像的rect
self.msg_image_rect.center = self.screen_rect.center # 对其文本在屏幕中央

def draw_button(self):
"""绘制一个浅绿色填充的按钮框,再绘制纯白色的文本"""
# self.screen.fill(self.button_color, self.rect) # 和屏幕上色类似, 第二个参数指定上色区域
pygame.draw.rect(self.screen, self.button_color, self.rect) # 和绘制子弹类似,这里将按钮框绘制以指定颜色和位置绘制在屏幕上
self.screen.blit(self.msg_image, self.msg_image_rect) # 和绘制飞船类似,将文本图像绘制到屏幕的指定的区域

scoreboard.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import pygame
from ship import Ship # 绘制飞船可用数目时需要创建Ship实例


class Scoreboard:
"""显示得分信息的类"""
def __init__(self, ai_game):
"""初始化显示得分涉及的属性"""
self.ai_game = ai_game # 将ai_game实参转换为自身的属性,给prep_ships()方法调用, 否则只能在主方法调用
self.screen = ai_game.screen
self.screen_rect = self.screen.get_rect()
self.settings = ai_game.settings
self.stats = ai_game.stats # 游戏状态实例,用于读取分数

# 显示得分信息时使用的字体设置
self.text_color = (30, 30, 30)
self.font = pygame.font.SysFont(None, 20)

# 渲染图像
self.prep_score() # 渲染初始得分图像
self.prep_high_score() # 渲染最高分图像
self.prep_level() # 渲染当前等级图像
self.prep_ships() # 渲染飞船可用数量

def prep_score(self):
"""将当前得分渲染成图像"""
round_score = round(self.stats.score, -1) # 将分数舍入到最近的10的整数倍
score_str = f"Score: {round_score:,}" # 格式说明符(:,)会在分数的千分位位置插入逗号
self.score_image = self.font.render(score_str, True, self.text_color, self.settings.bg_color) # 创建图像形式的字符串,并指定图像本身颜色以及背景色

# 在屏幕右上角显示得分
self.score_rect = self.score_image.get_rect()
self.score_rect.right = self.screen_rect.right - 20 # 对齐屏幕最右边坐标(800-20-48, 0)
self.score_rect.top = 10 # 分数上边缘与屏幕上边缘相距10像素(800-20-48, 10)

def prep_high_score(self):
"""将最高分渲染成图像"""
high_score = round(self.stats.high_score, -1) # 将分数舍入到最近的10的整数倍
high_score_str = f"High Score: {high_score:,}" # 格式说明符(:,)会在分数的千分位位置插入逗号
self.high_score_image = self.font.render(high_score_str, True, self.text_color, self.settings.bg_color)

# 在屏幕顶部中央显示最高分
self.high_score_rect = self.high_score_image.get_rect() # 获取最高分图形的rect对象
self.high_score_rect.centerx = self.screen_rect.centerx # 对齐屏幕中心点x坐标(400-10/2, 0)
self.high_score_rect.top = self.score_rect.top # 对齐当前得分的顶部y坐标(400-10/2, 10)
# self.high_score_rect.midtop = self.screen_rect.midtop # 这种方式会使得最高得分紧贴屏幕边缘,不够美观

def check_high_score(self):
"""检查是否创造了更高分"""
if self.stats.score > self.stats.high_score: # 比较当前分数和最高分
self.stats.high_score = self.stats.score # 替换最高分
self.prep_high_score() # 渲染最高分的图像

def prep_level(self):
"""将等级渲染成图像"""
level_str = str(f"Level: {self.stats.level}")
self.level_image = self.font.render(level_str, True, self.text_color, self.settings.bg_color)

# 将等级放在当前得分下方
self.level_rect = self.level_image.get_rect()
self.level_rect.right = self.score_rect.right # 对齐当前分数的最右边坐标(800-20-10, 0)
self.level_rect.top = self.score_rect.bottom + 10 # 对齐当前分数的底部+10坐标(800-20-10, 10+10)

def prep_ships(self):
"""在屏幕左上角以飞船图像绘制飞船可用数量"""
self.ships = pygame.sprite.Group() # 存储飞船数目的空编组
for ship_number in range(self.stats.ships_left): # 遍历飞船可用数量
ship = Ship(self.ai_game) # 创建飞船实例
ship.rect.x = 10 + ship_number * ship.rect.width # 飞船的x坐标等于自身的宽度*飞船数目+10像素

ship.rect.y = 0 # 飞船y坐标固定为0,即紧贴屏幕上边缘
self.ships.add(ship) # 将飞船加入编组

def show_score(self):
"""在屏幕上绘制得分"""
self.screen.blit(self.score_image, self.score_rect) # 绘制当前分数
self.screen.blit(self.high_score_image, self.high_score_rect) # 绘制最高得分
self.screen.blit(self.level_image, self.level_rect) # 绘制当前等级
self.ships.draw(self.screen) # pygame会对ships编组中的每个精灵调用Group类的draw方法将其绘制在屏幕上(根据image和rect)

game_stats

1
2
3
4
5
6
7
8
9
10
11
12
13
class Gamestats:
"""跟踪游戏的统计信息"""
def __init__(self, ai_game):
self.settings = ai_game.settings
self.reset_stats() # 相当于激活(初始化)了reset_stats()的ships_left属性, 之后通过此实例修改的都是reset_stats()的ships_left而不是给主方法创建了此属性
self.high_score = 0 # 在任何情况下都不应该重置最高分

def reset_stats(self):
"""初始化在游戏运行期间可能变化的统计信息"""
self.ships_left = self.settings.ship_limit # 初始化飞船总数
self.score = 0 # 初始分数为0分
self.level = 1 # 初始等级为1

settings.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Settings:
"""存储游戏中所有设置的类"""
def __init__(self):
"""初始化游戏的静态设置"""
# 屏幕设置
self.screen_width = 800 # 游戏窗口宽为800像素
self.screen_height = 500 # 游戏窗口高为500像素
# self.bg_color = (230, 230, 230) # 设置背景色(background color)为浅灰色
self.bg_color = "cadetblue4" # 使用预定义的颜色名字指定窗口背景色 https://www.pygame.org/docs/ref/color_list.html

# 飞船设置
self.ship_limit = 3 # 飞船可用数量

# 子弹设置
self.bullet_width = 3 # 子弹的宽
self.bullet_height = 15 # 子弹的高
self.bullet_color = (220, 220, 220) # 子弹颜色
self.bullet_allowed = 30 # 子弹数量

# 外星人设置
self.fleet_drop_speed = 5.0 # 外星人下降速度

# 以指定倍速加快游戏的节奏
self.speedup_scale = 1.2
self.score_scale = 1.5 # 分数提高倍数

self.initialize_dynamic_settings() # 开始新游戏时会调用此函数重置速度

def initialize_dynamic_settings(self):
"""初始化随游戏进行而变化的设置"""
self.ship_speed = 9.5 # 飞船移速
self.bullet_speed = 8.5 # 子弹射速
self.alien_speed = 1.2 # 外星人平移速度
self.fleet_direction = 1 # 1为向右移动, -1为向左移动
self.alien_points = 50 # 单个外星人击落得分

def increase_speed(self):
"""提高速度"""
self.ship_speed *= self.speedup_scale # 飞船提速
self.bullet_speed *= self.speedup_scale # 子弹提速
self.alien_speed *= self.speedup_scale # 外星人提速

self.alien_points = int(self.alien_points * self.score_scale) # 分数取整数
print(self.alien_points) # 打印分数倍数值

参考:

https://book.douban.com/subject/36365320/

https://www.pygame.org/docs/ref/color_list.html