设为首页收藏本站
  • 官方微信
    lmkj_wx 微信公众号 添加方式:
    1:扫描左侧二维码
  • 手机访问
    lmkj_sj
  •  找回密码
     立即注册

    QQ登录

    只需一步,快速开始

    查看: 29|回复: 0

    有哪些有效的方法可以避免模块间的循环依赖

    [复制链接]
    avatar
    • 打卡等级:魔龙套勇士
    • 打卡总天数:182
    • 打卡月天数:14
    • 打卡总奖励:19156
    • 最近打卡:2025-10-14 00:17:14

    7393

    主题

    230

    回帖

    9039

    积分

    管理员

    本站站长

    积分
    9039
    online_admin 发表于 2025-8-30 14:17:23 | 显示全部楼层 |阅读模式
    在传奇游戏脚本的模块化设计中,循环依赖(如 A 模块依赖 B 模块,B 模块又依赖 A 模块)是常见的 “隐性陷阱”,可能导致引擎初始化死锁、逻辑执行混乱、性能损耗等问题。结合传奇引擎(GOM、HXM2 等)的脚本特性(无编译时依赖检查、解释型执行),需通过设计规范、依赖解耦、通信优化三大维度规避,以下是具体有效方法:
    一、基于 “依赖倒置” 原则,引入抽象中间层
    循环依赖的核心原因之一是 “模块间直接依赖具体实现”,而非依赖抽象接口。通过引入公共抽象模块作为中间层,让原本循环依赖的模块都依赖中间层,而非直接互相依赖,从根本上打破依赖闭环。
    具体实现(以 “任务模块” 与 “奖励模块” 循环依赖为例):
    问题场景:
    任务模块(TaskModule)完成任务后需调用奖励模块(RewardModule)发放奖励;
    奖励模块发放特殊奖励(如 “任务专属装备”)时需调用任务模块校验 “是否完成对应任务”,形成TaskModule ↔ RewardModule循环依赖。
    解决方案:
    新增抽象接口模块(TaskRewardInterfaceModule),定义双向交互的标准化接口,不包含具体业务逻辑:
    接口 1:IssueTaskReward(PlayerID, TaskID) → 供任务模块调用,触发奖励发放;
    接口 2:VerifyTaskCompletion(PlayerID, TaskID) → 供奖励模块调用,校验任务完成状态。
    依赖关系重构:
    TaskModule依赖TaskRewardInterfaceModule(调用接口 1 发放奖励),不直接依赖RewardModule;
    RewardModule依赖TaskRewardInterfaceModule(调用接口 2 校验任务),不直接依赖TaskModule;
    TaskRewardInterfaceModule作为中间层,分别调用TaskModule和RewardModule的具体实现(通过引擎的 “动态函数注册” 机制关联)。
    传奇脚本示例(HXM2 引擎):
    scp
    // 抽象接口模块:TaskRewardInterfaceModule.scp
    // 接口1:触发奖励发放
    Function IssueTaskReward(PlayerID, TaskID)
        // 动态调用RewardModule的具体实现(避免直接依赖)
        Call RewardModule::DoIssueTaskReward(PlayerID, TaskID)
    EndFunction

    // 接口2:校验任务完成状态
    Function VerifyTaskCompletion(PlayerID, TaskID)
        // 动态调用TaskModule的具体实现
        Return Call TaskModule::CheckTaskDone(PlayerID, TaskID)
    EndFunction
    二、拆分模块职责,遵循 “单一职责原则”
    循环依赖常因模块 “职责过载” 导致(一个模块同时负责多个不相关功能,迫使其他模块依赖其多维度能力)。通过拆分模块为 “单一职责单元”,让依赖关系更清晰,避免双向依赖。
    具体实现(以 “战斗模块” 与 “道具模块” 循环依赖为例):
    问题场景:
    战斗模块(BattleModule)计算伤害时需调用道具模块(ItemModule)获取 “武器攻击加成”;
    道具模块使用 “战斗增益道具”(如 “临时攻击力药水”)时需调用战斗模块修改 “当前攻击属性”,形成循环依赖。
    解决方案:
    拆分出独立的 “属性管理模块”(AttrModule),专门负责角色属性的读取与修改,剥离原模块的属性相关职责:
    原BattleModule的 “获取攻击属性” 逻辑迁移至AttrModule;
    原ItemModule的 “修改攻击属性” 逻辑迁移至AttrModule。
    依赖关系重构:
    BattleModule依赖AttrModule(获取攻击属性计算伤害),不依赖ItemModule;
    ItemModule依赖AttrModule(使用道具时修改属性),不依赖BattleModule;
    形成单向依赖链:BattleModule → AttrModule ← ItemModule,消除循环。
    三、采用 “事件驱动” 通信,替代直接函数调用
    传奇脚本支持 “消息传递” 机制(如通过自定义MsgModule转发消息),用异步事件通信替代模块间的直接函数调用,可彻底切断依赖关系 —— 模块只需 “发送事件” 或 “订阅事件”,无需知道事件的处理者是谁。
    具体实现(以 “NPC 模块” 与 “任务模块” 循环依赖为例):
    问题场景:
    NPC 模块(NpcModule)中 “任务 NPC” 需调用任务模块(TaskModule)触发 “接取任务” 逻辑;
    任务模块完成任务后需调用 NPC 模块让 “任务 NPC” 发送对话提示,形成循环依赖。
    解决方案:
    基于 “通信模块”(MsgModule)实现事件驱动:
    事件定义:预设EVENT_TASK_ACCEPT(接取任务)、EVENT_TASK_FINISH(任务完成)两个事件;
    发送事件:NpcModule检测到玩家与 NPC 对话时,发送EVENT_TASK_ACCEPT事件(携带PlayerID、TaskID),不直接调用TaskModule;
    订阅事件:TaskModule订阅EVENT_TASK_ACCEPT事件,收到后执行接取逻辑;完成任务后发送EVENT_TASK_FINISH事件;
    处理事件:NpcModule订阅EVENT_TASK_FINISH事件,收到后触发 NPC 对话提示。
    关键优势:
    模块间无直接依赖,仅依赖MsgModule(基础公共模块);
    新增事件处理者(如 “奖励模块” 订阅EVENT_TASK_FINISH发放奖励)时,无需修改原有模块,扩展性更强。
    传奇脚本示例(GOM 引擎):
    scp
    // 通信模块:MsgModule.scp
    // 注册事件订阅者
    Function SubscribeEvent(ModuleName, EventType)
        // 将模块名与事件类型关联存储(如用数组保存)
        Local Index = GetArrayLength(EventSubscribers[EventType])
        EventSubscribers[EventType][Index] = ModuleName
    EndFunction

    // 发送事件
    Function SendEvent(EventType, Param1, Param2)
        // 遍历订阅该事件的所有模块,调用其事件处理函数
        For i=0 To GetArrayLength(EventSubscribers[EventType])-1
            Local Module = EventSubscribers[EventType][i]
            Call Module::OnEventReceived(EventType, Param1, Param2)
        EndFor
    EndFunction
    四、引入 “第三方依赖模块”,聚合公共逻辑
    当两个模块因 “共享同一类数据 / 逻辑” 形成循环依赖时,可将共享部分抽离为独立的第三方模块,让原模块都依赖第三方模块,而非互相依赖。
    具体实现(以 “玩家模块” 与 “社交模块” 循环依赖为例):
    问题场景:
    玩家模块(PlayerModule)需调用社交模块(SocialModule)获取 “好友列表” 显示在玩家面板;
    社交模块添加好友时需调用玩家模块校验 “对方是否在线”,形成循环依赖。
    解决方案:
    抽离 **“玩家状态模块”(PlayerStatusModule)**,聚合 “在线状态”“基础信息” 等共享数据:
    PlayerModule将 “玩家在线状态” 同步至PlayerStatusModule;
    SocialModule校验 “对方是否在线” 时,调用PlayerStatusModule的IsPlayerOnline(PlayerID)函数,不直接依赖PlayerModule;
    PlayerModule获取好友列表时,调用SocialModule,但SocialModule不再依赖PlayerModule。
    适用场景:
    共享逻辑较简单(如数据查询、状态校验),无需复杂业务交互的场景,可快速解耦循环依赖。
    五、建立 “依赖管理流程”,从开发阶段规避
    技术手段需配合流程规范才能彻底避免循环依赖,尤其在团队协作中,需通过设计审查、文档约束、工具辅助确保依赖关系合规。
    具体流程:
    模块设计阶段:绘制依赖关系图
    开发前用工具(如 Excel、Draw.io)绘制模块依赖图,明确每个模块的 “上游依赖”(依赖哪些模块)和 “下游被依赖”(被哪些模块依赖),提前排查循环依赖。
    示例:用箭头表示依赖方向(A → B表示 A 依赖 B),若出现A→B→C→A的闭环,立即重构设计。
    代码开发阶段:严格遵循 “单向依赖” 规范
    基础公共模块(如LogModule、MsgModule)不依赖任何业务模块;
    业务模块仅依赖 “基础公共模块” 或 “层级更低的业务模块”(如 “任务模块” 可依赖 “角色基础模块”,但不可依赖 “活动模块”);
    禁止跨层级依赖(如 “道具模块” 不可直接依赖 “沙巴克活动模块”)。
    代码审查阶段:依赖关系检查
    提交脚本前,通过以下方式检查:
    人工审查:团队成员交叉检查新代码是否引入未在依赖图中定义的依赖;
    脚本扫描:用 Python 编写简单扫描工具,遍历所有.scp文件中的Call 模块名::语句,自动生成依赖关系表,检测闭环。
    测试阶段:初始化顺序验证
    引擎启动时,按 “基础模块→第三方依赖模块→业务模块” 的顺序加载模块,若出现初始化卡住(死锁),立即定位循环依赖模块。
    六、特殊场景处理:兼容引擎局限性
    传奇引擎(尤其是老版本)存在 “脚本加载顺序固定”“无动态依赖解析” 等局限性,需针对性优化:

    固定模块加载顺序
    在引擎配置文件(如M2Server.ini)中指定模块加载顺序,确保 “被依赖模块” 先加载,依赖模块后加载。例如:
    ModuleLoadOrder=LogModule,MsgModule,AttrModule,TaskModule,RewardModule
    (AttrModule先于TaskModule和RewardModule加载,避免依赖加载失败)。
    延迟初始化依赖
    若无法避免间接依赖,可在模块初始化时先不加载依赖,待首次使用时再动态加载。例如:
    scp
    // 任务模块初始化时不直接加载奖励模块
    Function TaskModule::Init()
        // 仅初始化自身逻辑,不调用RewardModule
        IsInited = 1
    EndFunction

    // 首次发放奖励时动态获取依赖
    Function TaskModule::FinishTask(PlayerID, TaskID)
        If Not IsInited Then Return 0
        // 动态调用奖励模块(避免初始化时依赖)
        Call RewardModule::IssueReward(PlayerID, TaskID)
    EndFunction



    总结
    避免模块间循环依赖的核心是 **“解耦”**,具体可通过 “抽象中间层、职责拆分、事件驱动、第三方模块、流程规范” 五大手段实现。在传奇游戏脚本开发中,需结合引擎的解释型执行特性,优先采用 “事件驱动” 和 “抽象接口” 方案(无编译依赖,适配性强),同时通过依赖图审查和加载顺序控制,从开发到测试全流程规避风险。最终目标是形成 “基础模块→第三方模块→业务模块” 的单向依赖链,确保模块间逻辑清晰、执行高效。

    您需要登录后才可以回帖 登录 | 立即注册 qq_login

    本版积分规则

    QQArchiver 手机版 小黑屋 39传奇素材网 ( 蜀ICP备2022016510号-3 )

    快速回复 快速发帖 返回顶部 返回列表