用Pygame实现2D飞行射击游戏:从架构到开源实战
2026/9/8 9:33:47 网站建设 项目流程

简介:SoldatAirForce是一款基于Python Pygame库开发的飞行射击开源游戏,面向想通过真实项目学习Pygame与游戏开发逻辑的初学者和开发者。项目源代码完整公开,涵盖游戏主循环、窗口渲染、事件处理、精灵动画、碰撞检测以及子弹导弹发射等模块;借助Pygame的精灵与图像接口,飞机机动、爆炸特效和背景绘制均有简洁实现。压缩包约6.34MB,包含SAF 0.8早期版本的源码文件,便于逐行研读并二次修改。已有96人学习下载,适合作为入门游戏编程的实战范例。通过这份代码,读者可以理解游戏状态更新与实时响应的具体写法,掌握对象移动、物理模拟和多人对战所需的数据同步思路,并学习如何借助开源社区改进项目。 做游戏这件事,最难的往往不是代码,而是“开个头”。SoldatAirForce这个名字,懂点法语的朋友应该能看出来,“Soldat”是士兵的意思,合起来就是“空军士兵”。这是我用Pygame从零写的一个2D纵向卷轴飞行射击游戏,从策划到核心逻辑完成大概花了两周业余时间,最后以开源项目的形式放了出来。写这篇文章,是想把整个项目的设计思路、核心代码实现、踩过的坑,以及开源发布时要注意的细节都摊开聊一聊。不管你是Python游戏开发的新手,还是想在现有项目上改点东西做二次开发的爱好者,这篇内容应该都能帮你少走一些弯路。

1. 项目概述:SoldatAirForce是什么,为什么值得关注

1.1 一句话说清这个游戏

SoldatAirForce是一个纯Python实现的2D飞行射击游戏,玩家操控一架战斗机,在纵向滚动的天空战场中左右闪避、上下穿梭,击落迎面而来的敌机,收集分数,最终挑战关卡Boss。所有图形都由Pygame绘制,没有外部美术资源,代码里通过内置的几何图形和精灵类完成战机、子弹、爆炸特效的渲染。只要装好Python和Pygame,拉下代码就能直接跑。

这个项目解决的痛点很具体:想学游戏开发的人,往往被Unity、Unreal这种大型引擎庞大的编辑器界面劝退,或者被“先画美术资源、再写逻辑、再调物理”这种高门槛流程拦住。SoldatAirForce选择了最朴素的方式——一切皆代码,逻辑清晰、结构简洁。它适合三类人:第一是刚学完Python基础、想用实际项目练手的初学者;第二是想研究Pygame游戏运行机制的同学;第三是准备做毕业设计或课程作业,需要一个能跑、能讲、能改的游戏项目的人。

1.2 为什么选Pygame而不是其他引擎

市面上能用来做游戏的工具很多,Unity适合3D和商业项目,Godot上手也不算难,但Pygame有个无可替代的优势:贴近底层,所见即所得。它本质上是SDL2的Python封装,没有复杂的场景编辑器和组件系统,游戏循环、事件处理、碰撞检测、精灵管理这些核心概念全都直接暴露在代码里。你写一行pygame.draw.rect,屏幕上就会出现一个矩形,这种正向反馈对新手特别友好。

当然,Pygame的缺点也同样明显:没有内置物理引擎,性能上限低,不适合做画面精良的大型3D游戏。但SoldatAirForce这种2D射击游戏恰恰在Pygame最擅长的范围内。我把引擎选型当成工具选型来看,杀鸡用牛刀反而是负担,Pygame的轻量、直接,正好让项目保持在一个“一晚上能读懂全貌,一周能改出新玩法”的体量上。

对比项PygameUnityGodot
上手门槛低,有Python基础即可高,需要学习编辑器中,脚本和场景双线
安装体积极小,几MB级别数GB数百MB
性能上限适合2D小游戏高,支持3D中高,支持2D/3D
是否免费开源免费个人免费,商业收费开源免费

对于学习目的而言,Pygame的“小而完整”恰恰是它最难得的优点。

2. 环境准备与工程结构:好的开始是成功的一半

2.1 开发环境搭建与安装坑

我推荐的版本组合是Python 3.10以上加pygame 2.5.x。安装命令很简单:

pip install pygame

如果你在Windows或macOS上执行这条命令,通常几秒钟就能装好,因为官方已经发布了预编译的wheel包。但如果你用的是某些Linux发行版,很可能会遇到一个让人头大的报错:

error: failed to build 'pygame' when getting requirements to build wheel

这个报错的核心原因是系统缺少Pygame编译时依赖的SDL2开发库。处理方法有两种:一种是先安装系统依赖,以Ubuntu/Debian为例:

sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev

然后重新pip install pygame。另一种更省事的方法是直接安装预编译版本:

pip install pygame --only-binary=all

如果网络条件不好,不妨先规划好镜像源再安装,这类网络问题在实际开发中很常见。实测下来,先把系统依赖补齐再装,基本不会再有编译报错。

2.2 项目目录怎么组织才能不混乱

很多Pygame新手喜欢把所有代码堆在一个main.py里,刚开始没问题,但一旦游戏功能变多,几百上千行代码挤在一起,改一个敌人逻辑都要滚动半天屏幕,维护成本直线上升。我在SoldatAirForce里做了一次模块化拆分:

soldatairforce/ ├── main.py # 游戏入口,主循环 ├── settings.py # 全局配置,窗口尺寸、帧率、颜色、速度参数 ├── sprites.py # 玩家、敌机、子弹、爆炸效果的精灵类 ├── levels.py # 敌机波次生成和关卡难度控制 ├── ui.py # 计分、血量显示、开始与结束界面 └── assets/ # 预留的图片和音效目录

settings.py里统一存放所有魔数,比如PLAYER_SPEED = 8BULLET_SPEED = -12,这样调整平衡性时只需要改一个文件。sprites.py负责所有游戏对象的类定义,levels.py管理敌机刷新,ui.py处理界面上的一切。main.py只做三件事:初始化Pygame、创建精灵组、运行主循环。

这种结构带来的直接好处是:想加一个新敌人,只需要在sprites.py里加一个类,然后在levels.py里把它挂到波次数据里,不用动其他任何代码。对新手来说,这种“模块边界清晰”的设计比堆代码重要得多。

3. 核心玩法与代码实现:从零手写一个空战游戏

3.1 玩家控制:键盘响应与移动边界

玩家战机的控制是整个游戏最基础的交互。我用方向键和WASD双键位控制移动,空格键射击。Pygame的按键处理有两种思路:一种是监听KEYDOWNKEYUP事件,适合处理跳跃这类单次触发操作;另一种是每帧读取pygame.key.get_pressed()返回的按键状态,适合处理持续移动。射击游戏用第二种更顺手。

核心控制逻辑大致如下:

# sprites.py class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image = pygame.Surface((40, 50), pygame.SRCALPHA) pygame.draw.polygon(self.image, (100, 200, 255), [(20, 0), (0, 50), (40, 50)]) self.rect = self.image.get_rect() self.rect.midbottom = (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 60) def update(self, keys): dx, dy = 0, 0 if keys[pygame.K_LEFT] or keys[pygame.K_a]: dx -= PLAYER_SPEED if keys[pygame.K_RIGHT] or keys[pygame.K_d]: dx += PLAYER_SPEED if keys[pygame.K_UP] or keys[pygame.K_w]: dy -= PLAYER_SPEED if keys[pygame.K_DOWN] or keys[pygame.K_s]: dy += PLAYER_SPEED self.rect.x = max(0, min(SCREEN_WIDTH - self.rect.width, self.rect.x + dx)) self.rect.y = max(0, min(SCREEN_HEIGHT - self.rect.height, self.rect.y + dy))

这里有个细节值得注意:我没有直接对self.rect.x += dx然后不管,而是用了maxmin把移动结果钳制在屏幕边界内。如果不做边界处理,玩家按几下方向键战机就飞到屏幕外面去了,体验会非常差。边界值要考虑到战机贴图的半透明边缘,所以左右边界用矩形宽度做偏移,视觉上刚刚好。

3.2 敌机生成:从随机乱刷到波次控制

敌人怎么生成,直接决定游戏好不好玩。失败的方案是“每帧随机生成”,这样敌机出现完全没有规律,要么玩家被密集弹幕秒杀,要么半天等不到一架敌机。我在SoldatAirForce里采用了“时间轴+分数阈值”双控的波次生成方案。

levels.py里维护一个波次列表,每个元素是一个字典,定义了这个波次什么时候触发、生成什么类型的敌机、敌机数量是多少。核心逻辑如下:

# levels.py WAVE_DATA = [ {"trigger": 0, "enemy_type": "scout", "count": 5}, {"trigger": 20, "enemy_type": "scout", "count": 8}, {"trigger": 45, "enemy_type": "interceptor", "count": 4}, {"trigger": 70, "enemy_type": "mix", "count": 8}, {"trigger": 100, "enemy_type": "boss", "count": 1}, ] def get_wave(score): for wave in WAVE_DATA: if wave["trigger"] == score: return wave return None

敌人类型设计为三类:侦察机血少移动慢,适合给玩家练手;拦截机速度快、体型小,出现后会造成压迫感;Boss血厚、弹幕多,是整个关卡的节奏高潮。通过分数控制波次还有一个好处:玩家玩得越好,敌机出现得越快,难度曲线自然呈上升趋势,不需要额外写复杂的AI逻辑。

3.3 碰撞检测与爆炸特效:游戏逻辑的胜负手

飞行射击游戏里,碰撞检测是成败的关键。Pygame的pygame.sprite.Grouppygame.sprite.spritecollide已经封装了基础的矩形碰撞检测。但矩形检测有个问题:战机精灵本身是不规则三角形,直接用矩形框碰撞,经常出现“明明子弹擦着机翼飞过去了,却判定命中”的离谱情况。所以我在SoldatAirForce里对关键对象启用了collide_mask,用像素级掩码来检测。

用法很简单,先给每个精灵设置self.mask = pygame.mask.from_surface(self.image),然后调用:

hits = pygame.sprite.spritecollide(player, enemy_group, True, pygame.sprite.collide_mask)

第三个参数是dokill,命中后敌方精灵直接消失;第四个参数指定用collide_mask作为碰撞函数。

爆炸特效是另一个容易忽略的体验点。我用纯代码生成了爆炸动画:当敌机被击毁时,在死亡位置生成一个Explosion精灵,内部维护一个帧索引,每一帧绘制一个半径随机、颜色由亮黄到暗红的圆形,持续0.3秒后自动销毁。虽然简单,但视觉反馈足够明确,玩家的“击毁感”立刻拉满。

4. 性能优化与常见问题排查实录

4.1 帧率控制:为什么你的游戏在不同电脑上速度不一样

Pygame游戏有一个非常典型的性能问题:没有帧率控制时,游戏运行速度完全取决于电脑性能。在高配电脑上战机和子弹快得像瞬移,在低配电脑上又慢得像蜗牛。解决方法是利用pygame.time.Clock固定帧率。

# main.py clock = pygame.time.Clock() while running: dt = clock.tick(60) / 1000.0 # 转换为秒 # 更新游戏逻辑 game.update(dt) # 绘制 game.draw(screen) pygame.display.flip()

这里我引入了dt(delta time),所有移动逻辑都乘以dt,把“每帧移动固定像素”改为“每秒钟移动固定像素”。这样即使在高刷新率显示器上,游戏速度也不会突变。实测下来,帧率固定在60FPS是最舒服的,既流畅又不浪费CPU。

4.2 对象池:子弹多了以后变卡怎么办

早期的版本里,每颗子弹都是一个独立的sprite.Bullet对象,发射时创建,命中或出界时销毁。玩家按住空格键疯狂射击时,一秒钟可能产生十几颗子弹,加上敌机和爆炸特效,Python的垃圾回收机制会频繁工作,游戏开始出现肉眼可见的卡顿。

解决这个问题的标准方案是对象池。我在sprites.py里给子弹维护了一个缓存列表:

class BulletPool: def __init__(self, size=50): self.pool = [Bullet() for _ in range(size)] self.index = 0 def get(self, x, y): bullet = self.pool[self.index] self.index = (self.index + 1) % len(self.pool) bullet.reset(x, y) return bullet

对象池的核心思想就是“重复利用已创建的对象,而不是反复创建销毁”。对性能敏感的游戏,这个优化是必须做的。另外,超出屏幕的子弹要在update里及时标记为kill()并从精灵组移除,否则对象池也会被拖垮。

4.3 常见问题速查表

问题现象可能原因解决方案
窗口一闪而过主循环退出后未调用pygame.quit()或缺少事件循环确认while running循环正常且包含事件处理
图片有白底加载的PNG图片未去背景使用pygame.SRCALPHA或转set_colorkey设置透明色
子弹穿过敌机但不消失碰撞检测用矩形而精灵不规则改用pygame.sprite.collide_mask像素级碰撞
游戏速度随帧率波动没有使用Clock.tick和delta time固定帧率,移动逻辑乘以dt
安装时报SDL2编译错误系统缺少SDL2开发库先装系统依赖,再用--only-binary=all安装

5. 开源发布与后续扩展方向

5.1 发布到GitHub前必须做的事

好项目要有好的开源仪式感。我发布SoldatAirForce时,给仓库补全了三样东西:许可证、README、requirements.txt。

  • LICENSE:我选了MIT,简单宽泛,别人拿到代码可以自由使用和修改。如果想限制商业用途,也可以选GPL。
  • README:写清楚游戏是什么、运行环境、安装和启动命令,最好贴一张运行截图。很多开源项目代码写得不错,但README太简陋,直接劝退来访者。
  • requirements.txt:固定依赖版本,至少包含pygame>=2.0,这样别人用pip install -r requirements.txt一条命令装好环境。

发布的时候还有一个很容易忽略的坑:不要把__pycache__、虚拟环境目录和本地的assets文件提交到仓库。在项目根目录加一个.gitignore,写上__pycache__/*.pycvenv/等,能避免不少尴尬。

5.2 这个项目后续还可以往哪扩展

SoldatAirForce的核心框架搭好之后,扩展空间非常大。我自己列了几个方向:

  • 音效系统:用pygame.mixer加入射击音效、爆炸音效和背景音乐。没有音频素材时,可以用代码合成简单的音效,比如用正弦波生成“嗒嗒”射击声。
  • 本地双人模式:Pygame天然支持一个键盘控制多个角色,把Player类复制一份,改为不同按键即可。
  • 关卡编辑器:把WAVE_DATA抽成一个独立的JSON文件,玩家可以在不写代码的情况下自定义关卡。
  • 打包成可执行文件:用PyInstaller把项目打包成单个EXE,发给朋友直接双击就能玩,会比让他装Python环境友好得多。

我在开发过程中的真实体会是,Pygame项目最怕的不是功能复杂,而是结构混乱。只要保持模块边界清晰、参数集中管理、性能优化提前考虑这三点,一个简单的小游戏也能成为稳定运行的完整作品。

最后再分享一个小技巧:开发过程中把游戏截图存下来,最后做成一个动图或短视频放在仓库里,会比任何文字说明都有说服力。无偿开源的热情需要被看见,而一个能让人三秒看懂玩法的展示素材,就是最好的门面。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询