|
本帖最后由 5271081文字狱 于 2023-5-25 17:26 编辑
长文预告!
同步发表于https://zhuanlan.zhihu.com/p/632269503,如有不足之处欢迎指正,共同学习进步。系列教程/笔记目的在于帮助广大飞友以及开发者创造属于自己的插件,亦可协助科研人员进行自定义仿真模拟等。
可读写数据
简单来说,我们可以将XP中所有的DataRef归结为两种属性:可读数据和可写数据
- 可读数据:可以被读取,有“示数”,如飞机姿态,当前位置,环境温度,警告灯是否亮起,起落架是否放下等。
- 可写数据:可以被人为指定数据,如可以指定飞机位置(例如瞬移至进近3海里),指定油门量,杆量。
在上节笔者其实已经展示了如何读取数据,这节将注重于如何更改可写数据。更改数据有两种方法:
- 间接法-执行某个操作以间接更改某状态量。这种情况下该数据的来源通常是其他变量,或仅是显示某系统的状态。
- 直接法-直接改DataRef的数值。这种情况下该数据会直接影响飞机的状态,如速度、位置等。
所有可直接更改的数据都可以在DataRefTool里面指定数值,如下图,更改local_y的数值可以更改飞机高度(瞬间移动):
需要注意,并不是所有可写入的量的改变都有效。例如,sim/flightmodel/position/elevation代表飞机MSL高度,在DataRefTool中可以被更改,但飞机不会有任何响应,说明这个量只是某个其他数据的表征。这就好比仪表显示的内容只是传感器得出的结果,更改仪表数据并不会改变飞机状态,只是更改了显示出来的数字。相反,更改飞机状态则会影响仪表读数。若要更改飞机高度,直接修改sim/flightmodel/position/local_y是一个更好的选择。因此,请务必注意区分真正需要修改的量。
字面意思相近的DataRef可能有不同的含义,区分它们的方法是查阅Sim Innovations以及在DataRefTool中不断尝试。
不能直接更改的状态也可以通过执行一些动作而被改变。例如,【起落架是否放下】 这个状态是无法直接指定数值的,但我们可以执行 【收放起落架】 这个指令达到更改其数值的目的。
图中gear_handle_status为起落架状态,1代表放下,0代表收起。该数值无法直接从DataRefTool更改,但可以执行下图中的landing_gear_toggle指令(相当于执行收放起落架的指令)从而更改gear_handle_status的状态。
1
注意到,landing_gear_toggle没有数据,它只是代表“收放起落架”这个命令的CommandRef。
下面将从代码的角度详细讲解如何间接或直接更改数据。
具体的API
数据修改以及执行动作主要靠XPLMDataAccess和XPLMUtilities这两个API。前者用于直接修改数据,后者用于执行命令。完整文档请参考XP官方说明:https://developer.x-plane.com/sdk/plugin-sdk-documents/
直接修改
直接修改DataRef的数值使用XPLMSetDataf这个API(对于float类型)。对于其他类型该函数的最后一位字母会有不同,如i代表整数型,d代表double类型等。
- <font color="#000000">/*
- * XPLMSetDataf
- *
- * Write a new value to a single precision floating point data ref. This
- * routine is a no-op if the plugin publishing the dataref is disabled, the
- * dataref is NULL, or the dataref is not writable.
- *
- */
- XPLM_API void XPLMSetDataf(XPLMDataRef inDataRef, float inValue);</font>
复制代码
可以看到该函数的第一个参数是XPLMDataRef 类型的变量,可以通过XPLMFindDataRef这个函数获取(请参考:Part 2 - 数据读取
第二个变量即我们想要修改后的数字。以修改飞机高度local_y为例,我们每十秒将飞机高度在当前基础上增加1000英尺,可以参考以下程序,也可以下载笔者已经准备好的VS项目(VS2022)【点这里下载】。
- <font color="#000000">/*********************************************************************
- * @file main.cpp
- * @brief 数据写入、覆盖操作
- *
- * @author zswzy
- * @date 20230524
- *********************************************************************/
- #include <stdio.h>
- #include <string.h>
- #include "XPLMPlugin.h"
- #include "XPLMProcessing.h"
- #include "XPLMDataAccess.h"
- #include "XPLMUtilities.h"
- //======================================================================================
- static FILE* g_output_file;
- XPLMDataRef g_total_flight_time_sec_ref = NULL; //!< 总飞行时间
- XPLMDataRef g_local_y_m_ref = NULL; //!< 距离平均海平面的高度
- float total_flight_time_sec = 0.0;
- float local_y_m = 0.0;
- float local_y_new_m = 0.0; //!< 指定飞行高度
- //======================================================================================
- float CallBackDataWriter(float elapsedMe, float elapsedSim, int counter, void* refcon); // 主要功能所在的回调函数
- //======================================================================================
- /**
- * @brief 初始化函数。
- */
- PLUGIN_API int XPluginStart(
- char* outName,
- char* outSig,
- char* outDesc)
- {
- // 插件信息
- strcpy(outName, "ZSWZY Data Writer");
- strcpy(outSig, "zswzy.datawriter");
- strcpy(outDesc, "Change elevation");
- // 打开记录文件
- char outputPath[255];
- XPLMGetSystemPath(outputPath);
- strcat(outputPath, "DataWriter.txt");
- g_output_file = fopen(outputPath, "w");
- // 寻找 data ref 数据
- g_total_flight_time_sec_ref = XPLMFindDataRef("sim/time/total_flight_time_sec"); // 总飞行时间
- g_local_y_m_ref = XPLMFindDataRef("sim/flightmodel/position/local_y");
- // 注册回调函数:主要功能
- XPLMRegisterFlightLoopCallback(CallBackDataWriter, 1, NULL);
- return 1;
- }
- //======================================================================================
- /**
- * @brief 游戏关闭时执行
- */
- PLUGIN_API void XPluginStop(void)
- {
- // 注销回调函数
- XPLMUnregisterFlightLoopCallback(CallBackDataWriter, NULL);
- // 关闭文件
- fclose(g_output_file);
- }
- //======================================================================================
- /**
- * @brief 插件被Plugin Admin禁用时执行
- */
- PLUGIN_API void XPluginDisable(void)
- {
- // flush 文件,可在游戏运行时禁用插件以查看数据记录文件
- fflush(g_output_file);
- }
- //======================================================================================
- /**
- * @brief 插件启用时执行
- */
- PLUGIN_API int XPluginEnable(void)
- {
- return 1;
- }
- //======================================================================================
- /**
- * @brief 提供主要功能:记录并保存数据
- * @return float,XP将在不早于该时间间隔后再次执行该函数。
- */
- float CallBackDataWriter(
- float inElapsedSinceLastCall,
- float inElapsedTimeSinceLastFlightLoop,
- int inCounter,
- void* inRefcon)
- {
- // 获取数据;
- total_flight_time_sec = XPLMGetDataf(g_total_flight_time_sec_ref);
- local_y_m = XPLMGetDataf(g_local_y_m_ref);
- // 更改飞行高度:当前高度 + 1000 feets
- float elevation_m_add = 1000.0 / 3.28;
- local_y_new_m = local_y_m + elevation_m_add;
- XPLMSetDataf(g_local_y_m_ref, local_y_new_m);
- // 保存数据至文件
- fprintf(g_output_file, "flight time: %.4f s, current altitude (local_y): %.4f ft, set to %.4f ft.\n",
- total_flight_time_sec, local_y_m*3.28, local_y_new_m*3.28);
- return 10.0; //!< 执行间隔时间:XP将在不早于该时间间隔后再次执行该函数。
- }</font>
复制代码 程序主体结构和我们在上一节看到的一样,由XPluginStart, XPluginStop,XPluginDisable和XPluginEnable这四个主要函数构成。我们定义了g_total_flight_time_sec_ref 和g_local_y_m_ref 这两个变量用于获取游戏时间以及当前高度。注册了CallBackDataWriter这个回调函数,每10秒运行一次。在该函数内,使用XPLMSetDataf设置新的local_y高度为当前高度+1000英尺:
- <font color="#000000" size="2">local_y_m = XPLMGetDataf(g_local_y_m_ref);
- float elevation_m_add = 1000.0 / 3.28;
- local_y_new_m = local_y_m + elevation_m_add;
- XPLMSetDataf(g_local_y_m_ref, local_y_new_m);</font>
复制代码很多DataRef都是带有特定单位的,这点需要额外注意。
我们把该插件放置于默认的SF50上,看最终效果:
2
3
(日志中飞行时间归零是笔者中途重新加载了场景之故)
如果尝试过该插件,就会发现每次飞机高度变化是瞬时的,但仪表上高度的变化却不是(有一段延迟)。这说明表显高度值还包括了传感器动态或者是显示动态。虽然它们确实都是飞机的高度,但这种细微差异导致了在不同场景下可能存在的隐患。因此,再次强调,一定要明确自己想要的DataRef来自于何处。
突发奇想,如果local_y设得巨大,会不会到外太空?例如,设置为60km。。。
4
(西锐什么时候和SpaceX合作了??)
间接更改/执行操作XP的官方文档提供的XPLMUtilitiesAPI涵盖了执行动作的函数。
例如,执行起落架收放动作,首先找到该动作对应的标识符(可以用DataRefTool搜索landing关键词找到):sim/flight_controls/landing_gear_toggle,然后使用XPLMFindCommand函数获取该命令对应的指针(就像之前用XPLMFindDataRef获取数据那样),最后在需要执行的地方用XPLMCommandOnce调用该指针即可。具体代码如下:(可以在【这里】下载项目)
- <font color="#000000">/*********************************************************************
- * @file main.cpp
- * @brief 飞机操作命令,起落架收放。
- *
- * @author zswzy
- * @date 20230525
- *********************************************************************/
- #include <cstdio>
- #include <cstring>
- #include "XPLMPlugin.h"
- #include "XPLMProcessing.h"
- #include "XPLMDataAccess.h"
- #include "XPLMUtilities.h"
- //======================================================================================
- static FILE* g_output_file;
- XPLMCommandRef g_landing_gear_toggle = NULL; //!< 起落架收放动作
- XPLMDataRef g_total_flight_time_sec_ref = NULL; //!< 总飞行时间
- float total_flight_time_sec = 0.0;
- bool has_landing_gear_toggle = TRUE;
- //======================================================================================
- float CallBackToggleLandingGear(float inElapsedSinceLastCall, float inElapsedTimeSinceLastFlightLoop, int inCounter, void* inRefcon); // 主要功能所在的回调函数
- //======================================================================================
- /**
- * @brief 初始化函数。
- *
- * @param outName 插件名字
- * @param outSig 插件签名
- * @param outDesc 插件描述
- * @return 1: 正常
- */
- PLUGIN_API int XPluginStart(
- char* outName,
- char* outSig,
- char* outDesc)
- {
- // 插件信息
- strcpy(outName, "ZSWZY Landing Gear Action");
- strcpy(outSig, "zswzy.dataread");
- strcpy(outDesc, "Landing gear control");
- // 打开记录文件
- char outputPath[255];
- XPLMGetSystemPath(outputPath);
- strcat(outputPath, "CommandControl.txt");
- g_output_file = fopen(outputPath, "w");
- // 寻找 data ref 数据
- g_total_flight_time_sec_ref = XPLMFindDataRef("sim/time/total_flight_time_sec"); // 总飞行时间
- // XPLMFindCommand
- g_landing_gear_toggle = XPLMFindCommand("sim/flight_controls/landing_gear_toggle"); // 起落架收放动作
- if (g_landing_gear_toggle == NULL)
- {
- has_landing_gear_toggle = FALSE;
- fprintf(g_output_file, "ERROR: NO LANDING GEAR COMMAND!");
- }
- // 注册回调函数:主要功能
- XPLMRegisterFlightLoopCallback(CallBackToggleLandingGear, 1, NULL);
- return 1;
- }
- //======================================================================================
- /**
- * @brief 游戏关闭时执行
- *
- * @return 无
- */
- PLUGIN_API void XPluginStop(void)
- {
- // 注销回调函数
- XPLMUnregisterFlightLoopCallback(CallBackToggleLandingGear, NULL);
- // 关闭文件
- fclose(g_output_file);
- }
- //======================================================================================
- /**
- * @brief 插件被Plugin Admin禁用时执行
- *
- * @return 无
- */
- PLUGIN_API void XPluginDisable(void)
- {
- // flush 文件,可在游戏运行时禁用插件以查看数据记录文件
- fflush(g_output_file);
- }
- //======================================================================================
- /**
- * @brief 插件启用时执行
- *
- * @return 无
- */
- PLUGIN_API int XPluginEnable(void)
- {
- return 1;
- }
- //======================================================================================
- /**
- * @brief 提供主要功能:记录并保存数据
- *
- * @param inElapsedSinceLastCall
- * @param inElapsedTimeSinceLastFlightLoop
- * @param inCounter
- * @param inRefcon
- * @return float,XP将在不早于该时间间隔后再次执行该函数。
- */
- float CallBackToggleLandingGear(
- float inElapsedSinceLastCall,
- float inElapsedTimeSinceLastFlightLoop,
- int inCounter,
- void* inRefcon)
- {
- // 获取数据;
- total_flight_time_sec = XPLMGetDataf(g_total_flight_time_sec_ref);
- // 起落架收放动作
- if (has_landing_gear_toggle)
- {
- XPLMCommandOnce(g_landing_gear_toggle);
- }
- // 保存数据至文件
- fprintf(g_output_file, "flight time: %.4f s, toggle landing gear.\n", total_flight_time_sec);
- return 10.0; //!< 执行间隔时间:XP将在不早于该时间间隔后再次执行该函数。
- }</font>
复制代码 该程序每10秒执行一次起落架收/放动作。可以看到,程序定义了g_landing_gear_toggle这个变量并用
- <font color="#000000">g_landing_gear_toggle = XPLMFindCommand("sim/flight_controls/landing_gear_toggle");</font>
复制代码 获取了对应的命令指针。
更严格的做法是在获取之后验证其不为NULL才可以进一步使用。
之后在我们定义的回调函数中执行
- <font color="#000000">XPLMCommandOnce(g_landing_gear_toggle);</font>
复制代码 代表执行一次该命令。为什么会有“执行一次”这样奇怪的函数呢?因为与之相对的是执行一段时间,如有些旋钮需要持续旋转,有些按钮需要持续按压,这种情况下就不能用XPLMCommandOnce,而是用XPLMCommandBegin和XPLMCommandEnd指定该指令开始和结束的时机。具体可以参照XP11官方文档。
最后的效果就是每十秒会执行一次起落架收放。当然,请使用带可收放起落架的飞机进行测试。
有趣的是,对于Cessna172这种不可收放起落架的飞机,也可以正确找到"sim/flight_controls/landing_gear_toggle"这个标识符(而不是返回NULL),只是执行这个命令不会有任何的效果。
小结
本节主要展示了如何修改XP内部的可写数据,以及如何执行指令/动作。给出的两个例子虽然很简单,但已经最大程度展现了API的使用以及需要注意的地方。笔者认为,数据读写中的最费时的部分其实并不是程序编写,而是找到正确的数据/命令标识符。DataRefTool已经非常强大,但由于存在大量字面意思接近的标识符,找到真正有效且正确的那一个依然需要不少时间,尤其是对于复杂航电的飞机。在此期间请务必保持耐心,不断尝试。
|
|