说实话,一开始我是不打算升 UE4 AirSim 的。老版本跑了快一年,单机起降、航线飞行、简单的数据采集都稳定了,手头的仿真项目看着也没啥大毛病。但连续两次批量仿真让我开始坐不住——一次是物体位姿读出来和场景实际位置明显对不上,另一次是高频跑图像接口的时候偶尔直接卡死。翻了翻官方仓库针对 1.3.1 的 release notes,发现这一版恰好集中修复了坐标系转换、车辆配置结构和一批 API 稳定性的问题。那就升吧。
这篇文章不是官方文档的复述,而是我把 AirSim 从旧版本升到 1.3.1 的完整实操记录。里面包括升级前的检查清单、插件重建与接入的完整链路、以及我在编译期和运行期真实撞到的三个坑和对应的修复方案。如果你也在用 UE4 做无人机或自动驾驶仿真,手头的 AirSim 还停在老 tag,这篇应该能帮你省下不少排查时间。
1. 为什么我决定把AirSim从老版本升到1.3.1
1.1 老版本给我惹过的那些麻烦
先说我碰到的问题,让你评估自家项目有没有类似症状。第一个是位姿读取不稳定:我在做场景物体动态摆放的自动化测试时,用 simGetObjectPose 去取某个新生成物体的位置,返回的数据偶尔会偏出真实坐标一大截。起初我以为是随机噪声,后来把多次调用的结果打出来对比,发现偏移的数值和物体离原点的距离有关系,这不太像噪声,更像坐标系转换里的累积误差。
第二个问题出现在连续图像采集。我的脚本里循环调用 simGetImages 抓前视和下视四个相机的数据,频率大概在 10 到 15 赫兹。老版本在长时间循环下偶发无响应,需要强制重启仿真进程才能恢复,丢了半天的数据。第三个问题是多智能体初始化,同时启动两架无人机和一辆车的时候,偶尔有一两个 agent 没有按默认配置生成。这三个问题单个看都能忍,凑在一起就太影响效率了。
1.2 1.3.1的变更清单到底值不值得跟
我把 1.3.1 的 release notes 从头到尾过了一遍,重点不是看新增了什么功能,而是看它改了什么"基础结构"。对我最有用的三处是:第一,坐标系转换相关的修复,也就是 NED 和 UE world 坐标之间的换算逻辑被重新理了一遍,这直接对应我前面的位姿偏移问题;第二,设置系统里车辆配置的结构调整,本来散落在多个字段的设置开始统一收敛到 Vehicles 里;第三,一整批 API 的行为稳定化,特别是连续调用相关接口的健壮性。
这里提醒一句:看 release notes 不要只看"新增",更要看"变更"和"废弃"。因为很多新增功能你可能永远用不上,但变更和废弃往往会破坏你现有代码。1.3.1 的破坏性变更不算少,比如 settings.json 里部分键名的大小写和嵌套层级有调整,旧配置直接复用有风险。所以"值不值得跟"这个问题的答案,取决于你的业务里有多少代码踩在这些变更点上。我的项目三个点全踩了,升级就非常值。
1.3 心态调整:升级不是妙手回春,是搬好每一块砖
升级前我把目标定得很具体:不追求所有新功能,只求三件事——位姿读取准确、图像连续调用不卡死、多智能体能稳定初始化。然后给这一轮升级预留了三天时间,包括源码构建、接入、回归测试和修复。实践证明这个预算刚好够,但一点多余都没有。
我另一个心态准备是把升级看成"搬家":新家具进场之前,旧房子的结构得先摸清楚。指望升级脚本一键把配置都处理好,那是不现实的。AirSim 的编译和配置链路非常直白,但每一步都要亲手确认。接下来就从升级前的地基检查说起。
2. 动手升级前的地基检查:备份、UE4版本与工程结构
2.1 把项目和源码完整备份一次,顺便验证还原
这一步我做得比大多数人认真,因为吃过亏。早年间升级某中间件,备份只打了压缩包,没验证内容是不是干净,结果回滚时发现备份里少了关键配置,等于白忙。现在我的备份规则是三层:项目工程一份、AirSim 源码一份、可复现的环境记录一份。
项目工程这一份,重点是 UE 工程根目录下的 .uproject、Config、Source、Content 和 Plugins 五个位置。很多人只备份 Content,觉得代码和配置不重要,这是误区。Config 里存着项目设置和默认输入映射,Source 是你写在 UE 侧的 C++ 逻辑,Plugins 里是旧版 AirSim 插件本体。源码这份更简单,整个克隆下来的目录打包即可。环境记录可以是一个文本文件,写上当前 UE4 版本、VS 版本、系统 SDK 版本、settings.json 的位置和用途。
备份完了还要验证还原:在另一个目录解压工程,双击 .uproject 能正常打开,进入主场景不报红。这一步别省,它花十分钟,但能保证你后面就算折腾坏了也有退路。
2.2 核对UE4版本、VS工具链与Windows SDK
AirSim 是带原生插件的 UE4 工程,要先编译成 dll 再被编辑器加载,所以本机的原生编译工具链是绕不开的。我先打开 .uproject 文件,看它里面记录的引擎版本。不同 UE4 版本对应不同的编译行为,新版官方发布说明里一般会给出受支持的 UE 版本列表。经验上,1.3.1 时代最省事的组合是 UE 4.24 或 4.25。如果你的工程还在 4.22 或更老,我不建议直接硬升,优先把工程迁移到目标 UE 版本再配合使用。
VS 工具链也很关键。我用的是 VS2019,需要安装"C++ 桌面开发"工作负载,里面包含 MSVC 编译器和 Windows SDK。版本上,AirSim 的第三方依赖会检查较新的 SDK 头文件,如果你 Win10 SDK 组件太旧,编译到中间环节会报出各种奇奇怪怪的头文件缺失错误,这个问题第四部分我会重点讲。总之在动手编译前,先用 VS 安装器确认三件事:C++ 工作负载已勾选、Windows SDK 组件存在、SDK 版本不低于官方要求。
我整理了一张当时的核对表,你可以照着做版本检查:
| 检查项 | 我这次的值 | 检查目的 |
|---|---|---|
| UE4 引擎版本 | 4.24 | 决定插件编译的API基线 |
| VS 版本 | 2019 | 提供MSVC工具链 |
| Windows SDK | 较新的稳定版 | 决定第三方头文件能否通过 |
| AirSim 版本 | 旧版 -> v1.3.1 | 本次升级目标 |
2.3 理清插件目录结构,别做无脑覆盖
升级 AirSim 常见有两种错误姿势:一是把新插件文件直接覆盖到工程 Plugins/AirSim 里,导致新老二进制、中间文件混在一起;二是压根不管源码,在工程里用旧插件的二进制硬跑新配置,结果行为和想象完全不同。我这次采用从源码编译的路线,先把整个 AirSim 源码当成一个独立环境,构建成功后,再把生成的插件目录完整交给 UE 工程。
这里有个细节容易踩:AirSim 构建完成后,会在 Unreal/Plugins/AirSim 下生成 Binaries 和 Intermediate 目录。你复制给工程的是整个 Plugin 目录,不是只复制 Binaries。中间文件在复制后第一次加载时会被 UE 重新生成,如果漏掉 Source 目录,编辑器可能会提示模块结构缺失。我当时在工程目录下新建 Plugins/AirSim,然后把源码侧生成的整个目录复制过去,中间没做任何"选择性粘贴"。
3. 源码重建到插件接入:一次走通的完整链路
3.1 拉取v1.3.1标签源码
AirSim 的代码托管在官方仓库,升级不是去下载页面拿一个 zip,而是用 git 拉取指定 tag。我强烈建议用 tag 而不是直接拉默认分支,因为开发分支天天在动,你无法确定自己拿到的是哪个快照。我的命令是这样的:
git clone --depth 1 --branch v1.3.1 <AirSim官方仓库地址> AirSim-1.3.1 cd AirSim-1.3.1 git submodule update --init --recursive第三条命令很关键。AirSim 依赖一些第三方代码,是以子模块方式引入的,如果不初始化子模块,后面编译会报找不到头文件。这个坑很多快速上手教程都没提,我这次也在这里卡了十分钟。如果你用的是普通方式克隆而不是浅克隆,子模块同样需要手动初始化,和 --depth 参数无关。
3.2 用命令提示符编译:环境和输出判断
编译 AirSim 插件不需要先打开 UE 编辑器,而是在命令行环境里直接跑构建脚本。在 Windows 上,我的做法是打开"VS2019 的 x64 本机工具命令提示符",这个环境里 MSVC 编译器和 Windows SDK 的环境变量都已经配好,比普通 CMD 省很多事。切到源码根目录后执行:
build.cmd脚本会先用 CMake 配置第三方依赖,再调用 MSBuild 编译 UE4 插件。整个过程我跑了大概 15 分钟,跟机器配置有关。第一次跑的时候,输出会在第三方依赖阶段就报错,我当时没截图,但错误特征很清楚:找不到某个头文件。这种情况下先别怀疑自己改错了,优先检查子模块是不是没拉全,再检查 Windows SDK 组件。
编译成功的标志是脚本最后几行出现类似"Build succeeded"的输出,同时在 Unreal/Plugins/AirSim/Binaries/Win64 下面能看到生成的模块 dll。如果只是看到"build finished"而没有 dll,说明脚本没有真正走到插件编译那一步,要继续往上翻日志找原因。
3.3 把插件接进目标工程:从复制目录到重新编译
新插件的二进制生成后,把整个 AirSim 插件目录从源码的 Unreal/Plugins/AirSim 复制到自己工程的 Plugins/AirSim。如果工程里没有 Plugins 目录,就新建一个。复制完成后,我建议把旧工程里的 Intermediate 和 Saved 目录做一次归档而不是直接删除,因为里面可能有 UE 缓存,处理不当会让编辑器加载异常。
双击 .uproject 打开工程,这时候 UE 会检测到 Plugins 目录发生了变化,弹窗询问是否重新编译模块,选择是。等编译完成后,进入场景,会看到左侧 World Settings 里出现 AirSim 相关的属性,或者直接从场景模板拖一个玩家起点。至此插件已经接到了工程里,下一步就是配置和验证。这里有个小经验:第一次打开工程会让编辑器卡一阵子,因为它要重建插件相关的各类缓存,看着像死机其实没死,耐心等。
4. 实测撞上的三个坑:编译报错、配置失效与坐标偏移
4.1 第一个坑:编译期C1083错误,根子在第三方依赖缺失
我按 3.2 的流程编译,实际第一次是失败的,报错长这样:
fatal error C1083: Cannot open include file: 'json.hpp': No such file or directory看到 json.hpp 我就知道问题多半出在第三方依赖上,而不是主代码。AirSim 把 JSON 解析这类第三方库放在子模块里,浅克隆加子模块这条链路没有跑通就会缺文件。我用git submodule status看状态,发现确实有子模块显示为未初始化。解决方式就是补上子模块初始化命令:
git submodule update --init --recursive git submodule status这里我复盘了一个经验:AirSim 的 clone 命令和子模块初始化命令应该作为一个整体执行,很多博客在讲 AirSim 编译时只给 clone 不给 submodule 命令,结果新手在第一个编译阶段就卡死。你如果也遇到 C1083,别急着重装环境,先检查子模块。
4.2 第二个坑:旧settings.json在新版本里"半失效"
编译通过、插件加载成功,我以为一切要顺了,结果启动仿真时日志里出现一行:
[AirSim] No vehicle definition found in settings表面意思是"设置里没有找到车辆定义",但我明明在 settings.json 里写好了飞行器配置。排查过程我用了"先删后加"的思路:先把工程根目录下的 settings.json 移走,让 AirSim 使用内置默认配置启动,结果默认配置下飞行器正常出现。这就说明问题出在我的自定义配置上。
我把新旧配置逐行对比,发现 1.3.1 对车辆配置的键名、大小写和嵌套层级要求更严格了,旧的配置里有个别字段是旧的写法,在旧版本里会宽容处理,新版本就直接跳过或忽略,最终导致整个车辆定义没有被正确解析。解决办法是把官方样例配置复制过来,在它的基础上把我要的自定义项一项项加上,每加一项就启动一次仿真验证,很快定位到是哪几行失效。新配置的基本骨架大概是这样的:
{ "SettingsVersion": 1.2, "SimMode": "Multirotor", "Vehicles": { "MyDrone": { "VehicleType": "SimpleFlight", "AutoCreate": true } } }多说一句:配置迁移别用记事本肉眼比对,最好用支持结构折叠的编辑器,层级一目了然。
4.3 第三个坑:simGetObjectPose的坐标返回值变了
第三坑才真正坐实了前面说的坐标系转换修复。升级后,我写的外部脚本通过 RPC 调用 simGetObjectPose 读取场景中动态生成的物体位置,发现同一个物体、同一时刻的位置值,和升级前对不上。不是差一点点,而是有一个明显的系统性偏移,这个偏移量和物体在 UE 场景里的绝对位置有关。
排查阶段我先在 UE 编辑器里直接选中那个物体,看它 Inspector 里的真实坐标;再在脚本里把 simGetObjectPose 的返回值打印出来,两个值做减法。发现转换后的位置整体往某个方向平移了一段距离。原因不难理解:1.3.1 修正了 NED 与 UE world 之间坐标换算的基准点逻辑,旧版本里那个基准点用的是世界原点,新版本改成了车辆控制器的初始位置。对于场景原点附近的物体,新旧几乎一致;离原点越远,差异越大。
我最后的处理不是在业务代码里到处打补丁,而是在外部脚本封装了一层"坐标适配函数",把读回来的位置按新逻辑统一再换算成我熟悉的坐标系。业务层完全没动,以后升级还能继续沿用。
5. 升级后的功能验证:从单机起飞到多机长时压测
5.1 相机图像链路:把每种相机都点一遍
升级后的验证不能只跑一遍"能起飞就完事"。我习惯先把传感器链路全部打通。AirSim 的相机有前视、下视、后视、左视、右视,还有深度和分割视图。我的测试脚本连接上 RPC 后,逐个调用 simGetImage 和 simGetImages,把返回图像的尺寸、编码、时间戳打印出来,和升级前的记录对比。
这里发现一个需要留意的小变化:1.3.1 里图像返回的时间戳在单位或者含义上更规范了,如果你的旧代码里对时间戳做了差值运算,最好重新确认一下单位变化,否则算出来的延迟是错的。图像内容本身对比下来没有异常,RGB、深度、分割三个通道的尺寸和编码与预期一致,连续拉 200 帧没有出现卡死,之前老版本的偶发无响应在这个版本里没再出现。
5.2 飞控APIs:起飞、悬停、航线、降落的回归测试
图像链路正常后,我接着跑飞控相关接口。步骤很简单:起飞、悬停五秒、按预设的四个航点飞一圈、原地降落。每一步都要读取状态值确认行为正确。让我比较意外的是 reset 接口的行为变化。老版本里 reset 之后偶尔会残留一点速度或姿态角速度,导致下次起飞前位置看着对、但状态量不干净。1.3.1 的 reset 更彻底,位置、速度、姿态都清干净了。如果你的旧的业务逻辑里有大量依赖 reset 后车/机状态残留的怪代码,升级后反而要处理掉这些兼容补丁。
多智能体初始化是我之前的痛点,这次专门验证了同时启动两架无人机和一辆车,连续重复初始化五次,每次都成功,没有出现之前"个别 agent 未生成"的情况。这个问题的修复对我项目价值最大。
5.3 多机协同与30分钟长时压测
功能跑通后,我开启了一个 30 分钟的长时间压力测试:两架无人机按编队航线持续飞行,一辆车在下方沿环形路径跑,三个 agent 同时各接一组传感器数据流,中间不做任何人为干预。监听指标包括仿真帧率、RPC 调用往返耗时、进程内存变化。
结果帧率稳定在 60 左右,RPC 调用耗时最大值 30 毫秒以内,内存曲线平稳,30 分钟下来没有一次断流或卡死。对比升级前,这个稳定性明显好了一截。这里我要提醒一句:压力测试一定要看趋势,不要只看平均值。内存曲线如果持续缓慢上涨,再稳的帧率也藏着隐患。我这次在压测里加了一个简单的记录脚本,每 10 秒抓一次内存和调用耗时,最后画成曲线检查,比只看实时数值可靠得多。
6. 沉淀下来的三点升级经验
6.1 把"版本差异"封装成适配层,而不是到处改业务代码
这次升级里最正确的一个决定,就是外部脚本侧所有直接调用 AirSim API 的动作都收敛到了一个统一模块里。我并没有在业务代码里一个个去改坐标换算,而是在适配层做版本判断和新旧两套换算逻辑。这样升级带来的影响范围被隔离在一个文件里,就算下次还要升,影响面也小。对于搞仿真的人来说,业务算法才是你的主体,版本适配应该是"基础设施"而不是分散在各处的杂音。
6.2 升级记录文档:下次不用重新踩坑
我这次把每一步命令、每个报错、每处配置改动的截图和说明都记录在一份文档里,其中包括错误信息原文和修复前后对比。整个过程结束再回头看,这份文档价值完全不亚于升级本身。以后换机器、换版本、或者同事要搭同样的环境,直接把文档丢过去,十分钟就能复现整条链路,不用重新翻聊天记录和博客。
6.3 回滚策略:备份要能真正还原,才算有效备份
最后这点是我判断这次升级成功的兜底标准。我在动手前备份了工程和源码,并且在升级过程中如果某个阶段卡住超过一小时,我的计划是直接放弃新版本、用备份还原旧环境继续跑任务,绝不恋战。虽然最后没用到回滚,但这个退路让我在整个升级过程中心态稳定,敢做各种排查。一个能真正还原的备份,比一百份"看起来备份了"的压缩包都强。
整个升级从准备到压测通过,花了差不多三天。个人体会是,AirSim 这种开源仿真平台,跨版本升级的真正成本并不在编译那几步,而在坐标系语义、配置结构和 API 行为这些"看似没变、实际变了"的地方。把每个变更点当成一次"接口变更"来做适配,升级就是可控的。下一次如果再升,我会把地基检查做得更快,但第 4 节的三个坑,我希望是最后一次踩。