AFSim_二次开发_基于wsf插件扩展内置platform
基于wsf插件扩展内置platform
1
前言
2
意义
其实不只是需要扩展平台,其他如传感器、通信设备、武器、弹道等模型和算法都需要进行扩展,目的是将这些东西进行隐藏。如果基于afsim的脚本来编写所模型和算法,那么无异于直接将秘密公之于众,这对于技术分享和交流是再好不过了,但对于企业发展及国家安全是不利的,也是不允许的。封装才能获得效益和安全!!!
先看看效果:
-
工程展示:源码,语法文件
-
编译输出到wsf_plugin,grammar
-
wizard脚本:扩展类型,扩展属性和单位,script处理输出
-
调试warlock,查看ProcessInput处理
-
查看控制台,调试自定义脚本接口
3
平台创建过程
下面先来分析platform的创建过程吧。首先LoadPlatformInstance方法内部获取定义的platform名称

获取到名称后,AFSim通过一个延迟加载对象WsfDeferredInput来缓存,等其他对象依赖此对象时,或所有其他对象都创建并完成后再来将标记为延迟加载的对象进行创建。

这里先不分析延迟加载的逻辑,直接跳到下方创建平台的代码处。

这里的步骤是:
1)从脚本获取平台的类型
脚本定义平台的语法为:platform [name] [platform_type],其中name为平台的名称,platform_type为此平台的类型。使用simple_scenario.txt场景获取的baseType名称为BLUE_STRIKER,也就是platform直接使用的platform_type,BLUE_STRIKER的继承关系如下图:


WsfScenario的CreateTypeLists方法是在它本身的构造函数中调用的。查看代码,发现这里面会创建场景中几乎所有的类型列表对象。




这里就会解析脚本定义的继承链上的所有platform_type并保存,其中就包括BLUE_STRIKER与WsfPlatfom的映射关系。
上面通过克隆platform_type定义的对象,不具有唯一性,这一步就是为创建的平台赋值一个唯一Id,标识唯一性
4
扩展插件
参考sensor扩展插件的创建方式,我们要对platform进行扩展,那么就不是仅在wizard、warlock或mystic中使用,因此需要对核心wsf框架进行扩展。afsim有三个层面的扩展方式:
-
WsfScenarioExtension – 如果需要添加新的scenario脚本指令,则需继承此类,并为新的脚本指令提供ProcessInput解析
-
WsfSimulationExtension – 如果要访问simulation的扩展则需要继承此类,如添加simulation的执行状态的监听
-
WsfApplicationExtension – 将创建新脚本类型或使用simulation扩展或scenario扩展,需要继承此类
因此要扩展platform,并提供新的脚本指令,就需要对WsfApplicationExtension、WsfScenarioExtension进行继承并实现,下面来依次实现相关内容:
ExtendPlatform.hppExtendPlatform.cppPluginRegistration.cpp
同时,为了扩展脚本语法文件,还需添加一个extend_platform.ag文件(这个文件要最终放到bin下的所有gammer文件夹内)

2)插件注册
插件注册类代码可以参考《创建AFSim开发环境》这篇文章来配置依赖库和插件注册,这里就不多说了。然后重写WsfApplicationExtension的AddedToApplication和ScenarioCreated方法,将我们自定义的脚本接口类和扩展的ExtendPlatform类注册到afsim环境中,代码如下:
// PluginRegistration.cpp#include"wsfplugin_export.h"#include"ExtendPlatform.h"// wsf#include"WsfPlugin.hpp"#include"WsfPlatformTypes.hpp"#include"WsfApplicationExtension.hpp"class ExtendPlatformApplicationExtension : public WsfApplicationExtension{public:ExtendPlatformApplicationExtension() = default;~ExtendPlatformApplicationExtension() noexcept override = default;voidAddedToApplication(WsfApplication& aApplication)override{// 将脚本接口类添加到脚本类型处理逻辑中aApplication.GetScriptTypes()->Register(ut::make_unique<ScriptExtendPlatformClass>("ExtendPlatform", GetApplication().GetScriptTypes()));}voidScenarioCreated(WsfScenario& aScenario)override{// 将扩展的platform类型添加到平台类型列表WsfPlatformTypes::Get(aScenario).Add("EXTEND_PLATFORM",ut::make_unique<ExtendPlatform>(aScenario));}};extern "C"{WSF_PLUGIN_EXPORT voidWsfPluginVersion(UtPluginVersion& aVersion){aVersion = UtPluginVersion(WSF_PLUGIN_API_MAJOR_VERSION,WSF_PLUGIN_API_MINOR_VERSION,WSF_PLUGIN_API_COMPILER_STRING);}WSF_PLUGIN_EXPORT voidWsfPluginSetup(WsfApplication& aApplication){// 注册本插件工程// 此处使用自定义的Application扩展,ExtendPlatformApplicationExtensionaApplication.RegisterExtension("register_PlatformExtendPlugin",ut::make_unique<ExtendPlatformApplicationExtension>());}}
脚本接口类ScriptExtendPlatformClass封装的方法可以在txt文件中使用,就是将程序中的方法公布到txt文件中,实现往下看。
3)脚本接口类
脚本接口类提供了向txt脚本编写时能够访问dll中封装的接口,AFSim具有相当丰富的脚本接口,该接口定义了用户可以从WsfPlatform等定义的实体中调用的脚本方法。开发人员可以通过声明和定义可调用的新脚本方法,并将它们添加到管理脚本的UtScriptClass来扩展此接口:
UT_DECLARE_SCRIPT_METHODUT_DEFINE_SCRIPT_METHODAddMethodAddStaticMethod
上面这几种脚本定义宏,大家可查看sensor这篇ppt内容的介绍来了解使用方法。
本文以获取和设置平台造价(Cost)为例,实现了脚本接口类的实现代码(注:必须要重写Clone,原因见上面流程说明):
// ExtendPlatform.h#pragma once#include"WsfPlatform.hpp"class ExtendPlatform : public WsfPlatform{public:ExtendPlatform(const WsfScenario& aScenario);~ExtendPlatform() override;doubleGetCost()const{ return m_Cost; }voidSetCost(double cost){ m_Cost = cost; }// 返回脚本接口类的名称,即ExtendPlatformconstchar* GetScriptClassName()constoverride{ return "ExtendPlatform"; }//! @name Framework methods//@{// 注:必须加Clone方法,否则无法触发ProcessInput调用WsfPlatform* Clone()constoverride;boolProcessInput(UtInput& aInput)override;//@}protected:double m_Cost = 0; // 造价,单位:百万};#include"UtScriptClassDefine.hpp"#include"script/WsfScriptPlatformClass.hpp"class ScriptExtendPlatformClass : public WsfScriptPlatformClass{public:ScriptExtendPlatformClass(const std::string& aClassName,UtScriptTypes* aTypePtr);~ScriptExtendPlatformClass() noexcept override = default;UT_DECLARE_SCRIPT_METHOD(GetCost); // 声明,获取造价,double cost = <x>.GetCost();UT_DECLARE_SCRIPT_METHOD(SetCost); // 声明,设置造价,<x>.SetCost(double cost);};
// ExtendPlatform.cpp#include"ExtendPlatform.h"ExtendPlatform::ExtendPlatform(const WsfScenario& aScenario): WsfPlatform(aScenario){}ExtendPlatform::~ExtendPlatform(){}WsfPlatform* ExtendPlatform::Clone()const{return new ExtendPlatform(*this);}boolExtendPlatform::ProcessInput(UtInput& aInput){bool ret = true;// TODOreturn ret;}#include"script/WsfScriptContext.hpp"ScriptExtendPlatformClass::ScriptExtendPlatformClass(const std::string& aClassName,UtScriptTypes* aTypePtr): WsfScriptPlatformClass(aClassName, aTypePtr){SetClassName("ExtendPlatform");// 添加脚本可访问的方法AddMethod(ut::make_unique<GetCost>()); // double cost = <x>.GetCost();AddMethod(ut::make_unique<SetCost>()); // <x>.SetCost(double cost);}// 脚本方法实现// double cost = <x>.GetCost();// 参数:脚本接口类、扩展的平台类、脚本方法、参数数量、返回值类型、参数类型【多个参数格式: "string, int"】UT_DEFINE_SCRIPT_METHOD(ScriptExtendPlatformClass, ExtendPlatform, GetCost, 0, "double", ""){// 这里的aObjectPtr就是对应的扩展平台类ExtendPlatformdouble cost = aObjectPtr->GetCost();// 返回给脚本调用方aReturnVal.SetDouble(cost);}// 返回值为空也要写上voidUT_DEFINE_SCRIPT_METHOD(ScriptExtendPlatformClass, ExtendPlatform, SetCost, 1, "void", "double"){// 获取传入的参数值double cost = (double)aVarArgs[0].GetDouble();// 设置到对象aObjectPtr->SetCost(cost);}
上面的代码已经能编译成功了,并且在wsf_plugins目录下已经生成了插件dll,这时运行的话可能不会有任何反应,最多在PluginRegistration方法中的断点会进,因为我们还没有针对扩展的平台写任何脚本。
4)语法文件
要想让wizard在编写我们扩展的平台类EXTEND_PLATFORM给出自动补全提示信息的话,需要先定义语法文件.ag,并将语法文件放到bin下的所有grammar目录。详细的语法规则可在文档中搜索Grammar Guide和Grammar Format查看。
扩展的类型为EXTEND_PLATFORM,并添加了一个自定义属性Cost,因此需要在语法文件中标明此类是继承至Platform,还要指明编写Cost时的提示信息,如关键字、输入值和可选的单位列表:
# extend_platform.ag# Extended property(rule money-unit {(nocase { yuan | dollar })})# Extended platform type(struct EXTEND_PLATFORM:symbol (type platformType EXTEND_PLATFORM):base_type Platform(var Real cost){# Extended propertycost <Real> <money-unit>| <Platform>})
5)脚本参数读取
这个主要通过重写WsfPlatform的ProcessInput来实现,需要注意的是我们为cost设置了单位,因此在ProcessInput要对单位进行处理,代码如下:
boolExtendPlatform::ProcessInput(UtInput& aInput){bool ret = true;std::string command(aInput.GetCommand());if (command == "cost"){double cost;aInput.ReadValue(cost);std::string costUnit;aInput.ReadCommand(costUnit);if (costUnit == "dollar"){// 测试,1dollar = 6yuancost = cost * 6;}else if (costUnit == "yuan"){ }else{ }this->SetCost(cost);}else{ret = WsfPlatform::ProcessInput(aInput);}return ret;}
6)运行问题
按上面完成代码编写后,编译运行会在下方的函数中触assert(found)断言:

这里的意思是,在继承类中找不到父类定义的脚本接口函数,因此会报错。详细调试发现在我们扩展的ScriptExtendPlatformClass对象中有一些附加的脚本方法缺失,造成上面的方法在检测时报找不到匹配的接口方法,比如下图的RadarSigState脚本接口,

原因见下图,因为在Add时,只会添加到父类WsfPlatform中,在继承类中不会添加。

因此,这里需要修改源代码,将上图的方法改为可以传入继承类的名称,来将接口方法添加到继承类中。当然文末还有一种不修改源码的方式。
5
测试
上面编译好插件,就可以打开wizard来编写脚本了,我这里通过定义一个从扩展的EXTEND_PLATFORM类型创建一个platform,并编写两个定时script,一个用于获取当前造价,并输出到控制台;一个用于每次script触发都将当前造价值增加1,并更新造价。这里面调用的方法,就是在脚本接口类ScriptExtendPlatformClass声明并定义的接口方法。
platform F-22 EXTEND_PLATFORMicon f-22side bluecost 12 yuanadd processor addCost WSF_SCRIPT_PROCESSORupdate_interval 2 son_updatevar cost = ((ExtendPlatform)PLATFORM).GetCost();cost += 1;((ExtendPlatform)PLATFORM).SetCost(cost);end_on_updateend_processoradd processor showCost WSF_SCRIPT_PROCESSORupdate_interval 1 son_updatevar cost = ((ExtendPlatform)PLATFORM).GetCost();writeln("Current Cost: ", cost);end_on_updateend_processorend_platformend_time 2 h
从wizard中启动warlork,就可以看到控制台定时输出了递增的造价数据:

6
后记
上面是把扩展platform的流程打通了,是对官方trainning的补漏,没有具体的复杂业务处理。因为需要对源码进行简单修改,才能扩展platform,而平台类在afsim中仅作为承载的容器,其他功能算法都由不同的component来完成的,所以大家根据需要进行选择。
我这里也尝试写成了cmake的插件,只需要放到源码的wsf_plugins目录下并把修改后的几个文件替换afsim源码,然后一起cmake生成编译,它会自动将grammar和dll安装到对应的目录去。


boolScriptXXPlatform::Initialize(){// 重写 Initialize 以避免方法索引对齐导致的崩溃// 跳过基类 Initialize 中的索引对齐逻辑(UtScriptClass.cpp:579-654)// 但保留其他必要的初始化步骤bool ok = true;// 检查方法索引是否正确(来自 UtScriptClass.cpp:673-690)for (size_t i = 0; i < GetMethodCount(); ++i){InterfaceMethod* methodPtr = GetMethodEntry(i);if (methodPtr){// 检查索引一致性if (i != methodPtr->GetIndex()){// 索引不匹配,但我们不报错,因为这是预期的// 父类方法的索引可能与子类列表位置不同}// 初始化方法(解析参数类型和返回类型)if (!methodPtr->Initialize()){ok = false;}}}return ok;}
通过重写初始化方法避免了修改源码解决了,运行没有问题,可以调用wsfplatfom的方法和新建类的方法。
往
期
推
荐

夜雨聆风