- 游戏开发
【免费下载链接】vcmi
Open-source engine for Heroes of Might and Magic III
本篇以 VCMI 修改者文档 Animation Format 为核心,讲解如何用.json文件替代 Heroes of Might and Magic III 的.def动画文件:包括basepath、sequences、images三类配置的完整语法、按钮/城镇/生物动画的替换实例,以及生物动画 0–51 帧组的含义。结合渲染层源码(RenderHandler.cpp、ImageLocator.cpp)可以进一步说明引擎如何解析这些 JSON 配置、如何与原始.def回退共存,以及阴影/描边自动生成的运行时机制。
为什么需要 JSON 动画格式
VCMI 允许修改者用.json文件覆盖(override)HoMM3 的.def动画文件。相比.def,JSON 格式带来了三个实际好处(出自原文档):
- 可以单独覆盖动画中的某一帧(例如只换某个图标、某个按钮状态);
- 支持现代图片格式——TGA、PNG 等 VCMI 图片加载器支持的所有格式;
- 不需要任何专用工具——一个文本编辑器加图片素材即可完成修改。
从源码结构看,覆盖的判定逻辑在渲染初始化阶段完成:引擎先读取.def得到各组的原始帧数,再查找同名.json资源逐条覆盖对应帧槽位。这一过程位于 RenderHandler::getAnimationLayout:
- 将动画路径规范化为
SPRITES/(或SPRITES2X/、SPRITES3X/、SPRITES4X/)前缀,以支持不同缩放倍率的高清资源目录; - 若存在
.def,按defFile->getEntries()为每组resize出与原始文件相同的帧数——这保证了未覆盖的帧仍然来自原.def; - 再加载同名
.json,调用 initFromJson 逐帧写入布局表。
之后查询某帧时,getLocatorForAnimationFrame 优先返回 JSON 提供的ImageLocator;如果该槽位没有被 JSON 填写(或帧索引越界),则回退到默认的defFile:group:frame定位方式。这就是"只换部分帧、其余保持原样"的实现基础。
格式说明(Format Description)
JSON 动画文件支持两个互斥的顶层配置区:sequences(整组替换)和images(单帧替换),外加一个可选的basepath前缀。原文档给出的完整格式如下:
{ // 所有图片的基础路径,可选。 // 用于避免写很长的图片路径 "basepath" : "path/to/images/directory/", // 动画中的序列/分组列表 // 会用指定的文件列表替换原动画中的对应组 // 即使原动画更长也一样 "sequences" : [ { // 组索引,从 0 开始 "group" : 1, // 该组内的文件列表 "frames" : [ "frame1.png", "frame2.png" ... ], // 如需自动为此帧生成阴影。可选,0 = 无,1 = 普通阴影,2 = 剪切阴影(如冒险地图用) "generateShadow" : 1, // 如需自动为此帧生成覆盖层。可选,0 = 无,1 = 描边 "generateOverlay" : 1, }, ... ], // "sequences" 的替代方案。允许覆盖文件中的单个帧。 // 一般不应与 "sequences" 同时使用 "images" : [ { // 所属组。可选,默认 = 0 "group" : 0, // 该组内的帧索引 "frame" : 0, // 该帧对应的文件名 "file" : "filename.png", // 如需自动为此帧生成阴影。可选,0 = 无,1 = 普通阴影,2 = 剪切阴影 "generateShadow" : 1, // 如需自动生成覆盖层。可选,0 = 无,1 = 描边 "generateOverlay" : 1, }, ... ] }basepath:路径前缀
basepath是可选字段,所有相对图片名都会拼接在其后。解析代码位于 initFromJson:basepath = config["basepath"].String(),随后无论是sequences中的帧还是images中的file/defFile,都会执行basepath + node["file"].String()拼接。若basepath指向目录,末尾应带/(官方示例均如此书写)。
值得注意的细节:initFromJson还会把顶层的margins、width、height通过JsonUtils::inherit(toAdd, base)继承到每一帧上——也就是说,顶层可以统一声明裁剪边距和尺寸,逐帧省略。
sequences:按组整体替换
sequences数组的每一项对应一个动画组:
group:组索引,从 0 开始;frames:该组的新帧文件列表。解析时先source[groupID].clear()清空原组,再按列表顺序填入——这会替换整组内容,即使原动画更长(原文档明确说明此语义)。
images:按帧精准覆盖
images数组的每一项指定单个帧:
group:组索引,可选,缺省为 0;frame:组内帧索引;file:新图片文件名。
源码中有一个容易忽略的健壮性处理(RenderHandler.cpp 第 186–187 行):如果目标group在当前布局里还没有那么多帧(例如.def中该组不存在或帧数更少),引擎会source[group].resize(frame+1)自动扩容后再写入。此外,images条目还支持defFile字段——指向另一个.def文件的帧,解析时会同样拼上basepath,从而允许"从别的.def里借一帧"这类混合用法。
generateShadow 与 generateOverlay:运行时生成阴影和描边
这两个可选字段控制引擎是否对该帧在运行时自动生成阴影或描边。其取值与内部枚举严格对应,定义在 ImageLocator.h:
| 字段 | 取值 | 内部枚举 | 说明 |
|---|---|---|---|
generateShadow | 0 | ShadowMode::SHADOW_NONE | 不生成阴影 |
| 1 | ShadowMode::SHADOW_NORMAL | 普通投影 | |
| 2 | ShadowMode::SHADOW_SHEAR | 剪切投影(冒险地图单位常用) | |
generateOverlay | 0 | OverlayMode::OVERLAY_NONE | 不生成覆盖层 |
| 1 | OverlayMode::OVERLAY_OUTLINE | 白色 1px 描边 | |
| 2 | OverlayMode::OVERLAY_FLAG | 旗色覆盖层 |
这些值在 ImageLocator 构造函数 中被static_cast成对应枚举。实际生成发生在渲染阶段(loadScaledImage):当以阴影层模式(ONLY_SHADOW_HIDE_SELECTION/ONLY_SHADOW_HIDE_FLAG_COLOR)取图且generateShadow有效时,调用img->drawShadow(是否剪切);当以覆盖层模式(ONLY_FLAG_COLOR/ONLY_SELECTION)取图且generateOverlay == OVERLAY_OUTLINE时,调用img->drawOutline(Colors::WHITE, 1)。
从源码结构看,还有一条命名约定式的替代路径:如果未声明generateShadow/generateOverlay,渲染器会在图片路径后追加-SHADOW或-OVERLAY后缀去查找预制的阴影/覆盖层图片(RenderHandler.cpp 第 407–418 行)。即"运行时算法生成"与"手工提供后缀图"两种做法并存。
实例一:替换按钮
按钮类动画固定需要 4 个状态帧(原文档 Examples / Replacing a button 一节):
- Active(激活):按钮可用,玩家可以按;
- Pressed(按下):玩家已按下但尚未松开;
- Blocked(禁用):按钮被阻塞、不可交互。注意部分按钮永远不会被禁用,可以不提供这张图;
- Highlighted(高亮):只有部分按钮在特定情况下使用。例如主菜单中鼠标悬停在按钮上时显示高亮;又如可切换设置的开/关状态按钮。
原文档给出的示例 JSON:
{ "basepath" : "interface/MyButton", // 所有图片都位于此目录 "images" : [ {"frame" : 0, "file" : "active.png" }, {"frame" : 1, "file" : "pressed.png" }, {"frame" : 2, "file" : "blocked.png" }, {"frame" : 3, "file" : "highlighted.png" }, ] }这正是images按帧覆盖的典型用法:组 0(默认 group)的 0–3 帧分别对应四种状态。仓库内就存在多个同结构的真实文件,例如 checkbox.json 用两张 PNG 覆盖了大厅复选框的两个状态:
{ "basepath" : "lobby/", "images" : [ { "frame" : 0, "file" : "checkboxBlueOff.png"}, { "frame" : 1, "file" : "checkboxBlueOn.png"} ] }更多同类文件可参考 Mods/vcmi/Content/Sprites/ 目录下的deleteButton.json、dropdown.json、rangeHighlightsGreen.json等,它们演示了按钮、下拉框、高亮遮罩等不同 UI 元素的覆盖方式。
实例二:替换简单动画
对于冒险地图对象或城镇建筑这类单组循环动画,用sequences定义一组帧即可(原文档示例):
{ "basepath" : "myTown/myBuilding", // 所有图片都位于此目录 "sequences" : [ { "group" : 0, "frames" : [ "frame01.png", "frame02.png", "frame03.png", "frame04.png", "frame05.png" ... ] } ] }由于sequences的语义是"整组替换",这里的帧列表长度无需与原.def相同——即使原动画更长,也会被这份列表完全取代。
原文档中"Replacing creature animation(替换生物动画)"一节标记为 TODO,未给出完整示例;但方法一致:按下面的帧组编号为每组提供帧列表。下面一节给出了生物动画的完整组定义。
生物动画帧组(Creature Animation Groups)
生物动画由多个组(group)构成,每组代表一个特定动作。原文档完整列出了 VCMI 使用的组编号及语义,此处完整继承:
基础动画
- [0] Movement(移动):生物移动时使用;
- [1] Mouse over(鼠标悬停):随机待机动作,以及鼠标移过生物时播放;
- [2] Idle(待机):生物堆不执行动作时持续播放的基础动画;
- [3] Hitted(受击):生物堆被打中时播放;
- [4] Defence(防御):防御姿态下的替代受击动画,近战命中且正在防御时播放;
- [5] Death(死亡):生物堆死亡时播放;
- [6] Death (ranged)(死亡·远程):替代死亡动画,被远程攻击击杀时播放。
转向动画
- [7] Turn left(左转):旋转动画的前半部分,生物转向观察者一侧;
- [8] Turn right(右转):旋转动画的后半部分;
- [9]VCMI 未使用,存在于 H3 原始文件中;
- [10]VCMI 未使用,存在于 H3 原始文件中。
近战攻击动画
- [11] Attack (up):面朝上目标的攻击动画;
- [12] Attack (front):面朝前方目标的攻击动画;
- [13] Attack (down):面朝下目标的攻击动画。
远程攻击动画
- [14] Shooting (up):面朝上目标的远程攻击动画;
- [15] Shooting (front):面朝前方目标的远程攻击动画;
- [16] Shooting (down):面朝下目标的远程攻击动画。
特殊动画
- [17] Special (up):当找不到专用施法或群攻动画时使用的特殊动画;
- [18] Special (front):同上,面朝前方;
- [19] Special (down):同上,面朝下方。
H3 附加动画
- [20] Movement start(移动开始):移动动画开始前播放;
- [21] Movement end(移动结束):移动动画结束后播放。
VCMI 附加动画
- [22] Dead(已死亡):生物死亡后的静止画面。若未提供,则由 "Death" 组的最后一帧构成;
- [23] Dead (ranged)(已死亡·远程):远程攻击致死后的画面。若未提供,由 "Death (ranged)" 组最后一帧构成;
- [24] Resurrection(复活):生物复活时播放。若未提供,由 "Death" 动画的反转序列构成。
施法动画
- [30] Cast (up):面朝上目标施法时;
- [31] Cast (front):面朝前方目标施法时;
- [32] Cast (down):面朝下目标施法时。
群体攻击动画
- [40] Group Attack (up):生物攻击多个目标且主目标在上方时使用(如龙息、九头蛇类生物);
- [41] Group Attack (front):主目标在前方时使用;
- [42] Group Attack (down):主目标在下方时使用。
H3 附加动画(传送)
- [50] Teleportation start(传送开始):单位传送时在原位置播放。若未提供,将改用 movement start 动画;
- [51] Teleportation end(传送结束):单位传送时在目标位置播放。若未提供,将改用 movement end 动画。
覆盖的加载与回退机制(源码佐证)
理解以下几个实现细节,可以避免修改时踩坑:
- JSON 只覆盖、不删除。布局初始化时先按
.def的getEntries()把每组resize到原始帧数(RenderHandler.cpp 第 226–233 行),JSON 再覆盖指定槽位。因此未写进 JSON 的帧自动保持原样。 sequences是破坏性覆盖:对命中的组先clear()再填帧,所以用sequences重写某一组后,该组未列出的原帧不会再出现。- 多来源合并:
getResourcesWithName(jsonResource)会收集同名 JSON(RenderHandler.cpp 第 235–247 行),多个 Mod 层级的同名动画配置按加载器顺序叠加处理,后写者覆盖先写者。 - 缩放倍率目录:
SPRITES2X/、SPRITES3X/、SPRITES4X/前缀用于存放 2x/3x/4x 高清资源;只有开启 HD 贴图(settings["video"]["useHdTextures"])或缩放系数为 1 时才会使用带前缀的布局(getAnimationLayout)。源码注释也明确.def只按 1x 数据使用,放大素材应使用独立图片。 - 帧缺失时的兜底:查询不存在的组或越界帧会返回空
ImageLocator;帧定位器既无image也无defFile时,回退为ImageLocator(path, frame, group, mode),即直接按.def原始帧渲染(getLocatorForAnimationFrame)。若.def里也没有该帧,则记录错误并加载占位图DEFAULT(loadImageFromFileUncached)。 - SDL2 客户端行为一致:clientsdl2 渲染路径中有对称的 initFromJson 实现,同一套 JSON 语法对两种渲染后端通用。
实战检查清单
按原文档骨架 + 源码行为,制作/审查一份动画覆盖 JSON 时可依次确认:
- 文件是否为目标
.def的同名.json(放在 Mod 资源目录中,引擎按资源名匹配); basepath是否以/结尾且图片实际位于该目录;- 只换个别帧(按钮、图标)→ 用
images;整组重做(建筑、单位动作)→ 用sequences;二者一般不要混用(原文档建议); - 组号是否按上面的生物帧组表填写(生物动画尤其注意 30/40/50 段的施法、群攻、传送组);
- 需要运行时阴影/描边的帧(如冒险地图单位)是否声明了
generateShadow: 2(剪切)或generateOverlay: 1(描边); - 目标帧在原
.def中不存在时是否有意为之——引擎会自动扩容帧槽,不会报错。
小结
VCMI 的动画覆盖格式用一个轻量 JSON 就打通了"旧.def资产 + 现代图片格式 + 逐帧精准替换"的 Mod 工作流:basepath管路径、sequences管整组、images管单帧、generateShadow/generateOverlay管运行时生成的阴影与描边;生物动画则以 0–51 的固定组编号规范动作语义。格式语义由 docs/modders/Animation_Format.md 定义,解析与回退逻辑可在 clientsdl3/render/RenderHandler.cpp 与 client/render/ImageLocator.cpp 中逐行验证,真实用例可参考 Mods/vcmi/Content/Sprites/ 下的各 JSON 文件。
- 游戏开发
【免费下载链接】vcmi
Open-source engine for Heroes of Might and Magic III
相关推荐
Typi配置完全指南:从基础到高级断点设置
Typi配置完全指南:从基础到高级断点设置 Typi是一款强大的Sass mixin工具,专为简化响应式排版设计而开发。通过直观的配置方式和灵活的断点系统,即使
maldev 终极指南:恶意软件开发技术深度解析与实践教程
maldev 终极指南:恶意软件开发技术深度解析与实践教程 在当今网络安全领域,理解恶意软件的工作原理对于防御者来说至关重要。maldev 项目提供了一个独特的
示例工程网络安全SDN网络故障排查技巧:从丢包到性能优化 | SDN Handbook
SDN网络故障排查技巧:从丢包到性能优化 | SDN Handbook SDN(软件定义网络)凭借其灵活的流量控制和集中化管理能力,已成为现代网络架构的核心。然
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考