AFSIM-模型导入导出-源码级Bug修改
ModelImport 隶属于 Wizard 模块下的专用插件,主要功能是将 A 项目内的各类平台类型脚本打包整合,批量导入到 B 项目中直接调用使用。
其实日常使用里也有简便办法,直接把 A 项目对应的脚本文件复制粘贴到 B 项目目录,同样能正常调用里面编写好的平台与组件脚本。但这种手动拷贝方式弊端很明显,极易打乱打乱 B 项目原本规整的目录架构,造成文件杂乱堆砌。
接下来博主就带着大家一步步实操教学,手把手教大家正确使用 ModelImport 插件,完成 AFSIM 模型的规范导出与跨项目导入操作。
第一步:源码Bug修改
Bug 描述:在使用 ModelImport 插件将 A 项目中的模型导入到 B 项目时,插件只会在 B 项目里创建对应文件夹,但无法将 A 项目中的脚本文件真正复制过去,导致导入后平台无法正常加载使用。
问题源码位于:
wizard/plugins/ModelImport/source/ModelImportPlugin.cpp
具体出问题的位置,是ImportRecursionHelper这个接口的实现部分。
void ModelImport::Plugin::ImportRecursionHelper(const QString& aFilePath, const QDir& aImportToDir)
{
// This should not throw because the FileData's existence was checked for in ImportOkay().
const ModelImport::FileData& file = LookupFileData(Path(GetPath(), aFilePath));
aImportToDir.mkpath(file.PathToDir()); // Create subdirectories if necessary
// Check if file was already imported
if (QFile::exists(Path(aImportToDir.absolutePath(), aFilePath)) && !mImportedFiles.contains(aFilePath))
{
switch (mReimportSelection)
{
case Reimport::cYESTOALL:
break;
case Reimport::cNOTOALL:
return;
default:
switch (QMessageBox::question(
nullptr,
QString(),
QString("The file \"%1\" is already imported. Would you like Wizard to re-import it?").arg(aFilePath),
QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No | QMessageBox::StandardButton::YesToAll |
QMessageBox::StandardButton::NoToAll,
QMessageBox::StandardButton::YesToAll))
{
case QMessageBox::StandardButton::Yes:
break;
case QMessageBox::StandardButton::No:
return;
case QMessageBox::StandardButton::YesToAll:
mReimportSelection = Reimport::cYESTOALL;
break;
case QMessageBox::StandardButton::NoToAll:
mReimportSelection = Reimport::cNOTOALL;
return;
default:
break;
}
}
mImportedFiles << aFilePath;
// Copy file
QFile::copy(Path(GetPath(), aFilePath), Path(aImportToDir.absolutePath(), aFilePath));
// Import dependencies
for (const QString& dependency : file.mDependencies)
{
ImportRecursionHelper(dependency, aImportToDir);
}
// Import additional dependencies
for (const QString& dependency : file.mAdditionalDependencies)
{
ImportRecursionHelper(dependency, aImportToDir);
}
}
}
if (QFile::exists(Path(aImportToDir.absolutePath(), aFilePath)) && !mImportedFiles.contains(aFilePath))
问题 :源码第400行,当首次导入时文件在目标位置 不存在 , QFile::exists() 返回 false ,整个 if 块被跳过,导致:
-
❌ 文件不被复制
-
❌ mImportedFiles 不更新
-
❌ 依赖不被递归处理
原有设计意图分析:
从代码中可以推断出作者的原始设计意图:
-
mImportedFiles :防止同一导入会话中重复处理同一个文件(如多个模型共享同一个依赖)
-
QFile::exists() :检测目标位置是否已有文件,如果有则询问用户是否重新导入
-
Reimport 对话框 :让用户选择 Yes/No/YesToAll/NoToAll
写这段代码的程序员,忽略了最关键的一个场景 —— 第一次导入时,目标文件根本还不存在!
修改方案:
把原来的单一条件 QFile::exists(...) && !mImportedFiles.contains(...) 拆分为两层独立的 if :
-
第一层(L400-403) : mImportedFiles.contains() → 避免同一会话中重复处理
-
第二层(L406-437) : QFile::exists() → 仅在文件已存在时询问用户是否重新导入
-
L439-454 :移出所有 if 块,保证复制和依赖递归始终执行
Reimport 对话框的所有逻辑(Yes/No/YesToAll/NoToAll/cYESTOALL/cNOTOALL) 完全保留不变
修正后代码:
void ModelImport::Plugin::ImportRecursionHelper(const QString& aFilePath, const QDir& aImportToDir)
{
// This should not throw because the FileData's existence was checked for in ImportOkay().
const ModelImport::FileData& file = LookupFileData(Path(GetPath(), aFilePath));
aImportToDir.mkpath(file.PathToDir()); // Create subdirectories if necessary
// Check if file was already imported in this session
if (mImportedFiles.contains(aFilePath))
{
return;
}
// Check if file already exists in the target location
if (QFile::exists(Path(aImportToDir.absolutePath(), aFilePath)))
{
switch (mReimportSelection)
{
case Reimport::cYESTOALL:
break;
case Reimport::cNOTOALL:
return;
default:
switch (QMessageBox::question(
nullptr,
QString(),
QString("The file \"%1\" is already imported. Would you like Wizard to re-import it?").arg(aFilePath),
QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No | QMessageBox::StandardButton::YesToAll |
QMessageBox::StandardButton::NoToAll,
QMessageBox::StandardButton::YesToAll))
{
case QMessageBox::StandardButton::Yes:
break;
case QMessageBox::StandardButton::No:
return;
case QMessageBox::StandardButton::YesToAll:
mReimportSelection = Reimport::cYESTOALL;
break;
case QMessageBox::StandardButton::NoToAll:
mReimportSelection = Reimport::cNOTOALL;
return;
default:
break;
}
}
}
mImportedFiles << aFilePath;
// Copy file
QFile::copy(Path(GetPath(), aFilePath), Path(aImportToDir.absolutePath(), aFilePath));
// Import dependencies
for (const QString& dependency : file.mDependencies)
{
ImportRecursionHelper(dependency, aImportToDir);
}
// Import additional dependencies
for (const QString& dependency : file.mAdditionalDependencies)
{
ImportRecursionHelper(dependency, aImportToDir);
}
}
修改后请重新编译WizModelPlugin插件

第二步:创建一个空项目
我们先新建一个目录,在目录内新建空白文件。我这边新建名为EmptyProj的文件夹,再在里面创建EmptyProj.txt空白文件,随后用 Wizard 工具将其打开,这样就搭建好了一个纯净的空白测试项目环境。


第三步:创建一个测试用的想定脚本
接下来博主准备好了现成的测试项目,准备把里面的模型脚本,导入到刚刚搭建好的空白项目里进行实测。

用 Wizard 打开测试项目后能清晰看到,项目内预置了 F22 机型平台以及各类配套组件。在Project Browser中可查看完整文件目录结构,Type Brower里则能直观浏览所有平台机型与功能组件类型,效果如图所示。

这里提醒大家一个实操重点:我们日常编写平台类型脚本时,常会调用已写好的各类组件类型,在用"include_once"文件命令引用文件时,路径引用规则一定要留意。
不少朋友会疑惑,平时直接写文件名不加../也能正常引用,确实,不加上级路径才是 AFSIM 正统编写规范。
那我这里为何要特意加上../呢?原因就出在模型导入功能上:该功能解析引用路径时,是以被调用类型脚本自身所在位置作为基准路径,而非项目启动文件的位置。
若是不提前手动调整路径层级,执行模型导入生成文件时,就会弹出大量路径报错。严格来说这不算程序 BUG,但使用体验十分别扭。
说实话遇上这类细节逻辑疏漏,不难看出这款插件没经过充分实测就正式发布了。虽说日常实操里这个功能并不算高频常用,但既然存在就自有其价值,文章最后博主大胆聊聊它的适用场景以及后续可拓展的开发方向。至于这个麻烦的路径适配问题,就留给大家亲手实操摸索解决吧

。
博主就以测试项目里的aircraft.txt文件为例给大家演示,效果如下图所示。

至此,准备工作就完成了。
第四步:空项目导入测试项目中的模型
用 Wizard 打开刚才建好的空项目,点击菜单栏 Options 里的偏好设置 Preference,接着选中 ModelImport 选项,界面如下图所示。

点击 Browse 浏览按钮,选中并定位到我们准备好的测试项目路径,操作界面如图所示。

我们先在顶部View 菜单里,把Model Importer选项勾选显示出来。接着点击界面上的Generate Model Mapping File,弹出提示后选择Yes。这里可以多点几遍,第一遍是让 Wizard 自动帮我们生成模型映射配置文件。再点一次,弹窗里选择Merge(融合)或者Overwrite(覆盖)都可以,操作界面就像下图这样。


此时在 Model Importer 停靠窗口中,就能清晰看到测试项目里的 F22 平台类型,界面效果如下图所示。

直接双击列表里的 F22 条目,弹出确认窗口后点击 OK 即可,ModelImport插件已经帮你完成模型导入了。

这时在项目浏览器里能看到自动生成了 imports 文件夹,里面存放着 F22 平台以及它所有依赖组件的全套脚本文件。

第五步:导入模型的使用
在启动文件EmptyModel.txt中输入 include_once imports/imports.txt

或者右键imports.txt文件选择Add to Startup Files,加入到启动文件中

在三维地球视图(Map Display)空白位置右键选择添加平台,就能找到 F22机型,说明该模型已经成功加载进项目中了。


我们查看添加完成后的整体效果,再分别打开项目浏览器与类型浏览器核对,确认平台结构、组件配置都和原测试项目保持一致。

后续再导入其他外部项目模型,所有相关脚本都会统一存放在 imports 目录中,不会打乱原有项目的文件架构,做到互不干扰了。
官方说明
可以搜索Model Import关键字

翻译一下就是:
模型导入 - Wizard 工具
Wizard 中的模型导入对话框,提供了一个将平台类型及其依赖项从外部目录导入到 AFSIM 项目中的交互界面。
外部目录中会使用一个JSON 文件来记录导入信息。如果该文件不存在,用户可以根据提示自动生成。
复制的文件会保留原有的目录结构,并统一放入 ** 导入文件(Imports File)** 所在的目录(详见偏好设置)。
为方便使用,工具会自动生成一个文本文件,包含所有已导入的文件。该文件必须手动引入到场景文件中才能生效。
偏好设置(Preferences)
在偏好设置菜单中,用户可配置以下选项:
搜索路径(Search Path)
需要从中导入类型的外部项目目录路径。界面提供浏览(Browse)按钮,方便快速选择路径。
模型映射文件(Model Mapping File)
用于记录导入信息的JSON 文件名。默认值:importData.json
导入文件(Imports File)
自动生成的、用于统一引用所有导入文件的文本文件名。默认值:imports/imports.txt
显示模式(View Mode)
名称列表(Name List)
可排序的平台类型名称列表,为默认显示模式。
分类列表(Category List)
基于category关键字自动生成的标签分类列表。每个平台类型会显示在对应的标签下。可手动编辑模型映射文件调整标签。
文件树(File Tree)
以树形结构展示外部目录的真实文件布局。
生成模型映射文件(Generate Model Mapping File)
扫描外部目录,更新模型映射文件。工具会提示用户:覆盖现有数据或尝试合并自定义修改。
重新加载模型映射文件(Reload Model Mapping File)
从磁盘重新读取映射文件,刷新界面显示内容。
界面展示结果
Model Import 停靠窗口包含以下区域:
目录(Directory)
显示当前外部项目的路径(只读,不可修改)。
搜索(Search)
支持实时搜索已识别的类型。输入内容时界面自动刷新;按回车键可保存当前搜索记录,通过下拉菜单可重复使用;点击 X 按钮清空搜索框与历史记录。
显示区域(View Area)
根据偏好设置,以列表或树形结构展示可导入内容。双击条目即可将其导入当前项目。
在分类列表模式下:双击一个分类,可一键导入整个分类下的所有内容。
在文件树模式下:双击文件可直接导入该文件;双击文件夹,会提示导入文件夹内的全部内容。
文件导入规则
导入一个类型时,该类型的定义文件及其所有依赖项会递归复制到项目中。依赖项指文件头部使用include或include_once引入的所有文件。
⚠️ 注意:在代码块内部或类型定义之后才引入的文件,可能不会被自动复制。如果需要手动添加依赖项,可在模型映射文件的AdditionalDependencies节点中配置。
如果导入的文件在当前项目中已存在,工具会提示用户:跳过或覆盖。
工具在导入过程中还会额外生成两个文件
如下图所示,测试项目下多了两个文件。

importErrors.log日志文件,它专门用来记录生成导入配置时遇到的错误。大家可以看一下,这个文件大小是0KB,说明它是空的,也就代表我们这次生成导入配置的过程没有报错。
importData.json核心配置片段如下:
[
{
"File": "aircraft.txt",
"Path": "platforms",
"Dependencies": [
"sensors/radar/acq_radar.txt",
"processors/single_large_sam_tactics.txt",
"comms/base_comm.txt"
],
"AdditionalDependencies": [],
"Defines": [
{
"Name": "F22",
"Type": "platform_type",
"Inherits": "WSF_PLATFORM",
"Labels": []
}
]
},
文件整体是一个配置信息数组,每一条记录都包含File、Path、Dependencies等核心字段,分别用来描述模型文件、路径信息和依赖关系。
总结
这款功能还有很大优化余地,实际使用体验着实不够顺手。但不得不承认,整体架构设计水准依旧顶尖,对比国内不少厂商开发的建模工具与模型库体系,二者差距十分明显。
单论代码细节而言,生成依赖路径时,理应以偏好设置里的检索路径作为基准统一转为相对路径,使用起来会合理很多。我学识有限,不便随意评判官方这样设计的用意,也不确定是否是我的操作方式存在偏差,精通这块的同行欢迎在评论区一同交流探讨。
为何需要模型导入这个功能?
场景一:
-
公司有一个共享的模型库(包含各种平台、传感器、武器)
-
用户打开自己的 AFSIM 项目
-
用户在 ModelImport 里配置共享模型库路径
-
用户双击选择需要的模型
-
插件自动复制模型到当前项目的 imports 目录
-
用户在自己的场景文件里 include imports/imports.txt
-
用户就可以使用这些模型了
这套用法优势十分明显,既方便全员快速调用公司标准化模型库内的各类资源,也便于企业集中统一维护管理模型资产。
彻底告别以往每个项目都各自存放模型文件的混乱局面,从根源上避免脚本冗余堆积,有效杜绝项目脚本逐渐堆砌成难以维护的乱象。
场景二:
ModelImport 就是 AFSIM 的"模型应用商店" :
-
乙方上传(交付 imports 包)
-
甲方下载(通过 ModelImport 导入)
-
甲方使用(直接在场景中引用)
你只需要知道"我要用 F22",剩下的 ModelImport 帮你搞定。
个人认为的ModelImport 核心价值
将"建模能力"和"模型使用"分离,让不擅长建模的人也能轻松使用专业团队建好的模型。
这正是设计模式中的 Façade 模式 ——为复杂子系统提供一个简化的接口。
更深层次的思考
版本管理体系
当前只有Merge/Overwrite两种模式,缺少:
|
缺少能力 |
说明 |
|
版本追溯 |
知道模型是哪个版本的 |
|
增量更新 |
只更新变化的部分 |
|
回滚能力 |
恢复到之前的版本 |
|
兼容性声明 |
声明依赖的最低版本 |
质量保证体系
1.模型分级
|
等级 |
说明 |
验证要求 |
|
A级 |
官方认证,可用于作战 |
完整验证 + 实战测试 |
|
B级 |
已验证,可用于训练 |
完整验证 |
|
C级 |
社区贡献,需用户评估 |
基础验证 |
|
D级 |
实验性,不保证正确 |
无验证 |
2.持续集成
-
CI 自动验证:语法检查、依赖检查、命名规范检查、冲突检测
-
QA 人工审核:功能验证、文档审查
-
验证和审核通过后打上标签/签名发布到模型库
3.治理与生命周期
|
阶段 |
活动 |
|
规划 |
定义模型需求和接口 |
|
开发 |
乙方建模、验证 |
|
发布 |
签名、打包、发布 |
|
部署 |
甲方导入、使用 |
|
维护 |
乙方修复Bug、升级 |
|
退役 |
甲方移除、替换 |
ModelImport 不只是一个工具,而是一个生态系统的入口。工具本身的功能已经解决了"如何使用"的问题,但要实现健康可持续的生态,需要配套的 规范化体系 作为支撑。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)