Files
ft/Client/Assets/Scripts/Activity/MIGRATION_GUIDE.md
2026-06-29 21:18:33 +08:00

428 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 旧活动框架迁移到新框架指南
本文档指导如何将旧的活动框架DataCenter + Act迁移到新的 Activity 框架。
---
## 一、迁移前的准备
### 1.1 了解新框架的核心组件
| 组件 | 作用 |
|------|------|
| `ActivityManager` | 活动总控,管理所有活动的开启/关闭 |
| `AActivitySentry<TM, TD>` | 活动哨兵基类,负责活动的生命周期(开启/关闭)及数据管理器解析 |
| `AEventDataManager<T>` | 活动数据管理器,负责数据的加载/保存 |
| `AEventData` | 活动数据类,定义活动运行时数据结构 |
| `AbstractHomeEntranceBtn` | 主页入口按钮,自动管理活动入口 |
### 1.2 旧框架与新框架对照
| 旧框架 | 新框架 | 兼容情况 |
|--------|--------|---------|
| `EventPotionDataManager` | `AEventDataManager<T>` | ✅ 已兼容 |
| `EventPotionData` | `AEventData` | ✅ 需迁移 |
| `FishingPotionAct : AGameAct` | `AActivitySentry<TM, TD>` | ✅ 需迁移 |
| `eventAggregator.Publish<T>()` | `IEventAggregator` | ✅ **完全兼容** |
| `GameEventMgr` | 保留共存 | ✅ **完全兼容** |
| `PlayFabMgr` 数据存储 | `EventDataRepository` | ✅ **已内置** |
---
## 二、IsActive 设计说明
### 2.1 设计原理
`AEventData.IsActive` 是活动是否显示的关键判断,其设计原则:
```csharp
// 基类实现 - 只判断必要条件
public virtual bool IsActive => EventID > 0 && RemainingTime.TotalSeconds > 0;
```
**为什么要保留 RemainingTime 判断?**
- 活动过期时需要**立即隐藏**按钮
- `TimeConditionChecker` 只在活动时间变化时触发(非每秒检查)
- `AbstractHomeEntranceBtn.UpdateTimer` 每秒调用,通过 `RemainingTime` 确保过期立即隐藏
**为什么要移除 StartTime 判断?**
- `TimeConditionChecker` 已在活动开启时判断过开始时间
- 重复判断会增加性能开销
### 2.2 子类重写 IsActive
子类可以重写 `IsActive` 添加业务条件判断:
```csharp
public class CapsulePackData : AEventData
{
public int CurrentRoundCount;
// 重写 IsActive 添加业务条件
public override bool IsActive
{
get
{
// 先调用基类判断活动时间
if (!base.IsActive)
return false;
// 添加业务条件:回合数未满
var maxCount = MainConfig.PackManagerID?.Count ?? 0;
return CurrentRoundCount < maxCount;
}
}
}
```
**注意**
- 始终先调用 `base.IsActive` 检查活动时间
- 业务条件判断应简洁,避免复杂计算
---
## 三、迁移步骤
### Step 1: 创建活动数据类 (AEventData)
**位置**: `Activity/Data/`
```csharp
using Newtonsoft.Json;
namespace Activity.Data
{
/// <summary>
/// 酿造活动数据
/// 对应旧框架的 EventPotionData
/// </summary>
public class EventPotionData : AEventData
{
public int PotionID;
public int CurrentLevel;
public List<LevelProcessedList> LevelProcessedList = new();
public int Score;
public int Token;
// 可添加旧框架的其他字段...
}
}
```
### Step 2: 创建活动数据管理器 (AEventDataManager)
**位置**: `Activity/Data/``DataCenter/`
```csharp
using Activity.Data;
using asap.core;
using cfg;
using GameCore;
using UniRx;
using UnityEngine;
namespace game
{
/// <summary>
/// 酿造活动数据管理器
/// 继承 AEventDataManager自动管理数据加载/保存
/// </summary>
public class EventPotionDataManager : AEventDataManager<EventPotionData>
{
#region
/// <summary>
/// 对应旧框架的 InitLevel()
/// </summary>
protected override void OnDataInitialized(EventPotionData data, FishingEvent eventConfig)
{
// 自动获取膨胀率
data.InflationRate = GContext.container.Resolve<PlayerData>().InflationRate;
// 旧框架 InitLevel() 逻辑可以搬到这里
if (data.LevelProcessedList == null || data.LevelProcessedList.Count == 0)
{
data.LevelProcessedList = new List<LevelProcessedList>();
}
}
/// <summary>
/// 对应旧框架的 FixPotionData()
/// </summary>
public void FixData(EventPotionData data)
{
if (data == null) return;
// 数据修复逻辑从旧框架搬过来
if (data.PotionID <= 0)
{
data.PotionID = 1;
}
}
/// <summary>
/// 获取配置表数据 - 旧框架的方法保留
/// </summary>
public EventPotionMain GetEventPotionMain(int id)
{
return Tables.TbEventPotionMain.GetOrDefault(id);
}
public EventPotionStage GetEventPotionStage(int id)
{
return Tables.TbEventPotionStage.GetOrDefault(id);
}
#endregion
}
}
```
### Step 3: 创建活动哨兵 (AActivitySentry)
**位置**: `Activity/Sentry/`
```csharp
using Activity.Condition;
using Activity.HomeEntranceBtn;
using cfg;
using game;
namespace Activity.Sentry
{
/// <summary>
/// 酿造活动哨兵
/// 对应旧框架的 FishingPotionAct
/// </summary>
[ActivitySentry(eventType: 12, eventSubType: 1)] // 对应配置表中的 Type 和 SubType
public class EventPotionSentry : AActivitySentry<EventPotionDataManager, EventPotionData>
{
/// <summary>
/// 主页入口按钮实例
/// </summary>
public AbstractHomeEntranceBtn HomeEntranceBtn { get; set; }
#region
/// <summary>
/// 活动开启时调用
/// 对应旧框架 FishingPotionAct.StartAsync()
/// </summary>
protected override void OnEventActive(FishingEvent fishingEvent)
{
// 1. 获取或创建活动数据(自动从 PlayFab 加载)
var manager = ResolveManager();
var data = manager.RefreshData(fishingEvent);
// 2. 数据修复(对应旧框架的 FixPotionData
manager.FixData(data);
// 3. 通知按钮活动已开启(自动预加载资源、显示入口)
HomeEntranceBtn?.OnOpen(fishingEvent, manager);
Debug.Log($"[EventPotionSentry] Activity opened: {fishingEvent.ID}");
}
/// <summary>
/// 活动关闭时调用
/// 对应旧框架 FishingPotionAct.StopAsync()
/// </summary>
protected override void OnEventClosed(FishingEvent fishingEvent)
{
// 数据会自动保存(通过 EventDataRepository
// 通知按钮活动关闭
HomeEntranceBtn?.OnClose(fishingEvent);
Debug.Log($"[EventPotionSentry] Activity closed: {fishingEvent.ID}");
}
/// <summary>
/// 活动切换时调用(可选)
/// </summary>
protected override void OnEventChanged(FishingEvent previousEventConfig, FishingEvent newEventConfig)
{
Debug.Log($"[EventPotionSentry] Activity changed: {previousEventConfig?.ID} -> {newEventConfig.ID}");
}
#endregion
}
}
```
### Step 4: 创建主页入口按钮(可选但推荐)
**位置**: `Activity/HomeEntranceBtn/`
```csharp
using Activity.Data;
using game;
using UnityEngine;
namespace Activity.HomeEntranceBtn
{
/// <summary>
/// 酿造活动入口按钮
/// 自动化管理活动入口的显示、预加载、倒计时
/// </summary>
public class EventPotionEntranceBtn : AbstractHomeEntranceBtn<EventPotionData, EventPotionDataManager>
{
#region Abstract Implementation
/// <summary>
/// 关联的 Sentry 类型
/// </summary>
protected override System.Type AssociatedSentryType => typeof(EventPotionSentry);
/// <summary>
/// 按钮点击事件
/// </summary>
protected override void OnClickBtn()
{
// 打开活动面板 - 和旧框架一样
UIManager.Instance.ShowUINotLoading(UITypes.EventPotionPanel);
}
/// <summary>
/// 图标资源名称
/// </summary>
protected override string GetIconName() => "icon_potion";
/// <summary>
/// 需要预加载的资源列表
/// </summary>
protected override System.Collections.Generic.List<string> GetResourceNames()
{
return new System.Collections.Generic.List<string>
{
"Prefab_EventPotionPanel",
"Atlas_Potion"
};
}
#endregion
}
}
```
---
## 四、保留旧框架功能
### 4.1 eventAggregator 事件系统(完全兼容)
```csharp
// UI 层继续使用原来的方式,无需修改!
public class EventPotionPanel : MonoBehaviour
{
private IEventAggregator _eventAggregator = new EventAggregator();
private void OnClick()
{
// 和旧框架完全一样
_eventAggregator.Publish(new HideHomePanelEvent());
// 或者获取事件
_eventAggregator.GetEvent<MyEvent>().Subscribe(OnEvent);
}
}
```
### 4.2 GameEventMgr 全局事件(完全兼容)
```csharp
// 继续使用旧框架的全局事件,无需修改!
public class EventPotionPanel : MonoBehaviour
{
private void OnEnable()
{
GameEventMgr.Instance.AddListener(MyEvents.LEVEL_COMPLETE, OnLevelComplete);
}
private void OnDisable()
{
GameEventMgr.Instance.RemoveListener(MyEvents.LEVEL_COMPLETE, OnLevelComplete);
}
private void OnLevelComplete(int level)
{
// 处理事件
}
}
```
### 4.3 数据存储(自动兼容)
```csharp
// 新框架自动使用 PlayFabMgr 保存数据,无需手动调用 SyncData
public class EventPotionDataManager : AEventDataManager<EventPotionData>
{
public void AddScore(int score)
{
var data = LoadData();
data.Score += score;
// 自动保存到 PlayFab通过 EventDataRepository
SaveData(data);
}
}
```
---
## 五、配置表映射
旧框架的活动配置通常在 `FishingEvent` 配置表中定义,迁移时需要确保:
| 配置字段 | 说明 |
|----------|------|
| `Type` | 活动类型,对应 `ActivitySentryAttribute``eventType` |
| `SubType` | 活动子类型,对应 `ActivitySentryAttribute``eventSubType` |
| `TimeDefinition` | 时间定义,用于 `TimeConditionChecker` |
| `ConditionCheckers` | 条件检查器列表 |
```csharp
// 在 FishingEvent 配置表中添加条件检查器
// 新框架会自动读取并执行条件检查
```
---
## 六、迁移检查清单
- [ ] 创建活动数据类 `AEventData`
- [ ] 创建活动数据管理器 `AEventDataManager<T>`
- [ ] 迁移数据初始化逻辑InitLevel → OnDataInitialized
- [ ] 迁移数据修复逻辑FixPotionData → FixData
- [ ] 创建活动哨兵 `AActivitySentry`
- [ ] 标注特性 `[ActivitySentry(eventType: X, eventSubType: Y)]`
- [ ] 迁移活动入口逻辑StartAsync → OnEventActive
- [ ] 创建主页入口按钮(可选)
- [ ] 保留 eventAggregator 用法UI层代码不变
- [ ] 保留 GameEventMgr 用法(代码不变)
- [ ] 删除旧框架的 Act 类
---
## 七、常见问题
### Q1: 旧框架的 Act 有复杂的资源加载逻辑怎么办?
**A**: 在 `OnEventActive` 中保留资源加载逻辑,或在 `HomeEntranceBtn.GetResourceNames()` 中配置预加载资源。
### Q2: 活动需要网络请求怎么办?
**A**: 新框架暂未内置网络层,建议在 UI 层调用原有的 RTService 或在 Sentry 中添加网络逻辑。
### Q3: 多个同类型活动如何处理?
**A**: 当前 Sentry 设计为每个 (Type, SubType) 组合一个实例,如需多实例需要扩展框架。
---
## 八、相关文档
- [Activity 框架 README](./README.md)
- [条件检查器使用指南](./Condition/)
- [事件订阅器使用指南](./EventSubscriber/)