云计算百科
云计算领域专业知识百科平台

VCMI 引擎代码结构解析:客户端、服务器、Lib 与 AI 的分层架构及线程模型

  • 游戏开发

【免费下载链接】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)

客户端的三大职责

客户端负责三件事,这是理解客户端代码的钥匙:

  • 向人类玩家展示游戏状态(渲染画面、界面组件);
  • 捕获玩家的操作,并将其作为请求发送给服务器;
  • 展示服务器指示的状态变化(即应用服务器下发的 netpacks)。
  • 图形渲染

    文档明确指出:图形渲染高度依赖 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。

    小结:如何把各部分串起来

    把上述分层与线程模型拼在一起,可以得到一幅完整的运行图景:

  • 启动阶段:主线程(MainGUI)播放 intro 与驱动界面,initialize 线程并行加载游戏库(clientapp/EntryPoint.cpp);
  • 单机对局:runServer 线程(或在独立 vcmiserver 中)承载 CGameHandler 等服务器逻辑,所有状态修改都由它发起;runNetwork 线程作为进程内/网络通信枢纽,把服务器生成的 netpack 送达客户端并触发 UI 反馈与战斗 AI 反应;CGameState 在 lib/gameState/CGameState.h 中统一承载地图、玩家、战斗等全部状态;
  • 回合决策:冒险地图 AI(Nullkiller)在 NKAI::makeTurn 任务中做目标分解与优先级计划(AI/Nullkiller2/Engine/Nullkiller.cpp),并通过 TBB 并行化子任务;
  • 任何时候:直接运行 vcmiclient 时,consoleHandler 线程响应标准输入命令;游戏聊天命令则在 processCommand 线程中安全执行。
  • 后续若想深入某个子系统,推荐按此路线阅读仓库文档: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),仅供参考

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » VCMI 引擎代码结构解析:客户端、服务器、Lib 与 AI 的分层架构及线程模型
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!