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<TM, TD>)
│ ├── 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 接口:
public interface IConditionChecker
{
bool CanOpen(FishingEvent fishingEvent); // 判断是否能开启
int Priority { get; } // 优先级
List<Type> CareDataChangeWithType { get; } // 关心的数据类型变化
}
2. 活动哨兵 (IActivitySentry)
活动哨兵负责执行活动的开启和关闭操作。每个活动类型对应一个 Sentry 实例。
[ActivitySentry(eventType: 1, eventSubType: 1)]
public class MyActivitySentry : AActivitySentry<MyEventDataManager, MyEventData>
{
/// <summary>
/// 主页入口按钮实例
/// </summary>
public IHomeEntranceBtn HomeEntranceBtn { get; set; }
protected override void OnEventActive(FishingEvent fishingEvent)
{
// 1. 获取或创建活动数据(自动从 PlayFab 加载)
var manager = ResolveManager();
var data = manager.RefreshData<MyEventData>(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、膨胀率、活动状态等:
public class MyEventData : AEventData
{
// 自定义数据字段
}
4. 数据管理器 (AEventDataManager)
管理活动数据的加载、保存、刷新:
public class MyEventDataManager : AEventDataManager<MyEventData>
{
protected override void OnDataInitialized(MyEventData data, FishingEvent eventConfig)
{
// 数据初始化逻辑
}
}
5. 主页入口按钮 (AbstractHomeEntranceBtn)
活动在主页的入口按钮框架,提供活动入口的通用功能:
public class MyActivityEntranceBtn : AbstractHomeEntranceBtn<MyEventData>
{
#region Abstract Implementation
/// <summary>
/// 关联的 Sentry 类型,用于注册按钮到哨兵
/// </summary>
protected override Type AssociatedSentryType => typeof(MyActivitySentry);
/// <summary>
/// 按钮点击事件处理
/// </summary>
protected override void OnClickBtn()
{
// 打开活动面板
}
/// <summary>
/// 获取图标资源名称
/// </summary>
protected override string GetIconName() => "icon_my_activity";
/// <summary>
/// 获取需要预加载的资源列表
/// </summary>
protected override List<string> GetResourceNames() => new() { "Prefab_MyActivity" };
#endregion
}
核心功能
| 功能 | 说明 |
|---|---|
| 泛型类型 | TData 活动数据类型 |
| Sentry 集成 | 自动注册/注销按钮到对应的 ActivitySentry |
| 定时器 | 自动每秒更新活动倒计时显示 |
| 资源预加载 | 活动开启前预加载所需资源 |
| 生命周期 | 继承 EventButtonResource,支持完整的创建/销毁流程 |
UI 组件绑定
框架会自动查找并绑定以下子物体:
| 字段 | 默认路径 | 说明 |
|---|---|---|
textTimer |
text_time |
倒计时文本 |
btn |
自身 | 按钮组件 |
icon |
icon_task |
图标图片 |
⚠️ 注意: 确保预制体中包含对应名称的子物体,否则需要手动赋值或重写绑定逻辑
6. 事件订阅器 (IActivityEventSubscriber)
订阅游戏内事件,触发活动状态检查:
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<T>(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. 创建新的活动
- 定义活动数据类
public class MyEventData : AEventData
{
public int TokenCount;
public List<int> RewardsClaimed;
}
- 定义数据管理器
public class MyEventDataManager : AEventDataManager<MyEventData>
{
protected override void OnDataInitialized(MyEventData data, FishingEvent eventConfig)
{
data.InflationRate = GContext.container.Resolve<PlayerData>().InflationRate;
}
}
- 定义活动哨兵
[ActivitySentry(eventType: 1, eventSubType: 1)]
public class MyActivitySentry : AActivitySentry
{
/// <summary>
/// 主页入口按钮实例
/// </summary>
public AbstractHomeEntranceBtn HomeEntranceBtn { get; set; }
protected override void OnEventActive(FishingEvent fishingEvent)
{
// 创建活动数据
var manager = ResolveManager<MyEventDataManager>((EventType, EventSubType));
var data = manager.RefreshData(fishingEvent);
// 通知按钮活动已开启
HomeEntranceBtn?.OnOpen();
}
protected override void OnEventClosed(FishingEvent fishingEvent)
{
// 保存数据
HomeEntranceBtn?.OnClose();
}
}
- 定义主页入口按钮
public class MyActivityEntranceBtn : AbstractHomeEntranceBtn<MyEventData, MyEventDataManager>
{
protected override Type AssociatedSentryType => typeof(MyActivitySentry);
protected override void OnClickBtn()
{
// 打开活动面板
}
protected override string GetIconName() => "icon_my_activity";
protected override List<string> GetResourceNames() => new() { "Prefab_MyActivity" };
}
2. 添加新的条件检查器
public class MyConditionChecker : IConditionChecker
{
public int Priority => 10;
public List<Type> CareDataChangeWithType => new()
{
typeof(PlayerLevelData)
};
public bool CanOpen(FishingEvent fishingEvent)
{
var playerLevel = GContext.container.Resolve<PlayerData>().Level;
return playerLevel >= fishingEvent.ConditionParam;
}
}
然后在 ConditionCheckerFactory.Initialize() 中注册。
3. 添加事件订阅器
public class MyActivityEventSubscriber : IActivityEventSubscriber
{
private IDisposable _subscription;
public void Subscribe(ActivityManager manager)
{
// 订阅游戏内事件
_subscription = MessageBroker.Default.Receive<SomeGameEvent>()
.Subscribe(_ => manager.OnActivitySingleEvent(...));
}
public void Unsubscribe()
{
_subscription?.Dispose();
}
}
注册机制
ActivityScopeManager 统一管理
注册逻辑统一放在 ActivityScopeManager 中,避免重复代码:
| 方法 | 说明 |
|---|---|
IsRegistered<TM>() |
检查类型是否已注册 |
GetOrRegisterInstance<TM>(typeKey) |
获取或注册实例(自动创建 Scope) |
GetRegisteredTypeKey<TM>() |
获取已注册类型对应的 typeKey |
// 在 AActivitySentry 中获取管理器实例
protected TM ResolveManager()
{
return _scopeManager.GetOrRegisterInstance<TM>((EventType, EventSubType));
}
// 在 ActivityManager 中解析
public TM Resolve<TM>() where TM : AEventDataManager
{
if (!_scopeManager.IsRegistered<TM>()) return null;
var typeKey = _scopeManager.GetRegisteredTypeKey<TM>();
if (typeKey == null) return null;
if (!_scopeManager.HasActiveScope(typeKey.Value)) return null;
return _scopeManager.GetOrRegisterInstance<TM>(typeKey.Value);
}
注册流程
ResolveManager() 调用
│
├── GetOrRegisterInstance(typeKey)
│ │
│ ├── 获取或创建 Scope (GetActivityScope)
│ │
│ ├── 如果未注册:
│ │ │
│ │ ├── GContext.container.Register<TM>().PerScope()
│ │ │
│ │ └── _activeRegistry.Add(typeof(TM), typeKey)
│ │
│ └── 返回 scope.GetInstance((typeof(TM), typeof(TM).Name))
│
└── 返回 TM 实例
配置表
活动配置使用 FishingEvent 配置表,包含:
- Type - 活动类型
- SubType - 活动子类型
- TimeDefinition - 时间定义
- ConditionCheckers - 条件检查器列表
注意事项
- Scope 隔离: 每个 (Type, SubType) 组合对应一个独立的 DI Scope
- 数据持久化: 通过
IEventDataRepository进行数据存储 - 代币管理: 通过
IEventTokenService统一管理活动代币 - 条件检查: 支持多条件组合,全部通过才能开启活动
- 生命周期: Sentry 实现了
IDisposable,确保资源正确释放 - 按钮预制体: 确保 UI 组件路径正确,或手动在 Inspector 中赋值
- Sentry 注册: 子类必须正确实现
AssociatedSentryType,否则按钮无法注册 - 统一注册: 所有管理器实例通过
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 仍需手动注册
无需改动框架即可新增/删除活动。