KBEngine 文档KBEngine 文档
首页
源码学习
架构
API
资料
指南
GitHub
首页
源码学习
架构
API
资料
指南
GitHub
  • Part I 为什么长这样

    • 源码学习首页
    • 1. 导读与阅读方法
    • 2. BigWorld:问题、模型与核心概念
    • 3. KBEngine 系统全景
  • Part II 运行骨架

    • 4. 启动流程与进程模型
    • 5. EntityDef 与实体定义系统
    • 6. Python 运行时与脚本桥接
  • Part III 基础设施层

    • 7. 并发模型、线程与内存基础设施
    • 8. 网络基础设施:I/O 模型与进程间通信
    • 9. 分布式基础:ID、发现、注册与一致性
  • Part IV 通信与协作

    • 10. 序列化、Bundle 与网络消息
    • 11. RPC、EntityCall 与通信模式
    • 12. 属性同步与数据包广播
    • 13. 数据库、DBMgr 与持久化
  • Part V 空间、运动与拓扑

    • 14. Space、AOI 与视野系统
    • 15. 空间拓扑与动态扩容
    • 16. 移动、寻路与导航
    • 17. Ghost 系统
  • Part VI 脚本层行为

    • 18. 钩子、回调、定时器与事件
  • Part VII 前后端交互

    • 19. 客户端协议与前后端交互
  • Part VIII 运维、调试与稳定性

    • Ch20 可观测性:监控、性能分析与调试
    • Ch21 热更新、容错与运维工具
  • Part IX 串联与实战

    • Ch22 玩家完整生命周期
    • Ch23 BigWorld 与 KBEngine 对照
    • Ch24 实战源码走读
  • 阅读辅助

    • 全部目录
  • Appendix

    • 附录 A 源码阅读地图与下一步
    • 附录 B 关键算法速查
    • 附录 C 外部参考系统速查
    • 附录 D 专业术语速查
    • 附录 E 引擎适用场景与游戏类型选型指南
    • 附录 F 坐标系约定:BigWorld 与 KBEngine
    • 附录 G 服务器时间管理与世界时钟

Ch24 实战源码走读

核心问题:前面学了机制,这一章带你实际走进源码,跟踪五条完整链路。

24.1 统一走读方法

每条走读路径遵循同一方法论:

1. 起点:哪个函数、哪个组件
2. 跨组件消息:什么 msgID、什么 Bundle
3. 目标端落地:哪个 handler 处理
4. 回调收束:结果怎么回到发起者

本章五条走读链路可以先压成一张总览图。读源码时不要从文件目录开始,而要先选“哪一条链”:

推荐工具:

  • grep / rg:搜索函数名和消息名
  • IDE 跳转:从消息 ID 跳到 handler 注册处
  • Watcher:运行时验证关键变量值

24.2 实战一:一次登录请求怎样进入 BaseApp

走读路径

Client
  │  login(clientType, datas, loginName, password)
  ↓
Loginapp::login()
  │  kbe/src/server/loginapp/loginapp.cpp:876
  │  1. 参数校验(长度、非空)
  │  2. 检查 baseappmgr / dbmgr 是否就绪
  │  3. 调用脚本 onRequestLogin(loginName, password, clientType, datas)
  ↓
Python 脚本 → 返回 (accountName, password, error, ...)
  │
  ↓ (脚本允许登录)
PendingLoginMgr::add()
  │  kbe/src/lib/server/pendingLoginmgr.h
  │  创建 PLInfos,等待后续流程
  ↓
Loginapp → DBMgr: onAccountLogin
  │  消息:DbmgrInterface::onAccountLogin
  │  内容:loginName, password, datas
  ↓
Dbmgr::onAccountLogin()
  │  kbe/src/server/dbmgr/dbmgr.cpp:708
  │  转发给 interfaces_handler 做账号验证
  ↓
DBMgr → Loginapp: onLoginAccountQueryResultFromDbmgr
  │  返回:retcode, accountName, componentID, entityID, dbid, flags, deadline ...
  ↓
Loginapp → BaseAppMgr: registerPendingAccountToBaseapp / registerPendingAccountToBaseappAddr
  │  componentID > 0 走已有 BaseApp
  │  componentID == 0 走新分配
  ↓
BaseAppMgr → BaseApp: registerPendingLogin
  │  先把待登录信息登记到目标 BaseApp
  ↓
BaseAppMgr → Loginapp: onLoginAccountQueryBaseappAddrFromBaseappmgr
  │  返回:分配的 BaseApp 地址 (ip, tcp_port, udp_port)
  ↓
Loginapp → Client: onLoginSuccessfully(addr, port, ...)
  │  告知客户端 BaseApp 地址
  ↓
Client → BaseApp: loginBaseapp(accountName, password)
  │  直接连接 BaseApp
  ↓
BaseApp::loginBaseapp()
  │  kbe/src/server/baseapp/baseapp.cpp:3769
  │  验证 PendingLogin、来源地址、密码、账号标记
  │  entityID > 0 走“已有 Proxy”分支
  │  entityID == 0 时向 DBMgr 发 queryAccount
  ↓
BaseApp → Client: onCreatedProxies(rndUUID, entityID, entityType)
  │  kbe/src/server/baseapp/baseapp.cpp:3160
  │  Proxy 创建完成,通知客户端
  ↓
Proxy::onClientEnabled()
  │  客户端已就绪,触发脚本层的 onClientEnabled

关键状态

状态存储位置说明
PendingLoginMgrLoginApp 内存等待 DBMgr/BaseAppMgr 回复的登录请求
pending_logins_BaseAppMgr 内存LoginApp 与 BaseApp 之间的待分配映射
EntityLogDBMgr 数据库在线实体检出记录
rndUUIDProxy 实体64 位随机 UUID,用于重连
clientEnabled_Proxy 实体客户端是否已就绪

BigWorld 对照

BigWorld 的登录流程类似,但多了:

  • LoginChallenge:Cuckoo Cycle PoW 验证
  • ClientLoginRequest:封装登录状态的对象
  • DBAppMgr 分配:数据库操作由 DBAppMgr 路由到 DBApp

24.3 实战二:一次 EntityCall 怎样发出去并落地

走读路径(KBEngine)

Python 脚本层
  │  someEntity.remoteMethod(args)
  ↓
EntityRemoteMethod::tp_call()  /  ClientEntityMethod::tp_call()
  │  kbe/src/server/baseapp/entity_remotemethod.cpp
  │  kbe/src/server/cellapp/client_entity_method.cpp
  │  1. 从 MethodDescription 获取 utype (方法 ID)
  │  2. 创建 Bundle
  ↓
EntityCall::newCall_(msgHandler)
  │  kbe/src/lib/entitydef/entity_call.cpp
  │  1. Bundle::newMessage(msgHandler)
  │  2. 写入 entityID
  │  3. MethodDescription::addToStream(args) 序列化参数
  ↓
EntityCall::sendCall(bundle)
  │  根据 EntityCall 类型路由:
  │  - BaseEntityCall → 通过内部 Channel 发到 BaseApp
  │  - CellEntityCall → 通过内部 Channel 发到 CellApp
  │  - ClientEntityCall → 通过 Proxy 发到客户端
  ↓
网络传输 (TCP)
  │  Bundle → Packet → Channel::send()
  ↓
目标端 MessageHandler
  │  MessageHandlers::handle(msgID, ...)
  │  通过 msgID 查找注册的 handler
  ↓
Base/Cell Entity::onRemoteMethodCall(channel, stream)
  │  kbe/src/server/baseapp/entity.cpp
  │  kbe/src/server/cellapp/entity.cpp
  │  1. 从 stream 读取 entityID → 查找实体
  │  2. 从 stream 读取 methodUtype → 查找 MethodDescription
  │  3. MethodDescription::createFromStream(stream) 反序列化参数
  │  4. 调用 Python 方法:PyObject_CallMethod(entity, methodName, args)

方法 ID 的来源

MethodDescription 在 EntityDef 初始化时分配 utype:
  → ScriptDefModule::addMethodDescription()
  → 每个 .def 文件中定义的方法按顺序编号
  → utype 在同一实体的所有方法中唯一
  → 通过 utype 标识"调用哪个方法",而非方法名字符串

Ghost 上的特殊路径

如果目标实体是 ghost(非 real):
  ↓
RealEntityMethod::__py_call()
  │  不直接执行,而是转发给 real entity
  │  消息路由:ghost 的 CellApp → real 的 CellApp
  ↓
Real entity 端执行
  │  Entity::onRemoteMethodCall()
  │  在 real 上执行后,结果可能通过 ghost 同步回 ghost 所在的客户端

24.4 实战三:一个属性更新如何同步给客户端

走读路径(KBEngine)

Python 脚本层
  │  entity.someProp = newValue
  ↓
Entity::__py_setattr(name, value)
  │  kbe/src/server/baseapp/entity.cpp / kbe/src/server/cellapp/entity.cpp
  │  1. 查找 PropertyDescription
  │  2. DataType::isSameType 做类型检查
  │  3. PropertyDescription::onSetValue 写入脚本对象
  ↓
Entity::onDefDataChanged(...)
  │  kbe/src/server/cellapp/entity.cpp
  │  1. real entity 才继续
  │  2. 先同步给 ghost
  │  3. 再遍历当前 witnesses_ 推送给可见客户端
  │  4. 最后同步给实体自己的客户端
  ↓
Bundle 发送
  │  通过 Proxy / client mailbox 发到客户端
  ↓
Client: onUpdatePropertys / onUpdateData_xz_ypr_...
  │  客户端 SDK 接收
  │  1. 查找对应实体
  │  2. 更新属性值
  │  3. 如果是位置/朝向 → 做插值平滑

alias 机制的关键步骤

实体首次进入客户端视野时:
  → onEnterView: 在 Witness 侧为 EntityRef 分配 alias
  → alias 本质上是当前视野列表里的短编号
  → 后续同步用 alias 代替完整 entityID
  → 当前实现里发送给客户端时要求 aliasID <= 255

BigWorld 对照

BigWorld 在 Witness update 中多了 aoi_update_schemes 策略层:

aoi_update_scheme(可插拔策略)
  → 决定哪些属性同步给哪些 Witness
  → 支持按距离、按类型、按优先级等不同策略
  → KBEngine 没有 this 抽象,策略硬编码

24.5 实战四:一次写库请求如何经过 DBMgr

走读路径(KBEngine)

Python 脚本层
  │  entity.writeToDB(callback, shouldAutoLoad)
  ↓
Entity::writeToDB()
  │  kbe/src/server/baseapp/entity.cpp
  │  1. 收集所有 persistent 属性
  │  2. addPersistentsDataToStream() 序列化到 MemoryStream
  ↓
Base → Cell:同步 Cell 侧数据
  │  Cell 上也有 persistent 属性(如位置)
  │  Cell 整理后通过 EntityCall 回传给 Base
  ↓
Base 整理完整数据
  │  Base 侧 persistent 属性 + Cell 侧 persistent 属性
  ↓
Base → DBMgr: writeEntity
  │  消息:DbmgrInterface::writeEntity
  │  内容:实体数据流 + entityDBID + callbackID + shouldAutoLoad
  ↓
Dbmgr::writeEntity()
  │  kbe/src/server/dbmgr/dbmgr.cpp
  │  创建 DBTaskWriteEntity 投递到线程池
  ↓
DBTaskWriteEntity::db_thread_process()
  │  kbe/src/server/dbmgr/dbtasks.cpp:328
  │  在工作线程中执行 SQL:
  │  1. 如果 entityDBID == 0 → INSERT(新实体)
  │  2. 否则 → UPDATE(更新已有实体)
  │  3. 同时更新 EntityLog(在线检出记录)
  ↓
DBTaskWriteEntity::presentMainThread()
  │  回到主线程,将结果发送回 BaseApp
  ↓
BaseApp 收到写库结果
  │  Entity::onWriteToDBCallback()
  │  再触发 CallbackMgr 的回调

三段式写库

Base 侧持久化数据  ──┐
                      ├──→  合并  ──→  DBMgr ──→  MySQL
Cell 侧持久化数据  ──┘
  • Base 先把自己的 persistent 属性序列化
  • 向 Cell 请求 Cell 侧的 persistent 属性
  • 两部分数据合并后发送给 DBMgr
  • DBMgr 在工作线程执行 SQL,完成后回调 BaseApp

BigWorld 对照

BigWorld 的写库路径类似,但有归档 vs 直写的区别:

KBEngine: writeToDB → DBMgr → MySQL(即时写)
BigWorld:
  Archive (定期归档) → DBApp → MySQL
  Backup (跨进程备份) → 另一个 BaseApp(内存)
  writeToDB → DBAppMgr → DBApp → MySQL(即时写)

24.6 实战五:一个玩家进入 Space 后如何建立视野

走读路径(KBEngine)

Python 脚本层
  │  entity.teleport(spaceID, position, direction)
  ↓
Base → CellAppMgr: 请求创建 Cell 实体
  │  消息包含:entityID, spaceID, position, direction
  ↓
Base / Cell 路径落到目标 CellApp: 创建实体
  │  是落到已有 Space 的 Cell,还是先为 Space 分配 Cell,
  │  取决于当前 Space 运行态,而不是“每次 teleport 都重新负载均衡”
  ↓
CellApp::onCreateEntityFromCellappmgr()
  │  在目标 CellApp 上创建 Cell 实体
  │  实体被插入到 Space 的 CoordinateSystem
  ↓
CoordinateSystem::insert(entity)
  │  插入十字链表的 X 轴和 Z 轴
  ↓
RangeTrigger 检测
  │  新实体的 ViewTrigger 激活
  │  搜索视野范围内的所有实体
  ↓
AOI 事件触发
  │  onEnterView / onEnterAoI
  │  创建 Witness(如果是玩家实体)
  ↓
Witness 建立
  │  1. 为玩家实体创建 Witness
  │  2. 设置 ViewRadius(视野半径)
  │  3. 扫描视野内所有实体
  ↓
客户端同步
  │  对视野内每个实体:
  │  → onEntityEnterWorld(entityID, entityType, ...)
  │  → onUpdatePropertys(所有初始属性)
  │  → onUpdateBasePos / onUpdateBaseDir (位置朝向)
  ↓
tick 开始后
  │  实体移动 → AOI 更新 → Witness update → 客户端同步

BigWorld 对照

BigWorld 的路径类似,但多了 BSP 树动态分配:

1. Base 请求 CellAppMgr 创建 Cell 实体
2. CellAppMgr 通过 BSP 树确定实体应落在哪个 CellApp
3. 如果该 Space 尚未分配 CellApp → CellAppMgr 分配
4. BSP 树的 InternalNode 可能需要 grow/shrink
5. 实体创建后插入 Cell 的 CoordinateSystem
6. Witness 建立 → AOI 事件 → 客户端同步

24.7 推荐的实际阅读顺序

优先级排序

第一条链:登录(理解系统全貌)
  → Ch22 + 本实战一
  → 涉及组件最多:LoginApp → DBMgr → BaseAppMgr → BaseApp → Client
  → 理解后对系统整体有清晰认识

第二条链:EntityCall(理解 RPC 机制)
  → Ch11 + 本实战二
  → 涉及核心机制:序列化 → 消息路由 → handler 查找 → Python 调用

第三条链:属性同步(理解数据流)
  → Ch12 + 本实战三
  → 涉及性能关键路径:脏标记 → Witness → Bundle 复用 → 客户端

第四条链:写库(理解持久化)
  → Ch13 + 本实战四
  → 涉及数据安全:Base/Cell 协作 → 线程池 → SQL → 回调

第五条链:Space + 视野(理解空间系统)
  → Ch14 + 本实战五
  → 涉及最复杂的子系统:AOI → Witness → 客户端

阅读方法

  1. 第一遍:读走读路径,理解整体流程,不深入细节
  2. 第二遍:在 IDE 中实际跳转代码,在每个关键函数打断点
  3. 第三遍:启动集群,用 Bots 触发流程,观察 Watcher 和日志输出
  4. 第四遍:修改代码加 debug 日志,验证你的理解是否正确

24.8 小结

五条实战走读路径覆盖了 MMO 服务器最核心的五条链路:

链路起点终点涉及组件数核心机制
登录ClientBaseApp4+会话建立、实体恢复
EntityCallPython 脚本目标端 Entity2-3序列化、消息路由
属性同步setattrClient SDK2-3AOI、Witness、Bundle
写库Python 脚本DBMgr3Base/Cell 协作、线程池
Space 视野teleportClient3+AOI、CoordinateSystem、Witness

掌握这五条链路,就掌握了整个引擎的核心运行机制。其余所有功能(战斗、AI、社交、经济)都是在这五条链路上组装出来的。

Prev
Ch23 BigWorld 与 KBEngine 对照