KBEngine → KBEngine Nex
一次只做一步的 Assets 迁移向导
先生成 Nex 骨架,再展开旧业务脚本。启动脚本、资源路径、Python 3.13、completion IO、NavMesh 和客户端 SDK 分开处理。
原版:业务代码藏在 scripts
assets/ ├─ scripts/ │ ├─ base/ │ ├─ cell/ │ ├─ entity_defs/ │ └─ entities.xml ├─ res/ └─ start_server.bat
Nex:业务代码提升到根目录
assets/ ├─ base/ ├─ cell/ ├─ entity_defs/ ├─ entities.xml ├─ res/ └─ scripts/ └─ start_server.bat
备份与隔离
先建立可回退边界,不在旧项目上原地覆盖。
- 生产环境使用安全关闭流程,保留首个异常前的完整日志。
- 备份旧 Assets、数据库、客户端 SDK、配置和引擎 Commit。
- 旧 Assets 迁移期间保持只读,不在其中清理或重构。
- 第一次启动 Nex 使用新测试库;随后再用生产库副本演练。
生成 Nex 新骨架
让目标版本自己给出正确目录和启动脚本。
new_assets.bat,然后重命名新生成的 server_assets。- 打开
D:\KBELAB\kbengine\KBEngine-Nex。 - 双击根目录下的
new_assets.bat。 - 等待
server_assets生成完成,不要在生成过程中移动目录。 - 把
server_assets重命名为项目 Assets 名称,例如mygame_nex_assets。
推荐把新 Assets 放在 Nex 引擎根目录,与 `kbe` 同级。放在其他位置时,稍后必须显式设置 `KBE_ROOT`。
展开旧业务脚本
复制的是旧 `scripts` 里面的内容,不是 `scripts` 文件夹本身。
| 旧位置 | 新位置 | 判断 |
|---|---|---|
scripts/base | base | 复制内容 |
scripts/cell | cell | 复制内容 |
scripts/entity_defs | entity_defs | 复制内容 |
scripts/entities.xml | entities.xml | 复制并校验 |
scripts/data 等扩展目录 | Assets 根目录同名目录 | 按项目保留 |
根目录 start_server.* | scripts/start_server.* | 不复制旧文件 |
- 打开旧 Assets 的
scripts目录。 - 选择其中的业务目录和根级
entities.xml,把它们复制到新 Assets 根目录。 - 保留新 Assets 的
scripts目录及其中的 Nex 启动脚本,不用旧根目录启动脚本覆盖它。 - 在新 Assets 中搜索并删除全部
__pycache__目录和.pyc文件。
mygame_nex_assets/ ├─ base/ │ └─ Account.py ├─ bots/ ├─ cell/ │ └─ Avatar.py ├─ common/ ├─ data/ ├─ db/ ├─ entity_defs/ │ └─ Avatar.def ├─ interface/ ├─ logger/ ├─ login/ ├─ server_common/ ├─ user_type/ ├─ entities.xml ├─ res/ └─ scripts/ └─ start_server.bat
scripts/base/Account.py,复制层级错了;不要带着错误层级继续修 import。迁移资源与配置
Nex 配置兼容原版,旧 res 内容可以直接覆盖同名文件。
res 内容复制到新 Assets 的 res,同名文件直接覆盖。- 复制旧
res中的全部项目资源和配置。 - 粘贴到新 Assets 的
res,出现同名文件时选择覆盖。 - 确认数据库、网络、日志、Tick 和账号系统配置仍与部署环境一致。
- 记录所有使用中的地图;旧导航二进制仍要在下一阶段重建。
资源路径新规则
KBE_ROOT/kbe/res ; PROJECT_PATH ; PROJECT_PATH/res
entities.xml 不再写成 scripts/entities.xml;地图仍可写 spaces/xxx。
配置可直接覆盖
Nex 兼容原版配置,kbengine.xml、server_errors.xml 和日志配置可直接覆盖同名文件。
覆盖后仍要核对部署参数;导航二进制兼容性不在此范围内。
scripts/... 路径都要去掉 scripts/ 前缀。只移动文件而不修改代码,KBEngine.hasRes() 仍会返回 False。createSpawnPointDatas 前后对照
原版:Nex 中路径错误
res = r"scripts\data\spawnpoints\%s_spawnpoints.xml" % space_name
KBEngine.hasRes(res)
Nex:根级 data
res = "data/spawnpoints/%s_spawnpoints.xml" % space_name
KBEngine.hasRes(res)
scripts/data/...改为data/...。scripts/entities.xml改为entities.xml。scripts/base/...、scripts/cell/...、scripts/entity_defs/...去掉scripts/。spaces/...和server/...保持不变,不要增加res/前缀。- 资源逻辑路径统一使用
/,并严格匹配文件名大小写。
如果 Assets 不在引擎根目录,应在系统环境变量或项目启动配置中把 KBE_ROOT 指向 D:\KBELAB\kbengine\KBEngine-Nex,再从新 Assets 的 scripts 目录启动。
启动输出必须同时核对 KBE_ROOT、KBE_RES_PATH、KBE_BIN_PATH 和 KBE_VENV_PATH。业务读取资源优先使用 KBEngine.hasRes()、getResFullPath()、matchPath() 或 KBEngine.open()。
处理 Python 3.13 与 API
先消除确定性不兼容,再运行服务端。
| 不支持代码 | 修改 |
|---|---|
print a, b | print(a, b) |
mapping.has_key(key) | key in mapping |
弯引号 “”“ | ASCII 三引号 """ |
| Python 3.7 二进制扩展 | 使用 CPython 3.13 重新安装或编译 |
旧 readiness API:停止使用
registerReadFileDescriptor
registerWriteFileDescriptor
deregisterReadFileDescriptor
deregisterWriteFileDescriptor
新 completion API:按 FD 类型迁移
registerAcceptFileDescriptor
registerReadDataFileDescriptor
writeFileDescriptor
deregisterAccept/ReadData...
def on_accept(listener_fd, client_fd, error_code):
"""处理 accept 完成事件。 / Handle an accept completion."""
def on_read(fd, data, error_code):
"""处理读完成事件。 / Handle a read completion."""
def on_write_complete(fd, bytes_written, error_code):
"""处理写完成事件。 / Handle a write completion."""
- 旧回调中不要再自行阻塞调用
accept()/recv()。 - 监听 FD 与客户端 FD 使用不同注册接口,同一读 FD 不重复注册。
- 成功、断连、异常、停止四条路径都注销并关闭 FD。
- completion 和 asyncio 回调不可做长 CPU、同步网络或同步磁盘 IO。
- 原始 SQL 可能命中 Nex 2.8.2 黑名单,查 dbmgr WARNING,不要直接关闭保护。
- Redis 底层持久化已移除,依赖项目必须重新选择持久化方案。
重建导航与坐标适配
旧 `.navmesh` 不复用,不改名;从源模型生成新 `.bin`。
*.navmesh,当前 Nex 自动发现 *.bin。二进制格式也已升级,重命名文件无效。(x, y, z)。坐标系参考页中的 (-x, z, y) 不适用于当前 Nex。+Z / -Z 描述的是默认前向,yaw + 180 只在模型前向需要补偿时使用。- 从原始 OBJ / GLTF / FBX 模型开始,不从旧二进制 NavMesh 转换。
- 使用 NavMesh Generator 设置角色半径、高度、坡度、台阶、Tile 和体素参数。
- 用 TestAgent 验证门、坡道、楼层、障碍边界。
- 导出到
res/spaces/<mapping>/navmesh.bin。 - 加载完成回调触发后,才调用随机点、导航或射线 API。
getRenderObject(...) 表示按实体 ID 获取客户端场景对象,是需要替换的伪代码;上传代码中的 player 表示当前 KBE 玩家实体。KBE → Unity:(-x, y, z),-yaw
public override void onPositionChanged(KBVector3 oldValue)
{
base.onPositionChanged(oldValue);
Transform renderObj = GetRenderObject(id); // 伪代码 / Pseudocode
if (renderObj == null) return;
renderObj.position = new Vector3(
-position.x, position.y, position.z);
}
public override void onSmoothPositionChanged(KBVector3 oldValue)
{
base.onSmoothPositionChanged(oldValue);
Transform renderObj = GetRenderObject(id); // 伪代码 / Pseudocode
if (renderObj == null) return;
renderObj.position = new Vector3(
-position.x, position.y, position.z);
}
public override void onDirectionChanged(KBVector3 oldValue)
{
base.onDirectionChanged(oldValue);
Transform renderObj = GetRenderObject(id); // 伪代码 / Pseudocode
if (renderObj == null) return;
renderObj.eulerAngles = new Vector3(
direction.y, -direction.z, direction.x);
}
本地玩家上传:
Vector3 p = renderObj.position;
Vector3 r = renderObj.eulerAngles;
player.position = new KBVector3(-p.x, p.y, p.z);
player.direction = new KBVector3(r.z, r.x, -r.y);
KBE → Cocos:位置 (x, y, z),模型前向需要时 yaw + 180
onPositionChanged(oldValue: Vector3): void {
super.onPositionChanged(oldValue);
const renderObj = getRenderObject(this.id); // 伪代码 / Pseudocode
if (!renderObj) return;
renderObj.setPosition(
this.position.x, this.position.y, this.position.z);
}
onSmoothPositionChanged(oldValue: Vector3): void {
super.onSmoothPositionChanged(oldValue);
const renderObj = getRenderObject(this.id); // 伪代码 / Pseudocode
if (!renderObj) return;
renderObj.setPosition(
this.position.x, this.position.y, this.position.z);
}
onDirectionChanged(oldValue: Vector3): void {
super.onDirectionChanged(oldValue);
const renderObj = getRenderObject(this.id); // 伪代码 / Pseudocode
if (!renderObj) return;
const yaw = ((this.direction.z + 180) % 360 + 360) % 360;
renderObj.setRotationFromEuler(
this.direction.y, yaw, this.direction.x);
}
本地玩家上传:
const p = renderObj.position;
const r = renderObj.eulerAngles;
player.position = new Vector3(p.x, p.y, p.z);
player.direction = new Vector3(
r.z, r.x, ((r.y - 180) % 360 + 360) % 360);
KBE → Godot:位置 (x, y, z),模型前向需要时 yaw + 180
public override void onPositionChanged(KBVector3 oldValue)
{
base.onPositionChanged(oldValue);
Node3D renderObj = GetRenderObject(id); // 伪代码 / Pseudocode
if (renderObj == null) return;
renderObj.GlobalPosition = new Vector3(
position.x, position.y, position.z);
}
public override void onSmoothPositionChanged(KBVector3 oldValue)
{
base.onSmoothPositionChanged(oldValue);
Node3D renderObj = GetRenderObject(id); // 伪代码 / Pseudocode
if (renderObj == null) return;
renderObj.GlobalPosition = new Vector3(
position.x, position.y, position.z);
}
public override void onDirectionChanged(KBVector3 oldValue)
{
base.onDirectionChanged(oldValue);
Node3D renderObj = GetRenderObject(id); // 伪代码 / Pseudocode
if (renderObj == null) return;
float yaw = Mathf.PosMod(direction.z + 180.0f, 360.0f);
renderObj.GlobalRotationDegrees = new Vector3(
direction.y, yaw, direction.x);
}
本地玩家上传:
Vector3 p = renderObj.GlobalPosition;
Vector3 r = renderObj.GlobalRotationDegrees;
player.position = new KBVector3(p.X, p.Y, p.Z);
player.direction = new KBVector3(
r.Z, r.X, Mathf.PosMod(r.Y - 180.0f, 360.0f));
角色高度偏移应单独加在 Y 轴,并在上传时减去同一个值。
KBE → Godot:位置 (x, y, z),模型前向需要时 yaw + 180
func onPositionChanged(old_value: Vector3) -> void:
super.onPositionChanged(old_value)
var render_obj: Node3D = get_render_object(id) # 伪代码 / Pseudocode
if render_obj == null:
return
render_obj.global_position = Vector3(
position.x, position.y, position.z)
func onSmoothPositionChanged(old_value: Vector3) -> void:
super.onSmoothPositionChanged(old_value)
var render_obj: Node3D = get_render_object(id) # 伪代码 / Pseudocode
if render_obj == null:
return
render_obj.global_position = Vector3(
position.x, position.y, position.z)
func onDirectionChanged(old_value: Vector3) -> void:
super.onDirectionChanged(old_value)
var render_obj: Node3D = get_render_object(id) # 伪代码 / Pseudocode
if render_obj == null:
return
var yaw := fposmod(direction.z + 180.0, 360.0)
render_obj.global_rotation_degrees = Vector3(
direction.y, yaw, direction.x)
本地玩家上传:
var p := render_obj.global_position
var r := render_obj.global_rotation_degrees
player.position = Vector3(p.x, p.y, p.z)
player.direction = Vector3(
r.z, r.x, fposmod(r.y - 180.0, 360.0))
KBE → UE:位置 (x*100, z*100, y*100),yaw + 90
void Avatar::onPositionChanged(const KBVector3f& oldValue)
{
AvatarBase::onPositionChanged(oldValue);
AActor* RenderObj = GetRenderObject(); // 伪代码 / Pseudocode
if (!RenderObj) return;
RenderObj->SetActorLocation(FVector(
position.x * 100.0f,
position.z * 100.0f,
position.y * 100.0f));
}
void Avatar::onSmoothPositionChanged(const KBVector3f& oldValue)
{
AvatarBase::onSmoothPositionChanged(oldValue);
AActor* RenderObj = GetRenderObject(); // 伪代码 / Pseudocode
if (!RenderObj) return;
RenderObj->SetActorLocation(FVector(
position.x * 100.0f,
position.z * 100.0f,
position.y * 100.0f));
}
void Avatar::onDirectionChanged(const KBVector3f& oldValue)
{
AvatarBase::onDirectionChanged(oldValue);
AActor* RenderObj = GetRenderObject(); // 伪代码 / Pseudocode
if (!RenderObj) return;
const float roll = -FMath::RadiansToDegrees(direction.x);
const float pitch = -FMath::RadiansToDegrees(direction.y);
const float yaw = FMath::RadiansToDegrees(direction.z) + 90.0f;
RenderObj->SetActorRotation(FRotator(pitch, yaw, roll));
}
本地玩家上传:
const FVector p = RenderObj->GetActorLocation();
const FRotator r = RenderObj->GetActorRotation();
player->position.x = p.X / 100.0f;
player->position.y = p.Z / 100.0f;
player->position.z = p.Y / 100.0f;
player->direction.x = FMath::DegreesToRadians(-r.Roll);
player->direction.y = FMath::DegreesToRadians(-r.Pitch);
player->direction.z = FMath::DegreesToRadians(r.Yaw - 90.0f);
胶囊中心到脚底的高度是项目数据,应在上传 Y 高度时单独扣除,不要写死案例值。
| 统一输入 | Unity | Cocos / Godot | Unreal |
|---|---|---|---|
KBE 位置 (2, 3, 4) | (-2, 3, 4) | (2, 3, 4) | (200, 400, 300) |
KBE yaw 30° | 330° | 210° | 120° |
(2, 3, 4) 和 yaw 30°;再测试出生、平滑移动、传送、点击寻路和重连。navigate() 与 navigateToDetour() 是性能和导航能力的取舍,不是迁移前后的替代关系。navigate()
更轻量,适合单层地图、平面移动和大量轻量 AI。服务端不负责沿 NavMesh 高度移动,客户端必须自行贴合地形,并且无法处理多层建筑导航。
navigateToDetour()
使用 Detour 路径并由服务端贴合 NavMesh 高度,适合坡道、台阶和多层建筑。开销高于 navigate(),并且不支持 2D 导航。
实体 layer 必须与 getRandomPoints、navigate*、raycast 的 layer 一致。角度偏移要按客户端的度/弧度单位换算并归一化。
最小启动并重生成 SDK
从首个错误开始修,服务端稳定后再接客户端。
scripts 目录,双击 start_server.bat,验证 App 链和空数据库;之后生成同版本客户端 SDK。按以下顺序观察首个 ERROR:machine → logger → interfaces → dbmgr → baseappmgr/cellappmgr → baseapp/cellapp → loginapp。
- 启动输出的四个路径变量正确。
- 根级 `entities.xml` 和全部实体定义加载成功。
- 测试库表初始化完成,Bootstrap BaseApp 和首个 Space 正常。
- 几何加载完成回调触发,导航调用没有抢跑。
- Interfaces completion socket 能连接、收发、断连并清理。
- 正常安全关闭后没有残留 App 或未写入数据。
scripts/gensdk.* 生成当前 Nex SDK。任何 entities.xml、`.def`、Flags、UType、属性或方法变化都要求更新客户端。不要混用旧 SDK。至少回归登录、角色、进入空间、实体创建/销毁、属性、远程方法、移动、传送和重连。
验收、压测与上线
功能通过不是终点,还要验证数据、Tick 和回滚。
按日志找根因
先对照现象,不要同时改多个模块。
`ModuleNotFoundError` 或找不到 `entities.xml`
assets/scripts/base。Nex 应是 assets/base,实体声明应是根级 assets/entities.xml;同时核对启动输出中的 KBE_RES_PATH。出生点未创建,`KBEngine.hasRes()` 返回 `False`
scripts/data/spawnpoints/...。Nex 的 data 已提升到 Assets 根目录,应改为 data/spawnpoints/...,并用 KBEngine.getResFullPath() 确认最终命中的是新 Assets 下的文件。`RuntimeError` 提到 `registerReadFileDescriptor`
找不到 NavMesh 或提示版本不匹配
.navmesh 不兼容。用当前 NavMesh Generator 从源模型重生成 .bin,放在实际 mapping 目录,不能只改扩展名。客户端镜像、朝向相反或位置放大错误
Python 包导入失败
SQL 在 Nex 中突然被拒绝
核对入口
版本变化时优先重新检查这些来源。