实体系统:工厂、Think、I/O、游戏系统与事件
Source 的玩法几乎全部以“实体(entity)”为单位组织:玩家、武器、炸弹、触发器、出生点、游戏规则代理……本页讲解实体如何被创建、如何按时间被驱动、如何通过地图里配置的输入/输出互相通信,以及实体之外的两条横切机制——IGameSystem 与游戏事件。
相关页面:游戏 DLL 契约 · 实体复制 · 地图加载 · 实体句柄、实体列表与空间分区
相关源码文件#
- 工厂与创建:
game/server/util.h、game/server/util.cpp、game/server/mapentities.cpp、game/shared/predictable_entity.h - 数据描述:
public/datamap.h - Think 与模拟:
game/server/baseentity.h、game/shared/baseentity_shared.cpp、game/server/physics_main.cpp、game/shared/physics_main_shared.cpp - I/O:
game/server/cbase.cpp、game/server/baseentity.cpp - 实体列表:
game/server/entitylist.cpp - 游戏系统:
game/shared/igamesystem.h、game/shared/igamesystem.cpp - 事件与消息:
engine/GameEventManager.cpp、game/shared/usermessages.cpp
1. 创建:类名 → 工厂 → 实体#
LINK_ENTITY_TO_CLASS( mapClassName, DLLClassName )在服务端展开为一个静态CEntityFactory<DLLClassName>:game/server/util.h:127-128;其构造函数把自己装入全局工厂字典,Create实例化实体并返回网络化组件:game/server/util.h:98-111。例如 CS:GO 用它把player映射到CCSPlayer:game/server/cstrike15/cs_player.cpp:410。- 工厂字典是
CUtlDict<IEntityFactory*>,重复安装同名工厂会断言,未知类名创建时告警并返回NULL:game/server/util.cpp:74-175。 CreateEntityByName可选强制 edict 索引,通过字典创建后通知实体列表:game/server/mapentities.cpp:39-61。- 客户端同名宏的语义不同:它在
GetClassMap()中登记“地图类名 → 客户端类工厂”,并把地图类名写回对应的ClientClass,主要服务于客户端预测创建与纯客户端实体:game/shared/predictable_entity.h:141-161。
实体构造完成后如何取得 edict、进入全局实体列表并获得 EHANDLE,见 实体句柄、实体列表与空间分区。
从地图实体字符串到 Spawn#
MapEntity_ParseAllEntities 循环解析 { ... } 块;每块由 MapEntity_ParseEntity 读取 classname、经过可选过滤器后创建实体,并调用 ParseMapData 把键值写入实体:game/server/mapentities.cpp:520-565、game/server/mapentities.cpp:629-686。全部解析完毕后处理模板并统一 Spawn/Activate:game/server/mapentities.cpp:567-568。层级化生成会先按父子深度排序,保证父实体先于子实体生成:game/server/mapentities.cpp:571-592。
2. 数据描述(datadesc)#
每个实体类用 BEGIN_DATADESC 声明一张字段表,表项可以是存档字段、地图键值、输入处理函数或输出:public/datamap.h:365。输入 DEFINE_INPUTFUNC 记录外部名与成员函数指针:public/datamap.h:204;输出 DEFINE_OUTPUT 记录字段偏移与外部名:public/datamap.h:211。这张表同时服务于存档/读档、地图键值解析与实体 I/O(见第 4 节)。
3. 时间驱动:Think 与模拟#
SetThink(fn)设置当前的思考函数,SetNextThink(t)设置触发时间,支持具名上下文的多个 Think:game/server/baseentity.h:2869、game/shared/baseentity_shared.cpp:951、game/shared/baseentity_shared.cpp:1234。- 每个服务器 tick,
Physics_RunThinkFunctions复制一份“需要模拟或思考的实体列表”,对每个实体Physics_SimulateEntity;不模拟时只模拟玩家(以处理排队的用户命令):game/server/physics_main.cpp:2282-2339。Physics_SimulateEntity对“由玩家驱动模拟”的实体,只要该玩家近期仍有命令到达就跳过(交由玩家命令推进),否则收回控制权;其余实体调用PhysicsSimulate:game/server/physics_main.cpp:2209-2272。到期的 Think 由PhysicsRunThink执行:game/shared/physics_main_shared.cpp:1891。 - 循环期间禁用立即删除(
UTIL_DisableRemoveImmediate),删除请求进入延迟列表,在GameFrame的固定点统一Release:game/server/physics_main.cpp:2312-2334、game/server/entitylist.cpp:421-438、game/server/gameinterface.cpp:1462、game/server/gameinterface.cpp:1502。
约束:在 Think/触碰回调里删除实体不会立刻生效,同一帧内其它代码仍可能拿到该实体的指针;这是延迟删除设计的直接后果。
4. 实体 I/O:地图逻辑的“连线”#
CBaseEntityOutput::FireOutput遍历该输出的动作列表,把每个动作作为事件加入全局队列;动作可以有限次触发,次数用尽后从列表移除:game/server/cbase.cpp:265-330。CEventQueue::ServiceEvents取出所有到期事件:先按名字查找目标(FindEntityByName),再处理直接指针目标,都找不到时把目标当作类名再找一次;仍找不到只打印开发者消息:game/server/cbase.cpp:911-990。它在GameFrame的实体 Think 之后被调用:game/server/gameinterface.cpp:1494-1498。CBaseEntity::AcceptInput沿 datadesc 继承链查找外部名匹配的输入,必要时把参数转换为声明的字段类型(转换失败告警并返回 false),然后调用处理函数:game/server/baseentity.cpp:4457-4530。
观察到的边界行为:
FireOutput中“使用默认参数”的分支把调用方传入的fDelay加到动作自身延迟上,而“参数覆盖”分支只使用动作自身延迟:game/server/cbase.cpp:280与game/server/cbase.cpp:289。这是两个分支之间的行为差异;是否有意未见注释说明。
5. IGameSystem:实体之外的全局系统#
IGameSystem 为全局单例提供与 DLL、关卡、存档、帧同步的回调;IGameSystemPerFrame 额外提供服务端的“实体 Think 前/后”与“发送客户端更新前”,或客户端的“渲染前/每帧/渲染后”回调:game/shared/igamesystem.h:29-128。
CAutoGameSystem在静态构造时自动挂链表,InitAllSystems时按加入顺序加入全局列表并逐个Init,任何一个失败则整个 DLL 初始化失败:game/shared/igamesystem.cpp:176-245。- 关闭按逆序进行:
game/shared/igamesystem.cpp:252-255。 - 服务端帧回调的调用点见
GameFrame:game/server/gameinterface.cpp:1468、game/server/gameinterface.cpp:1494;FrameUpdatePostEntityThink之前会先处理“希望被安全移除”的系统:game/shared/igamesystem.cpp:344-350。
典型例子:延迟补偿管理器就是一个 CAutoGameSystemPerFrame,在实体 Think 之后记录历史位置(见 延迟补偿);服务端 VScript 也是一个游戏系统,在关卡初始化前启动虚拟机(见 VScript)。
6. 游戏事件(Game Events)#
游戏事件是带命名字段的广播消息(例如 round_start、item_purchase),由引擎的 CGameEventManager 统一分发:
FireEventIntern查找事件描述,未注册的事件直接丢弃;然后按监听者类型分发:服务端监听者不接收纯客户端事件,客户端监听者只接收客户端事件,CLIENTSTUB(代表远程客户端)不接收仅服务端或本地事件:engine/GameEventManager.cpp:495-560。- 发送给远程客户端时事件被序列化为
CSVCMsg_GameEvent,客户端反序列化:engine/GameEventManager.cpp:610、engine/GameEventManager.cpp:684。 - 事件定义从资源文件加载并注册:
engine/GameEventManager.cpp:833、engine/GameEventManager.cpp:951。
服务端玩法代码通过 gameeventmanager->CreateEvent/FireEvent 使用它,例如回合开始:game/shared/cstrike15/cs_gamerules.cpp:8715-8739。
7. 用户消息(User Messages)#
用户消息是发给指定接收者的 Protobuf 消息(HUD 提示、伤害指示、投票等)。服务端 SendUserMessage 直接转给引擎:game/server/gameinterface.cpp:4031-4034;CS:GO 的消息编号在 ECstrike15UserMessages 中定义:game/shared/cstrike15/cstrike15_usermessages.proto:69-107;客户端用 CUserMessages::BindMessage 按类型号与分屏槽位绑定处理器:game/shared/usermessages.cpp:112-141。例如热身结束时发送 CS_UM_WarmupHasEnded:game/shared/cstrike15/cs_gamerules.cpp:9528-9529。
约束与边界#
- 实体句柄
EHANDLE的序列号机制、实体列表与空间分区(ISpatialPartition)已移至 实体句柄、实体列表与空间分区。 - 有意未展开存档/读档(
CSaveRestoreData):引擎在多人游戏中拒绝存档与读档(仅开发用 ConVarsave_multiplayer_override可绕过),与 CS:GO 多人主路径关系很小:engine/host_saverestore.cpp:117-118、engine/host_saverestore.cpp:488-492。datadesc 中的存档字段仍会被编译,但本 Wiki 不追踪其格式。 - 有意未展开实体模板(point_template):它是通用地图机制,只在解析完实体后统一处理(
game/server/mapentities.cpp:567-568);game/server/cstrike15与game/shared/cstrike15中检索不到point_template,仓库也不含地图文件,无法确认官方地图是否依赖它。 - 客户端实体(
C_BaseEntity)的插值与OnDataChanged事件只在 游戏 DLL 契约 与 实体复制 中概述。