ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python和pygame实现二维飞行模拟器:从起飞到坠毁完整实战

用Python和pygame实现二维飞行模拟器:从起飞到坠毁完整实战 很多开发者尤其是刚入门的朋友都会有这样一段经历语法书啃了一本网课也刷了不少可一旦要自己动手做一个小项目还是不知道该从哪里下手。如果你恰好对“飞行”这件事有执念那这个问题就会变成——我能不能用自己的代码让一样东西飞起来“让代码飞起来”听起来玄其实完全可以落地。本文将手把手带你完成一个二维飞行模拟器用 Python 和 pygame 实现飞机在屏幕中起飞、转向、下坠和坠毁的完整过程。它不依赖任何游戏引擎也不涉及复杂的三维数学只需要基础物理和面向对象知识即可跟上。标题叫《中国人能飞》不是一句口号而是本文想表达的一个工程事实在今天任何一个中国开发者都可以靠一台电脑、一个解释器、几行代码亲手在屏幕上实现“飞行”这件曾经很遥远的事情。读完本文你会掌握游戏循环、实时物理模拟、键盘交互和简单碰撞检测等核心技能并得到一个可以继续扩展的飞行模拟器项目。如果你是想练手的中级开发者或者只是想弄明白“飞行模拟游戏背后的原理”的新手这篇教程都很适合。1. 用代码实现“能飞”这件事1.1 为什么选择 Python 与 pygame先说结论Python 的语法足够简单适合把“物理模型”讲清楚pygame 提供了窗口、绘图、键盘事件等基础能力让我们不必从零去操作系统底层接口。两者组合起来是最快做出一个可视化飞行模拟器的路线之一。如果你用 C 和 OpenGL当然也能做但要处理的样板代码会多很多窗口创建、渲染管线、事件循环每一项都是不小的工程量。对大多数想快速验证“飞行”逻辑的开发者来说Python pygame 的学习成本最低阅读代码的人也更容易看懂。更重要的是飞行模拟的核心在于“物理模型”重力怎么加、推力怎么算、速度怎么更新。这些逻辑和用什么渲染库无关。先把物理模型写明白以后换 Unity、Unreal 或者更专业的飞行仿真工具时思路是可以平移的。1.2 这个项目到底要做什么先给你描述一下最终效果方便心里有个数屏幕中央附近有一架白色三角形飞机。按住空格键发动机点火飞机获得推力松开空格推力消失。按左、右方向键可以旋转推力方向让飞机向左或向右倾斜飞行。屏幕左上角实时显示速度、高度、推力大小和推力角度。飞机受重力影响如果推力不足或方向不对会慢慢往下掉最终接触地面并显示“坠毁”提示。按下 R 键飞机重置回初始位置重新起飞。这个项目虽然简单却是一个完整的小型模拟系统。你写完以后可以很自然地把代码扩展到三维视角、更真实的空气动力学、粒子火焰效果甚至是结合陀螺仪数据的飞控演示。1.3 需要什么基础如果你已经学过 Python 的基础语法知道什么是变量、循环、函数和类那这个项目直接就能做。即便你还不太熟悉pygame的事件体系也没关系本文会把每一段代码都拆开讲。如果完全没有 Python 基础那需要先补一补import的用法、def定义函数、class定义类、list和dict的基本操作。以上内容花不到半天时间就能过一遍。2. 飞行模拟的两个核心组成2.1 物理模型重力、推力、速度和位置飞行模拟的本质是把现实世界的运动规律翻译成数学公式再翻译成代码。二维场景中我们只需要关心飞机在平面里的运动。先复习一个最基本的关系加速度改变速度。速度改变位置。写成代码就是vx ax * dt vy ay * dt x vx * dt y vy * dt这里的dt是两帧之间的时间差单位是秒。比如游戏以 60 FPS 运行那么每帧dt大约等于1/60秒。为什么要用dt而不是每帧固定加一个值因为不同电脑的帧率可能不同如果每帧固定加 5 个像素的位移那么 30 FPS 的电脑上飞机运动速度就会比 60 FPS 的电脑慢一半。用dt做乘法后物理效果就和帧率解耦了。在模拟器里飞机受力主要有三种重力始终向下。发动机推力方向和大小可以由玩家控制。空气阻力方向与速度相反大小与速度的平方近似成正比。把这三个力叠加就可以计算飞机每一帧的加速度ax 推力_x - 阻力_x ay 重力_y 推力_y - 阻力_y这里要特别提醒一个容易出错的地方pygame 的窗口坐标系中Y 轴是向下的。也就是说屏幕顶部 Y 坐标小底部 Y 坐标大。所以“重力向下”在代码里是一个正的加速度值而不是负值。很多初学者在这里栽跟头写着写着发现飞机越飞越高其实就是把重力方向写反了。2.2 渲染循环让画面连续动起来有了物理模型还需要一个“不断重复”的循环让画面连续刷新。pygame 程序的基本结构是这样的while running: # 1. 处理键盘、鼠标、窗口事件 # 2. 更新所有对象的状态 # 3. 把当前画面绘制到屏幕上 # 4. 调用 display.flip() 显示新画面这个过程叫“游戏循环”。clock.tick(FPS)用来控制帧率让循环不会疯狂空转。上面第 4 步很关键如果你只是绘制但没有调用pygame.display.flip()窗口画面不会更新看起来就像黑屏卡死一样。2.3 输入交互从键盘到飞机pygame 有两种常见的按键处理方式。方式一监听事件。适合“按下一次就算一次”的操作比如按 R 键重置飞机。if event.type pygame.KEYDOWN: if event.key pygame.K_r: aircraft.reset()方式二读取持续按键状态。适合“按住不放会持续生效”的操作比如按住空格键持续给推力。keys pygame.key.get_pressed() if keys[pygame.K_SPACE]: power 1.0这两种方式经常会被新手混用。如果只用KEYDOWN来加推力玩家按住空格只能触发一次飞机点一下就熄火根本飞不起来。本次项目的油门控制必须用pygame.key.get_pressed()。3. 开发环境准备3.1 版本与环境说明本文示例以常见环境为准版本需要根据你的项目实际情况调整重点演示配置思路。操作系统Windows 10/11、macOS、主流 Linux 发行版都可以。Python 版本建议使用 Python 3.8 或更高版本。pygame 版本建议使用 2.x 版本。开发工具VS Code、PyCharm或者任意你用着顺手的编辑器。如果你按照下面的命令操作后仍然找不到pygame模块可以优先检查是否安装了多个 Python 版本以及pip到底对应哪一个 Python。3.2 创建项目目录先在任意目录下创建项目文件夹名称建议用英文flight_simulator/ ├── settings.py ├── aircraft.py └── main.pysettings.py负责存放所有全局配置比如窗口大小、颜色、物理参数。aircraft.py负责定义飞机类和物理更新逻辑。main.py是主程序入口负责初始化窗口、处理事件和启动游戏循环。为什么要拆成三个文件因为代码规模一旦超过一两百行全部塞在一个文件里就会变得难以维护。把配置、实体逻辑和主流程分开后面调参数、加功能都更方便。3.3 安装 pygame打开命令行切换到项目目录后执行python -m pip install pygame如果你下载速度较慢可以临时使用国内镜像源python -m pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后可以输入下面这条命令验证python -m pygame.examples.aliens如果弹出一个游戏窗口说明 pygame 安装成功。顺手把电脑的 Python 版本也确认一下python --version接下来的代码都假设你已经在项目目录里了。4. 完整实战实现二维飞行模拟器4.1 第一步编写全局配置 settings.py新建settings.py把模拟器用到的参数集中放进去。这样以后想调重力、推力和屏幕尺寸只需要改这一个文件不需要去翻逻辑代码。# 文件路径flight_simulator/settings.py 飞行模拟器的全局常量配置 # 窗口与帧率 WIDTH 800 HEIGHT 600 FPS 60 # 物理参数 GRAVITY 300.0 # 重力加速度单位像素/秒^2 THRUST 550.0 # 发动机最大推力加速度单位像素/秒^2 DRAG 0.001 # 空气阻力系数与速度平方成正比 MAX_SPEED 600.0 # 速度上限单位像素/秒 # 地面位置 GROUND_Y HEIGHT - 60 # 地面所在的纵坐标 # 颜色RGB BG_COLOR (20, 30, 60) GROUND_COLOR (40, 120, 60) AIRCRAFT_COLOR (240, 240, 240) THRUST_COLOR (255, 140, 0) UI_COLOR (255, 255, 255) ALERT_COLOR (220, 50, 50)解释几个关键参数GRAVITY 300.0在现实中重力加速度约等于 9.8 米/秒²。在像素世界里我们直接把它设为 300 像素/秒²方便观察效果。数值越大飞机下坠越快。THRUST 550.0最大推力加速度。它必须大于重力加速度否则飞机点火后也无法上升。550 比 300 大正好留出足够的操纵余地。DRAG 0.001阻力系数不能太大否则飞机很快被减速也不能太小否则几乎感觉不到阻力。0.001 是一个适合这个量级的经验值。MAX_SPEED 600.0限制最大速度防止飞机在长时间加速后变成“瞬间移动”也避免数值异常。4.2 第二步设计飞机类 aircraft.py新建aircraft.py定义飞机对象。这个类负责保存飞机的位置、速度、推力状态并且提供update和draw两个核心方法。# 文件路径flight_simulator/aircraft.py 飞机对象的定义与物理更新 import math import pygame import settings class Aircraft: def __init__(self, x, y): # 位置与速度 self.x float(x) self.y float(y) self.vx 0.0 self.vy 0.0 # 发动机状态 self.thrust_power 0.0 # 当前推力大小范围 0~1 self.thrust_angle -math.pi / 2 # 推力方向默认竖直向上 self.crashed False # 是否已坠毁 property def speed(self): 获取当前速度大小 return math.hypot(self.vx, self.vy) def set_thrust(self, power, angle): 设置推力大小与方向 if self.crashed: return self.thrust_power max(0.0, min(1.0, power)) self.thrust_angle angle def reset(self, x, y): 重置飞机到指定位置 self.x float(x) self.y float(y) self.vx 0.0 self.vy 0.0 self.thrust_power 0.0 self.thrust_angle -math.pi / 2 self.crashed False def update(self, dt): 根据当前受力和时间增量更新飞机的速度与位置 if self.crashed: return # 初始加速度重力向下 ax 0.0 ay settings.GRAVITY # 叠加发动机推力 if self.thrust_power 0: ax math.cos(self.thrust_angle) * settings.THRUST * self.thrust_power ay math.sin(self.thrust_angle) * settings.THRUST * self.thrust_power # 空气阻力方向与速度相反大小与速度平方相关 ax - settings.DRAG * self.vx * abs(self.vx) ay - settings.DRAG * self.vy * abs(self.vy) # 更新速度 self.vx ax * dt self.vy ay * dt # 限制最大速度 speed self.speed if speed settings.MAX_SPEED: scale settings.MAX_SPEED / speed self.vx * scale self.vy * scale # 更新位置 self.x self.vx * dt self.y self.vy * dt # 与地面碰撞 if self.y settings.GROUND_Y: self.y settings.GROUND_Y self.vx 0.0 self.vy 0.0 self.crashed True def draw(self, surface): 在 pygame 窗口上绘制飞机与火焰 # 先画发动机火焰 if self.thrust_power 0 and not self.crashed: tail_x self.x - math.cos(self.thrust_angle) * 16 tail_y self.y - math.sin(self.thrust_angle) * 16 flame_len 12 int(18 * self.thrust_power) flame_x1 tail_x - math.cos(self.thrust_angle) * flame_len flame_y1 tail_y - math.sin(self.thrust_angle) * flame_len pygame.draw.line( surface, settings.THRUST_COLOR, (tail_x, tail_y), (flame_x1, flame_y1), 4, ) # 再画飞机本体用三角形表示 cos_a math.cos(self.thrust_angle) sin_a math.sin(self.thrust_angle) local_points [ (20, 0), (-10, -10), (-6, 0), (-10, 10), ] points [] for px, py in local_points: rx px * cos_a - py * sin_a ry px * sin_a py * cos_a points.append((self.x rx, self.y ry)) pygame.draw.polygon(surface, settings.AIRCRAFT_COLOR, points) pygame.draw.circle( surface, (255, 0, 0), (int(self.x), int(self.y)), 3, )这里面的坐标旋转公式rx px * cos_a - py * sin_a ry px * sin_a py * cos_a是一个二维旋转矩阵的展开式。它把飞机的局部坐标机头在前、两个尾翼在后旋转到当前推力方向对应的角度上。如果你只是做一个小项目可以暂时不需要深入推导矩阵背后的数学但建议记住这个形式它在很多图形开发任务里都会用到。4.3 第三步编写主循环 main.py新建main.py整个程序的起点就在这里。# 文件路径flight_simulator/main.py 飞行模拟器主入口 import math import pygame import settings from aircraft import Aircraft def load_font(size): 尽量加载中文字体找不到时回退到 pygame 默认字体 for name in [microsoftyahei, simhei, notosanscjk, pingfangsc]: path pygame.font.match_font(name) if path: return pygame.font.Font(path, size) return pygame.font.Font(None, size) def draw_ui(surface, aircraft, font): 在窗口左上角绘制飞行状态信息 lines [ fSpeed: {aircraft.speed:6.1f} px/s, fHeight: {max(0.0, settings.GROUND_Y - aircraft.y):6.1f} px, fThrust: {aircraft.thrust_power:4.2f}, fAngle: {math.degrees(aircraft.thrust_angle):6.1f} deg, , CONTROL:, SPACE: fire / release, LEFT/RIGHT: turn thrust, R: reset, ESC: quit, ] y 10 for text in lines: surface.blit(font.render(text, True, settings.UI_COLOR), (10, y)) y 24 def main(): pygame.init() screen pygame.display.set_mode((settings.WIDTH, settings.HEIGHT)) pygame.display.set_caption(Python Flight Simulator - Fly With Code) clock pygame.time.Clock() font load_font(18) start_x settings.WIDTH * 0.3 start_y settings.GROUND_Y - 200 aircraft Aircraft(start_x, start_y) running True while running: dt clock.tick(settings.FPS) / 1000.0 # 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_ESCAPE: running False elif event.key pygame.K_r: aircraft.reset(start_x, start_y) # 读取持续按键状态 keys pygame.key.get_pressed() power 1.0 if keys[pygame.K_SPACE] else 0.0 angle aircraft.thrust_angle if keys[pygame.K_LEFT]: angle - 2.0 * dt if keys[pygame.K_RIGHT]: angle 2.0 * dt # 更新飞机 aircraft.set_thrust(power, angle) aircraft.update(dt) # 绘制画面 screen.fill(settings.BG_COLOR) pygame.draw.rect( screen, settings.GROUND_COLOR, (0, settings.GROUND_Y, settings.WIDTH, settings.HEIGHT - settings.GROUND_Y), ) aircraft.draw(screen) draw_ui(screen, aircraft, font) if aircraft.crashed: crash_text font.render( CRASHED! Press R to restart, True, settings.ALERT_COLOR, ) screen.blit( crash_text, (settings.WIDTH // 2 - 140, settings.HEIGHT // 2), ) pygame.display.flip() pygame.quit() if __name__ __main__: main()4.4 第四步运行与验证在项目目录中运行python main.py如果没有报错会弹出一个深蓝色窗口白色三角形飞机悬停在半空中。下面是我建议的调试顺序模拟一个完整的起飞流程刚开始飞机距离地面 200 像素由于没有推力它会缓慢下坠。在飞机接触地面前按住空格键。此时发动机点火橙色火焰出现飞机应该开始克服重力向上爬升。松开空格键推力消失飞机由于重力逐渐变慢并开始下坠。按住左方向键推力方向会向左偏飞机一边上升一边向左移动按住右方向键同理。如果你故意让飞机落地会出现红色提示CRASHED! Press R to restart。按下 R 键飞机重置回初始位置。4.5 物理参数的调试心得如果你把飞机调整到一个很别扭的姿态会发现它不容易拉起来。这是正常的。这个模型的视角有点类似“矢量推力飞行器”飞机机头指向哪个方向推力就往哪个方向推而重力始终向下。实际调参时可以按照下面的顺序先调整GRAVITY决定飞行手感有多“沉”。再调整THRUST确保最大推力明显大于重力否则无法起飞。最后调整DRAG看飞机是否在高速时变得过于灵活或者过于迟钝。一个小技巧把FPS固定为 60相当于给你自己设定了一个标准时间基准。只要正确使用了dt即使帧率波动物理效果也基本一致。5. 常见问题与排查思路初学 pygame 飞行模拟时会遇到一些高频问题。下面给出一个速查表再针对性地展开说明。问题现象常见原因解决思路提示找不到 pygame未安装或安装到其他 Python 环境使用python -m pip install pygame安装窗口黑屏缺少pygame.display.flip()检查主循环确认每帧都刷新画面飞机持续触发一次推力后熄火使用了KEYDOWN控制油门改用pygame.key.get_pressed()飞机一直往下掉推力小于重力或推力方向错误检查THRUST与GRAVITY的大小关系飞机飞出屏幕没有做边界限制增加边界判断或按 R 键重置中文显示为方块系统缺少匹配字体使用英文字体或加载自定义字体文件5.1 安装 pygame 失败最常见的错误是ModuleNotFoundError: No module named pygame原因通常有两个一是确实没安装二是命令行里的pip对应当前 Python 环境而编辑器使用的是另一个 Python 环境。解决办法是在项目使用的同一个 Python 解释器下执行python -m pip install pygame在 VS Code 里可以先用python -c import sys; print(sys.executable)查看当前解释器路径确认和终端里使用的是同一个。5.2 窗口黑屏或没有响应如果你在代码里绘制了画面却没有看到变化先检查有没有调用pygame.display.flip()pygame 默认使用双缓冲机制。你对surface上做的所有绘制操作其实都是在后台缓冲区进行的只有调用flip()后系统才会把后台内容整体切换到屏幕上。缺少这一步窗口就会保持初始状态看起来像黑屏。另外如果事件循环里使用了阻塞操作比如time.sleep()也会导致窗口无响应。建议通过dt或clock.tick()来控制刷新节奏而不是阻塞整个进程。5.3 飞机无法持续获得推力很多人第一次写按键控制时只用了KEYDOWN事件if event.type pygame.KEYDOWN and event.key pygame.K_SPACE: aircraft.thrust_power 1.0这样做的效果是玩家按一下空格推力只开启一瞬间然后马上消失。正确的做法是在主循环里每帧读取按键状态keys pygame.key.get_pressed() power 1.0 if keys[pygame.K_SPACE] else 0.0get_pressed()返回的是当前时刻键盘上所有按键的状态数组只要按住不放每一帧拿到的值都是被按下的。这个差异是游戏开发中非常经典的一个坑。5.4 飞机总是往下掉先确认两个问题THRUST是否大于GRAVITY如果最大推力加速度小于重力加速度即便全力点火合力仍然向下飞机自然飞不起来。推力方向是否正确本文默认推力方向是竖直向上也就是-math.pi / 2。在 pygame 坐标系中角度为 0 时表示向右角度为-pi/2时表示向上。如果角度设置为正的pi/2推力会直接指向屏幕底部飞机只会加速撞地。5.5 帧率波动影响运动速度如果你把物理更新写成了“每帧位移固定 5 像素”那么帧率越高飞机飞得越快。正确做法是引入dtdt clock.tick(FPS) / 1000.0 aircraft.update(dt)这样速度的计算公式变成“每秒多少像素”而不是“每帧多少像素”物理行为就不会随着帧率变化而产生明显差异。5.6 中文字体显示为方块文章中的load_font()已经尽可能去匹配微软雅黑、黑体、思源黑体等常见中文字体。但如果在某些精简版 Linux 系统上运行系统中可能没有中文字库这时窗口里的中文会显示为方块。处理方法有两种把 UI 文案全部改成英文代码里已经这样做UI 默认就是英文所以这个项目其实不会遇到中文字体问题。如果你在插件扩展中需要使用中文可以下载一个 TTF 字体文件放到项目目录然后通过pygame.font.Font(path/to/font.ttf, size)加载。6. 进阶方向与工程实践建议6.1 从欧拉法到更稳定的积分本文的物理更新使用的是一阶欧拉积分公式简单实现容易但在步长较大或力变化较快时会有一定误差。对于飞行模拟如果你想获得更平滑、更稳定的轨迹可以考虑改用“半隐式欧拉法”先用新速度更新位置再用旧速度更新位置。更精确的方案是 RK4四阶龙格-库塔法它在游戏物理引擎和科研仿真中很常见。把update中的积分逻辑替换成 RK4小项目的物理精度会有明显提升。不过对于本文这个教学项目一阶欧拉已经足以表达整个逻辑不必为了“精确”而增加难度。6.2 模块化与配置管理很多入门项目最后写成一坨几百行的代码原因就是边写边加功能没有及时拆分。建议像本文一样从一开始就分文件配置单独放。实体类单独放。主流程单独放。当你以后想给飞机加上“尾焰粒子”“仪表盘动画”“起落架音效”时你只需要在这些模块里新增内容不会动到其他文件的核心逻辑。另外所有物理参数都应当放到配置中。一个常见坏习惯是直接把300、550这样的数字写在update里。等过两天回过头来调参你根本分不清哪个数字是重力、哪个数字是推力。用有名字的常量就是在给未来的自己减少麻烦。6.3 增加轨迹与粒子效果如果你想让“飞行”的效果更直观可以记录飞机最近几十个位置点然后绘制一条浅色轨迹线trail_points.append((aircraft.x, aircraft.y)) if len(trail_points) 80: trail_points.pop(0) pygame.draw.lines(screen, (100, 100, 200), False, trail_points, 2)这条轨迹会清楚地展示重力影响下飞机的抛物线运动非常有助于理解“加速度如何改变速度、速度如何改变位置”这个核心问题。你还可以把橙色火焰改成一串小粒子每个粒子有自己的随机速度和生命周期。技术难度不大但对整体观感的提升非常明显。6.4 安全与边界意识最后要特别说明的是本文的物理模型只适用于教学不应当直接套用到任何真实飞行器控制系统中。真实世界的飞行要处理气动力、力矩、传感器噪声、控制律、卫星导航、地面站通信等一系列复杂问题而且错误代码可能导致严重事故。如果你未来要接触真实的飞控项目请务必备份参数先在仿真环境验证再逐步迁移到硬件。任何时候修改控制逻辑都要有完整测试记录和回滚方案。编程里“能飞”是一回事工程上“安全地飞”是另一回事两者都需要长期积累。7. 下一步学习路线这个小项目做完之后你已经掌握了游戏循环、事件处理、矢量运动、实时交互和简单碰撞检测。下一步可以根据兴趣选择不同方向如果想更深入了解物理模拟可以研究 RK4 积分、刚体旋转、力矩模型尝试做一个带角速度的飞机姿态模拟。如果想做更好看的可视化可以转向三维图形先学PyOpenGL再过渡到 Unity 或 Unreal Engine。如果对游戏开发体系感兴趣可以学习 Pymunk、Box2D 这类 2D 物理引擎对比自己手写物理和引擎物理的差别。如果想进入航空航天仿真领域可以从 MATLAB/Simulink 的飞行仿真工具箱入手也可以了解 PX4、ArduPilot 等开源飞控但一定要在仿真环境和合法授权的前提下操作。飞行从来不是一件容易的事把一个物体稳稳留在空中需要持续对抗重力。代码世界里也一样一个可运行的模拟器背后是物理、数学和工程习惯的共同作用。希望你在完成这个项目后能真切地感受到从想法到键盘上的实现距离并没有想象中那么远。如果这篇文章对你有帮助可以先收藏起来按着步骤动手写一遍再慢慢往里面添加属于自己的飞行功能。
返回列表