简介:面向Python初学者与游戏开发爱好者,这份外星人入侵游戏Pygame源码包完整覆盖了飞船上下移动、空格发射子弹、外星人生成与碰撞、计分、速度升级、最高分记录及剩余飞船显示等核心玩法,是学习Pygame框架和游戏逻辑的实用范例,也适合作为课程设计或毕业设计的参考项目。源码基于win10+Python3.7+pygame开发,压缩包共688个文件、13.42MB,其中以557个Python源文件为主,便于逐行阅读和修改,另有27个pyd、24个pyc等编译文件及20个exe可执行文件、少量dll依赖库和txt说明文档,可直接运行体验,也可查看配置与环境依赖,降低部署门槛。目前已有2533人浏览学习,整个项目结构和模块划分清晰,可搭配《Python编程从入门到实践》外星人入侵学习笔记系列进一步理解代码设计,从而掌握游戏循环、事件处理、精灵组与碰撞检测等关键机制,快速上手自定义角色、关卡和界面,延展出更多玩法。
1. 项目概述:为什么我还在推荐这个“老掉牙”的游戏项目
如果你学过Python,大概率听说过《外星人入侵》这个项目。它出自《Python编程从入门到实践》这本书,几乎成了Python学习者绕不开的练手项目。说实话,我第一次写完这个项目的时候,游戏简陋得只能算是个“能动的小船打方块”,但正是这个项目让我真正理解了面向对象编程、事件驱动模型和游戏主循环这三个概念到底是怎么回事。
很多人问我,现在AI编程都这么强了,还有必要手动写一个pygame游戏吗?我的看法是:正因为有了AI辅助,你才更需要理解游戏代码的底层逻辑。你不可能靠提示词生成一个满足需求的外星人入侵游戏而不去改任何代码,但如果你懂游戏循环、精灵组、碰撞检测这些基本概念,你就能让AI帮你干活,你来掌舵。这篇文章就以“外星人入侵游戏pygame源代码”为线索,把整个项目的拆解思路、核心代码、踩坑经历一次说清楚。
这个项目适合谁看?刚学完Python基础语法、想找一个综合性项目练手的同学;以及对类、继承、模块化设计还没有完整概念的人。我会把整个项目按模块拆开讲,每个模块对应一个pygame的核心机制,这样你一边抄代码一边就懂了原理。
2. 整体设计与思路拆解
2.1 游戏机制到底怎么设计的
外星人入侵的玩法一句话就能概括:玩家操控一艘飞船左右移动,按空格射击子弹,抵御一波又一波从屏幕顶部向下移动的外星人。听起来简单,但里面牵涉到的编程概念一点都不简单。
我先把整个游戏的实体对象理一遍:飞船、子弹、外星人、外星人舰队、按钮、记分牌。每个实体在代码里都是一个类,这些类各管各的事,互不干扰。飞船负责响应键盘事件,子弹负责移动和碰撞检测,外星人负责在屏幕上移动和向下推进。
这种设计思路的核心是职责分离。游戏主程序只负责初始化、事件处理和画面刷新,具体的业务逻辑都封装在各自的对象里。比如飞船只知道自己怎么移动,它不需要知道子弹是怎么发射的;外星人只知道自己怎么移动,它不需要知道飞船在哪。这种解耦的好处是:改其中一个模块不会把其他模块搞崩。
游戏的状态管理也值得一提。整个游戏只有三种状态:active(游戏进行中)、inactive(游戏未开始或结束)、paused(暂停)。我用一个标志位game_active来控制游戏是否运行,标志位为True时才更新飞船、子弹和外星人的位置。这个标志位看着简单,但它决定了空闲画面和游戏画面之间的切换逻辑,也是新手最容易搞混的地方。
2.2 为什么选择pygame而不是其他游戏框架
你可能会问:现在做游戏不是都用Unity、Godot吗?为什么Python教程里还用pygame?这里面有两个层面的原因。
第一,pygame是纯Python扩展库,它的核心价值在于帮助你理解游戏开发的最基本原理,不需要你接触游戏引擎的复杂编辑器。Unity和Godot可以让你拖拽式地做游戏,但你拖出来的行为背后,封装掉的恰恰是最有价值的编程逻辑。而pygame的一切都摆在明面上:窗口是你创建的,事件是你轮询的,画面是你逐帧绘制的。这种“透明感”恰恰是学习阶段最需要的。
第二,pygame的生态比较成熟,跨平台支持好。Windows、macOS、Linux都能跑,文档和教程数量庞大,遇到问题基本都能搜到答案。而且它对2D游戏的支持足够强大,自带Surface(画布)、Sprite(精灵)、Group(精灵组)等模块,处理2D碰撞检测更是拿手好戏。
下面是我的建议:如果你想非常快速地做一个游戏DEMO,或者验证一个游戏创意,pygame绝对够用;如果你想做商业级3D游戏,就别在pygame上花时间了,直接去学引擎。坐标系、事件循环、碰撞检测这些底层逻辑,其实引擎和pygame是相通的,先在一个简单环境里打好基础,再去学引擎会更轻松。
2.3 代码架构的模块化划分
很多初学者写pygame代码喜欢把所有东西塞进一个文件里,我不建议这样写。外星人入侵项目的完整代码量在300行左右,全部塞在一个文件里虽然能运行,但后期改起来非常痛苦。我的做法是分模块组织代码:
alien_invasion.py:主程序,负责游戏初始化、主循环和事件处理settings.py:游戏配置类,集中管理所有参数(屏幕尺寸、速度、颜色等)ship.py:飞船类,管理飞船的显示和移动bullet.py:子弹类,管理子弹的生成、移动和销毁alien.py:外星人类,管理外星人的显示和移动game_functions.py:事件处理函数集,把键盘事件和游戏状态对应起来button.py:按钮类,处理开始按钮等交互元素scoreboard.py:记分牌类,显示得分、等级和最高分
这种模块化划分让每个文件都聚焦一件事,后面我加音效、加爆炸动画、加难度递增都只需要改对应的文件,主程序基本不用动。
3. 核心代码实现与原理详解
3.1 第一步:主循环与窗口初始化
pygame程序的核心是while主循环。这个循环做的事情就是三件事:处理事件、更新状态、重绘画面。我来解释一下为什么是这个顺序。
事件处理放在最前面是合理的,因为用户的键盘输入是驱动游戏变化的起点。按下左箭头,飞船向左移动;松开左箭头,飞船停止移动。这些输入如果不在每一帧最开始处理,就会产生延迟感。
更新状态放在中间,是指根据事件处理的结果,修改游戏实体的位置和状态。比如飞船移动了,子弹飞出了屏幕,外星人撞到了飞船。这些逻辑是按帧执行的,每一帧都在计算实体当前应该在哪。
重绘画面放在最后,是把上面计算好的状态画到屏幕上。pygame用pygame.display.flip()做双缓冲刷新——先在一个后台画布上画好一整帧内容,再一次性提交到屏幕显示。这样能最大程度避免画面闪烁。注意别用pygame.display.update()来刷新全屏,update()更适合刷新局部区域,全屏刷新用flip()更高效。
窗口初始化的代码大概是这样的:
import pygame from settings import Settings from ship import Ship import game_functions as gf def run_game(): pygame.init() ai_settings = Settings() screen = pygame.display.set_mode( (ai_settings.screen_width, ai_settings.screen_height)) pygame.display.set_caption("Alien Invasion") ship = Ship(ai_settings, screen) while True: gf.check_events(ai_settings, screen, ship, bullets) ship.update() gf.update_bullets(ai_settings, screen, ship, aliens, bullets) gf.update_aliens(ai_settings, screen, ship, aliens, bullets) gf.update_screen(ai_settings, screen, ship, aliens, bullets)这里有一个重要的点:为什么设置类要单独写?因为你后续肯定要调参——飞船速度太慢了,子弹太快了,外星人太多了。如果这些参数硬编码在游戏代码里,每次调参都要去翻代码、改数字,很容易改出bug。把参数全部集中在settings.py里,调起来就是一分钟的事,这才是正确的做法。
3.2 飞船控制:事件驱动是怎么运作的
飞船类是我介绍“类”概念的第一站。它继承自pygame的Sprite类,这样后面才能用精灵组管理它。
import pygame class Ship(Sprite): def __init__(self, ai_settings, screen): super().__init__() 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.center = float(self.rect.centerx) self.moving_right = False self.moving_left = False def update(self): if self.moving_right and self.rect.right < self.screen_rect.right: self.center += self.ai_settings.ship_speed if self.moving_left and self.rect.left > 0: self.center -= self.ai_settings.ship_speed self.rect.centerx = self.center def blitme(self): self.screen.blit(self.image, self.rect)这里最值得琢磨的是移动标志位moving_right和moving_left的用法。pygame不像编程比赛里那样监听“按键按下”就立刻移动一次,而是靠标志位持续移动——按下方向键时设置标志位为True,松开时设置为False。这样飞船的移动是“按住就走,松开即停”的连续行为,而不是一下一下地蹦。
判断边界条件时,有一个细节我吃过亏:如果self.rect.right <= self.screen_rect.right写成<,飞船到边界时会有大概一像素的缝隙,看起来就像贴不到底;改成<=或者像我示例里那样判断<再加上碰撞检测,效果更自然。这里的self.center = float(self.rect.centerx)是防止位置精度丢失——rect的坐标是整数,直接加速度再强制转换会导致计算误差,用浮点数保存位置,最后再赋回rect,移动才精准。
按键事件的处理放在game_functions.py里,核心是这段:
def check_events(ai_settings, screen, stats, play_button, ship, aliens, bullets): for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() elif event.type == pygame.KEYDOWN: check_keydown_events(event, ai_settings, screen, stats, ship, aliens, bullets) elif event.type == pygame.KEYUP: check_keyup_events(event, ship)注意pygame.event.get()会取出队列里的所有事件,所以要用for循环遍历。有人会问:如果事件太多循环一遍性能会不会差?实际上pygame的事件队列很小,玩家一秒钟产生的键盘事件数量级是几千条,这个开销完全可以忽略。
3.3 子弹机制与精灵组管理
子弹的实现最能体现pygame的Group类的威力。游戏的子弹是不断生成、不断飞出屏幕的,如果用列表来管理,你得手动遍历删除飞出去的子弹,代码写起来又繁琐又容易漏。
pygame的Group类帮我们解决这个问题。它是一个容器,可以往里面不断添加Sprite对象,也可以把不满足条件的Sprite一次性清掉。我用pygame.sprite.Group()创建了一个bullets分组,每按一次空格就往组里添加一颗子弹。
子弹类的核心代码如下:
class Bullet(Sprite): def __init__(self, ai_settings, screen, ship): super().__init__() self.screen = screen self.rect = pygame.Rect(0, 0, ai_settings.bullet_width, ai_settings.bullet_height) self.rect.centerx = ship.rect.centerx self.rect.top = ship.rect.top self.y = float(self.rect.y) self.color = ai_settings.bullet_color self.speed_factor = ai_settings.bullet_speed def update(self): self.y -= self.speed_factor self.rect.y = self.y def draw_bullet(self): pygame.draw.rect(self.screen, self.color, self.rect)这里我用pygame.Rect创建了一个矩形当子弹,没有用到图片文件。这样做的好处是省资源,而且子弹的外观可以通过draw.rect直接绘制,调颜色改尺寸都很方便。你完全可以用图片做子弹,但性能上没有优势,开发效率反而低。
子弹生成之后,主循环里通过bullets.update()更新所有子弹的位置。接着要删除已经飞出屏幕的子弹,否则子弹对象会越来越多,内存被白白占用:
def update_bullets(ai_settings, screen, stats, sb, ship, aliens, bullets): bullets.update() for bullet in bullets.copy(): if bullet.rect.bottom <= 0: bullets.remove(bullet)这里的关键是遍历副本。你不能在遍历bullets的同时修改它,这在Python里会报运行时错误。bullets.copy()创建了一个新的引用集合,遍历副本删除原集合中的元素,这样安全又高效。
3.4 外星人的生成、移动与碰撞
外星人的生成逻辑比飞船和子弹复杂,因为你不是生成一个,而是生成一整排、一整片。我的做法是:先计算一行能放多少个外星人,再通过两层循环铺满整个屏幕宽度。核心部分在game_functions.py里:
def get_number_aliens_x(ai_settings, alien_width): available_space_x = ai_settings.screen_width - 2 * alien_width number_aliens_x = int(available_space_x / (2 * alien_width)) return number_aliens_x def create_alien(ai_settings, screen, aliens, alien_number, row_number): alien = Alien(ai_settings, screen) alien_width = alien.rect.width alien.x = alien_width + 2 * alien_width * alien_number alien.rect.x = alien.x alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number aliens.add(alien) def create_fleet(ai_settings, screen, ship, aliens): alien = Alien(ai_settings, screen) number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width) number_rows = get_number_rows(ai_settings, ship.rect.height, alien.rect.height) for row_number in range(number_rows): for alien_number in range(number_aliens_x): create_alien(ai_settings, screen, aliens, alien_number, row_number)为什么每个外星人之间留出“一个外星人宽度”的间隔?因为如果贴得太紧,视觉上就是一团色块,玩家根本分不清是几个外星人。每个外星人占2 * alien_width的宽度,所以一行能放的外星人数量就是可用宽度除以这个值。
外星人的移动是整体移动——整个舰队的左上角坐标决定了所有外星人的显示位置。我通过一个fleet_direction标志配合fleet_drop_speed控制方向:碰到右边缘就向下移动一段距离,然后反向向左。这个逻辑实现了外星人经典的“之”字形下压走位。
当外星人到达屏幕底部或者撞到飞船,游戏结束。这一步的检测用pygame的spritecollideany可以一行搞定:
if pygame.sprite.spritecollideany(ship, aliens): ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)spritecollideany会检测一个精灵是否和指定组里的任意精灵碰撞,返回第一个碰到的对象,没有碰撞返回None。这是pygame最常用的碰撞检测手段,比手动遍历矩形相交判断简单太多了。
子弹打中外星人的检测类似,不过要注意这里使用的是组与组的碰撞:
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)groupcollide返回一个字典,键是被打中的子弹,值是该子弹打中的外星人列表。这里两个参数True, True表示碰撞后两个对象都消失。如果子弹穿透力强,想一次打多个,可以把第一个参数改成False。
3.5 记分系统与难度递增
游戏完整了之后,我给它加上计分和难度升级逻辑。计分逻辑本身不复杂,但有几个细节值得说说。
分数的存储我用了一个GameStats类,它同时保存了剩余飞船数和当前分数。为什么不用全局变量?因为分数在游戏结束后要重置,重新开始时要从0加载,放在类里更便于管理生命周期。
class GameStats(): def __init__(self, ai_settings): self.ai_settings = ai_settings self.reset_stats() self.game_active = False self.high_score = 0 def reset_stats(self): self.ships_left = self.ai_settings.ship_limit self.score = 0 self.level = 1计分的核心逻辑在于:不同颜色的外星人分值应该不同,这样玩家才有优先打高分目标的策略选择。外星人每次移动到底部后,我让它们缩小一点并加快速度,形成难度梯度。等级每提升一级,外星人速度增加1.1倍,这个倍率影响很微妙——太小了玩家觉得无聊,太大了玩家觉得愤怒,1.1倍是我反复调试后觉得比较合适的节奏。
显示分数时,把数字转成字符串后在屏幕右上角渲染。这里我踩过一个坑:中文字体在pygame里默认渲染不出来,会出现方块。如果你在记分牌上写“分数:100”,默认字体不支持中文,必须加载系统自带的中文字体:
font = pygame.font.SysFont('simhei', 36)或者用英文标签“Score: 100”,省事不少。这个坑几乎每个做pygame中文显示的人都会遇到,提前给你提个醒。
4. 常见问题与排查技巧实录
4.1 pygame安装失败的终极解决办法
很多新手在第一步装pygame就卡住了。最典型的问题是error: failed to build 'pygame' when getting requirements to build wheel,这个报错一看就是pip在尝试从源代码编译pygame而不是直接下载预编译包。原因通常是操作系统的Python版本太新或太老,或者pip版本过低,导致找不到对应的轮子(wheel)文件。
我的经验是把pip先升级到位:
python -m pip install --upgrade pip pip install pygame --pre加上--pre参数的意思是允许安装预发行版,有时候最新版的pygame正式版还没发布,但预发行版已经兼容新版本的Python。如果你在Windows上一直装不上,还有一个更稳的方案:直接去pygame的官网下载对应你Python版本的.whl文件,然后本地安装:
pip install pygame-2.5.2-cp311-cp311-win_amd64.whl如果装了Python 3.12以上,pygame官方版本支持可能滞后,我的建议是装一个Python 3.10或3.11的虚拟环境来跑这个项目,省心非常多。虚拟环境的创建也很简单:
python -m venv alien_venv alien_venv\Scripts\activate # Windows source alien_venv/bin/activate # macOS/Linux4.2 游戏卡顿和帧率问题的排查思路
有段时间我的游戏跑起来特别卡,外星人一多就掉帧。排查之后发现罪魁祸首是每条子弹的update里都加载了一份图片资源。子弹加载一次图片还好,但如果你的代码里每生成一个子弹就pygame.image.load()一次图片,那么几十颗子弹就要加载几十次图片,性能直接崩了。
正确的做法是:图片资源在类的__init__里一次性加载,放到类属性里,所有实例共用一份。如果你写的代码里update方法中也出现pygame.image.load(),赶紧把它挪到__init__里。
另一个卡顿原因是碰撞检测的频率。groupcollide对每组几百个精灵做矩形相交检测,运行效率并没有想象中高。如果外星人和子弹特别多,可以考虑先用简单的距离判断过滤掉明显不可能碰撞的物体,再做精确检测。但在大多数情况下,几百个精灵的碰撞检测不至于明显掉帧,优先排查图片加载问题更有效。
还有一个容易忽略的点:主循环里如果没有控制帧率,游戏会以你电脑能跑的最高速度运行,快的时候几千帧都有,这时不仅飞船快到失控,CPU也被白白吃满了。解决方法是加一个时钟控制:
clock = pygame.time.Clock() # 在主循环末尾加上: clock.tick(60) # 限制最大帧率为60这样游戏逻辑就以固定的时间步长运行,不同性能的电脑上的表现保持一致。
4.3 “我按方向键没反应”的排查手册
按键没反应是一个高频问题。我第一次遇到这个问题的时候,代码看着完全没问题,后来才发现是事件处理分支顺序写错了。
pygame.event.get()在每一帧循环中只会返回这一帧内产生的事件。如果你在处理事件的循环里又调用了pygame.event.get(),第二次获取到的就是一个空列表,所有按键丢失了。所以整个主循环里只能有一个获取事件的入口,其他逻辑全部围绕这个入口分发。
还有一个常见原因:被其他窗口抢走了焦点。pygame窗口没有焦点时,键盘事件不会发送给游戏窗口,此时按什么都没反应。排查方法是打印当前事件列表:
for event in pygame.event.get(): print(event)如果打印出一堆MouseMotion但就是没有KEYDOWN,基本可以确定是窗口焦点问题。
4.4 代码写完了运行没画面,黑屏是为什么
黑屏问题通常有两个原因。一个是背景色没设置:pygame.display.set_mode()创建出来的窗口默认是黑色的,如果你没有在update_screen里调用screen.fill(ai_settings.bg_color)填充背景色,那窗口就一直黑着,看起来就像游戏崩了。第二是主循环里的pygame.display.flip()被跳过,或者写在了条件分支里,导致画面永远不会刷新。
另外还要确认你的窗口大小设置没有参数错误,比如写下(1000,)而不是(1000, 800)这种元组错误,set_mode()会直接抛异常。这类问题虽然细碎,但报错日志都会指向具体的行号和错误信息,顺着提示改就行。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| pip安装时报编译错误 | pip版本过旧,找不到预编译wheel | 升级pip,或用--pre参数安装预发行版 |
| 中文显示为方块 | pygame默认字体不支持中文 | 用pygame.font.SysFont('simhei', 36)加载中文字体 |
| 游戏运行极快,控制不住 | 没有限制帧率 | 用Clock.tick(60)控制帧率 |
| 按键有时失灵 | 重复调用event.get() | 确保主循环只有一个事件轮询入口 |
| 子弹打到外星人但没消失 | groupcollide参数配置错误 | 碰撞后两组都需销毁时,两个参数都设为True |
| 窗口黑屏没画面 | 忘记填充背景色 | 在update_screen中调用screen.fill() |
| 外星人不下移 | fleet_direction未正确翻转 | 检查边缘检测逻辑,到达边缘时翻转方向 |
5. 扩展思路:这个项目还能变成什么样子
外星人入侵完成后,你可以往不同方向扩功能,扩展的过程也是练代码的过程。我个人尝试过几个方向,效果都还不错。
第一,给游戏加一个“Boss关”。每过5关生成一个巨大的外星人,血条厚,攻击方式多,打死了有额外加分。实现思路其实不复杂,就是定义一个Boss类和BossBullet类,Boss每隔几秒发射一次子弹,玩家需要躲子弹、打Boss。碰撞检测和普通外星人是一样的,只是逻辑多一点。
第二,改成双人模式。一个玩家用键盘方向键控制飞船,另一个玩家用WASD控制另一艘飞船,各自计分。这个改动量不大,主要是事件处理里需要分辨两个玩家的按键,以及飞船、子弹的实例都各建一套。
第三,增加道具系统。外星人被打死后有概率掉落“加速”“三连发”“护盾”等道具,玩家吃到后获得不同的增益效果。实现方式是新增一个PowerUp类,它的update逻辑和子弹差不多,只是方向向下,和飞船做碰撞检测。
我记得有一次我给自己限时48小时,强行给游戏加了音效和粒子特效,结果音效容易弄,粒子特效反而费了不少功夫。用pygame做粒子特效的常规办法是生成一堆小矩形,赋予不同的速度和透明度,逐帧更新位置和生命周期。这部分代码量不大,但看起来效果很酷——至少比我预期中酷。
6. 写在最后的实操心得
做外星人入侵这个项目,我前后重写了三遍。第一遍跟着书抄,什么也没记住;第二遍不看书自己写,卡在碰撞检测卡了一整天;第三遍从头设计整体架构,才算真正把这个项目的精髓吃透了。我自己的体会是,这个项目最好的打开方式不是一次写完,而是分三次写:第一次照着参考代码敲通,体验完整的开发流程;第二次不要看代码,只靠需求描述从零写出一个能玩的版本;第三次加入自己的设计改动,比如新的武器、新的敌人、新的计分规则。三遍下来,你对pygame的掌握深度会远超只抄一遍代码的人。
最后分享一个小技巧:调试pygame游戏时别依赖print,视觉信息比文本信息直观得多。你可以在画面里临时绘制调试矩形,把碰撞盒显示出来,一眼就能看出精灵之间的碰撞区域是否和视觉位置一致。
# 在update_screen末尾临时加一段调试代码 pygame.draw.rect(screen, (255, 0, 0), ship.rect, 2) pygame.draw.rect(screen, (0, 255, 0), alien.rect, 2)画出红色和绿色的矩形框,如果可见画面位置和碰撞框对不上,问题就一目了然。调试完把这行删掉就行。这个习惯我保留到现在,做任何游戏项目都会先用可视化方式检查碰撞区域。外星人入侵只是一个起点,把它的代码吃透了,你再用pygame做其他2D游戏时,会发现大多数机制不过是这个项目里各组件的排列组合而已。
本文还有配套的精品资源,点击获取