Dota 2 录像文件里有什么
面向开发者介绍 Dota 2 的 .dem 格式:录像如何下载和压缩,实体与战斗日志包含哪些数据,以及可以用哪些解析器读取。

打开 .dem 文件,里面并没有视频帧,而是比赛期间游戏服务器发送的网络消息。读取录像,就是把这串消息重新应用到游戏实体模型中。观看录像时,客户端做的就是这件事;需要提取数据时,解析器也做同样的工作。本文介绍我们构建 ReplayDojo 处理流程时的经验:如何从比赛 ID 出发,在浏览器中呈现 3D 录像。
获取录像文件
Valve 通过录像服务器集群提供比赛录像,下载地址由三个值组成,格式如下。
比赛 ID 是公开的,而服务器集群编号和 salt 并不包含在玩家通常能看到的信息中。常见做法是通过 OpenDota 的比赛接口获取;对于它已收录的比赛,接口会返回可直接使用的 replay_url。OpenDota 不可用时,用户粘贴的完整 Valve 地址已经包含所需信息,因此我们的导入功能也直接接受这种地址作为备用方式。Valve 删除录像后,地址就会失效,通常发生在比赛结束一到两周后。
http://replay<cluster>.valve.net/570/<match_id>_<replay_salt>.dem.bz2.dem 文件包含什么
文件由一系列带长度前缀的消息构成,其中大部分使用 Protocol Buffers。几乎所有有用数据都集中在以下两类消息中。
数据包实体(Packet entities):所有联网实体的增量编码更新,包括英雄、小兵、建筑、守卫、物品,以及保存金钱、经济、补刀等信息的玩家与团队资源。实体结构定义(send tables)嵌在文件开头,因此后来开发的解析器仍能解码旧版本的录像。
战斗日志(Combat log):每次伤害、治疗、技能或物品使用、状态修饰、死亡,以及金钱或经验变化都会产生一条记录,包含攻击者、目标和效果来源(inflictor)。击杀、助攻和谁击杀了 Roshan 都从这里获取。
文件中还有游戏事件、聊天,以及信号标记和赛后总结等用户消息。录像不包含鼠标移动、镜头位置,或其他仅保留在玩家本机上的信息。
解析器
通常没有必要从头编写实体解码器。下面是我们熟悉的开源选择。
manta(Go),由 Dotabuff 维护。它偏底层,速度快;通过注册回调处理消息和实体变化。我们用它提取位置、状态和每个采样点的英雄数据。
clarity(Java),方案成熟,战斗日志处理能力较好。7.40 及之后版本的录像需要 clarity 4.0.1 或更新版本。
也有 Rust 和 Python 解析器。决定使用前,先检查最近更新时间,因为每次大版本更新都可能改变实体结构或消息类型。
从实体数据还原位置
英雄的位置分成单元格索引与格内偏移两部分,需要结合二者还原世界坐标。实体更新并不均匀:站着不动的英雄不会发送更新,团战中的英雄则每个 tick 都会更新。为了回放,我们将每个单位重新采样为固定每秒 15 次,把位置、朝向和动画状态作为关键帧,保存到紧凑的二进制文件中;变化较慢的数据,如金钱、物品、技能和冷却,则每两秒采样一次,保存在 JSON 中。一场 45 分钟的比赛最终只有几 MB,浏览器一两秒就能加载。
随后一次性推导出击杀、防御塔、Roshan 和团战事件,汇总到统一的事件索引中。击杀信息、时间轴、记分板和镜头都读取同一索引,确保显示的数字一致。
我们遇到的坑
游戏时间不等于录像时间。文件从大厅阶段开始记录,远早于开战号角,而且暂停期间 tick 仍在推进。应从比赛开始的 tick 起算并减去暂停时长,否则所有时钟都会偏差几分钟。
并非每次死亡都算击杀。不朽之守护(Aegis)和重生(Reincarnation)导致的死亡,会让英雄几秒后在原地复活。反补则是被队友击杀。将这些情况算作击杀,会导致统计与记分板不一致。
召唤物和小兵造成的击杀:记分板把击杀归给英雄,战斗日志却记录打出最后一击的单位。需要与玩家的击杀计数交叉核对。
内部名称:实体仍使用英雄改名前的引擎名称。例如 npc_dota_hero_furion 是先知(Nature’s Prophet),windrunner 是风行者(Windranger);物品的类名也不一定与物品键名一致。应维护一张别名表。
输入 Dota 2 比赛 ID,即可在浏览器中免费观看 3D 回放。
打开比赛