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 服务器时间管理与世界时钟

19. 客户端协议与前后端交互

这一章回答:从客户端的视角看,实体是怎么被创建、更新、销毁的?服务器发来的字节流怎么变成 Python 对象?断线重连时世界状态怎么恢复?

相关 API 回查

  • 客户端接口:KBEngine(client)、Entity(client)
  • 机器人客户端接口:KBEngine(bots)、Entity(bots)、PyClientApp(bots)
  • 数据类型补充:基本数据类型

19.1 本章核心问题

  • 客户端 SDK 怎么收发消息?ClientInterface 定义了哪些消息?
  • 客户端视角的实体生命周期是怎样的?(创建→属性初始化→进入空间→更新→离开→销毁)
  • 收到 onUpdatePropertys 后客户端做什么?位置/朝向怎么插值平滑?
  • detailLevel 在客户端怎么表现?
  • 断线重连的客户端侧怎么工作?
  • TCP vs KCP 的可靠性有什么区别?
  • 客户端 EntityCall 调用 Exposed 方法的信任边界是什么?

19.2 客户端 SDK 的协议层

KBEngine ClientInterface 消息定义

// 文件:kbe/src/lib/client_lib/client_interface.h(简化)
NETWORK_INTERFACE_DECLARE_BEGIN(ClientInterface)

    // === 连接与认证 ===
    CLIENT_MESSAGE_DECLARE_STREAM(onHelloCB, NETWORK_VARIABLE_MESSAGE)
    CLIENT_MESSAGE_DECLARE_STREAM(onLoginSuccessfully, NETWORK_VARIABLE_MESSAGE)

    // === 实体创建 ===
    CLIENT_MESSAGE_DECLARE_ARGS3(onCreatedProxies, NETWORK_VARIABLE_MESSAGE,
        uint64, rndUUID, ENTITY_ID, eid, std::string, entityType)

    // === 实体生命周期 ===
    CLIENT_MESSAGE_DECLARE_STREAM(onEntityEnterWorld, NETWORK_VARIABLE_MESSAGE)
    CLIENT_MESSAGE_DECLARE_ARGS1(onEntityLeaveWorld, NETWORK_FIXED_MESSAGE,
        ENTITY_ID, eid)
    CLIENT_MESSAGE_DECLARE_STREAM(onEntityEnterSpace, NETWORK_VARIABLE_MESSAGE)
    CLIENT_MESSAGE_DECLARE_ARGS1(onEntityLeaveSpace, NETWORK_FIXED_MESSAGE,
        ENTITY_ID, eid)
    CLIENT_MESSAGE_DECLARE_ARGS1(onEntityDestroyed, NETWORK_FIXED_MESSAGE,
        ENTITY_ID, eid)

    // === 远程方法调用 ===
    CLIENT_MESSAGE_DECLARE_STREAM(onRemoteMethodCall, NETWORK_VARIABLE_MESSAGE)

    // === 属性同步 ===
    CLIENT_MESSAGE_DECLARE_STREAM(onUpdatePropertys, NETWORK_VARIABLE_MESSAGE)

    // === 断线重连 ===
    CLIENT_MESSAGE_DECLARE_ARGS1(onReloginBaseappFailed, NETWORK_FIXED_MESSAGE,
        SERVER_ERROR_CODE, failedcode)
    CLIENT_MESSAGE_DECLARE_STREAM(onReloginBaseappSuccessfully, NETWORK_VARIABLE_MESSAGE)

NETWORK_INTERFACE_DECLARE_END()

BigWorld ClientInterface

// 文件:BigWorld-Engine-14.4.1/programming/bigworld/lib/connection/client_interface.hpp(简化)

// 实体进入 AOI
MF_BEGIN_CLIENT_MSG(enterAoI)
    EntityID id;
    IDAlias idAlias;
END_STRUCT_MESSAGE()

// 实体离开 AOI
MF_VARLEN_CLIENT_MSG(leaveAoI)

// 创建实体(完整数据)
MF_VARLEN_CLIENT_MSG(createEntity)
MF_VARLEN_CLIENT_MSG(createEntityDetailed)

// 实体属性/方法
MF_CALLBACK_MSG(entityProperty)
MF_CALLBACK_MSG(entityMethod)

// 控制权
MF_BEGIN_CLIENT_MSG(controlEntity)
    EntityID id;
    bool on;
END_STRUCT_MESSAGE()

// 设置游戏时间
MF_BEGIN_CLIENT_MSG(setGameTime)
    GameTime gameTime;
END_STRUCT_MESSAGE()

消息收发的完整路径

客户端发送(脚本调用 → 网络):
  Python: entity.base.requestMatchmaking()
    │
    ├── EntityCall::tp_call()
    │     → newCall(bundle)
    │     → 序列化参数到 bundle
    │
    ├── Channel::send(bundle)
    │     → Bundle 拆分为 Packet
    │     → TCP/KCP 发送
    │
    └── 到达服务器

客户端接收(网络 → 脚本回调):
  网络层收到 Packet
    │
    ├── PacketReader::processMessages()
    │     → 解析消息 ID
    │     → 查找 MessageHandler
    │
    ├── ClientInterface::onRemoteMethodCall handler
    │     → 反序列化 ENTITY_ID + 方法 utype + 参数
    │     → 找到目标 Entity
    │     → Entity::onRemoteMethodCall(stream)
    │
    └── Python 回调被触发
          entity.onMatchmakingResult(result)

19.3 客户端视角的实体生命周期

完整生命周期

先用一张图看客户端侧状态变化,下面的文本再按消息逐段展开:

                  服务器                           客户端
                    │                                │
  1. 登录成功        │── onLoginSuccessfully ──────→ │ 建立连接
                    │                                │
  2. 创建玩家实体    │── onCreatedProxies ──────────→ │ 创建 Entity
                    │    (uuid, eid, entityType)      │ 初始化属性
                    │                                │
  3. 进入世界        │── onEntityEnterWorld ────────→ │ entity.onEnterWorld()
                    │    (eid, scriptType, ...)       │ 绑定 cellEntityCall
                    │                                │
  4. 进入空间        │── onEntityEnterSpace ────────→ │ entity.onEnterSpace()
                    │    (eid, spaceID, ...)          │ 设置当前 spaceID
                    │                                │
  5. 其他实体进入AOI │── onEntityEnterWorld ────────→ │ 创建其他 Entity
                    │── onUpdatePropertys ──────────→ │ 设置初始属性
                    │                                │
  6. 属性更新        │── onUpdatePropertys ──────────→ │ entity.onUpdatePropertys()
                    │    (eid, prop1=val1, ...)       │ 更新显示
                    │                                │
  7. 远程方法调用    │── onRemoteMethodCall ─────────→ │ entity.onMethod(args)
                    │                                │
  8. 实体离开世界    │── onEntityLeaveWorld ────────→ │ entity.onLeaveWorld()
                    │    (eid)                       │ 非玩家实体通常随即销毁
                    │                                │
  9. 玩家离开空间    │── onEntityLeaveSpace ────────→ │ entity.onLeaveSpace()
                    │    (eid)                       │
 10. 预进入世界即销毁 │── onEntityDestroyed ──────────→ │ destroyEntity()
                    │    (eid)                       │ 典型是尚未 enterWorld 的实体
                    │                                │
 11. 玩家下线        │── 断开连接 ──────────────────→ │ 清理所有实体

onCreatedProxies:玩家实体的创建

// 文件:kbe/src/lib/client_lib/clientobjectbase.cpp(简化)
void ClientObjectBase::onCreatedProxies(Network::Channel* pChannel,
    uint64 rndUUID, ENTITY_ID eid, std::string& entityType)
{
    entityID_ = eid;
    rndUUID_ = rndUUID;
    connectedBaseapp_ = true;

    // 创建玩家的 baseEntityCall
    EntityCall* entityCall = new EntityCall(
        EntityDef::findScriptModule(entityType.c_str()),
        NULL, appID(), eid, ENTITYCALL_TYPE_BASE);

    client::Entity* pEntity = createEntity(entityType.c_str(), NULL,
        !hasBufferedMessage, eid, true, entityCall, NULL);

    // 如果实体属性先于创建消息到达,则先回放属性流再初始化脚本对象
    if (hasBufferedMessage)
    {
        this->onUpdatePropertys(pChannel, *iter->second.get());
        pEntity->initializeEntity(NULL);
        pEntity->isInited(true);
        pEntity->callPropertysSetMethods();
    }
}

这里的缓冲发生在 ClientObjectBase::onUpdatePropertys_():如果目标实体还不存在,就把该实体的属性流放进 bufferedCreateEntityMessage_。等 onCreatedProxies() 或 onEntityEnterWorld() 真正创建出实体后,再回放这段流。

onEntityEnterWorld:进入世界

// 文件:kbe/src/lib/client_lib/clientobjectbase.cpp(简化)
void ClientObjectBase::onEntityEnterWorld(Network::Channel* pChannel,
    MemoryStream& s)
{
    ENTITY_ID eid;
    ENTITY_SCRIPT_UID scriptType;
    // 真实实现还会读取 alias/scriptType/isOnGround 等字段
    // 如果实体不存在,会在这里创建其他玩家/NPC 的客户端实体
    // 如果是玩家本人,则补齐 cellEntityCall 并触发 onBecomePlayer/onEnterWorld
}

对客户端来说,onEntityEnterWorld() 才是“进入世界对象集合”的关键事件;onEntityEnterSpace() 只是补充 spaceID_、位置缓存和 onEnterSpace() 回调。

onEntityDestroyed:实体销毁

void ClientObjectBase::onEntityDestroyed(Network::Channel* pChannel, ENTITY_ID eid)
{
    client::Entity* entity = pEntities_->find(eid);
    if (entity == NULL)
        return;

    // 如果还在世界中,先离开
    if (entity->inWorld())
    {
        if (entityID_ == eid)
            clearSpace(false);         // 玩家自身被销毁 → 清空整个空间
        entity->onLeaveWorld();
    }

    // 从实体表移除并销毁
    pEntities_->erase(eid);
    entity->destroy();
}

BigWorld 实体生命周期

// 文件:BigWorld-Engine-14.4.1/programming/bigworld/client/entity.cpp(简化)
void Entity::onEnterWorld()
{
    MF_ASSERT(this->spaceID() != NULL_SPACE_ID);
    this->onEnterSpace(false);

    // 调用 Python 回调
    if (pPyEntity_.hasAttribute("onEnterWorld"))
    {
        pPyEntity_.callMethod("onEnterWorld", args, ScriptErrorPrint());
    }
}

void Entity::onLeaveWorld()
{
    if (pPyEntity_.hasAttribute("onLeaveWorld"))
    {
        pPyEntity_.callMethod("onLeaveWorld", ScriptErrorPrint());
    }

    // 清理资源
    resourceRefs_.clear();
    traps_.clear();
}

void Entity::onDestroyed()
{
    // 断开所有引用
    pPyEntity_.onEntityDestroyed();
}

BigWorld 的进入 AOI 机制:

服务器发送 enterAoI(id, idAlias)
  → 客户端创建 Entity shell(空壳)
  → 服务器发送 createEntity / createEntityDetailed(属性数据)
  → Entity 填充属性
  → onEnterWorld() 被调用

离开 AOI:
  → 服务器发送 leaveAoI(id)
  → onLeaveWorld() 被调用
  → 实体被销毁

19.4 属性同步的客户端侧

onUpdatePropertys 处理

// 文件:kbe/src/lib/client_lib/entity.cpp(简化)
void Entity::onUpdatePropertys(MemoryStream& s)
{
    while (s.length() > 0)
    {
        ENTITY_PROPERTY_UID uid;
        s >> uid;

        if (uid > 0)
        {
            // 查找属性描述
            PropertyDescription* pPropDesc =
                pScriptModule_->findClientPropertyDescription(uid);

            if (pPropDesc)
            {
                // 从流中创建 Python 对象
                PyObject* pyData = pPropDesc->createFromStream(&s);

                // 设置到实体属性
                pPropDesc->setToObj(this, pyData);
                Py_DECREF(pyData);
            }
        }
    }
}

关键:客户端这一层没有服务器那种 dirty 收束逻辑。ClientObjectBase::onUpdatePropertys_() 找到实体后就直接调用 entity->onUpdatePropertys(s);如果实体还没创建,只缓存流。

位置/朝向的插值与平滑

直接覆盖位置会导致实体"瞬移"——从上一帧的位置跳到新位置。客户端需要插值来平滑移动。

服务器每 tick(100ms)发送一次位置:
  t=0.0s: pos = (0, 0, 0)
  t=0.1s: pos = (1, 0, 0)
  t=0.2s: pos = (2, 0, 0)

客户端渲染帧率 60fps(~16.7ms/帧):

  无插值:
    frame 0: (0,0,0)  frame 1: (0,0,0)  ...  frame 6: (1,0,0)  ← 突然跳了一格
    frame 7: (1,0,0)  ...  frame 12: (2,0,0) ← 又跳了一格

  线性插值:
    frame 0: (0, 0, 0)
    frame 1: (0.17, 0, 0)
    frame 2: (0.33, 0, 0)
    ...
    frame 6: (1.0, 0, 0)     ← 平滑到达
    frame 7: (1.17, 0, 0)
    ...
    frame 12: (2.0, 0, 0)    ← 平滑到达

BigWorld 的 Velocity-based 插值:

// BigWorld 客户端使用 velocity 属性做预测性插值
// Entity 有 PY_DEFERRED_ATTRIBUTE(velocity)
// 客户端收到新位置后,根据 velocity 预测中间帧的位置

// 如果 velocity 足够准确,客户端可以在两次服务器更新之间
// 用 velocity * deltaTime 预测位置,减少插值延迟

detailLevel 在客户端的表现

服务器根据距离选择同步哪些属性(Ch12)。客户端也需要知道当前实体在哪个 detailLevel:

detailLevel=0(近处):全部属性都同步
  → 客户端显示完整模型、装备、buff 图标、名称...

detailLevel=1(中距离):部分属性
  → 客户端显示简化模型、名称...

detailLevel=2(远处):只有位置和朝向
  → 客户端只显示一个点/简化图标

客户端不需要显式知道 detailLevel——
服务器已经按 detailLevel 过滤了属性,
客户端只需要渲染收到的属性。

19.5 断线重连的客户端侧

KBEngine 重连流程

客户端侧看,重连不是“继续使用旧 socket”,而是重新建连后让服务端把世界状态补回来:

客户端检测到断线
  │
  ├── 1. 使用上次 onLoginSuccessfully 保存的 baseappIP_ / baseappPort_
  │
  ├── 2. 重新连接 BaseApp
  │     → 发送 BaseappInterface::reloginBaseapp
  │     → 携带 name / password / rndUUID / entityID
  │
  ├── 3a. 重连成功
  │     BaseApp 回复 onReloginBaseappSuccessfully
  │       → createClientProxies(proxy, true)
  │       → Proxy::onGetWitness()
  │       → 重新推送 onCreatedProxies / 属性 / world / space 消息
  │       → 客户端重建实体和视野
  │
  └── 3b. 重连失败
        BaseApp 回复 onReloginBaseappFailed(failedcode)
          → rndUUID 不匹配 / 实体不存在 / 会话非法

重连时客户端如何重建世界状态

重连成功后,服务器会重推完整状态:

1. onCreatedProxies(uuid, eid, entityType)
     → 重建玩家实体并拿回 baseEntityCall

2. onUpdatePropertys(stream)
     → 填充玩家实体的所有属性

3. onEntityEnterWorld(...)
     → 重建 world / cell 侧状态

4. onEntityEnterSpace(spaceID, eid, ...)
     → 玩家进入空间

5. [对视野内每个实体]
     onEntityEnterWorld → onUpdatePropertys → onEntityEnterSpace
     → 重建所有可见实体

客户端不依赖本地快照恢复世界;重连成功后的主路径是服务端重新推送当前状态。

BigWorld 的重连

BigWorld 的重连类似,但有一个额外的控制权恢复步骤:

重连成功
  │
  ├── 恢复网络连接
  ├── 服务器发送 controlEntity(id, true)
  │     → 客户端重新获得实体的控制权
  ├── 服务器推送完整世界状态
  └── 客户端恢复渲染

19.6 消息的顺序性与可靠性

Channel 语义:可靠有序,底层可切到 KCP

KBEngine 客户端最终面对的是 Channel 提供的可靠有序消息流;默认常见部署是 TCP,也可以切到底层 KCP 封装。

TCP 场景下:

  1. 可靠传输:每个包都会被确认,丢失则重传
  2. 有序到达:包按发送顺序到达
  3. 流量控制:滑动窗口控制发送速率
TCP 下的消息顺序保证:

BaseApp 发送:
  msg1: onUpdatePropertys(health=80)
  msg2: onRemoteMethodCall(onDamage, 50)
  msg3: onUpdatePropertys(health=30)

客户端收到顺序一定是 msg1 → msg2 → msg3
  → health 先变成 80,然后收到 onDamage 调用,最后 health 变成 30
  → 逻辑正确

KCP:低延迟的可靠传输实现

KBEngine 也提供 KCP 的发送/接收实现,但这里不要把它和 BigWorld Mercury 混成一类:

1. KCP 是在 UDP 之上提供“可靠、有序”交付,不是按消息级别选择可靠性。
2. KBEngine 当前客户端协议语义不因为切到 KCP 就变成“不可靠属性同步”。

KCP 的价值主要在于:
  1. 更积极的重传与 RTT 估计
  2. 避开 TCP 某些额外等待
  3. 保持业务消息的顺序交付

KBEngine KCP 集成:
  kbe/src/lib/network/kcp_packet_sender.h
  kbe/src/lib/network/kcp_packet_receiver.h

BigWorld Mercury:自建 UDP 可靠性

// 文件:BigWorld-Engine-14.4.1/programming/bigworld/lib/network/udp_channel.hpp(简化)
class UDPChannel : public Channel
{
    // 可靠性机制
    void checkResendTimers(UDPBundle& bundle);
    bool resend(SeqNum seq, UDPBundle& bundle);
    bool handleCumulativeAck(SeqNum seq);

    // 发送窗口
    int windowSize() const;
    int numOutstandingPackets() const;
};

BigWorld 的 Mercury 不是 KCP,而是自建的 UDP 可靠性层(Ch10 讲过的 ReliableType):

Mercury 可靠性级别:
  RELIABLE_NO:不保证送达(位置更新,丢了也无所谓)
  RELIABLE_DRIVER:保证送达的驱动消息
  RELIABLE_PASSENGER:随驱动消息一起可靠传输
  RELIABLE_CRITICAL:关键消息,必须可靠

TCP vs KCP vs Mercury:
  TCP:所有消息可靠有序,延迟较高
  KCP:所有消息可靠有序,延迟较低
  Mercury:按消息选择可靠性,最低延迟但实现最复杂

19.7 客户端 EntityCall:Exposed 方法的信任边界

客户端调用服务端方法

Python(客户端): entity.base.requestBuyItem(itemId=1001, count=5)
  │
  ├── EntityCall::tp_call()
  │     → newCall(bundle)
  │     → bundle << BaseappInterface::onRemoteMethodCall
  │     → bundle << entityID << ENTITYCALL_TYPE_BASE
  │     → bundle << methodUtype
  │     → 序列化参数 (itemId=1001, count=5)
  │
  ├── 通过 Channel 发送到 BaseApp
  │
  └── BaseApp 收到 onRemoteMethodCall
        → Entity::onRemoteMethodCall(channel, stream)
        → 检查方法是否标记为 Exposed
        → 如果不是 Exposed → 拒绝(安全边界)
        → 如果是 Exposed → 执行方法

Exposed 方法的信任边界

// 文件:kbe/src/lib/entitydef/method.h(简化)
class MethodDescription
{
    enum EXPOSED_TYPE
    {
        NO_EXPOSED,                  // 客户端不可调用
        EXPOSED,                     // 客户端可调用
        EXPOSED_AND_CALLER_CHECK     // 客户端可调用 + 传入调用者 EntityID
    };
};
信任边界示意图:

客户端                    服务器
  │                        │
  │  只有 Exposed 方法     │
  │  可以被调用 ────────→  │  验证:方法是否 Exposed?
  │                        │  验证:调用者身份(Channel 绑定)
  │                        │
  │  非 Exposed 方法       │  拒绝!
  │  无法调用 ────────→    │  → condemn channel
  │                        │
  │  Exposed + CallerCheck │  额外验证:调用者 EntityID
  │  ───────────────────→  │  → 脚本层可以检查 callerID 是否合法

安全模型:

  1. 方法级白名单:只有 Exposed 标记的方法可以被客户端调用
  2. 身份绑定:Channel 和 Entity 绑定——客户端只能调用自己的 base/cell 方法
  3. CallerCheck:脚本层额外验证调用者身份(防止客户端伪造)
  4. 消息 ID 隐藏:非 Exposed 方法的消息 ID 不暴露给客户端
攻击场景:
  恶意客户端发送 onRemoteMethodCall(entityID=其他玩家ID, method=heal, args=100)
    │
    └── 服务器检查:
          1. Channel 绑定的 EntityID ≠ 请求中的 entityID → 拒绝
          2. 或者 method 不是 Exposed → 拒绝
          3. 或者 entityID 根本不存在 → 忽略

客户端→Cell 的间接调用

客户端不能直接与 CellApp 通信!

entity.cell.onMove(x, y, z)
  │
  ├── 客户端发送到 BaseApp
  │     BaseappInterface::onRemoteCallCellMethodFromClient
  │     bundle << entityID << methodUtype << args
  │
  ├── BaseApp 收到后转发到 CellApp
  │     CellappInterface::onEntityCall
  │     bundle << entityID << ENTITYCALL_TYPE_CELL << methodUtype << args
  │
  └── CellApp 收到后执行方法
        Entity::onRemoteMethodCall(channel, stream)

间接调用的原因:
  1. 安全:BaseApp 作为中间层可以验证客户端身份
  2. 简化:客户端只需要与 BaseApp 建立一个连接
  3. 灵活:BaseApp 可以在转发前/后添加逻辑

19.8 两套项目的客户端协议对比

维度KBEngineBigWorld
传输协议TCP(默认)/ KCP(可选)UDP(Mercury 自建可靠性)
消息定义ClientInterface 宏ClientInterface + MF_BEGIN_CLIENT_MSG
实体创建onCreatedProxies / onEntityEnterWorld → createEntityenterAoI → createEntity → onEnterWorld
实体销毁onEntityLeaveWorld / onEntityDestroyedleaveAoI → onLeaveWorld → onDestroyed
属性更新onUpdatePropertys(流式)entityProperty(按属性)
方法调用onRemoteMethodCall(流式)entityMethod(按方法)
控制权controlledBy(脚本层)controlEntity(协议层显式通知)
重连方式onReloginBaseappSuccessfully → 全量重推类似
客户端调用服务端Exposed 方法 + Channel 身份验证Exposed 标签 + ExposedMessageRange
位置插值脚本层实现Velocity-based 预测插值
ID 别名aliasID(属性 ID 压缩)IDAlias(实体 ID 压缩)
可靠性选择全可靠(TCP/KCP)按消息选择(ReliableType 四级)

核心差异:

  1. 传输层:KBEngine 默认 TCP(全可靠),BigWorld 用 UDP + 按消息可靠性选择。BigWorld 的方案延迟更低但实现更复杂

  2. 实体生命周期粒度:KBEngine 同时有 onEntityEnterWorld / onEntityLeaveWorld / onEntityDestroyed 三层消息;其中 onEntityDestroyed 更像补偿路径。BigWorld 也明确区分 AOI 进入离开和实体最终销毁

  3. 控制权协议:BigWorld 有显式的 controlEntity 消息通知客户端获得/失去控制权。KBEngine 在脚本层处理

  4. 属性更新粒度:KBEngine 用流式 onUpdatePropertys(一次消息可以包含多个属性)。BigWorld 用 entityProperty(一次消息一个属性)

19.9 关键源码入口

KBEngine

概念文件
ClientInterfacekbe/src/lib/client_lib/client_interface.h
ClientAppkbe/src/lib/client_lib/clientapp.h
客户端 Entitykbe/src/lib/client_lib/entity.h
ClientObjectBasekbe/src/lib/client_lib/clientobjectbase.h
onCreatedProxieskbe/src/lib/client_lib/clientobjectbase.cpp
onUpdatePropertyskbe/src/lib/client_lib/entity.cpp
onEntityDestroyedkbe/src/lib/client_lib/clientobjectbase.cpp
KCP 集成kbe/src/lib/network/kcp_packet_sender.h

BigWorld

概念文件
ClientInterfaceBigWorld-Engine-14.4.1/programming/bigworld/lib/connection/client_interface.hpp
客户端 EntityBigWorld-Engine-14.4.1/programming/bigworld/client/entity.hpp
PyEntityBigWorld-Engine-14.4.1/programming/bigworld/client/py_entity.hpp
EntityManagerBigWorld-Engine-14.4.1/programming/bigworld/client/entity_manager.hpp
UDPChannelBigWorld-Engine-14.4.1/programming/bigworld/lib/network/udp_channel.hpp
ExposedMessageRangeBigWorld-Engine-14.4.1/programming/bigworld/lib/network/exposed_message_range.hpp

19.10 源码走读路径

路径一:跟踪客户端实体的完整生命周期

  1. kbe/src/lib/client_lib/client_interface.h — ClientInterface 消息定义
  2. kbe/src/lib/client_lib/clientobjectbase.cpp — onCreatedProxies / onEntityEnterWorld / onEntityLeaveWorld / onEntityDestroyed
  3. kbe/src/lib/client_lib/entity.cpp — onUpdatePropertys / onRemoteMethodCall

路径二:对比 BigWorld 的 AOI 进入/离开

  1. BigWorld-Engine-14.4.1/programming/bigworld/lib/connection/client_interface.hpp — enterAoI / leaveAoI
  2. BigWorld-Engine-14.4.1/programming/bigworld/client/entity.cpp — onEnterWorld / onLeaveWorld
  3. BigWorld-Engine-14.4.1/programming/bigworld/client/entity_manager.hpp — EntityManager

路径三:理解客户端→服务端的 Exposed 调用

  1. kbe/src/lib/client_lib/client_interface.h — onRemoteMethodCall 消息
  2. kbe/src/server/baseapp/proxy.h — 客户端消息接收和验证
  3. kbe/src/lib/entitydef/method.h — EXPOSED_TYPE 枚举

19.11 小结

  • ClientInterface 定义了客户端收发的所有消息:创建/销毁/进入空间/属性更新/方法调用/重连
  • 实体生命周期主线是 onCreatedProxies / onEntityEnterWorld / onEntityEnterSpace / 更新 / onEntityLeaveWorld / onEntityLeaveSpace:onEntityDestroyed 更多是补偿性销毁路径
  • 客户端属性更新是直接覆盖:收到新值立即应用到 Entity,不做 dirty 标记
  • 位置插值在客户端脚本层实现:服务器每 tick 发一次位置,客户端在两次更新之间插值平滑
  • detailLevel 在客户端是隐式的:服务器已按距离过滤了属性,客户端只需渲染收到的属性
  • 断线重连通过全量重推恢复世界状态:服务器重新发送所有实体的创建和属性数据
  • TCP 天然可靠有序,KCP 延迟更低:KBEngine 两种都支持,BigWorld 用自建 Mercury UDP 可靠性
  • Exposed 方法是客户端→服务端的信任边界:只有标记为 Exposed 的方法才能被客户端调用
  • 客户端不直接与 CellApp 通信:通过 BaseApp 中转,BaseApp 负责身份验证和消息路由
  • BigWorld 有显式的 controlEntity 协议:通知客户端获得/失去实体控制权