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 不只是一个工具,而是一个生态系统的入口。工具本身的功能已经解决了"如何使用"的问题,但要实现健康可持续的生态,需要配套的 规范化体系 作为支撑。
