Best Practices — 最佳实践

本文档汇总了使用 RoninNetease 框架时的推荐模式、设计决策和常见陷阱。


1. 子系统设计

✅ 推荐

单一职责: 每个子系统只负责一个领域。

# 好:职责分离
class CombatSystem(ServerSubsystem):    # 战斗逻辑
    pass

class InventorySystem(ServerSubsystem): # 物品管理
    pass

class SpawnSystem(ServerSubsystem):     # 实体生成
    pass

通过 CommandBus 解耦: 让子系统通过命令总线通信,而非直接引用。

# 好:通过 CommandBus
class QuestSystem(ServerSubsystem):
    def onReady(self):
        manager = SubsystemManager.getInstance()
        self._unreg = manager.bus.register('quest.complete', self.on_quest_done)

onReady 中初始化跨系统依赖: onInit 只初始化自身,等所有系统创建完毕后再在 onReady 中建立连接。

class MySystem(ServerSubsystem):
    def onInit(self):
        self.data = {}          # ✅ 初始化自身状态

    def onReady(self):
        self.other = OtherSystem.getInstance()          # ✅ 获取其他系统
        self.scheduleFixed('my_fixed', period=1.0)     # ✅ 启动固定调度器

❌ 避免


2. 组件设计

✅ 推荐

纯数据容器: 组件只存储数据,不含业务逻辑。

class Health(Component):
    hp = 100
    maxHp = 100

细粒度组件: 拆分大组件为多个小组件,便于查询和复用。

使用 @DefineFields 做验证:

@DefineFields({
    'level': FieldSchema(default=1, validator=lambda v: v >= 1),
    'xp': FieldSchema(default=0, validator=lambda v: v >= 0),
})
class PlayerStats(Component):
    pass

标记持久化字段:

@PersistKeys('slots')
class Inventory(Component):
    slots = [''] * 36

❌ 避免


3. 查询模式

✅ 推荐

@Query 接收组件类作为位置参数,用 EntityId 获取实体 ID:

from architect.query import Query, EntityId

@Query(Health, EntityId, required=[CombatStats])
def damage_tick(self, health, entityId):
    # 注:required 中的 CombatStats 仅用于筛选,不注入到参数
    health.hp -= 1

onUpdate 中调用查询方法:

class DamageSystem(ServerSubsystem):
    canTick = True

    def onUpdate(self, dt):
        self.damage_tick()  # 遍历所有匹配实体

按需使用手动获取:

health = getComponent(entityId, Health)
if health and health.hp < 20:
    self.heal(entityId)

❌ 避免


4. 事件系统

✅ 推荐

使用 @EventListener 装饰器:

@EventListener('EntityHurtEvent')
def onEntityHurt(self, event):
    entityId = event.id        # ✅ 使用属性访问
    damage = event.damage

区分引擎事件与自定义事件:

@EventListener('ServerPostInitEvent')   # 引擎事件
def onInit(self, event): ...

@CustomEvent('MyDataSyncEvent')          # 自定义事件
def onSync(self, event): ...

5. 调度系统

✅ 推荐

利用多阶段调度:

@Sched.Tick(SchedUpdateFlags.BeforeUpdate)  # 输入收集
def collect_input(self): ...

@Sched.Tick()                                # 逻辑更新
def update_logic(self): ...

@Sched.Tick(SchedUpdateFlags.AfterUpdate)    # 状态同步
def sync_state(self): ...

固定调度器在 onReady 中启动:

@Sched.Fixed('save_scheduler')
def auto_save(self): ...

def onReady(self):
    self.scheduleFixed('save_scheduler', period=30.0)  # ✅ 在 onReady

❌ 避免


6. 性能诊断

✅ 推荐

关键路径计时:

def onUpdate(self, dt):
    with profiler.record('Combat.damage_loop'):
        self.damage_tick()

周期性输出报告:

if self.ticks % 300 == 0:
    snap = profiler.flush()
    for key, stats in snap.items():
        if stats['avgMs'] > 5.0:   # 只输出超过 5ms 的
            print('[WARN] %s: avg=%.2fms max=%.2fms' %
                  (key, stats['avgMs'], stats['maxMs']))

7. UI 系统

✅ 推荐

signal() 不是装饰器,返回 (getter, setter) 元组:

from architect.ui.client import signal, Sink

class MyHUD(UiSubsystem):
    def onCreate(self):
        self.hpGet, self.hpSet = signal(100)

    @Sink  # 无参数
    def refresh(self):
        hp = self.hpGet()
        self.find('/hpLabel').SetText(str(hp))

@UiDef@Screen@Hud@AutoCreate 都是类装饰器

@UiDef('myHud')
@Screen
@AutoCreate
class MyHUD(UiSubsystem):
    pass

❌ 避免


8. 远程调用 (RPC)

✅ 推荐

使用 remote.clientremote.server 单例:

from architect.remote.common import remote

# 客户端调用服务端(fire-and-forget)
remote.client.call('MySystem.method', arg1, arg2)

# 客户端调用服务端(需要返回值)
fut = remote.client.invoke('MySystem.method', arg1, arg2)
fut.done(lambda result: print(result))

# 服务端调用客户端
remote.server.call(playerId, 'ClientSystem.method', arg1)
remote.server.callEvery('ClientSystem.method', arg1)  # 广播

# 注册可远程调用的方法
class MySystem(ServerSubsystem):
    @Remote
    def my_method(self, caller_playerId, *args, **kwargs):
        pass

❌ 避免


9. 常见陷阱

9.1 可变默认值

class Health(Component):
    hp = 100         # ✅ 不可变默认值 OK
    maxHp = 100

组件的类属性用作默认值。对于需要实例独立值的字段,在 createComponent 后初始化。

9.2 API 名称错误

❌ 错误 ✅ 正确
self.getManager() SubsystemManager.getInstance()
callRemote(...) remote.client.invoke(...)remote.client.call(...)
@signal (装饰器) signal(default) 函数调用,返回 (get, set)
@Sink(initiator=hp) @Sink (无参数)
@Query(target='{Health}') @Query(Health, EntityId)
serveFixed('name') self.scheduleFixed('name', period=1.0)
Sched.Tick.BeforeUpdate SchedUpdateFlags.BeforeUpdate
@UiDef('name') 在方法上 @UiDef('name')

9.3 装饰器注册顺序

确保 modMain.py 中先调用 createServer() / createClient(),再让模块被导入。


10. 目录结构建议

your_mod/
├── modMain.py               # 入口
├── conf.py                  # 配置
├── subsystems/              # 子系统
├── components/              # 组件
├── plugins/                 # 用户插件
└── utils/                   # 工具函数

11. Python 2.7 注意事项


下一步