找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2000|回复: 0

[X-Plane] 【X-Plane 插件开发】Part 3 - 数据写入及动作执行

[复制链接]
发表于 2023-5-26 00:25:23 | 显示全部楼层 |阅读模式
本帖最后由 5271081文字狱 于 2023-5-25 17:26 编辑



长文预告!

同步发表于
https://zhuanlan.zhihu.com/p/632269503,如有不足之处欢迎指正,共同学习进步。系列教程/笔记目的在于帮助广大飞友以及开发者创造属于自己的插件,亦可协助科研人员进行自定义仿真模拟等。



可读写数据

简单来说,我们可以将XP中所有的DataRef归结为两种属性:可读数据和可写数据
  • 可读数据:可以被读取,有“示数”,如飞机姿态,当前位置,环境温度,警告灯是否亮起,起落架是否放下等。
  • 可写数据:可以被人为指定数据,如可以指定飞机位置(例如瞬移至进近3海里),指定油门量,杆量。


在上节笔者其实已经展示了如何读取数据,这节将注重于如何更改可写数据。更改数据有两种方法:
  • 间接法-执行某个操作以间接更改某状态量。这种情况下该数据的来源通常是其他变量,或仅是显示某系统的状态。
  • 直接法-直接改DataRef的数值。这种情况下该数据会直接影响飞机的状态,如速度、位置等。


所有可直接更改的数据都可以在DataRefTool里面指定数值,如下图,更改local_y的数值可以更改飞机高度(瞬间移动):

Untitled.png


需要注意,并不是所有可写入的量的改变都有效。例如,sim/flightmodel/position/elevation代表飞机MSL高度,在DataRefTool中可以被更改,但飞机不会有任何响应,说明这个量只是某个其他数据的表征。这就好比仪表显示的内容只是传感器得出的结果,更改仪表数据并不会改变飞机状态,只是更改了显示出来的数字。相反,更改飞机状态则会影响仪表读数。若要更改飞机高度,直接修改sim/flightmodel/position/local_y是一个更好的选择。因此,请务必注意区分真正需要修改的量。
字面意思相近的DataRef可能有不同的含义,区分它们的方法是查阅Sim Innovations以及在DataRefTool中不断尝试。
对于XP中的坐标系,将在之后单独详解。

不能直接更改的状态也可以通过执行一些动作而被改变。例如,【起落架是否放下】 这个状态是无法直接指定数值的,但我们可以执行 【收放起落架】 这个指令达到更改其数值的目的。


图中gear_handle_status为起落架状态,1代表放下,0代表收起。该数值无法直接从DataRefTool更改,但可以执行下图中的landing_gear_toggle指令(相当于执行收放起落架的指令)从而更改gear_handle_status的状态。

1

1


注意到,landing_gear_toggle没有数据,它只是代表“收放起落架”这个命令的CommandRef。

下面将从代码的角度详细讲解如何间接或直接更改数据。

具体的API
数据修改以及执行动作主要靠XPLMDataAccessXPLMUtilities这两个API。前者用于直接修改数据,后者用于执行命令。完整文档请参考XP官方说明:https://developer.x-plane.com/sdk/plugin-sdk-documents/


直接修改
直接修改DataRef的数值使用XPLMSetDataf这个API(对于float类型)。对于其他类型该函数的最后一位字母会有不同,如i代表整数型,d代表double类型等。

  1. <font color="#000000">/*
  2. * XPLMSetDataf
  3. *
  4. * Write a new value to a single precision floating point data ref. This
  5. * routine is a no-op if the plugin publishing the dataref is disabled, the
  6. * dataref is NULL, or the dataref is not writable.                           
  7. *
  8. */
  9. XPLM_API void XPLMSetDataf(XPLMDataRef inDataRef, float inValue);</font>
复制代码

可以看到该函数的第一个参数是XPLMDataRef 类型的变量,可以通过XPLMFindDataRef这个函数获取(请参考:Part 2 - 数据读取
第二个变量即我们想要修改后的数字。以修改飞机高度local_y为例,我们每十秒将飞机高度在当前基础上增加1000英尺,可以参考以下程序,也可以下载笔者已经准备好的VS项目(VS2022)【点这里下载】。

  1. <font color="#000000">/*********************************************************************
  2. * @file   main.cpp
  3. * @brief  数据写入、覆盖操作
  4. *
  5. * @author zswzy
  6. * @date   20230524
  7. *********************************************************************/

  8. #include <stdio.h>
  9. #include <string.h>

  10. #include "XPLMPlugin.h"
  11. #include "XPLMProcessing.h"
  12. #include "XPLMDataAccess.h"
  13. #include "XPLMUtilities.h"

  14. //======================================================================================

  15. static FILE* g_output_file;

  16. XPLMDataRef                g_total_flight_time_sec_ref        = NULL;                        //!< 总飞行时间
  17. XPLMDataRef                g_local_y_m_ref = NULL;                                        //!< 距离平均海平面的高度

  18. float total_flight_time_sec = 0.0;
  19. float local_y_m = 0.0;
  20. float local_y_new_m = 0.0; //!< 指定飞行高度

  21. //======================================================================================

  22. float CallBackDataWriter(float elapsedMe, float elapsedSim, int counter, void* refcon);  // 主要功能所在的回调函数

  23. //======================================================================================

  24. /**
  25. * @brief 初始化函数。
  26. */
  27. PLUGIN_API int XPluginStart(
  28.         char* outName,
  29.         char* outSig,
  30.         char* outDesc)
  31. {

  32.         // 插件信息
  33.         strcpy(outName, "ZSWZY Data Writer");
  34.         strcpy(outSig,        "zswzy.datawriter");
  35.         strcpy(outDesc, "Change elevation");

  36.         // 打开记录文件
  37.         char        outputPath[255];
  38.         XPLMGetSystemPath(outputPath);
  39.         strcat(outputPath, "DataWriter.txt");
  40.         g_output_file = fopen(outputPath, "w");

  41.         // 寻找 data ref 数据
  42.         g_total_flight_time_sec_ref = XPLMFindDataRef("sim/time/total_flight_time_sec");        // 总飞行时间
  43.         g_local_y_m_ref = XPLMFindDataRef("sim/flightmodel/position/local_y");

  44.         // 注册回调函数:主要功能
  45.         XPLMRegisterFlightLoopCallback(CallBackDataWriter, 1, NULL);

  46.         return 1;
  47. }

  48. //======================================================================================

  49. /**
  50. * @brief 游戏关闭时执行
  51. */
  52. PLUGIN_API void XPluginStop(void)
  53. {
  54.         // 注销回调函数
  55.         XPLMUnregisterFlightLoopCallback(CallBackDataWriter, NULL);        

  56.         // 关闭文件
  57.         fclose(g_output_file);
  58. }

  59. //======================================================================================

  60. /**
  61. * @brief 插件被Plugin Admin禁用时执行
  62. */
  63. PLUGIN_API void XPluginDisable(void)
  64. {
  65.         // flush 文件,可在游戏运行时禁用插件以查看数据记录文件
  66.         fflush(g_output_file);
  67. }

  68. //======================================================================================

  69. /**
  70. * @brief 插件启用时执行
  71. */
  72. PLUGIN_API int XPluginEnable(void)
  73. {
  74.         return 1;
  75. }

  76. //======================================================================================

  77. /**
  78. * @brief 提供主要功能:记录并保存数据
  79. * @return float,XP将在不早于该时间间隔后再次执行该函数。
  80. */
  81. float CallBackDataWriter(
  82.                                 float       inElapsedSinceLastCall,
  83.                                 float       inElapsedTimeSinceLastFlightLoop,
  84.                                 int         inCounter,
  85.                                 void*                inRefcon)
  86. {
  87.         // 获取数据;
  88.         total_flight_time_sec = XPLMGetDataf(g_total_flight_time_sec_ref);
  89.         local_y_m = XPLMGetDataf(g_local_y_m_ref);

  90.         // 更改飞行高度:当前高度 + 1000 feets
  91.         float elevation_m_add = 1000.0 / 3.28;
  92.         local_y_new_m = local_y_m + elevation_m_add;
  93.         XPLMSetDataf(g_local_y_m_ref, local_y_new_m);

  94.         // 保存数据至文件
  95.         fprintf(g_output_file, "flight time: %.4f s, current altitude (local_y): %.4f ft, set to %.4f ft.\n",
  96.                 total_flight_time_sec, local_y_m*3.28, local_y_new_m*3.28);

  97.         return 10.0; //!< 执行间隔时间:XP将在不早于该时间间隔后再次执行该函数。
  98. }</font>
复制代码
程序主体结构和我们在上一节看到的一样,由XPluginStart, XPluginStop,XPluginDisable和XPluginEnable这四个主要函数构成。我们定义了g_total_flight_time_sec_ref g_local_y_m_ref 这两个变量用于获取游戏时间以及当前高度。注册了CallBackDataWriter这个回调函数,每10秒运行一次。在该函数内,使用XPLMSetDataf设置新的local_y高度为当前高度+1000英尺:



  1. <font color="#000000" size="2">local_y_m = XPLMGetDataf(g_local_y_m_ref);
  2. float elevation_m_add = 1000.0 / 3.28;
  3. local_y_new_m = local_y_m + elevation_m_add;
  4. XPLMSetDataf(g_local_y_m_ref, local_y_new_m);</font>
复制代码
很多DataRef都是带有特定单位的,这点需要额外注意。

我们把该插件放置于默认的SF50上,看最终效果:

2

2


3

3


(日志中飞行时间归零是笔者中途重新加载了场景之故)
如果尝试过该插件,就会发现每次飞机高度变化是瞬时的,但仪表上高度的变化却不是(有一段延迟)。这说明表显高度值还包括了传感器动态或者是显示动态。虽然它们确实都是飞机的高度,但这种细微差异导致了在不同场景下可能存在的隐患。因此,再次强调,一定要明确自己想要的DataRef来自于何处。

突发奇想,如果local_y设得巨大,会不会到外太空?例如,设置为60km。。。

4

4


未命名1685031849.png

(西锐什么时候和SpaceX合作了??)



间接更改/执行操作XP的官方文档提供的XPLMUtilitiesAPI涵盖了执行动作的函数。
例如,执行起落架收放动作,首先找到该动作对应的标识符(可以用DataRefTool搜索landing关键词找到):sim/flight_controls/landing_gear_toggle,然后使用XPLMFindCommand函数获取该命令对应的指针(就像之前用XPLMFindDataRef获取数据那样),最后在需要执行的地方用XPLMCommandOnce调用该指针即可。具体代码如下:(可以在【这里】下载项目)


  1. <font color="#000000">/*********************************************************************
  2. * @file   main.cpp
  3. * @brief  飞机操作命令,起落架收放。
  4. *
  5. * @author zswzy
  6. * @date   20230525
  7. *********************************************************************/

  8. #include <cstdio>
  9. #include <cstring>

  10. #include "XPLMPlugin.h"
  11. #include "XPLMProcessing.h"
  12. #include "XPLMDataAccess.h"
  13. #include "XPLMUtilities.h"

  14. //======================================================================================

  15. static FILE* g_output_file;

  16. XPLMCommandRef        g_landing_gear_toggle                = NULL;                //!< 起落架收放动作
  17. XPLMDataRef                g_total_flight_time_sec_ref        = NULL;                //!< 总飞行时间

  18. float total_flight_time_sec = 0.0;
  19. bool has_landing_gear_toggle = TRUE;

  20. //======================================================================================

  21. float CallBackToggleLandingGear(float inElapsedSinceLastCall, float inElapsedTimeSinceLastFlightLoop, int inCounter, void* inRefcon);  // 主要功能所在的回调函数

  22. //======================================================================================

  23. /**
  24. * @brief 初始化函数。
  25. *
  26. * @param outName        插件名字
  27. * @param outSig        插件签名
  28. * @param outDesc        插件描述
  29. * @return 1: 正常
  30. */
  31. PLUGIN_API int XPluginStart(
  32.         char* outName,
  33.         char* outSig,
  34.         char* outDesc)
  35. {

  36.         // 插件信息
  37.         strcpy(outName, "ZSWZY Landing Gear Action");
  38.         strcpy(outSig,        "zswzy.dataread");
  39.         strcpy(outDesc, "Landing gear control");

  40.         // 打开记录文件
  41.         char        outputPath[255];
  42.         XPLMGetSystemPath(outputPath);
  43.         strcat(outputPath, "CommandControl.txt");
  44.         g_output_file = fopen(outputPath, "w");

  45.         // 寻找 data ref 数据
  46.         g_total_flight_time_sec_ref = XPLMFindDataRef("sim/time/total_flight_time_sec");        // 总飞行时间

  47.         // XPLMFindCommand
  48.         g_landing_gear_toggle = XPLMFindCommand("sim/flight_controls/landing_gear_toggle"); // 起落架收放动作
  49.         if (g_landing_gear_toggle == NULL)
  50.         {
  51.                 has_landing_gear_toggle = FALSE;
  52.                 fprintf(g_output_file, "ERROR: NO LANDING GEAR COMMAND!");
  53.         }

  54.         // 注册回调函数:主要功能
  55.         XPLMRegisterFlightLoopCallback(CallBackToggleLandingGear, 1, NULL);

  56.         return 1;
  57. }

  58. //======================================================================================

  59. /**
  60. * @brief 游戏关闭时执行
  61. *
  62. * @return 无
  63. */
  64. PLUGIN_API void XPluginStop(void)
  65. {
  66.         // 注销回调函数
  67.         XPLMUnregisterFlightLoopCallback(CallBackToggleLandingGear, NULL);        

  68.         // 关闭文件
  69.         fclose(g_output_file);
  70. }

  71. //======================================================================================

  72. /**
  73. * @brief 插件被Plugin Admin禁用时执行
  74. *
  75. * @return 无
  76. */
  77. PLUGIN_API void XPluginDisable(void)
  78. {
  79.         // flush 文件,可在游戏运行时禁用插件以查看数据记录文件
  80.         fflush(g_output_file);
  81. }

  82. //======================================================================================

  83. /**
  84. * @brief 插件启用时执行
  85. *
  86. * @return 无
  87. */
  88. PLUGIN_API int XPluginEnable(void)
  89. {
  90.         return 1;
  91. }

  92. //======================================================================================

  93. /**
  94. * @brief 提供主要功能:记录并保存数据
  95. *
  96. * @param inElapsedSinceLastCall
  97. * @param inElapsedTimeSinceLastFlightLoop
  98. * @param inCounter
  99. * @param inRefcon
  100. * @return float,XP将在不早于该时间间隔后再次执行该函数。
  101. */
  102. float CallBackToggleLandingGear(
  103.                                 float       inElapsedSinceLastCall,
  104.                                 float       inElapsedTimeSinceLastFlightLoop,
  105.                                 int         inCounter,
  106.                                 void*                inRefcon)
  107. {
  108.         // 获取数据;
  109.         total_flight_time_sec = XPLMGetDataf(g_total_flight_time_sec_ref);

  110.         // 起落架收放动作
  111.         if (has_landing_gear_toggle)
  112.         {
  113.                 XPLMCommandOnce(g_landing_gear_toggle);
  114.         }

  115.         // 保存数据至文件
  116.         fprintf(g_output_file, "flight time: %.4f s, toggle landing gear.\n", total_flight_time_sec);

  117.         return 10.0; //!< 执行间隔时间:XP将在不早于该时间间隔后再次执行该函数。
  118. }</font>
复制代码
该程序每10秒执行一次起落架收/放动作。可以看到,程序定义了g_landing_gear_toggle这个变量并用

  1. <font color="#000000">g_landing_gear_toggle = XPLMFindCommand("sim/flight_controls/landing_gear_toggle");</font>
复制代码
获取了对应的命令指针。

更严格的做法是在获取之后验证其不为NULL才可以进一步使用。

之后在我们定义的回调函数中执行

  1. <font color="#000000">XPLMCommandOnce(g_landing_gear_toggle);</font>
复制代码
代表执行一次该命令。为什么会有“执行一次”这样奇怪的函数呢?因为与之相对的是执行一段时间,如有些旋钮需要持续旋转,有些按钮需要持续按压,这种情况下就不能用XPLMCommandOnce,而是用XPLMCommandBeginXPLMCommandEnd指定该指令开始和结束的时机。具体可以参照XP11官方文档。



最后的效果就是每十秒会执行一次起落架收放。当然,请使用带可收放起落架的飞机进行测试。
有趣的是,对于Cessna172这种不可收放起落架的飞机,也可以正确找到"sim/flight_controls/landing_gear_toggle"这个标识符(而不是返回NULL),只是执行这个命令不会有任何的效果。

小结
本节主要展示了如何修改XP内部的可写数据,以及如何执行指令/动作。给出的两个例子虽然很简单,但已经最大程度展现了API的使用以及需要注意的地方。笔者认为,数据读写中的最费时的部分其实并不是程序编写,而是找到正确的数据/命令标识符。DataRefTool已经非常强大,但由于存在大量字面意思接近的标识符,找到真正有效且正确的那一个依然需要不少时间,尤其是对于复杂航电的飞机。在此期间请务必保持耐心,不断尝试。











您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表