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