Skip to content

Python 逻辑层常见错误 ​

本页面向编写 Assets、EntityDef、Base/Cell 脚本和客户端协议的开发者。重点解决“脚本为什么没有执行”“数据为什么没有同步”“Entity 为什么处于错误状态”,不展开 C++ 编译、vcpkg、CI 和操作系统内核问题。

先学会看日志 ​

逻辑层问题建议按以下顺序排查:

  1. 找到第一条 Python traceback 或 EntityDef ERROR,不要只看最后一条连锁错误;
  2. 确认错误来自 BaseApp、CellApp、DBMgr、LoginApp、Interfaces 还是 Bots;
  3. 记录 Entity 类型、Entity ID、Space ID、方法名和当前生命周期状态;
  4. 对照 .def 检查属性、RPC 参数和脚本实现;
  5. 判断错误发生在本地调用、序列化、远程传输还是目标回调;
  6. 最后才考虑引擎底层问题。

一条有用的业务日志应包含上下文:

python
ERROR_MSG(
    "Avatar::enterSpace failed: entityID=%i, spaceID=%i, state=%s" %
    (self.id, spaceID, self.state)
)

不要在公共入口使用下面这种写法:

python
try:
    doSomething()
except Exception:
    pass

它会吞掉 traceback,使最初的类型或生命周期错误在更远的位置变成空引用、协议错误或状态不一致。确实需要补充上下文时,应记录后重新抛出:

python
try:
    self.inventory.addItem(itemData)
except Exception:
    ERROR_MSG(
        "Avatar::addItem failed: entityID=%i, itemData=%r" %
        (self.id, itemData)
    )
    raise

EntityDef 与脚本加载 ​

启动时报 EntityDef 解析错误 ​

现象

DBMgr、BaseApp 或 CellApp 启动失败,日志包含 .def、entities.xml、types.xml、unknown type、duplicate utype 或 XML parse error。

常见原因

  1. XML 标签没有闭合、大小写错误或层级错误;
  2. 属性或 RPC 使用了未定义的数据类型;
  3. Entity 没有加入 entities.xml,或名称与文件名不一致;
  4. Interface、EntityComponent 或插件定义的路径/名称错误;
  5. 显式 UType 重复;
  6. 同一个类型别名被宿主 Assets 或插件重复声明。

验证方式

  • 从日志中找到第一个出现的文件名、Entity 名和字段名;
  • 临时只保留本次新增的一项定义,确认错误是否随它消失;
  • 检查 .def 引用的类型是否已在 types.xml 或插件类型文件中声明;
  • 检查 Entity 名、.def 文件名和 Python 类名是否完全一致;
  • 不要只检查最后一个报错,前一个定义失败会导致后续大量 unknown type。

修复方案

修正第一处定义错误后完整重启服务。EntityDef 属于协议与类结构,不应依赖普通脚本热更新生效。若变更涉及客户端可见定义,还需要重新生成客户端 SDK。

详见 Entity 和 EntityComponent。

ModuleNotFoundError 或找不到 Entity Python 类 ​

现象

引擎已经读取 .def,但加载脚本时报模块不存在、类不存在,或 Entity 类型初始化失败。

常见原因

  1. 脚本没有放在对应组件的搜索路径中;
  2. Base 脚本放到了 Cell 目录,或反之;
  3. 文件名、类名和 Entity 名大小写不一致;
  4. 模块导入过程中先发生了另一个 Python 异常;
  5. 循环导入导致类尚未完成定义;
  6. 实际启动的是另一套 Assets。

验证方式

查看 traceback 最底部的原始异常。ModuleNotFoundError 有时只是上层表现,真正原因可能是模块内部导入失败。确认启动输出中的 KBE_RES_PATH,并在日志中打印目标模块的 __file__ 验证实际加载位置。

修复方案

让模块职责与组件目录一致,保持 Entity 名、文件名和类名一致。公共代码放入公共脚本路径,避免 Base 与 Cell 模块互相导入实现类;共享常量和纯函数应下沉到无组件依赖的模块。

修改 .def 后热更新没有生效 ​

现象

新增属性或 RPC 后执行 KBEngine.reloadScript(),现有 Entity 仍没有该成员,客户端协议也没有变化。

原因

普通热更新重载 Python 逻辑,不会重新建立 EntityDef 协议表,也不会重建已经存在的 Entity 实例。

修复方案

以下变化应重启相关服务:

  • 属性的新增、删除、类型、Flags 或持久化设置;
  • BaseMethods、CellMethods、ClientMethods 的签名;
  • EntityComponent、Interface、types.xml 或插件清单;
  • 进程入口和模块级初始化结构。

客户端可见协议变化还应重新生成 SDK。仅修改普通函数实现时才优先使用热更新。详见 服务端热更新。

属性与数据类型 ​

属性赋值时报 TypeError、ValueError 或 can't set ​

现象

给 Entity 或 EntityComponent 属性赋值时抛出异常,或者在 RPC/写库阶段出现 addToStream: pyValue(...) is wrong。

常见原因

  1. Python 值与 EntityDef 类型不一致;
  2. 整数超出 UINT8、INT32 等类型范围;
  3. STRING、UNICODE、BLOB 与 str/bytes 混用;
  4. VECTOR2/3/4 的长度或元素类型错误;
  5. ARRAY 元素类型错误;
  6. FIXED_DICT 缺少字段、字段名错误或嵌套字段类型不匹配;
  7. 把 None 赋给不接受空值的协议类型。

验证方式

从最外层属性或 RPC 参数开始,记录期望类型和实际 type(value).__name__。复杂结构逐层检查,不要只打印一个很大的 dict:

python
def validateAvatarInfo(info):
    assert isinstance(info, dict), type(info).__name__
    assert isinstance(info.get("dbid"), int), type(info.get("dbid")).__name__
    assert isinstance(info.get("name"), str), type(info.get("name")).__name__

修复方案

在数据进入 Entity 属性或 RPC 边界前完成转换和校验。不要依赖底层把错误值自动转换成默认值;底层异常是协议契约被违反的信号,应修正调用方。

RPC 报参数数量或类型错误,但不知道是哪一个参数 ​

现象

调用 EntityCall 时抛出 TypeError,日志提示方法参数不匹配或序列化失败。

验证方式

按下面的顺序对照:

  1. .def 中方法所属区域是 BaseMethods、CellMethods 还是 ClientMethods;
  2. 参数数量和顺序;
  3. 每个 <Arg> 对应的 Python 类型;
  4. 嵌套 ARRAY/FIXED_DICT 的具体字段;
  5. 当前调用使用的是 entity.base、entity.cell、entity.client 还是其他 EntityCall;
  6. 客户端是否仍使用旧 SDK。

建议在业务入口保留方法名、Entity ID 和参数类型:

python
DEBUG_MSG(
    "Avatar::reqUseItem: entityID=%i, itemID=%r(%s), count=%r(%s)" %
    (self.id, itemID, type(itemID).__name__, count, type(count).__name__)
)

修复方案

以 .def 为唯一协议来源修正调用方。不要在接收方法中通过 *args 猜测参数,也不要为了绕过错误随意放宽协议类型。

修改容器后客户端或数据库没有得到预期结果 ​

现象

修改 ARRAY、FIXED_DICT 或 EntityComponent 内部内容后,本地对象看起来变化了,但客户端同步、持久化或属性回调没有按预期发生。

常见原因

  1. 修改的是普通 Python 临时副本,而不是 Entity 属性;
  2. 深层原地修改没有经过项目预期的属性更新路径;
  3. 属性 Flags 不包含目标组件或客户端;
  4. 业务修改后没有在预期时机写库;
  5. 客户端看到的是 DetailLevel、AOI 或初始化同步后的另一份状态。

验证方式

记录修改前后的 Entity 属性值、对象身份、属性回调和写库时机。先用“构造完整新值后重新赋给属性”的方式验证是否属于原地变更通知问题。

修复方案

对需要同步或持久化的复杂属性,优先通过明确的领域方法修改,并在方法末尾形成一次清晰的属性更新。避免让外部代码任意持有并修改深层容器,这也有助于校验权限和维护不变量。

属性在 Base、Cell 或客户端访问不到 ​

现象

同名属性在某个组件存在,在另一个组件访问时报 AttributeError,或客户端始终收不到。

原因

属性可见范围由 EntityDef Flags 决定。Base、Cell、Client、其他客户端和持久化是不同语义,不能因为同一个 Entity 在多个位置存在,就假设所有属性都会自动复制。

验证与修复

检查属性 Flags 和当前代码运行组件。只把真正需要的数据暴露到 Cell 或 Client;敏感服务端状态不要为了方便访问而扩大同步范围。涉及客户端的修改需要重启并重新生成 SDK。

RPC 与 EntityCall ​

客户端调用 Base/Cell 方法没有反应 ​

现象

客户端发起 RPC 后服务端方法没有执行,也没有业务结果。

常见原因

  1. 方法未在 .def 中声明 Exposed;
  2. 客户端调用了错误的 Base/Cell EntityCall;
  3. Cell 尚未创建或玩家尚未进入 Space;
  4. 客户端 SDK 过期;
  5. 服务端方法签名与 .def 不一致;
  6. 请求被服务端的调用者、会话或频率校验拒绝;
  7. 方法内部立即抛出异常,但只检查了客户端日志。

验证方式

在方法入口记录日志。如果入口日志没有出现,检查协议定义、EntityCall 和生命周期;如果入口出现但没有结果,继续查看完整 Python traceback 和业务分支。

修复方案

只对客户端确实需要调用的方法增加 Exposed,并在服务端重新校验权限、目标归属、状态和参数。Exposed 表示网络可达,不表示调用可信。

self.cell、self.base 或 self.client 是 None ​

现象

调用 self.cell.xxx()、self.base.xxx() 或 self.client.xxx() 时出现空引用或不可用错误。

常见原因

  • Cell 还没有创建完成,Base 就调用了 self.cell;
  • Cell 已销毁或正在迁移,旧逻辑仍继续调用;
  • Entity 没有客户端,或客户端已经断线;
  • 当前 Entity 类型本身不具备对应部分;
  • 异步回调返回时生命周期已经变化。

验证方式

记录 onGetCell、onLoseCell、客户端获得/失去、Entity 销毁和异步请求开始/结束的时间顺序。不要只在调用前打印一次布尔值,因为远程操作期间状态仍可能变化。

修复方案

将操作绑定到明确状态机:

  • 只有 onGetCell 后才发送依赖 Cell 的请求;
  • onLoseCell 后清理等待中的空间操作;
  • 调用客户端前确认当前存在有效客户端;
  • 异步回调中重新检查 Entity 是否仍有效、请求是否仍属于当前状态版本。

EntityCall 保存后调用失效 ​

现象

缓存的远程 EntityCall 一开始可用,稍后调用没有结果或目标已不存在。

原因

EntityCall 指向远程 Entity 的某一部分。Entity 销毁、Cell 销毁、迁移、组件退出或客户端断线后,旧引用可能失效。

修复方案

不要把 EntityCall 当作永久本地对象。长期关系优先保存稳定业务标识,需要调用时重新定位;关键请求增加 request ID、超时、幂等和结果确认。清理 Entity 时同步移除订阅、等待队列和缓存引用。

详见 EntityCall。

服务端调用 ClientMethods,客户端没有收到 ​

常见原因

  1. Entity 当前没有客户端;
  2. 客户端尚未创建对应 Entity,或已经离开 View;
  3. 调用的是错误 Entity 的 client;
  4. SDK 过期或客户端实现类没有对应方法;
  5. 客户端事件系统暂停了 Out 事件;
  6. 参数序列化失败;
  7. 网络已断开但服务端断线状态尚在收敛。

验证方式

依次检查服务端调用前状态、服务端网络日志、客户端 SDK 收包日志和游戏层事件日志。必须区分“服务器没有发送”“SDK 收到但排队”“游戏层没有注册处理”。

修复方案

把重要状态保存在客户端可恢复的 Entity 属性或明确请求/响应流程中,不要假设一次 ClientMethods 调用必然被游戏层立即处理。场景加载期间暂停事件时,还要控制恢复后的事件有效期和消费量。

Entity 生命周期 ​

createCellEntity 后立即使用 Cell 失败 ​

现象

Base 调用 createCellEntity 后下一行立即访问 self.cell,结果为空或 RPC 没有发送。

原因

创建 Cell 是跨进程异步流程。函数调用返回只表示请求已发起,不表示 Cell 已创建完成。

修复方案

把依赖 Cell 的逻辑放到 onGetCell 之后,或在 Base 上维护等待队列:

python
def enterSpace(self, spaceCell):
    self._pendingEnter = True
    self.createCellEntity(spaceCell)

def onGetCell(self):
    if self._pendingEnter:
        self._pendingEnter = False
        self.cell.onEnterReady()

同时处理创建失败、重复请求、玩家断线和目标 Space 已销毁的情况。

destroyCellEntity 后旧回调改变了新状态 ​

现象

玩家快速切换 Space、重连或连续发起进入请求时,旧的创建/销毁回调覆盖了新的状态,出现进入错误场景、重复创建或空 Cell。

原因

跨组件操作异步完成,回调顺序不一定等同于业务请求顺序。

修复方案

为每次空间切换生成递增的操作版本或 request ID。回调执行时比较当前版本,只允许最新请求推进状态;旧回调只做必要清理,不得覆盖新状态。

Entity 已销毁,Timer、协程或数据库回调仍访问它 ​

现象

onDestroy 后出现属性访问错误、重复保存、重复发奖,或异步回调修改了已无效对象。

常见来源

  • 重复 Timer 未删除;
  • asyncio Task 未取消;
  • 数据库或 Interfaces 回调迟到;
  • 全局管理器仍保存 Entity 引用;
  • 事件订阅未解除;
  • 旧状态的 RPC 回调晚于新状态到达。

修复方案

把异步资源的所有权放在创建它的 Entity 或组件中,并在销毁/离开状态时统一释放:

  • 保存 Timer ID 并删除重复 Timer;
  • 保存 Task,销毁时取消;
  • 回调携带 request ID,并重新检查当前状态;
  • 全局集合在 onDestroy/onLoseCell 中移除成员;
  • 清理逻辑保持幂等,可被调用多次而不产生副作用。

EntityComponent 的回调没有触发 ​

现象

组件实现了 onTimer、Cell 事件或生命周期回调,但运行时没有收到。

常见原因

  1. 组件实例没有真正挂到当前 Entity;
  2. 回调实现放错 Base/Cell 目录;
  3. 组件定义、属性名和脚本类映射不一致;
  4. 订阅发生在旧 Cell,迁移或重建后没有恢复;
  5. 热更新只改变类代码,但旧实例/订阅状态没有重建;
  6. 回调名称或签名写错。

验证方式

在组件构造、附加、Cell 获取/丢失、迁移和销毁位置记录 owner ID 与组件实例。确认事件订阅跟随当前 Cell 生命周期,而不是只在首次创建时执行一次。

修复方案

让组件在明确的附加/获得 Cell 阶段建立订阅,在丢失 Cell/销毁阶段解除,并保证迁移后能够重新建立。不要依赖 owner Entity 实现一个无业务意义的同名空方法来“唤醒”组件回调。

Timer 与异步任务 ​

Timer 重复触发或无法删除 ​

现象

同一业务动作执行多次,调用 delTimer 报无效 ID,或热更新后出现多个周期任务。

常见原因

  1. 每次进入状态都新增 Timer,却没有先删除旧 Timer;
  2. 一次性 Timer 回调后仍使用旧 ID 删除;
  3. 保存了错误 Entity/组件创建的 Timer ID;
  4. 状态切换或销毁时没有清理;
  5. 热更新重新执行初始化逻辑并重复注册。

修复方案

一个业务职责只保留一个 Timer 所有者和一个 ID。删除后立即清零,回调中先验证 ID 和当前状态:

python
def startThink(self):
    self.stopThink()
    self._thinkTimerID = self.addTimer(0.2, 0.2, TIMER_THINK)

def stopThink(self):
    if self._thinkTimerID:
        self.delTimer(self._thinkTimerID)
        self._thinkTimerID = 0

def onTimer(self, timerID, userArg):
    if timerID != self._thinkTimerID or userArg != TIMER_THINK:
        return
    if self.state != STATE_ACTIVE:
        self.stopThink()
        return
    self.think()

Timer 不应作为唯一的保存保证或关服状态推进机制。详见 计时器。

asyncio 协程让 BaseApp/CellApp 卡顿 ​

现象

使用 async/await 后仍出现慢 Tick,多个请求同时完成时尤其明显。

原因

协程只在等待期间让出执行权,恢复后的 Python 代码仍运行在组件调度线程。CPU 密集循环、同步 IO、大结果解析和同一 Tick 唤醒大量任务都会阻塞逻辑线程。

修复方案

  • 使用真正异步的网络客户端;
  • 限制并发任务数和每 Tick 消费量;
  • 分批处理大型结果;
  • 不在协程中调用同步文件、HTTP 或数据库库;
  • Entity 销毁时取消其任务;
  • 用慢 Tick 和 Python 回调耗时区分“等待慢”与“执行慢”。

详见 asyncio 支持。

热更新后旧对象仍执行旧逻辑 ​

现象

新创建 Entity 使用新代码,部分旧 Entity、Timer 回调、闭包或缓存对象仍表现为旧行为。

原因

热更新替换模块与类方法,并不会自动重建所有实例状态,也不会替换已经保存的函数对象、闭包、Task 或第三方注册回调。

修复方案

把热更新设计为显式迁移:更新版本字段、重建需要的订阅/策略对象,并清理旧 Timer 和回调。无法安全迁移的结构性修改应重启,不要把热更新当作无状态重新加载。

数据库与持久化 ​

Persistent 属性没有保存 ​

现象

运行时属性已经改变,但重新登录或从数据库创建 Entity 后恢复成旧值。

常见原因

  1. 属性没有设置 Persistent;
  2. 修改发生在 Cell 临时状态,没有同步到负责保存的数据;
  3. 没有调用写库接口,或预期的自动保存尚未发生;
  4. Entity 没有有效数据库 ID;
  5. 写库回调失败但被忽略;
  6. 关服或销毁流程早于异步写库完成;
  7. 读写使用了不同数据库接口。

验证方式

记录 Entity ID、数据库 ID、保存原因、请求时间、回调结果和数据库接口名。直接查询数据库确认“没有写入”还是“读取了另一条记录”。

修复方案

由 Base 维护需要长期保存的权威状态,在明确业务节点调用写库并处理结果。高价值操作需要保存状态机、幂等标识和失败重试,不能只在 onDestroy 或某个 Timer 中碰运气保存。

writeToDB 回调没有执行或执行太晚 ​

常见原因

  1. 数据库请求失败或排队;
  2. Entity 在回调前已销毁;
  3. 回调对象或签名不正确;
  4. 大量 Entity 同时保存造成积压;
  5. 回调中再次触发保存,形成保存风暴;
  6. 业务只检查 BaseApp 日志,没有检查 DBMgr 的第一处错误。

修复方案

为保存请求记录 request ID 与开始时间,限制同一 Entity 的并发写入,合并重复保存,并为关键操作设计超时后的状态处理。异步写库意味着“不阻塞等待数据库”,不意味着“没有队列、不会失败或一定在关服前完成”。

createEntityFromDBID 返回空或重复创建 ​

现象

从数据库加载 Entity 时回调得到空对象,或同一个数据库 ID 在多个流程中被重复创建。

常见原因

  1. Entity 类型或数据库接口名错误;
  2. 数据库 ID 不存在或为 0;
  3. 该数据库 Entity 已经处于激活状态;
  4. 登录、重连和角色选择并发发起加载;
  5. 回调到达时账号状态已经变化;
  6. 把 Runtime Entity ID 与数据库 ID 混用。

修复方案

明确区分 entity.id 与数据库 ID。以账号或角色为粒度建立“未加载、加载中、已激活、卸载中”状态,加载中拒绝重复请求;回调必须校验请求版本和当前会话。

原生数据库回调阻塞逻辑线程 ​

现象

数据库请求本身异步,但回调返回大量数据后 BaseApp/CellApp 出现慢 Tick。

原因

结果回调、反序列化和后续 Python 处理仍在逻辑线程执行。一次返回过多行或在回调中进行复杂聚合会造成长帧。

修复方案

限制查询结果大小,分页或分批消费,把报表和重分析任务放到外部服务。原生命令入口还应校验权限与参数,不允许客户端直接拼接 SQL 或数据库命令。

AOI、Space 与客户端同步 ​

Entity 已进入 Space,但客户端看不到 ​

常见原因

  1. 玩家 Cell 尚未获得 Witness;
  2. 两个 Entity 不在同一 Space;
  3. 距离超出 View 范围;
  4. 目标 Entity 没有 Client 部分或没有客户端可见属性;
  5. Entity 尚未进入玩家 View,服务端就调用了其 ClientMethods;
  6. 位置、坐标轴或 layer 数据错误;
  7. 客户端没有实现生成的 Entity 类或创建事件处理。

验证方式

同时记录双方 Entity ID、Space ID、position、Witness 状态以及进入/离开 View 回调。客户端侧确认“未收到创建消息”还是“已创建但没有渲染”。

修复方案

先让 Space、Witness 和 View 关系正确,再检查客户端表现。不要用全局广播补偿 AOI 配置错误,否则会显著增加网络包量并泄露本不应可见的状态。

属性在服务端改变,客户端没有更新 ​

常见原因

  1. 属性 Flags 不包含 Client 或对应可见范围;
  2. Entity 尚未进入该客户端 View;
  3. 修改的是 Base 属性,但客户端同步定义在 Cell 可见路径;
  4. 修改了临时对象或深层容器,没有形成预期属性更新;
  5. DetailLevel/易变属性策略降低了同步频率或精度;
  6. 客户端 SDK 过期;
  7. 客户端收到更新但游戏层事件暂停或覆盖了值。

验证方式

从服务端属性赋值、Witness 打包、客户端 SDK 收包到游戏层 setter/事件逐段记录。不要只根据画面判断网络是否同步。

修复方案

按实际可见性设置 Flags,并控制更新频率。位置、朝向等高频属性应使用易变属性和 DetailLevel 策略;低频关键状态应通过明确属性或 RPC 同步。

View 范围增大后 CellApp 明显变慢 ​

原因

AOI 成本取决于每个 Witness 可见的 Entity 数量和变化频率。扩大 View 半径会同时增加空间关系、进入/离开事件、属性同步和网络包量;密集场景中增长可能远高于半径本身的比例。

修复方案

  • 先统计单个 Witness 平均/峰值可见 Entity 数;
  • 减少不需要客户端知道的 Entity 和属性;
  • 降低高频属性同步精度或频率;
  • 按玩法拆分 Space、区域或对象层级;
  • 使用真实移动和战斗行为压测 Cell Tick、AOI 与包量。

不要仅通过提高 Tick 预算掩盖可见关系设计问题。

导航与移动 ​

常见原因

  1. navigatePathPoints 在 maxSearchDistance 内找不到目标导航点;
  2. 起点不在 navmesh;
  3. 目标与起点不连通;
  4. layer 错误;
  5. 目标坐标或 Y 轴来自另一坐标系;
  6. 当前 Entity 已销毁、没有 Cell 或状态禁止移动。

验证方式

记录起点、目标、layer、maxSearchDistance、maxMoveDistance 和返回的路径点。分别验证“目标能否投影到网格”和“起点到目标是否存在完整路径”。

修复方案

maxSearchDistance 只控制目标附近寻找可用导航点的范围,maxMoveDistance 控制已有路径最多移动多远。增大搜索距离不能修复断裂的 navmesh,也不应把目标投影到与玩家意图无关的远处区域。

导航在转角停顿或实体移动异常 ​

常见原因

  1. 业务短时间重复发起导航,持续取消旧控制器;
  2. 旧导航完成回调覆盖了新 AI 状态;
  3. navmesh 转角、Agent 半径、坡度或间隙不匹配;
  4. 业务手动调用内部单步推进接口制造首帧移动;
  5. 服务端导航高度、客户端地形贴合和客户端插值互相覆盖;
  6. maxMoveDistance 提前结束移动;
  7. layer 或目标点映射错误。

验证方式

  • 暂停 AI 重入,只执行一次导航;
  • 记录控制器 ID、创建、取消、完成和回调顺序;
  • 输出完整路径点和每个转角;
  • 对比服务端位置、客户端收到的位置和最终渲染位置;
  • 在导航工具中检查相同 Agent 参数下的连通性。

修复方案

一个 AI 移动状态只拥有一个有效控制器。新请求应明确接管旧请求,并用操作版本忽略过期回调。控制器由 CellApp Tick 统一推进,不应从 Python 逻辑层手动推进首帧。

详见 导航网格。

导航完成后 Y 轴上升或跳变 ​

原因

导航期间服务端位置可能按 navmesh 高度同步,结束后客户端又恢复地形、物理或自身插值高度。如果过早清理用于保持贴地的导航同步状态,两套高度来源会发生切换。

验证方式

分别记录服务端 Y、网络同步 Y、客户端 Entity Y、地形采样 Y 和渲染对象 Y,确认是哪一层首次改变。

修复方案

统一高度权威来源和状态切换时机。不要为了让 isOnNavigate 表面闭合而清理仍承担贴地语义的底层状态;也不要在客户端无条件用地形高度覆盖服务端位置。

移动超速回调频繁触发 ​

常见原因

  1. 客户端帧卡顿后一次发送过大位移;
  2. 客户端预测、服务器校正和重发叠加;
  3. 传送或服务端强制移动没有进入正确状态;
  4. 速度、单位或 Tick 时间理解不一致;
  5. 玩家确实发送了非法移动。

修复方案

记录客户端位置、服务端位置、时间差、单包距离、累计距离和当前移动状态。传送、击退、载具等合法例外应由明确状态授权,不要简单提高全局速度阈值。对异常客户端仍应由服务端权威校验和校正。

客户端协议与 SDK ​

客户端提示 EntityDef MD5 不一致 ​

常见原因

  1. 修改了客户端可见属性、方法、类型或 UType 后没有生成 SDK;
  2. 客户端工程仍打包旧生成文件;
  3. SDK 和服务器使用了不同 Assets 或插件清单;
  4. 连接到了错误的测试/生产环境;
  5. 插件 EntityDef 的客户端协议发生变化。

修复方案

使用目标服务器同一套 KBE_RES_PATH、Assets 和插件配置重新生成 SDK,清理客户端构建缓存后重新打包。不要关闭摘要校验或手工修改摘要常量。

只修改未暴露给客户端的 Base/Cell RPC 或内部属性,不应改变客户端摘要。如果确实发生变化,应比较变更前后的生成文件和最小 EntityDef diff,作为引擎回归问题排查。

详见 SDK 自动生成。

客户端 Entity 已生成,但业务类没有回调 ​

常见原因

  1. 实现类名称与生成基类约定不一致;
  2. 没有继承正确的生成基类;
  3. ClientMethods 签名与生成代码不同;
  4. 游戏层事件没有注册或已取消注册;
  5. Out 事件处于暂停队列;
  6. 客户端存在两套 SDK,运行时加载了旧类;
  7. Entity 已离开 View,回调属于旧对象。

验证方式

按“网络收包 → SDK 消息分发 → Entity 查找 → 生成基类方法 → 游戏层事件”逐层加日志。确认 Entity ID 与实例生命周期一致,避免旧对象和新对象同名造成误判。

启动时最常见的逻辑层问题 ​

服务端一直显示 finding dbmgr ​

这通常不是当前 BaseApp/CellApp 的逻辑错误,而是 DBMgr 没有完成初始化。逻辑开发者优先检查 DBMgr 日志中更早出现的:

  • EntityDef/XML 解析错误;
  • Entity Python 模块导入异常;
  • types.xml 或插件类型冲突;
  • 数据库账号、数据库名或连接失败;
  • 数据模型同步失败。

先修复 DBMgr 的第一条错误,等待日志会随之消失。若 DBMgr 本身正常但组件仍无法发现,再交给部署或引擎维护人员检查 Machine、网卡和进程配置。

修改代码后日志完全没有变化 ​

常见原因

  1. 修改了错误 Assets 下的同名文件;
  2. 当前组件没有重载该模块;
  3. 修改的是 Base 脚本,但观察 CellApp 日志,或反之;
  4. Logger 过滤了当前日志级别;
  5. 代码路径根本没有被调用;
  6. 旧进程没有退出,客户端连接的仍是旧服务。

验证方式

确认启动输出中的资源路径,在模块加载或目标入口打印包含文件路径、组件和版本号的临时调试日志。定位完成后移除高频日志,避免在 Tick、AOI 或 RPC 热路径持续输出。

提交逻辑层问题时需要提供什么 ​

为了让问题可以复现,请提供:

  • 引擎 commit 或版本;
  • 出错组件名称;
  • 完整 Python traceback 和它之前的第一条 ERROR;
  • Entity 类型、Entity ID、数据库 ID、Space ID;
  • 相关 .def、types.xml、Python 方法和调用方;
  • Base/Cell/Client 中哪一侧发起、哪一侧应该接收;
  • 生命周期顺序,例如创建 Base、创建 Cell、进入 Space、断线、销毁;
  • 稳定复现步骤、期望结果和实际结果;
  • 若涉及客户端,提供 SDK 类型、生成时间和协议摘要。

提交日志前移除密码、management token、私钥和账号凭据,但保留方法名、参数类型、Entity 上下文和时间顺序。