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

    QQ登录

    只需一步,快速开始

    查看: 31|回复: 0

    循环依赖的模块如何进行解耦

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

    7393

    主题

    230

    回帖

    9039

    积分

    管理员

    本站站长

    积分
    9039
    online_admin 发表于 2025-8-30 14:24:36 | 显示全部楼层 |阅读模式
    在传奇游戏脚本开发中,当模块间已形成循环依赖(如 A 依赖 B,B 依赖 A,或 A→B→C→A 的多模块循环),解耦需结合循环类型(直接双向依赖 / 间接多模块循环)和模块职责,通过 **“抽象隔离、职责重构、通信方式转换”** 三大核心策略打破依赖闭环。以下是具体可操作的解耦方法,结合传奇引擎(GOM、HXM2)特性设计:
    一、识别循环依赖的类型与核心依赖点
    解耦前需先明确循环依赖的具体形式,避免盲目重构。可通过以下步骤分析:

    绘制依赖链:列出模块间的直接调用关系(如TaskModule调用RewardModule::Send,RewardModule调用TaskModule::Check);
    定位核心依赖点:找到循环中 “非必要的双向调用”(如奖励模块是否必须直接调用任务模块?能否通过其他方式获取任务状态?);
    判断循环类型:
    直接双向依赖:两个模块互相直接调用(如 A↔B);
    间接多模块循环:三个及以上模块形成依赖闭环(如 A→B→C→A)。
    二、直接双向依赖(A↔B)的解耦方法
    针对两个模块互相直接调用的场景,核心是 **“打破直接调用”**,通过 “抽象接口” 或 “事件通信” 实现间接交互。
    方法 1:引入抽象接口层(依赖倒置原则)
    适用于:两个模块因 “互相需要对方的核心能力” 形成循环(如任务模块需要奖励模块发奖励,奖励模块需要任务模块校验任务状态)。

    实施步骤:

    抽离接口定义:创建独立的接口模块(如TaskRewardInterface),定义双方需要的交互接口(仅声明,不实现);
    原模块依赖接口:让 A 和 B 都依赖接口模块,而非直接依赖对方;
    接口绑定实现:在接口模块中关联 A 和 B 的具体实现(通过引擎的 “函数注册” 机制)。

    传奇脚本示例(以TaskModule与RewardModule循环依赖为例):

    scp
    // 1. 抽象接口模块:TaskRewardInterface.scp(仅定义接口)
    // 接口1:任务完成后触发奖励(供TaskModule调用)
    Function TriggerReward(PlayerID, TaskID)
        // 绑定RewardModule的具体实现(引擎启动时注册)
        Call RegisteredRewardModule::DoSendReward(PlayerID, TaskID)
    EndFunction

    // 接口2:校验任务是否完成(供RewardModule调用)
    Function CheckTaskCompleted(PlayerID, TaskID)
        // 绑定TaskModule的具体实现
        Return Call RegisteredTaskModule::IsTaskDone(PlayerID, TaskID)
    EndFunction

    // 2. 任务模块:TaskModule.scp(依赖接口,不依赖RewardModule)
    Function FinishTask(PlayerID, TaskID)
        // 调用接口触发奖励,不直接调用RewardModule
        Call TaskRewardInterface::TriggerReward(PlayerID, TaskID)
    EndFunction

    // 3. 奖励模块:RewardModule.scp(依赖接口,不依赖TaskModule)
    Function SendSpecialReward(PlayerID, TaskID)
        // 调用接口校验任务,不直接调用TaskModule
        Local IsCompleted = Call TaskRewardInterface::CheckTaskCompleted(PlayerID, TaskID)
        If IsCompleted Then
            AddItem(PlayerID, 1001, 1)  // 发放特殊道具
        EndIf
    EndFunction

    // 4. 引擎启动时注册实现(初始化脚本)
    Call TaskRewardInterface::RegisterTaskModule("TaskModule")
    Call TaskRewardInterface::RegisterRewardModule("RewardModule")

    核心优势:彻底切断 A 与 B 的直接依赖,后续替换任一模块(如换奖励发放逻辑)只需修改接口绑定,不影响对方。
    方法 2:用事件驱动替代直接调用
    适用于:模块间交互为 “触发式”(如 A 完成某个操作后通知 B,B 无需实时响应),无需同步获取结果。

    实施步骤:

    定义事件类型:预设循环依赖中涉及的交互事件(如EVENT_TASK_FINISH、EVENT_REWARD_ISSUED);
    模块发送事件:A 完成操作后发送事件(不关心谁处理),不再直接调用 B;
    模块订阅事件:B 订阅 A 的事件,收到后执行逻辑,不再直接调用 A。

    传奇脚本示例(以NpcModule与TaskModule循环依赖为例):

    scp
    // 1. 通信模块:EventBus.scp(管理事件订阅与发送)
    // 订阅事件
    Function Subscribe(ModuleName, EventType)
        EventSubscribers[EventType][GetArrayLength(EventSubscribers[EventType])] = ModuleName
    EndFunction

    // 发送事件
    Function Publish(EventType, Param1, Param2)
        For i=0 To GetArrayLength(EventSubscribers[EventType])-1
            Local Module = EventSubscribers[EventType][i]
            Call Module::OnEvent(EventType, Param1, Param2)  // 调用订阅者的事件处理函数
        EndFor
    EndFunction

    // 2. NpcModule.scp(发送事件,不依赖TaskModule)
    Function OnPlayerTalkToNpc(PlayerID, NpcID)
        If NpcID == 1001  // 任务NPC
            // 发送“接取任务”事件,不直接调用TaskModule
            Call EventBus::Publish("EVENT_TASK_ACCEPT", PlayerID, 2001)  // 2001为任务ID
        EndIf
    EndFunction

    // 3. TaskModule.scp(订阅事件,不依赖NpcModule)
    Function Init()
        // 订阅“接取任务”事件
        Call EventBus::Subscribe("TaskModule", "EVENT_TASK_ACCEPT")
    EndFunction

    // 事件处理函数
    Function OnEvent(EventType, PlayerID, TaskID)
        If EventType == "EVENT_TASK_ACCEPT"
            StartTask(PlayerID, TaskID)  // 执行接取任务逻辑
            // 任务完成后发送事件,NpcModule可订阅该事件做后续处理
            Call EventBus::Publish("EVENT_TASK_FINISH", PlayerID, TaskID)
        EndIf
    EndFunction

    核心优势:模块间完全解耦,新增交互方(如 “日志模块” 订阅任务完成事件)无需修改原模块。
    三、间接多模块循环(A→B→C→A)的解耦方法
    多模块形成的依赖闭环(如 A 依赖 B,B 依赖 C,C 依赖 A)更隐蔽,需通过 **“拆分核心逻辑” 或 “引入中介模块”** 打破链条。
    方法 1:拆分 “过度耦合的模块”,剥离共享职责
    适用于:循环中某模块承担了过多职责(如 C 同时负责 “数据存储” 和 “业务逻辑”,导致 A 和 B 都依赖它)。

    实施步骤:

    识别 “职责过载模块”:找到循环中被多个模块依赖的 “核心模块”(如 C),分析其功能是否可拆分;
    剥离共享功能:将 C 中被 A、B 依赖的共享功能(如数据查询、状态存储)抽离为独立模块(如DataModule);
    重构依赖链:让 A、B、C 都依赖新的共享模块,而非互相依赖。

    案例:A(战斗模块)→ B(道具模块)→ C(角色模块)→ A(战斗模块)的循环依赖:

    问题:C(角色模块)同时负责 “属性计算” 和 “状态存储”,A 依赖 C 获取属性,C 依赖 A 更新战斗状态;
    解耦:拆分出AttrDataModule(专门存储属性数据),A、B、C 均依赖AttrDataModule,形成:
    A→AttrDataModule,B→AttrDataModule,C→AttrDataModule,循环被打破。
    方法 2:引入 “中介模块”,统一协调交互
    适用于:多模块因 “协同完成同一流程” 形成循环(如任务、奖励、成就模块协同处理玩家升级流程)。

    实施步骤:

    创建中介模块:新增一个模块(如ProcessCoordinator),负责协调多模块的交互流程;
    模块只与中介交互:原循环中的模块(A、B、C)不再互相调用,仅通过中介模块传递数据或触发操作;
    中介控制流程:中介模块按预定逻辑调用各模块,确保依赖方向为 “中介→模块” 的单向依赖。

    案例:任务模块(A)→ 奖励模块(B)→ 成就模块(C)→ 任务模块(A)的循环:

    新增UpgradeCoordinator(升级流程中介),重构后:
    玩家升级时,UpgradeCoordinator依次调用 A(检查任务进度)、B(发放升级奖励)、C(解锁成就);
    A、B、C 仅依赖UpgradeCoordinator,不再互相依赖,循环被打破。
    四、解耦后的验证与优化
    解耦后需验证是否彻底消除循环依赖,并优化可能引入的性能开销:

    依赖链验证:
    重新绘制模块依赖图,确认是否存在闭环;
    测试引擎启动顺序(按 “基础模块→中介 / 接口模块→业务模块” 加载),若启动无死锁、无初始化失败,说明解耦成功。
    性能优化:
    事件驱动可能引入延迟:对高频事件(如战斗伤害)使用 “同步事件”(立即处理),低频事件(如任务完成)使用 “异步队列”(批量处理);
    接口层调用开销:通过 “函数指针缓存” 减少动态调用耗时(如 HXM2 引擎可缓存Call目标函数地址)。
    总结
    循环依赖的解耦核心是 **“打破直接双向调用,转为单向依赖或间接交互”**:

    对双向依赖,优先用 “抽象接口”(需实时结果)或 “事件驱动”(可异步);
    对多模块循环,通过 “拆分职责” 或 “引入中介” 重构依赖链。

    在传奇脚本中,需结合引擎的解释执行特性,避免过度设计(如简单循环可用事件驱动快速解耦),同时确保解耦后模块的独立性和可维护性 —— 最终目标是让每个模块只关注自身职责,通过明确的接口或事件与其他模块协作。

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

    本版积分规则

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

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