ARTICLE DETAIL

资讯详情

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

X-Plane插件开发入门:新建工程、编译部署与加载调试全流程

X-Plane插件开发入门:新建工程、编译部署与加载调试全流程 X-Plane 插件开发这条路说难不算难说容易也是一堆小坑。上一期教程我们把 SDK 下载、插件本质这些前置工作梳理了一遍这期就直接进入正题从新建一个空白工程开始到写出一份能被 X-Plane 正确识别并加载的插件文件完整走一遍新建 X-Plane 插件的流程。我会以 Windows 系统为演示平台但给的思路都是跨平台通用的。你不需要先精通 C/C只需要能读懂基础代码就行因为咱们要做的插件说白了就是一个动态库X-Plane 启动的时候会把它“放进自己的进程里跑”。这篇保姆教程的目标很简单让你亲手建一个插件并且能在 Log.txt 里看到自己的插件被加载、被调用的痕迹先建立“我能行”的信心。搞通了这一步后面想给飞机加仪表、做自动驾驶辅助、写自动飞行程序都是在这个骨架上继续长肉。1. 动手前先搞懂插件的底盘1.1 插件不是补丁是宿主机里的“动态模块”很多人以为 X-Plane 插件和那些“美化包”“机模包”一样扔进文件夹就完事。其实不一样。X-Plane 插件是一段编译好的机器码运行的时候会被加载进模拟器自己的进程空间里直接调用 X-Plane 开放的 API 来读写模拟器内部数据。可以把它想象成给一台车加装行车记录仪车本身有电源接口有固定支架记录仪只需要遵守这个接口规范就能正常工作。X-Plane 扮演车插件是记录仪SDK 就是那本接口说明书。插件通过 SDK 提供的函数可以读取飞机的航向、速度、引擎状态也可以模拟按键、绘制屏幕内容甚至能改变飞行的物理参数。这种能力是普通机模包远远比不了的。具体到文件形态Windows 下插件的后缀是.xpl但它本质上就是一个 DLLLinux 下的.xpl是 ELF 共享库macOS 下的.xpl是加载包。X-Plane 在启动时会扫描Resources/plugins目录把里面所有符合规范的插件用一套固定的流程加载起来。这张表建议存下来以后排查问题用得上。操作系统插件文件形态本质Windowsxxx.xplDLL 动态链接库Linuxxxx.xpl共享库.so 格式macOSxxx.xpl包Mach-O 动态库1.2 三个关键词XPLM、SDK、插件 ID我在上一期提过 XPLM它是 X-Plane Plugin Manager 的缩写就是 X-Plane 提供给插件开发者的一套底层管理接口。SDK 则是围绕 XPLM 配套的整套开发包里面包含头文件、库文件、示例代码和文档。这里必须强调版本关系。X-Plane 10 时代 SDK 是 2.xX-Plane 11 对应 SDK 3.0X-Plane 12 对应 SDK 3.2API 代号上能看到 XPLM300、XPLM301、XPLM320 这样的名字。你写代码的时候用最新版 SDK 头文件但如果要兼容多个版本就需要在代码里判断运行时的 XPLM 版本。新手阶段不用管太深但要记住“用高版本头文件编译的插件扔到老版本 X-Plane 里可能无法加载”这是一个非常经典的坑。插件 ID 则是一串字符串用于在 X-Plane 里唯一标识你的插件。它主要用在插件之间互相通信的场景现在你还用不到但养成规范命名的习惯没有坏处建议用“域名反写插件名”的格式比如com.example.helloplugin。1.3 选一套顺手的工具链既然要走“保姆路线”工具链直接给结论。Windows 用户两个选择Visual Studio 全家桶或者 CMake MinGW-w64。我推荐后者因为轻量、命令清晰也更贴近 Linux/macOS 的编译方式以后跨平台不折腾。X-Plane SDK 可以从官网的 SDK 页面下载压缩包名字一般是XPlaneSDK-3.x.x.zip解压后记住里面的SDK/CHeaders目录这是我们要引用的头文件路径SDK/Libraries下面按系统分了几个子目录对应 Windows、Linux、macOS 的库文件。如果你平时习惯了用 IDE 插件辅助生成代码骨架也可以试试但我的建议是IDE 插件可以帮你补全函数名却没法替你理解 X-Plane 的生命周期。第一次写插件务必手打一遍核心代码眼睛学会和手学会是两回事。2. 搭建最小工程骨架2.1 标准入口点每个插件都必须提供的五个函数X-Plane 加载插件时不会随便找一个函数就叫它跑它有一套固定约定。每个插件必须导出下面这五个函数缺一个都可能导致加载失败XPluginStart插件加载时被调用负责初始化XPluginStop插件卸载时被调用负责清理XPluginEnable插件“启用”时被调用XPluginDisable插件“禁用”时被调用XPluginReceiveMessage接收来自 X-Plane 或其他插件消息的入口。下面是一个最小可用的插件源码先新建一个hello.c文件#include XPLMProcessing.h #include XPLMUtilities.h #include string.h #include stdio.h static int hello_counter 0; static float my_flightloop(float elapsedMe, float elapsedSim, int counter, void *refcon) { hello_counter; if (hello_counter % 30 1) { char buffer[256]; snprintf(buffer, sizeof(buffer), HelloXPLM: frame %d\n, hello_counter); XPLMDebugString(buffer); } return 1.0f; } PLUGIN_API int XPluginStart(char *outName, char *outSignature, char *outDescription) { strcpy(outName, Hello X-Plane Plugin); strcpy(outSignature, com.example.helloplugin); strcpy(outDescription, A minimal new plugin for tutorial.); XPLMRegisterFlightLoopCallback(my_flightloop, 1.0f, NULL); return 1; } PLUGIN_API void XPluginStop(void) { XPLMUnregisterFlightLoopCallback(my_flightloop, NULL); } PLUGIN_API int XPluginEnable(void) { return 1; } PLUGIN_API void XPluginDisable(void) { } PLUGIN_API void XPluginReceiveMessage(XPLMPluginID inFrom, int inMsg, void *inParam) { }这一段代码就是整个插件的“心脏”。XPluginStart里的三个strcpy不是随便填的outName会显示在 X-Plane 的插件管理器里outSignature是唯一标识outDescription是描述信息。而XPLMRegisterFlightLoopCallback这行尤其重要它注册了一个“每 1 秒调用一次”的回调函数真正让代码跑起来的就靠它。2.2 为什么签名和描述不能随便写签名com.example.helloplugin的作用比你想象的更大。X-Plane 内部维护着一张插件注册表如果两个插件撞了签名系统可能只加载其中一个或干脆报错。很多老插件之间互相通信也是靠签名配合XPLMFindPluginBySignature函数来定位的。你一个人开发的时候可能无所谓但一旦发到社区签名就是你的门牌号。描述信息就轻松很多主要是给人看的可以写清楚“这个插件是干什么的”。不过 X-Plane 加载插件时对这三个字符串的长度有一定限制别填超了。我这里没做边界检查实际项目里建议用snprintf限制长度否则真遇到长字符串容易溢出。2.3 工程目录和 CMake 配置创建一个新项目时推荐按下面的结构组织文件别把所有东西堆在一个目录里hello_plugin/ ├─ CMakeLists.txt ├─ src/ │ └─ hello.c └─ build/CMakeLists.txt 可以这样写替换成你自己的 SDK 路径cmake_minimum_required(VERSION 3.16) project(hello_plugin) set(CMAKE_C_STANDARD 11) set(XPLANE_SDK F:/XPlaneSDK CACHE PATH X-Plane SDK path) add_library(hello MODULE src/hello.c) target_include_directories(hello PRIVATE ${XPLANE_SDK}/SDK/CHeaders) if(WIN32) target_compile_definitions(hello PRIVATE IBM1 APL0 LIN0) target_link_directories(hello PRIVATE ${XPLANE_SDK}/SDK/Libraries/Win) target_link_libraries(hello PRIVATE XPLM) endif()注意到上面加了IBM1 APL0 LIN0这种宏定义这是 X-Plane SDK 自己用来区分平台的开关。SDK 头文件里会通过这套宏来适配不同系统的导出符号定义。如果你用的是 Visual Studio 直接建工程也需要在预处理定义里加上同样的宏否则头文件里的PLUGIN_API可能展开成错误的结果这是新手最容易忽略的细节之一。3. 让代码动起来飞行循环回调3.1 飞行循环回调到底是什么上面的代码里my_flightloop这个函数会被 X-Plane 周期性调用这就是飞行循环回调。它的名字可能让人以为只跟“飞机飞行”有关实际上它是插件获取 CPU 时间片的核心方式。类比一下就懂了飞机座上每个人的状态都在不断变化X-Plane 每隔一段时间会“点名”一次插件问它“你要不要干点啥”。插件在这个回调里读取当前状态、做计算、改变飞机的参数。X-Plane 不会按你的节奏来它有自己的主循环这个回调只是让你在它的主循环里“插一脚”。回调函数的四个参数各有用途elapsedMe距离你上一次被调用的真实时间单位是秒elapsedSim距离上一次被调用的模拟时间注意和真实时间的区别counter每次回调都会递增的计数可以用来做时间统计refcon注册回调时传入的引用指针想在回调里访问自己的结构体就靠它。我只用了hello_counter在外部做计数没有用到counter参数。很多新手会混淆这两个概念这里提一嘴counter是 X-Plane 全局维护的计数器不是你插件的计数器所以有点耐心不要指望它从 0 开始。3.2 返回值的秘密my_flightloop函数最后return 1.0f这个返回值可不是随便写的。飞行循环回调的返回值决定 X-Plane“多久以后再叫你一次”单位是秒。返回正数比如1.0f表示 1 秒后再调用返回-1.0f表示每一帧绘制时都调用也就是尽量频繁很多实时零件需要这样做返回0.0f表示停止回调除非你再手动注册或调度。如果想做成一个每秒跑 30 次的循环直接return 1.0f / 30.0f就可以了。返回值的精度其实没那么关键X-Plane 会按照主循环的时间粒度去处理不是精确到微秒的实时系统别在这种地方过度设计。3.3 先把日志“喊”出来我第一次写插件的时候最难的不是代码本身而是“怎么确认我的插件真的在被执行”。XPLMDebugString就是最朴素的调试手段它会把字符串直接写进 X-Plane 的Log.txt。你可以在回调里每隔 N 帧写一行日志观察输出节奏是否符合预期这是验证一切功能的基础。if (hello_counter % 30 1) { char buffer[256]; snprintf(buffer, sizeof(buffer), HelloXPLM: frame %d\n, hello_counter); XPLMDebugString(buffer); }为什么特意用snprintf而不是直接拼字符串因为XPLMDebugString接收的是普通const char*如果直接写Hello hello_counter在 C 语言里是不成立的新手在 C/C 里很容易写出这种拼接错误。先用计数器控制打印频率避免每帧都刷日志把文件撑爆这个习惯能帮你省很多麻烦。4. 编译、部署、验证加载4.1 一步步把源码变成 .xpl 文件工程建好之后就是编译。如果用 MinGW-w64最简单的方式是打开终端进入工程目录执行类似下面的命令mkdir -p build gcc -stdc11 -O2 -shared \ -o build/hello.xpl \ -I F:/XPlaneSDK/SDK/CHeaders \ -I F:/XPlaneSDK/SDK/Libraries/Win \ -fPIC src/hello.c-shared告诉编译器生成一个共享库-fPIC在 Linux 上必须加Windows 下可加可不加。-I指向的路径是 SDK 的头文件目录。如果你用的是官方最新的 SDKWindows 下它的库文件名通常是XPLM.lib编译时可以通过-L和-lXPLM链接进去。如果用 CMake直接在build目录里cmake .. -DXPLANE_SDKF:/XPlaneSDK cmake --build .生成的结果如果是 Windows 平台通常是hello.dll而不是hello.xpl这很正常。add_library(hello MODULE)在 CMake 上生成的默认后缀就是.dll你需要手动把产物重命名为hello.xpl或者在 CMake 里设置set_target_properties(hello PROPERTIES SUFFIX .xpl)顺带提醒一句如果你用的是 Visual Studio 的“动态链接库”项目模板记得确认生成的是 64 位还是 32 位。X-Plane 11 和 X-Plane 12 都是 64 位程序插件也必须编译成 64 位否则 X-Plane 会直接拒绝加载。这是个很无语但常见的坑。4.2 文件该放在哪个目录编译成功只是第一步放错目录等于白干。正确的位置是你的X-Plane目录/Resources/plugins/hello/hello.xplResources/plugins是 X-Plane 扫描插件的主目录。官方支持直接把hello.xpl丢在Resources/plugins根目录但强烈建议按“插件名/插件文件”的结构放这样做有几个好处一是卸载插件时整个文件夹删掉就行二是 X-Plane 12 支持插件子文件夹有独立的.ini配置三是避免多个插件文件混在一起难以管理。再强调一次不要放到Resources/plugins下面的win_x64子目录里那是 X-Plane 自带内部文件的位置新手经常从这里听到“你把插件装错了”的反馈。如果是 Steam 版插件目录在游戏安装目录里不在 Steam 的公共文档目录这一点买 Steam 版的朋友尤其要注意。4.3 用 Log.txt 判断加载成功失败把 hello.xpl 复制到位后启动 X-Plane然后打开 X-Plane 根目录下的Log.txt搜索hello。成功加载时日志里会有一行类似这样Loaded: /path/to/X-Plane/Resources/plugins/hello/hello.xpl (com.example.helloplugin).这一行同时包含了插件路径和签名非常直观。如果没看到这一行说明插件根本没有被加载接下来要去日志里找failed或crash相关的字眼。我建议养成一个习惯每次改完插件代码重新编译后都看一眼 Log.txt 末尾的加载记录确认新版本的插件真的被加载了再进游戏能节约大量时间。5. 新手最容易踩的坑5.1 插件没有加载从头排查结合这几年带新手写插件的经验没加载成功的原因排前三的分别是现象最常见原因Log.txt 里完全没有插件相关信息放错目录、插件是 32 位、文件名后缀不对日志里有 “failed to load”SDK 版本不匹配、动态库依赖缺失X-Plane 直接闪退代码里有未处理异常、回调参数误用先说第一个场景很多人把插件放到别的位置或者干脆忘了重命名我建议先从目录结构查起。第二个场景常见的是你用了新版 SDK 编译却在老版本 X-Plane 上跑此时插件会报类似 “XPLM_xxx not found” 的错误。第三个场景最隐蔽往往是回调函数里用了不安全的操作比如直接解引用空指针或者访问了已经被释放的内存。5.2 回调不执行时间间隔也乱了代码照着抄了X-Plane 也加载成功了就是看不到日志输出这种“半死不活”的状态比加载失败更让人抓狂。通常是因为注册回调之后回调函数本身被其他线程阻塞了或者返回值的逻辑写错了。我见过一个案例回调返回-1.0f心想“1 秒后调用”结果每帧都被调用日志刷得飞快。因为-1.0f是“每帧都调用”的意思不是“1 秒”。还有人是回调里忘记写返回值C 语言里未定义返回值的行为无法预料调度就乱了。遇到这种问题先在回调第一行写XPLMDebugString(tick\\n)确认回调有没有进再谈别的。另外XPLMRegisterFlightLoopCallback和XPLMCreateFlightLoop/XPLMScheduleFlightLoop是两套不同的 API。前者简单直接适合新手后者支持更精细的暂停、恢复和定时调度但概念更多。你现在用前者练手完全够用不要过早拥抱复杂。5.3 那些让人崩溃的环境细节Windows 环境下面有一个特别容易踩的坑中文路径。X-Plane 插件目录如果有中文有些编译器生成的插件能加载有些会加载失败表现非常随机。解决办法是插件开发期间把 X-Plane 放到纯英文路径下SDK 也放到纯英文路径下这能避免许多底层文件系统兼容问题。还有一件事我特别想提醒不要在插件里用printf打印调试信息。X-Plane 插件是动态库运行在 X-Plane 进程里它的标准输出很可能没有控制台可看你打印了一堆数据却什么也看不到。统一用XPLMDebugString才能确保信息进日志。这一点很多从传统 C 程序转过来的朋友容易忽略浪费半小时才发现“原来输出根本没地方去”。6. 从“能跑”到“能干活”6.1 读一条实时数据数据引用的第一课插件只打印自己的帧数没意思真正让 X-Plane 插件有价值的是读取和修改飞行数据。X-Plane 把内部所有状态都暴露成数据引用英文叫 DataRef。读数据只需要三步先找到引用的名字再通过XPLMFindDataRef获取句柄最后按类型读取数值。比如读取当前飞机指示空速可以在回调里加上#include XPLMDataAccess.h static XPLMDataRef airspeed_ref; int XPluginStart(...) { ... airspeed_ref XPLMFindDataRef(sim/flightmodel/position/indicated_airspeed); } float my_flightloop(...) { if (airspeed_ref ! NULL) { float speed XPLMGetDataf(airspeed_ref); char buffer[256]; snprintf(buffer, sizeof(buffer), Airspeed: %.1f kt\n, speed); XPLMDebugString(buffer); } return 1.0f / 2.0f; }sim/flightmodel/position/indicated_airspeed这个名字是不是看着眼熟X-Plane 自带的 DataRef 编辑器或者社区整理的 DataRef 在线文档里都有完整列表。如果XPLMFindDataRef返回NULL说明名字写错了或者插件加载时该数据源不存在所以在使用前一定要判空这是开发插件时最重要的防御性编程习惯。6.2 修改数据油门、副翼、自动驾驶从这里开始读取只是单行道插件的魅力在于“写”。X-Plane SDK 提供了一系列XPLMSetDataf、XPLMSetDatai、XPLMSetDatad函数可以修改可写数据引用。比如把通用航空飞机的油门推杆拉满XPLMDataRef throttle_ref XPLMFindDataRef(sim/cockpit2/engine/actuators/throttle_ratio_all); if (throttle_ref ! NULL) { XPLMSetDataf(throttle_ref, 1.0f); }但是有一个必须提醒的概念不是所有 DataRef 都可写。有些数据是只读的强行写入会被 X-Plane 忽略甚至带来奇怪的行为。判断是否可写除了看在线文档也可以在代码里调用XPLMCanWriteDataRef来检查。新手最容易在这里栽跟头我当年把只读数据当可写数据用结果飞机根本没反应排查了半天才发现方向错了。6.3 从示例插件到真正工具后续扩展思路现在你已经有了一个能加载、能回调、能读写数据的插件骨架哪怕它只做了一点点事这也是你自己的新插件了。这个骨架接下来可以发展的方向非常多增加一个菜单项让玩家手动开关插件功能用XPLMDrawString在屏幕上绘制 HUD 信息用XPLMRegisterCommandHandler注册新命令监听按键消息做快捷键操作连接外部设备把数据发给串口或网络。我个人写插件多年后的体会是搭骨架的阶段不要贪心每次只加一个小功能验证没问题后再加下一个。X-Plane 主循环本身很复杂你永远不知道自己的回调会不会干扰到其他插件所以每步都验证才是最高效的路线。这次的新建插件教程就到这里。代码不多但把 X-Plane 插件的生命周期、飞行循环回调、DataRef 读写这几根最重要的柱子立起来了。下一次我们可以在这个基础上做一个带菜单和屏幕绘制的实用工具到时候你会发现自己已经能独立做出像模像样的插件了。
返回列表