# Activity 活动框架 活动管理系统,负责监听事件、判断条件、调用 Sentry 执行开关。 ## 架构概览 ``` Activity/ ├── ActivityManager.cs # 核心管理器 - 协调整个活动系统 ├── ActivityScopeManager.cs # DI Scope 管理器 - 每个活动独立的依赖注入容器 ├── Condition/ # 条件检查模块 │ ├── IConditionChecker.cs # 条件检查器接口 │ ├── ConditionCheckerFactory.cs # 条件检查器工厂 │ ├── TimeConditionChecker.cs # 时间条件检查器 │ ├── AccountLevelConditionChecker.cs # 账号等级条件检查器 │ ├── FishCountConditionChecker.cs # 捕鱼数量条件检查器 │ └── ABTestConditionChecker.cs # AB测试条件检查器 ├── Data/ # 数据层 │ ├── IEventData.cs # 活动数据接口 │ ├── AEventData.cs # 活动数据基类 │ ├── AEventDataManager.cs # 活动数据管理器基类 │ ├── IEventDataRepository.cs # 数据仓库接口 │ └── EventDataRepository.cs # 数据仓库实现 ├── Sentry/ # 活动哨兵模块 │ ├── IActivitySentry.cs # 哨兵接口 │ ├── AActivitySentry.cs # 哨兵基类(AActivitySentry) │ ├── ActivitySentryManager.cs # 哨兵管理器 │ └── ActivitySentryAttribute.cs # 哨兵特性 ├── EventSubscriber/ # 事件订阅模块 │ ├── IActivityEventSubscriber.cs # 事件订阅器接口 │ ├── ActivitySubscriberManager.cs # 订阅器管理器 │ └── *.cs # 具体订阅器实现 ├── HomeEntranceBtn/ # 主页入口按钮 │ ├── IHomeEntranceBtn.cs # 按钮接口 │ └── AbstractHomeEntranceBtn.cs # 按钮基类 ├── Event/ # 事件定义 │ └── GMActivityOpenedEvent.cs # GM开启活动事件 └── Services/ # 服务层 ├── IEventTokenService.cs # 代币服务接口 └── EventTokenService.cs # 代币服务实现 ``` ## 核心概念 ### 1. 条件检查器 (IConditionChecker) 负责判断活动是否应该开启。框架支持多种内置条件检查器: - **TimeConditionChecker** - 时间条件(活动开放时间段) - **AccountLevelConditionChecker** - 账号等级条件 - **FishCountConditionChecker** - 捕鱼数量条件 - **ABTestConditionChecker** - AB测试分组条件 每个条件检查器实现 `IConditionChecker` 接口: ```csharp public interface IConditionChecker { bool CanOpen(FishingEvent fishingEvent); // 判断是否能开启 int Priority { get; } // 优先级 List CareDataChangeWithType { get; } // 关心的数据类型变化 } ``` ### 2. 活动哨兵 (IActivitySentry) 活动哨兵负责执行活动的开启和关闭操作。每个活动类型对应一个 Sentry 实例。 ```csharp [ActivitySentry(eventType: 1, eventSubType: 1)] public class MyActivitySentry : AActivitySentry { /// /// 主页入口按钮实例 /// public IHomeEntranceBtn HomeEntranceBtn { get; set; } protected override void OnEventActive(FishingEvent fishingEvent) { // 1. 获取或创建活动数据(自动从 PlayFab 加载) var manager = ResolveManager(); var data = manager.RefreshData(fishingEvent); // 2. 通知按钮活动已开启(自动预加载资源、显示入口) HomeEntranceBtn?.OnOpen(fishingEvent, manager); } protected override void OnEventClosed(FishingEvent fishingEvent) { // 数据会自动保存(通过 EventDataRepository) // 通知按钮活动关闭 HomeEntranceBtn?.OnClose(fishingEvent); } protected override void OnEventChanged(FishingEvent previousEventConfig, FishingEvent newEventConfig) { // 同一 (Type, SubType) 下切换到新的配置时触发 // 框架不会对 previousEventConfig 额外调用 OnEventClosed,只会: // 1. 更新 CurrentEventConfig // 2. 调用 OnEventChanged(previous, new) // 3. 调用 OnEventActive(new) } } ``` ### 3. 活动数据 (IEventData) 活动运行时数据,包含活动ID、膨胀率、活动状态等: ```csharp public class MyEventData : AEventData { // 自定义数据字段 } ``` ### 4. 数据管理器 (AEventDataManager) 管理活动数据的加载、保存、刷新: ```csharp public class MyEventDataManager : AEventDataManager { protected override void OnDataInitialized(MyEventData data, FishingEvent eventConfig) { // 数据初始化逻辑 } } ``` ### 5. 主页入口按钮 (AbstractHomeEntranceBtn) 活动在主页的入口按钮框架,提供活动入口的通用功能: ```csharp public class MyActivityEntranceBtn : AbstractHomeEntranceBtn { #region Abstract Implementation /// /// 关联的 Sentry 类型,用于注册按钮到哨兵 /// protected override Type AssociatedSentryType => typeof(MyActivitySentry); /// /// 按钮点击事件处理 /// protected override void OnClickBtn() { // 打开活动面板 } /// /// 获取图标资源名称 /// protected override string GetIconName() => "icon_my_activity"; /// /// 获取需要预加载的资源列表 /// protected override List GetResourceNames() => new() { "Prefab_MyActivity" }; #endregion } ``` #### 核心功能 | 功能 | 说明 | |------|------| | **泛型类型** | `TData` 活动数据类型 | | **Sentry 集成** | 自动注册/注销按钮到对应的 ActivitySentry | | **定时器** | 自动每秒更新活动倒计时显示 | | **资源预加载** | 活动开启前预加载所需资源 | | **生命周期** | 继承 EventButtonResource,支持完整的创建/销毁流程 | #### UI 组件绑定 框架会自动查找并绑定以下子物体: | 字段 | 默认路径 | 说明 | |------|---------|------| | `textTimer` | `text_time` | 倒计时文本 | | `btn` | 自身 | 按钮组件 | | `icon` | `icon_task` | 图标图片 | > ⚠️ **注意**: 确保预制体中包含对应名称的子物体,否则需要手动赋值或重写绑定逻辑 ### 6. 事件订阅器 (IActivityEventSubscriber) 订阅游戏内事件,触发活动状态检查: ```csharp public class MyActivitySubscriber : IActivityEventSubscriber { public void Subscribe(ActivityManager manager) { // 订阅游戏事件 } public void Unsubscribe() { // 取消订阅 } } ``` ## 工作流程 ### 初始化流程 ``` ActivityManager.Init() ├── ConditionCheckerFactory.Initialize() // 初始化条件检查器工厂 ├── InitAllManage() // 注册 EventDataRepository、扫描 Sentry 和 Subscriber ├── InitializeAllSentries() // 为每个活动类型创建 Sentry 实例 ├── SubscribeGlobalEvents() // 订阅全局事件 └── CheckAllActivities() // 检查所有活动状态 ``` ### 活动状态检查流程 ``` 数据变化 或 定时检查 │ ▼ ActivityManager.OnDataChanged(data) │ ├── 根据数据类型找到相关活动 │ ├── 遍历活动调用 ShouldActivityOpen() │ │ │ └── 遍历所有条件检查器 (IConditionChecker.CanOpen) │ └── 根据结果调用 Sentry.Open() 或 Sentry.Close() ``` ### 活动开关执行流程 ``` Sentry.Open(fishingEvent) │ ├── 设置 CurrentEventConfig │ ├── 调用 OnEventChanged() (活动切换时) │ ├── 调用 OnEventActive() │ │ │ └── 通知 HomeEntranceBtn.OnOpen() │ │ │ └── 预加载资源 -> OnLoadEventResource() │ └── 创建/刷新活动数据 ``` ### 按钮注册流程 ``` Awake() │ ├── RegisterBtnInSentry() // 注册到 Sentry │ │ │ └── 获取 TypeKey 从 AssociatedSentryType │ │ │ └── 设置 sentry.HomeEntranceBtn = this │ ├── 查找并绑定 UI 组件 │ ├── 设置按钮点击监听 │ └── 检查活动状态 -> OnOpen() 或 等待 ``` ## 使用示例 ### 1. 创建新的活动 1. **定义活动数据类** ```csharp public class MyEventData : AEventData { public int TokenCount; public List RewardsClaimed; } ``` 2. **定义数据管理器** ```csharp public class MyEventDataManager : AEventDataManager { protected override void OnDataInitialized(MyEventData data, FishingEvent eventConfig) { data.InflationRate = GContext.container.Resolve().InflationRate; } } ``` 3. **定义活动哨兵** ```csharp [ActivitySentry(eventType: 1, eventSubType: 1)] public class MyActivitySentry : AActivitySentry { /// /// 主页入口按钮实例 /// public AbstractHomeEntranceBtn HomeEntranceBtn { get; set; } protected override void OnEventActive(FishingEvent fishingEvent) { // 创建活动数据 var manager = ResolveManager((EventType, EventSubType)); var data = manager.RefreshData(fishingEvent); // 通知按钮活动已开启 HomeEntranceBtn?.OnOpen(); } protected override void OnEventClosed(FishingEvent fishingEvent) { // 保存数据 HomeEntranceBtn?.OnClose(); } } ``` 4. **定义主页入口按钮** ```csharp public class MyActivityEntranceBtn : AbstractHomeEntranceBtn { protected override Type AssociatedSentryType => typeof(MyActivitySentry); protected override void OnClickBtn() { // 打开活动面板 } protected override string GetIconName() => "icon_my_activity"; protected override List GetResourceNames() => new() { "Prefab_MyActivity" }; } ``` ### 2. 添加新的条件检查器 ```csharp public class MyConditionChecker : IConditionChecker { public int Priority => 10; public List CareDataChangeWithType => new() { typeof(PlayerLevelData) }; public bool CanOpen(FishingEvent fishingEvent) { var playerLevel = GContext.container.Resolve().Level; return playerLevel >= fishingEvent.ConditionParam; } } ``` 然后在 `ConditionCheckerFactory.Initialize()` 中注册。 ### 3. 添加事件订阅器 ```csharp public class MyActivityEventSubscriber : IActivityEventSubscriber { private IDisposable _subscription; public void Subscribe(ActivityManager manager) { // 订阅游戏内事件 _subscription = MessageBroker.Default.Receive() .Subscribe(_ => manager.OnActivitySingleEvent(...)); } public void Unsubscribe() { _subscription?.Dispose(); } } ``` ## 注册机制 ### ActivityScopeManager 统一管理 注册逻辑统一放在 `ActivityScopeManager` 中,避免重复代码: | 方法 | 说明 | |------|------| | `IsRegistered()` | 检查类型是否已注册 | | `GetOrRegisterInstance(typeKey)` | 获取或注册实例(自动创建 Scope) | | `GetRegisteredTypeKey()` | 获取已注册类型对应的 typeKey | ```csharp // 在 AActivitySentry 中获取管理器实例 protected TM ResolveManager() { return _scopeManager.GetOrRegisterInstance((EventType, EventSubType)); } // 在 ActivityManager 中解析 public TM Resolve() where TM : AEventDataManager { if (!_scopeManager.IsRegistered()) return null; var typeKey = _scopeManager.GetRegisteredTypeKey(); if (typeKey == null) return null; if (!_scopeManager.HasActiveScope(typeKey.Value)) return null; return _scopeManager.GetOrRegisterInstance(typeKey.Value); } ``` ### 注册流程 ``` ResolveManager() 调用 │ ├── GetOrRegisterInstance(typeKey) │ │ │ ├── 获取或创建 Scope (GetActivityScope) │ │ │ ├── 如果未注册: │ │ │ │ │ ├── GContext.container.Register().PerScope() │ │ │ │ │ └── _activeRegistry.Add(typeof(TM), typeKey) │ │ │ └── 返回 scope.GetInstance((typeof(TM), typeof(TM).Name)) │ └── 返回 TM 实例 ``` ## 配置表 活动配置使用 `FishingEvent` 配置表,包含: - **Type** - 活动类型 - **SubType** - 活动子类型 - **TimeDefinition** - 时间定义 - **ConditionCheckers** - 条件检查器列表 ## 注意事项 1. **Scope 隔离**: 每个 (Type, SubType) 组合对应一个独立的 DI Scope 2. **数据持久化**: 通过 `IEventDataRepository` 进行数据存储 3. **代币管理**: 通过 `IEventTokenService` 统一管理活动代币 4. **条件检查**: 支持多条件组合,全部通过才能开启活动 5. **生命周期**: Sentry 实现了 `IDisposable`,确保资源正确释放 6. **按钮预制体**: 确保 UI 组件路径正确,或手动在 Inspector 中赋值 7. **Sentry 注册**: 子类必须正确实现 `AssociatedSentryType`,否则按钮无法注册 8. **统一注册**: 所有管理器实例通过 `ActivityScopeManager` 统一注册和管理 ## 可插拔性评估 ### 什么是可插拔? 可插拔 = 无需修改框架核心代码,即可新增或删除活动。 ### 当前框架可插拔性 | 组件 | 可插拔? | 机制 | |------|---------|------| | **Sentry** | ✅ | `[ActivitySentry]` 属性自动发现 | | **DataManager** | ✅ | 通过 Sentry 泛型参数自动关联 | | **HomeEntranceBtn** | ✅ | 继承 AbstractHomeEntranceBtn 即可 | | **ConditionChecker** | ❌ | 需手动添加到 `ConditionCheckerFactory` | | **EventSubscriber** | ❌ | 需手动注册到 `ActivitySubscriberManager` | ### 新增活动的影响 以 CapsulePack(扭蛋)为例: | 需创建的文件 | 框架修改? | |-------------|-----------| | `CapsulePackSentry.cs` | ❌ 通过 `[ActivitySentry]` 自动发现 | | `CapsulePackManager.cs` | ❌ 通过 Sentry 泛型自动关联 | | `IceCapsuleEntranceButton.cs` | ❌ 继承基类即可 | | `CapsulePackPanel.cs` | ❌ 业务代码 | | Excel 配置表 | ✅ 正常配置 | **结论**:新增活动**不需要修改框架代码**,只需添加业务文件。 ### 删除活动的影响 | 删除内容 | 影响 | |---------|------| | Sentry | 删除文件即失效 ✅ | | DataManager | 删除文件即失效 ✅ | | HomeEntranceBtn | 移除预制件即可 ✅ | | 配置表 | 删除 Excel 即可 ✅ | **结论**:删除活动**影响最小**,框架自动处理。 ### 结论 **框架可插拔性较好**: - 新增/删除 Sentry、DataManager、HomeEntranceBtn **完全可插拔** ✅ - UI Panel 取决于具体实现方式 - ConditionChecker 和 EventSubscriber 仍需手动注册 **无需改动框架即可新增/删除活动**。