简介:本资源是一套基于Python的《外星人入侵》小游戏完整开发源码,面向编程初学者与Python入门学习者,旨在通过可运行、可调试的真实项目,系统训练面向对象编程、事件驱动逻辑、图形界面渲染及游戏循环机制等核心能力。压缩包共21个文件,含9个Python源码(如alien_invasion.py主程序、settings.py配置模块、ship.py/alien.py/bullet.py等游戏实体类)、5个XML配置文件(用于IDEA项目结构与UI布局定义)、2个BMP位图资源(ship.bmp、alien.bmp)、2个IML项目文件、1个JSON高分存储文件及readme.txt说明文档,整体仅53KB,轻量易读。已有397人学习下载,代码结构清晰、模块职责分明,配套注释充分,便于逐层理解游戏初始化、碰撞检测、得分统计与状态管理等关键逻辑。读者可直接运行体验,亦可修改飞船速度、外星人数量或添加音效等拓展功能,是实践Pygame开发、夯实Python工程能力的优质教学型项目。
1. 这不是“Hello World”,而是一套可调试、可扩展、可部署的 Pygame 游戏工程骨架
你打开alien_invasion.py,运行后看到飞船在左下角、外星舰队整齐下压、子弹划出轨迹、击中外星人时有爆炸效果——这确实像教科书里的“外星人入侵”Demo。但真正拉开upload.zip一看:9 个.py文件各司其职、leaderboard.json实时落盘、settings.py封装了 17 项可调参数、.idea/下完整保留了 PyCharm 的模块依赖与运行配置、甚至button.py里连鼠标悬停变色都做了状态缓存……这不是玩具代码,而是一个按工业级模块划分、带持久化能力、支持难度分级、预留 UI 扩展接口的 Python 游戏工程骨架。
它解决的不是“怎么画一个矩形”,而是“如何让初学者在不碰 SDL 底层、不写 C 扩展的前提下,理解游戏主循环的帧同步控制、对象生命周期管理、事件驱动与状态机切换、资源加载与内存释放边界”。适合两类人:一类是刚学完if/for/class想验证所学的 Python 新手,另一类是需要快速交付教学 Demo 或技术面试小项目的中级开发者——你改三行settings.py就能调出“地狱难度”,加一个Leaderboard.save_to_cloud()方法就能对接 REST API,所有扩展点都已预埋。
提示:项目未使用
pygame.mixer音频模块,所有音效占位符均以# TODO: play_sound('explosion.wav')形式存在,避免初学者因音频驱动兼容性问题卡在第一步。图形资源仅依赖ship.bmp和alien.bmp,无透明通道要求,Windows/macOS/Linux 均可开箱即用。
2. 从settings.py到alien_invasion.py:解构 Pygame 游戏的三层控制流
Pygame 游戏不是线性脚本,而是由配置层 → 状态层 → 渲染/逻辑层构成的闭环系统。本项目将这三层严格分离,每个.py文件只承担单一职责,这是它比网上多数“单文件 300 行”教程更易维护的关键。
2.1 配置层:settings.py是整个游戏的“中央参数总线”
该文件定义了所有可外部调整的常量,而非硬编码在逻辑中。例如:
# settings.py class Settings: def __init__(self): # 屏幕基础 self.screen_width = 1200 self.screen_height = 800 self.bg_color = (230, 230, 230) # 浅灰背景 # 飞船行为 self.ship_speed_factor = 1.5 self.ship_limit = 3 # 命数 # 子弹参数 self.bullet_speed_factor = 3.0 self.bullet_width = 3 self.bullet_height = 15 self.bullet_color = 60, 60, 60 self.bullets_allowed = 3 # 同屏最大子弹数 # 外星人移动 self.alien_speed_factor = 1.0 self.fleet_drop_speed = 10 self.fleet_direction = 1 # 1 表示右移,-1 表示左移 # 游戏节奏(每击落一排外星人,速度提升) self.speedup_scale = 1.1 self.score_scale = 1.5注意:
speedup_scale和score_scale不是固定值,而是通过GameStats类的increase_speed()方法动态应用。这意味着“难度随进度增长”不是靠 if 判断实现的,而是通过参数乘法器实时重载——你修改speedup_scale = 1.3,下一排外星人移动速度立刻提升 30%,无需重启游戏。
2.2 状态层:game_stats.py与scoreboard.py构成数据中枢
GameStats不是简单的计分器,而是游戏全生命周期的状态容器。它管理:
- 当前分数、最高分、等级、剩余飞船数
- 游戏是否处于活动状态(
self.game_active = False控制主循环是否处理键盘事件) - 是否触发“升级”逻辑(
self.level_up()调用settings.increase_speed())
而scoreboard.py则负责将这些状态可视化渲染,且做了性能优化:
# scoreboard.py def prep_score(self): """将得分转换为一幅渲染图像""" rounded_score = int(round(self.stats.score, -1)) # 四舍五入到十位 score_str = "{:,}".format(rounded_score) # 千分位分隔:1,234,567 self.score_image = self.font.render(score_str, True, self.text_color, self.ai_settings.bg_color) # 重用已有 surface,避免频繁创建销毁 self.score_rect = self.score_image.get_rect() self.score_rect.right = self.screen_rect.right - 20 self.score_rect.top = 20关键点在于prep_score()每次只在分数变化时调用,而非每帧重绘。self.score_image是缓存的 Surface 对象,get_rect()复用已有尺寸计算——这对 Pygame 这种 CPU 密集型渲染框架至关重要。
2.3 主循环层:alien_invasion.py如何协调 7 个对象实例
主文件alien_invasion.py的核心是run_game()方法,它构建了一个事件驱动 + 固定帧率的主循环:
# alien_invasion.py def run_game(self): while True: self.clock.tick(60) # 锁定 60 FPS,避免 CPU 空转 self._check_events() # 检查键盘/鼠标事件(含退出、射击、暂停) if self.stats.game_active: self.ship.update() # 飞船位置更新(响应按键) self._update_bullets() # 子弹移动、碰撞检测、销毁 self._update_aliens() # 外星人移动、触底检测、舰队转向 self._update_screen() # 统一刷新:背景→飞船→子弹→外星人→记分板这里的关键设计是_update_bullets()内部的碰撞检测逻辑:
# alien_invasion.py def _update_bullets(self): self.bullets.update() # 调用所有 bullet 实例的 update() # 删除超出屏幕的子弹(节省内存) for bullet in self.bullets.copy(): if bullet.rect.bottom <= 0: self.bullets.remove(bullet) # 检测子弹与外星人碰撞(groupcollide 返回字典:{bullet: [alien1, alien2]}) collisions = pygame.sprite.groupcollide( self.bullets, self.aliens, True, True ) if collisions: for aliens in collisions.values(): self.stats.score += self.ai_settings.alien_points * len(aliens) self.sb.prep_score() # 分数变化,触发记分板重绘 self._check_high_score() # 检查是否破纪录pygame.sprite.groupcollide()是 Pygame 提供的高效碰撞检测 API,它底层使用矩形包围盒(AABB)算法,比手动遍历rect.colliderect()快 5 倍以上。True, True参数表示:碰撞后自动删除子弹和外星人——这正是“击落”逻辑的原子操作,无需额外清理代码。
3.Leaderboard.py与leaderboard.json:本地高分榜的持久化实现细节
很多教程把“保存最高分”写成f.write(str(score)),但这在真实场景中会引发竞态问题:当玩家快速退出、崩溃或同时运行多个实例时,JSON 文件可能被截断或写入乱码。本项目采用原子写入 + 异常兜底 + 版本校验三重保障。
3.1Leaderboard类的健壮初始化
# Leaderboard.py import json import os class Leaderboard: def __init__(self, filename='leaderboard.json'): self.filename = filename self.scores = [] self._load_scores() def _load_scores(self): """安全加载 JSON,失败时返回空列表并记录日志""" try: if os.path.exists(self.filename): with open(self.filename, 'r', encoding='utf-8') as f: data = json.load(f) # 校验 JSON 结构:必须是 list,且每个元素含 'name' 和 'score' if isinstance(data, list): for item in data: if not isinstance(item, dict) or 'name' not in item or 'score' not in item: raise ValueError("Invalid leaderboard entry format") self.scores = data else: raise ValueError("Leaderboard JSON must be a list") else: self.scores = [] except (json.JSONDecodeError, ValueError, OSError) as e: print(f"[WARN] Failed to load {self.filename}: {e}. Using empty leaderboard.") self.scores = [] def add_score(self, name: str, score: int): """添加新成绩,并保持按分数降序排列(最多存 10 条)""" new_entry = {'name': name[:12], 'score': score, 'timestamp': time.time()} self.scores.append(new_entry) # 按 score 降序,取前 10 self.scores.sort(key=lambda x: x['score'], reverse=True) self.scores = self.scores[:10] self._save_scores() def _save_scores(self): """原子写入:先写临时文件,再 rename 替换原文件""" temp_file = self.filename + '.tmp' try: with open(temp_file, 'w', encoding='utf-8') as f: json.dump(self.scores, f, indent=2, ensure_ascii=False) os.replace(temp_file, self.filename) # 原子替换,Linux/macOS 安全;Windows 下等价于 overwrite except OSError as e: print(f"[ERROR] Failed to save leaderboard: {e}")提示:
os.replace()在 POSIX 系统上是原子操作,可避免写入中断导致文件损坏;在 Windows 上,若目标文件存在,replace()会直接覆盖,行为一致。临时文件.tmp后缀确保崩溃时残留文件可被识别清理。
3.2 在game_stats.py中集成高分持久化
GameStats类新增了对Leaderboard的引用,并在游戏结束时触发保存:
# game_stats.py from Leaderboard import Leaderboard class GameStats: def __init__(self, ai_settings): self.ai_settings = ai_settings self.reset_stats() self.game_active = False self.leaderboard = Leaderboard() # 实例化高分榜 def reset_stats(self): self.ships_left = self.ai_settings.ship_limit self.score = 0 self.level = 1 def save_final_score(self, player_name="Player"): """游戏结束时调用,保存当前分数""" if self.score > 0: self.leaderboard.add_score(player_name, self.score)而alien_invasion.py中的_check_play_button()方法在点击“Play”按钮后,会清空当前分数并重置状态:
# alien_invasion.py def _check_play_button(self, mouse_x, mouse_y): button_clicked = self.play_button.rect.collidepoint(mouse_x, mouse_y) if button_clicked and not self.stats.game_active: # 重置游戏状态 self.stats.reset_stats() self.stats.game_active = True # 清空当前局分数,但保留历史高分 self.sb.prep_score() self.sb.prep_high_score() self.sb.prep_level() self.sb.prep_ships()这种设计保证了:每次新游戏都是干净状态,而Leaderboard.json只在save_final_score()显式调用时更新,避免频繁 I/O 拖慢帧率。
4.button.py与ship.py:可复用 UI 组件与游戏对象的设计范式
Pygame 本身不提供 Button、Slider 等控件,本项目将Button抽象为独立类,其设计体现了状态驱动 UI的思想——按钮不是静态图片,而是根据hover、clicked、disabled等状态动态渲染不同样式。
4.1Button类的状态管理与渲染优化
# button.py import pygame.font class Button: def __init__(self, ai_settings, screen, msg): self.screen = screen self.screen_rect = screen.get_rect() # 按钮尺寸与颜色(正常/悬停/按下) self.width, self.height = 200, 50 self.button_color = (0, 255, 0) # 默认绿色 self.hover_color = (0, 200, 0) # 悬停深绿 self.clicked_color = (0, 150, 0) # 按下更深绿 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 # 按钮标签(预渲染,避免每帧调用 render) self.msg_image = None self.msg_image_rect = None self._prep_msg(msg) # 内部状态 self.hovered = False self.pressed = False self.enabled = True def _prep_msg(self, msg): """预渲染按钮文字,提高性能""" self.msg_image = self.font.render(msg, True, self.text_color, self.button_color) self.msg_image_rect = self.msg_image.get_rect() self.msg_image_rect.center = self.rect.center def check_hover(self, mouse_pos): """检查鼠标是否悬停在按钮上""" if self.enabled: self.hovered = self.rect.collidepoint(mouse_pos) else: self.hovered = False def check_click(self, mouse_pos, mouse_down): """检查鼠标点击(按下+释放)""" if not self.enabled: return False if mouse_down and self.rect.collidepoint(mouse_pos): self.pressed = True return False # 按下时不触发动作 elif not mouse_down and self.pressed and self.rect.collidepoint(mouse_pos): self.pressed = False return True # 释放时触发动作 else: self.pressed = False return False def draw_button(self): """根据当前状态绘制按钮""" if not self.enabled: color = (128, 128, 128) # 禁用时灰色 elif self.pressed: color = self.clicked_color elif self.hovered: color = self.hover_color else: color = self.button_color # 绘制按钮背景 pygame.draw.rect(self.screen, color, self.rect, border_radius=8) pygame.draw.rect(self.screen, (0, 0, 0), self.rect, 2, border_radius=8) # 黑色边框 # 绘制文字(复用预渲染的 surface) self.screen.blit(self.msg_image, self.msg_image_rect)关键点在于:
check_click()实现了“按下-释放”两阶段检测,避免鼠标拖拽误触发;draw_button()根据hovered/pressed/enabled三态选择颜色,border_radius=8实现圆角,视觉更现代;- 文字
msg_image在初始化时预渲染,避免font.render()每帧调用(该函数是 CPU 密集型操作)。
4.2Ship类的面向对象封装:从“画图”到“实体”
ship.py不是简单地blit(ship_image, (x,y)),而是将飞船建模为具有物理属性、行为方法、状态标志的游戏实体:
# ship.py import pygame class Ship: def __init__(self, ai_settings, screen): self.screen = screen self.ai_settings = ai_settings # 加载图像并获取外接矩形 self.image = pygame.image.load('images/ship.bmp') self.rect = self.image.get_rect() self.screen_rect = screen.get_rect() # 初始位置:底部居中 self.rect.centerx = self.screen_rect.centerx self.rect.bottom = self.screen_rect.bottom # 连续移动标志(按住方向键时持续移动) self.moving_right = False self.moving_left = False self.moving_up = False self.moving_down = False # 精确位置(float),避免 rect.centerx 只接受 int 导致移动卡顿 self.centerx = float(self.rect.centerx) self.centery = float(self.rect.centery) def update(self): """根据移动标志更新飞船位置""" if self.moving_right and self.rect.right < self.screen_rect.right: self.centerx += self.ai_settings.ship_speed_factor if self.moving_left and self.rect.left > 0: self.centerx -= self.ai_settings.ship_speed_factor if self.moving_up and self.rect.top > self.screen_rect.top: self.centery -= self.ai_settings.ship_speed_factor if self.moving_down and self.rect.bottom < self.screen_rect.bottom: self.centery += self.ai_settings.ship_speed_factor # 更新 rect 位置(centerx/centery 是 float,rect.centerx 只接受 int,需取整) self.rect.centerx = int(self.centerx) self.rect.centery = int(self.centery) def blitme(self): """在指定位置绘制飞船""" self.screen.blit(self.image, self.rect) def center_ship(self): """重置飞船到屏幕底部居中""" self.centerx = float(self.screen_rect.centerx) self.centery = float(self.screen_rect.bottom) self.rect.centerx = int(self.centerx) self.rect.centery = int(self.centery)注意:
self.centerx/self.centery使用float存储,解决了rect.centerx += 0.5因取整丢失精度导致的“移动迟滞”问题。update()中先用浮点运算,最后再int()赋值给rect,这是 Pygame 移动平滑的关键技巧。
5. 从源码到可执行:PyInstaller 打包与跨平台部署实操指南
源码可运行 ≠ 用户可安装。本节给出将alien_invasion.py打包为双击运行的.exe(Windows)、.app(macOS)、可执行二进制(Linux)的完整流程,重点解决 Pygame 打包的三大经典痛点:图标缺失、BMP 资源路径错误、控制台黑窗口。
5.1 环境准备与依赖冻结
首先确认 Pygame 版本兼容性(本项目基于 Pygame 2.0+):
pip install pygame==2.5.2 pip install pyinstaller==6.7.0然后生成精确依赖清单,避免打包冗余库:
pipreqs . --encoding=utf8 --force # 输出 requirements.txt,内容应为: # pygame==2.5.25.2 资源路径适配:解决images/ship.bmp打包后找不到的问题
PyInstaller 打包后,images/目录不会自动包含。需在alien_invasion.py开头添加资源定位逻辑:
# alien_invasion.py 开头添加 import sys import os def resource_path(relative_path): """获取资源绝对路径(兼容 PyInstaller 打包)""" try: # PyInstaller 创建临时文件夹,将路径存入 _MEIPASS base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) # 使用示例(替换原代码中的路径) self.ship_image = pygame.image.load(resource_path('images/ship.bmp'))同理,在settings.py中所有资源路径(如ship.bmp,alien.bmp)都需通过resource_path()加载。
5.3 PyInstaller 打包命令与参数详解
在项目根目录(含alien_invasion.py)执行:
# Windows 打包为无控制台窗口的 GUI 程序,嵌入图标,隐藏 console pyinstaller --onefile --windowed --icon=images/ship.ico --name="AlienInvasion" alien_invasion.py # macOS 打包为 app 包(需先安装 pyinstaller-macos-app) pyinstaller --onefile --windowed --icon=images/ship.icns --name="AlienInvasion" alien_invasion.py # Linux 打包(默认无图标,需后续手动添加) pyinstaller --onefile --windowed --name="alien_invasion" alien_invasion.py关键参数说明:
--onefile:打包为单个可执行文件(非目录);--windowed:禁用控制台窗口,避免游戏启动时闪黑框;--icon:指定图标文件(Windows 用.ico,macOS 用.icns,Linux 通常忽略);--name:输出文件名,不带后缀(Windows 自动加.exe)。
打包成功后,可执行文件位于dist/目录。首次运行会生成leaderboard.json,后续分数自动保存。
5.4 验证与分发:最小化运行时依赖检查
用户电脑未装 Python 也能运行,但需验证以下三点:
- 字体兼容性:
pygame.font.SysFont(None, 48)使用系统默认字体,Windows/macOS/Linux 均有 fallback,无需额外打包字体文件; - BMP 解码:Pygame 内置 BMP 解码器,无需
PIL或pillow; - 音频占位:因项目未启用声音,
pygame.mixer模块未被导入,避免 ALSA/PulseAudio 兼容性问题。
最终分发包只需dist/AlienInvasion.exe(Windows)或dist/AlienInvasion.app(macOS),体积约 12MB(含 Pygame 运行时),可直接邮件发送或网盘分享。
本文还有配套的精品资源,点击获取