【免费下载链接】vcmi
Open-source engine for Heroes of Might and Magic III
项目地址:
https://gitcode.com/gh_mirrors/vc/vcmi
点击查看 免费下载
本文以 docs/developers/Code_Structure.md 为主线,结合 VCMI 仓库(Heroes of Might and Magic III 的开源引擎)的实际源码,系统梳理引擎的三大核心二进制(VCMI_lib / VCMI_client / VCMI_server)的分工、游戏状态与奖励系统的组织方式、JSON 配置体系,以及覆盖主线程、网络线程、服务器线程、控制台线程与 TBB 并行任务的完整线程模型。读完本文,你将能够看懂 VCMI 的代码地图,知道"游戏逻辑改动应该发生在哪一层、状态如何同步给客户端、各后台线程各自承担什么职责",并在后续阅读 Bonus_System.md、Serialization.md、Networking.md 等文档时建立起整体坐标系。
总体图景:三个核心项目
VCMI 的代码被划分为几个主要部分:client(客户端)、server(服务器)、lib(共享库)和 AIs(人工智能),每一部分都对应独立的二进制文件。
整个引擎包含三个核心项目:
- VCMI_lib(动态库,Windows 上为 dll、Linux/macOS 上为 so):包含客户端与服务器共享的全部代码;
- VCMI_client(可执行文件):面向玩家的游戏客户端;
- VCMI_server(可执行文件):承载全部游戏机制与事件的服务端。
这是一条根本性的架构纪律:服务器负责所有游戏机制(mechanics)与事件(events)的处理,客户端只负责把游戏状态和事件呈现给玩家,并收集玩家的输入。在一局游戏中,始终只有一个服务器,但可以有一个或多个客户端——每个电脑玩家对应一个客户端。当使用独立 vcmiserver 进程时,整个服务器由单一线程代表(见下文"服务器线程")。
状态同步原则(本文最重要的一句话):游戏状态与游戏机制在客户端和服务器之间是同步的。任何对游戏状态或游戏机制的修改都必须由服务器完成,并由服务器向客户端发送相应的通知(netpacks)。客户端永远不能私自改写游戏状态。
从源码角度看,这条原则的落点清晰可见:服务器产生的通知以 CPackForClient 系列包的形式下发,客户端侧通过 CGameState::apply(CPackForClient & pack) 将这些包应用到本地游戏状态(见 lib/gameState/CGameState.h),网络包的实体定义集中在 lib/networkPacks 目录。
游戏状态(Game State)
游戏状态本质上就是 CGameState 类的一个对象,以及所有可以从它访问到的东西:地图(含地图上的对象)、玩家状态、游戏选项等等。
从 lib/gameState/CGameState.h 可以看到该类在当代仓库中的实际成员组织,这比文档中的一句话描述更具体:
- 配置信息:initialOpts(来自开局前设置的副本,未随机化)与 scenarioOps(实际场景选项);
- 地图:std::unique_ptr<CMap> map,即整张地图及其中所有对象;
- 玩家与队伍:std::map<PlayerColor, PlayerState> players、std::map<TeamID, TeamState> teams;
- 战斗:currentBattles(当前进行的战斗列表)与 nextBattleID(可分配给下一场战斗的 ID);
- 英雄池:heroesPool(地图上所有尚未被雇佣的英雄);
- 回合信息:actingPlayers(当前正在行动的玩家集合,通常只有一个,simturns 时除外)与 blockedContacts(尚未接触、不能互相交互的同步回合玩家对);
- 全局数据:day(游戏总天数)、globalEffects(全局奖励节点 CBonusSystemNode)、currentRumor(当前传闻)、replayLog(由服务器填充并随每个存档保存的录像记录);
- 脚本环境:scriptingEnvironment、scriptingPool 与 mapEventDispatcher(地图事件脚本分发器,仅服务器侧使用);
- 线程同步:static std::shared_mutex mutex——注意注释明确指出它"实际上只被冒险地图 AI 使用"。
此外 CGameState 还承担着按 netpack 更新自身、计算路径、创建神器/卷轴实例、判断玩家关系与接触许可等职责,是整个引擎状态层的核心枢纽。
奖励系统(Bonus System)
奖励系统是 VCMI 最重要、也最复杂的组成部分之一,它负责把来自英雄、生物、宝物、魔法、建筑等各处的增益(bonus)叠加与结算统一起来。文档明确将其单独成文:Bonus_System.md。
在代码层面,CGameState 本身就是 CBonusSystemNode 的派生体系成员(见 lib/gameState/CGameState.h 中对 ../bonuses/CBonusSystemNode.h 的引用),奖励节点构成一棵可传递的树,节点自身的奖励加上从父节点继承的奖励共同决定最终数值。奖励系统的具体规则与实现细节,请以 Bonus_System.md 为准。
配置系统(Configuration)
VCMI 的大部分配置文件采用 JSON 格式,位于仓库的 config 目录。从当前仓库的实际内容看,该目录包含丰富的配置实体:
- 基础数据:artifacts.json(宝物)、heroClasses.json(英雄职业)、skills.json(技能)、spellSchools.json(魔法学派)、terrains.json(地形)、obstacles.json(障碍)、roads.json / rivers.json(道路与河流)等;
- 子目录分类:creatures/、factions/、heroes/、objects/、spells/、widgets/、ai/ 等按类型分组的 JSON 文件;
- 图形与界面:battles_graphics.json、cursors.json、fonts.json、mainmenu.json 等;
- 校验与脚本:schemas/ 目录存放 62 个 JSON Schema,用于校验各类配置;scriptsCombat.json 与 scriptsSpells.json 关联 Lua 战斗/法术脚本(见 luascript 与 scripts 目录)。
JSON 解析与写入
配置系统的底层是 JSON 解析器与写入器,对应源码位于 lib/json 目录(包含 JsonNode、JsonSerializer、JsonDeserializer、JsonTreeSerializer、JsonUpdater 等实现)。这套工具不仅服务于配置读取,也用于 CGameState::updateEntity 这类基于 JsonNode 数据的热更新路径(见 lib/gameState/CGameState.h),以及 Lua 脚本系统与存档中的 JSON 数据处理。
客户端(Client)
客户端的三大职责
客户端负责三件事,这是理解客户端代码的钥匙:
图形渲染
文档明确指出:图形渲染高度依赖 SDL。早期版本没有对 SDL 内部结构做封装,大部分渲染工作是通过 SDL_BlitSurface 进行表面(surface)的 blitting,并配合少量辅助函数(如文本打印)与帧率控制代码。原文档提到的 client/SDL_Extensions 与 client/SDL_Framerate 属于早期目录布局;从当前仓库结构看,渲染相关代码已经演化并组织到 client/render(通用渲染)、clientsdl2/render 与 clientsdl3/render(分别对应 SDL2 / SDL3 后端)等目录中,帧率控制则由 client/gui/FramerateManager.cpp 承担。帧率管理的目标与文档描述一致:比简单的 SDL_Delay(milliseconds) 更智能地维持合适帧率。
在渲染体系中,Interface 对象系统(interface object system)非常有用。它的基类是 CIntObject(见 client/gui/CIntObject.h),是整个 GUI 组件库及其他对象的公共基类,提供了定位、尺寸、焦点、事件接收等基础设施,上层成百上千的界面控件(冒险地图界面、战斗界面、窗口、对话框)都派生自它。
服务器(Server)
服务器负责三件事:
服务器的核心实现在 server 目录:CGameHandler(游戏事件处理的主入口)、TurnTimerHandler(回合计时)、ServerSpellCastEnvironment(法术施放环境)以及 server/battles(战斗裁决)、server/processors(各类 netpack 处理器)、server/activities 等子模块共同构成服务端逻辑。当玩家处于单机游戏或主持多人游戏时(无论是地图选择阶段还是已载入的游戏),服务器线程都会持续存在;使用独立 vcmiserver 时,整个服务器由该线程代表(见 client/ServerRunner.cpp)。
Lib:客户端与服务端的共享库
VCMI_Lib 是包含客户端与服务器公共代码的库,目的是避免代码重复。它承担以下职责:
- 处理大部分 Heroes III 原始文件(.lod 资源包、.txt 设置文件);
- 存储客户端与服务器共享的信息,例如游戏状态本身;
- 管理军队、建筑、宝物、法术、奖励及其他游戏对象(对应 lib/entities、lib/bonuses、lib/spells、lib/mapObjects 等目录);
- 处理一般游戏机制与相关动作——注意文档特别说明:这里仅限冒险地图对象相关的机制,属于历史遗留,所有游戏机制都应当由服务器处理,这也是新代码应当遵循的方向;
- 网络与序列化。
序列化(Serialization)
序列化框架能够序列化:基本类型、若干标准容器(含智能指针)、以及自定义对象。其设计基于 boost serialization 库。
除了基础功能外,它还提供了一种轻量级的传输方式:对 CGObjectInstance 对象只发送其索引/ID,从而避免在客户端与服务器之间搬运完整对象数据。
从当前仓库的 lib/serializer 目录看,框架由多个互补的组件构成:
- 二进制序列化:BinarySerializer.h / BinaryDeserializer.h,以及 CSerializer.h 中定义的统一接口;
- 存档文件:CSaveFile / CLoadFile(写/读存档文件);
- 内存序列化:CMemorySerializer(内存缓冲);
- JSON 序列化:JsonSerializer / JsonDeserializer / JsonTreeSerializer / JsonUpdater;
- 类型注册:RegisterTypes.h、CTypeList.cpp、ESerializationVersion.h(版本控制);
- 网络连接:GameConnection(基于序列化框架的进程间/网络传输通道,详见 Networking.md)。
序列化的完整细节由专门的文档承载:Serialization.md。
人工智能(AI)模块
VCMI 的 AI 分为战斗 AI与冒险地图 AI两大类,各有一个"当代主力"与一个"历史遗留":
战斗 AI(Combat AI)
- Battle AI(AI/BattleAI):当前正在使用、较新的战斗 AI。它包含威胁图(ThreatMap)、攻击可能性评估(AttackPossibility)、战斗评估器(BattleEvaluator)、法术目标评估(SpellTargetsEvaluator)等组件,用于为每一支我方单位选出最优行动;
- Stupid AI(AI/StupidAI):旧版且已废弃的战斗 AI。
冒险地图 AI(Adventure AI)
- NullkillerAI(AI/Nullkiller2):当前正在开发的、基于智能体(agent)的系统,由目标(goals)和英雄驱动。它的组织方式非常清晰:Goals/(目标建模)、Behaviors/(行为分解)、Analyzers/(局势分析)、Pathfinding/(寻路)、Markers/ 与 Engine/(调度引擎)等子目录分层协作;
- VCAI(AI/VCAI):旧版且已废弃的冒险地图 AI。
Nullkiller 每回合的核心流程 Nullkiller::makeTurn()(见 AI/Nullkiller2/Engine/Nullkiller.cpp)直观体现了"目标驱动"的含义:它通过多轮 pass(pass <= settings->getMaxPass())循环,把 CaptureObjectsBehavior、ClusterBehavior、DefenceBehavior、EscapeBehavior、GatherArmyBehavior、ExplorationBehavior 等行为分解(decompose)为任务,再按优先级分层(PriorityEvaluator::PriorityTier,从 INSTAKILL 到 MAX_PRIORITY_TIER)构建计划并筛选执行,同时通过 lockedHeroes 管理英雄占用状态,避免同一英雄被多个任务争用。其外部入口 AIGateway::makeTurn() 位于 AI/Nullkiller2/AIGateway.cpp。
线程模型(Threading Model)
VCMI 的线程模型分为长生命周期线程与短生命周期线程两类。以下是线程的名称(可在日志与调试器中看到),以及它们各自的工作职责。
长生命周期线程
| MainGUI | 主线程,应用启动时创建。负责输入处理(包括因玩家操作而更新屏幕)与最终渲染步骤 |
| runNetwork | 网络线程。始终运行 boost::asio io_service 并处理经由它收到的所有回调 |
| runServer | 服务器线程。单机游戏或主持多人游戏期间持续存在,同样常驻运行自己的 boost::asio io_service |
| consoleHandler | 控制台线程。通常无事可做,只在直接运行 vcmiclient 时处理标准输入上的控制台命令 |
MainGUI(主线程):它是应用启动时创建的主线程。注意一个平台细节:在部分操作系统(如 Linux)上,主线程的名称就是应用程序本身的名称,因此代码里只在非 Unix 平台调用 setThreadName("MainGUI")(见 clientapp/EntryPoint.cpp),线程名仅用于日志,调试器中仍显示默认名称。
runNetwork(网络线程):这个名称带有历史色彩——单机游戏时它不再使用网络,而是进程内通信(intra-process communication)。无论哪种情况,该线程都永久运行 boost::asio io_service,并处理经由它收到的所有回调——无论是来自网络的包,还是来自其他线程的数据。由于这些处理属于 netpack 处理的一部分,以下动作也发生在此线程上:
- 战斗 AI 动作:战斗 AI 通常运行在网络线程上,作为单位行动(unit taking turn)netpack 事件的反应;
- 任何 netpack 的 UI 反馈:注意这包括等待动画(无论战斗内还是冒险地图)——播放动画期间,网络线程会等待动画结束;
- 冒险地图 AI 对 netpack 的初始反应:不过 AI 通常会把事件分派给 AI 线程,实际处理在 AI 线程中完成。
runServer(服务器线程):只要玩家处于单机游戏或主持多人游戏(无论在地图选择阶段还是已载入的游戏中),该线程就存在。与网络线程一样,它也永久运行自己的 boost::asio io_service,处理所有来自玩家(人类或 AI)的请求,无论经由网络还是进程内通信。使用独立 vcmiserver 时,整个服务器由这一条线程代表(对应 client/ServerRunner.cpp 中的 setThreadName("runServer"))。
consoleHandler(控制台线程):见 lib/CConsoleHandler.cpp。该线程通常什么都不做,只在用户直接运行 vcmiclient 时处理标准输入上输入的控制台命令。
Intel TBB 的使用
VCMI 广泛使用 Intel Threading Building Blocks(TBB):
- NullkillerAI 使用 TBB 方法(主要是 parallel_for)并行化大量任务;
- 随机地图生成器(lib/rmg)积极使用 TBB 提供的线程池;
- 客户端在后台线程中进行图片放大(upscaling),以避免可见的卡顿。
在 AI 侧,TBB 任务(tbb::task)构成两类重要工作单元:
- AI 主任务(NKAI::makeTurn):每当 AI 开始新回合时创建,AI 回合结束时终止。绝大多数 AI 事件处理在这个任务中完成,但部分动作要么整体作为 tbb 任务卸载,要么用 parallel_for 等方法并行化;
- AI 辅助任务(NKAI::<various>):冒险地图 AI 收到需要处理、又不希望锁住发起调用的网络线程的事件时,会创建此类任务。
短生命周期线程
| autofightingAI | 自动战斗(autocombat)发起线程,用于 AI 首次选动作,避免界面冻结 |
| initialize | 初始化线程,游戏启动时在播放过场动画的同时加载游戏库 |
| processCommand | 处理游戏聊天中输入的命令,避免长时间执行或持有互斥锁的问题 |
autofightingAI(自动战斗发起线程):战斗 AI 通常运行在网络线程上(作为单位行动的 netpack 事件反应),但玩家按下快捷键或按钮触发 AI 的初始激活是在输入处理(MainGUI)线程中完成的。为了在 AI 选择第一个动作时避免卡顿,这个首次动作放在临时线程上执行——见 client/battle/BattleInterface.cpp 中的 setThreadName("autofightingAI")。
initialize(初始化线程):游戏启动时,为了避免加载延迟,大部分游戏库初始化在单独线程中完成,同时主线程播放开场动画——见 clientapp/EntryPoint.cpp,代码注释明确写着"只有在主线程中才能正常播放 intro,所以必须把加载移到独立线程"(可通过 VCMI_NO_THREADED_LOAD 编译宏关闭;Android 平台上还额外显示原生进度条)。初始化完成后主线程再 join() 该线程并继续图形初始化(见 clientapp/EntryPoint.cpp)。
processCommand(控制台命令处理线程):部分可在游戏聊天中输入的命令要么处理耗时较长,要么期望在**不持有任何互斥锁(如 interface mutex)**的情况下运行。为避免这些问题,所有在游戏聊天中输入的命令都在独立线程中执行——见 client/adventureMap/CInGameConsole.cpp。
小结:如何把各部分串起来
把上述分层与线程模型拼在一起,可以得到一幅完整的运行图景:
后续若想深入某个子系统,推荐按此路线阅读仓库文档:Bonus_System.md(奖励系统)、Serialization.md(序列化)、Networking.md(网络通信)、AI.md(AI 架构)、Lua_Scripting_System.md(脚本系统)与 RMG_Description.md(随机地图生成器,即 TBB 线程池的重度使用者)。
赞
【免费下载链接】vcmi
Open-source engine for Heroes of Might and Magic III
项目地址:
https://gitcode.com/gh_mirrors/vc/vcmi
点击查看 免费下载
相关推荐
Interceptor实战宝典:Windows键盘驱动的专业深度解析
终极指南:如何免费激活Cursor AI编程助手Pro版完整功能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网硕互联帮助中心





评论前必须登录!
注册