Project02/ProjectFish/Docs/TutorialSystemDesign_Simple.md

620 lines
14 KiB
Markdown
Raw Permalink Normal View History

2025-11-24 10:58:30 +08:00
# 新手引导系统设计(简化版)
## 一、核心思路
**一句话概括**:用一个 `TutorialManager` 管理引导流程,通过 Widget 的 `Tag` 控制显示/隐藏/高亮。
---
## 二、只需要3个东西
### 1. TutorialManager一个管理类
```cpp
// TutorialManager.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "TutorialManager.generated.h"
// 简单的步骤配置
USTRUCT(BlueprintType)
struct FTutorialStep
{
GENERATED_BODY()
// 步骤ID
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 StepID = 0;
// 可见的Widget标签其他都隐藏
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FName> VisibleWidgetTags;
// 高亮的Widget标签
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FName> HighlightWidgetTags;
// 提示文本(可选)
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FText HintText;
// 对话资产(可选)
UPROPERTY(EditAnywhere, BlueprintReadWrite)
class UDialogueAsset* DialogueAsset;
};
UCLASS()
class PROJECTFISH_API UTutorialManager : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
// 开始引导
UFUNCTION(BlueprintCallable, Category = "Tutorial")
void StartTutorial();
// 进入下一步
UFUNCTION(BlueprintCallable, Category = "Tutorial")
void NextStep();
// 完成引导
UFUNCTION(BlueprintCallable, Category = "Tutorial")
void FinishTutorial();
// 是否在引导中
UFUNCTION(BlueprintPure, Category = "Tutorial")
bool IsInTutorial() const { return bInTutorial; }
// 获取当前步骤
UFUNCTION(BlueprintPure, Category = "Tutorial")
int32 GetCurrentStep() const { return CurrentStepIndex; }
// 检查Widget是否应该显示
UFUNCTION(BlueprintPure, Category = "Tutorial")
bool ShouldShowWidget(FName WidgetTag) const;
// 检查Widget是否应该高亮
UFUNCTION(BlueprintPure, Category = "Tutorial")
bool ShouldHighlightWidget(FName WidgetTag) const;
// 隐藏遮罩层
UFUNCTION(BlueprintCallable, Category = "Tutorial")
void ShowMask(bool bShow);
protected:
// 引导步骤配置(在编辑器中配置)
UPROPERTY(EditDefaultsOnly, Category = "Tutorial")
TArray<FTutorialStep> TutorialSteps;
UPROPERTY()
bool bInTutorial = false;
UPROPERTY()
int32 CurrentStepIndex = -1;
private:
void ApplyCurrentStep();
void BroadcastStepChanged();
};
```
```cpp
// TutorialManager.cpp
#include "TutorialManager.h"
void UTutorialManager::StartTutorial()
{
bInTutorial = true;
CurrentStepIndex = 0;
ApplyCurrentStep();
}
void UTutorialManager::NextStep()
{
if (!bInTutorial) return;
CurrentStepIndex++;
if (CurrentStepIndex >= TutorialSteps.Num())
{
FinishTutorial();
return;
}
ApplyCurrentStep();
}
void UTutorialManager::FinishTutorial()
{
bInTutorial = false;
CurrentStepIndex = -1;
// 保存到存档
// GameInfoManager->SetTutorialCompleted(true);
ShowMask(false);
}
bool UTutorialManager::ShouldShowWidget(FName WidgetTag) const
{
if (!bInTutorial) return true; // 正常模式全显示
if (!TutorialSteps.IsValidIndex(CurrentStepIndex))
return true;
const FTutorialStep& Step = TutorialSteps[CurrentStepIndex];
// 如果没有配置,默认显示
if (Step.VisibleWidgetTags.Num() == 0)
return true;
// 白名单模式:只显示列表中的
return Step.VisibleWidgetTags.Contains(WidgetTag);
}
bool UTutorialManager::ShouldHighlightWidget(FName WidgetTag) const
{
if (!bInTutorial) return false;
if (!TutorialSteps.IsValidIndex(CurrentStepIndex))
return false;
const FTutorialStep& Step = TutorialSteps[CurrentStepIndex];
return Step.HighlightWidgetTags.Contains(WidgetTag);
}
void UTutorialManager::ApplyCurrentStep()
{
BroadcastStepChanged();
if (!TutorialSteps.IsValidIndex(CurrentStepIndex))
return;
const FTutorialStep& Step = TutorialSteps[CurrentStepIndex];
// 如果有对话,播放对话
if (Step.DialogueAsset)
{
// 调用对话系统播放
// DialogueSystem->PlayDialogue(Step.DialogueAsset);
}
// 显示遮罩
ShowMask(true);
}
void UTutorialManager::BroadcastStepChanged()
{
// 通知所有UI刷新
// 可以用事件委托或直接遍历所有Widget
}
void UTutorialManager::ShowMask(bool bShow)
{
// 创建/显示/隐藏遮罩Widget
// 遮罩Widget是一个全屏的半透明黑色背景
}
```
---
### 2. Widget上设置Tag
在UMG编辑器中给每个需要控制的Widget设置Tag
```
HomeUI:
├─ Btn_Sail (Tag: "Btn_Sail")
├─ Btn_Market (Tag: "Btn_Market")
├─ Btn_Shop (Tag: "Btn_Shop")
└─ Btn_Backpack (Tag: "Btn_Backpack")
```
---
### 3. Widget蓝图中检查是否显示
在每个Widget的蓝图中添加简单逻辑
```
Event Construct:
├─ Get TutorialManager
├─ Bind to OnStepChanged Event
└─ Call UpdateVisibility()
UpdateVisibility():
├─ For each child widget:
│ ├─ Get widget tag
│ ├─ TutorialManager->ShouldShowWidget(tag)
│ └─ Set Visibility (Visible / Collapsed)
└─ Update Highlight state
```
---
## 三、15步引导配置简单表格
`TutorialManager``TutorialSteps` 数组中配置:
| 步骤 | VisibleWidgetTags | HighlightWidgetTags | 说明 |
|------|-------------------|---------------------|------|
| 0 | [] | [] | 初始对话DialogueAsset配置 |
| 1 | [Btn_Sail] | [Btn_Sail] | 只显示出航按钮并高亮 |
| 2 | [MapNode_0] | [MapNode_0] | 地图只显示一个节点 |
| 3 | [] | [] | 进入Loading |
| 4 | [MovementHint] | [] | 显示移动提示 |
| 5 | [FishingHint] | [] | 显示钓鱼提示 |
| 6 | [BattleUI] | [] | 战斗界面(隐藏背包/返航) |
| ... | ... | ... | ... |
**配置方式**:在编辑器的 `Project Settings -> Tutorial Manager` 中直接填表格。
---
## 四、UI改造最简单的方式
### 方式1蓝图中直接判断推荐
在每个Widget的蓝图中
```blueprint
Event Construct:
├─ GetGameInstance
├─ GetSubsystem(TutorialManager)
├─ Bind Event: OnStepChanged -> UpdateVisibility
└─ Call UpdateVisibility
Function UpdateVisibility:
├─ If TutorialManager->IsInTutorial():
│ ├─ Set Btn_Sail Visibility: ShouldShowWidget("Btn_Sail")
│ ├─ Set Btn_Market Visibility: ShouldShowWidget("Btn_Market")
│ └─ ...
└─ Else: Show All
```
### 方式2C++中统一处理
创建一个简单的基类:
```cpp
// TutorialWidget.h
#pragma once
#include "Blueprint/UserWidget.h"
#include "TutorialWidget.generated.h"
UCLASS()
class UTutorialWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
protected:
// 在子类中填写Widget名 -> Tag的映射
UPROPERTY(EditDefaultsOnly, Category = "Tutorial")
TMap<FName, UWidget*> WidgetTagMap;
UFUNCTION()
void UpdateVisibility();
private:
UPROPERTY()
class UTutorialManager* TutorialManager;
};
```
```cpp
// TutorialWidget.cpp
#include "TutorialWidget.h"
#include "TutorialManager.h"
void UTutorialWidget::NativeConstruct()
{
Super::NativeConstruct();
TutorialManager = GetGameInstance()->GetSubsystem<UTutorialManager>();
if (TutorialManager)
{
// 绑定步骤改变事件
// TutorialManager->OnStepChanged.AddDynamic(this, &UTutorialWidget::UpdateVisibility);
}
UpdateVisibility();
}
void UTutorialWidget::UpdateVisibility()
{
if (!TutorialManager || !TutorialManager->IsInTutorial())
{
// 正常模式:全部显示
for (auto& Pair : WidgetTagMap)
{
if (Pair.Value)
Pair.Value->SetVisibility(ESlateVisibility::Visible);
}
return;
}
// 引导模式根据Tag判断
for (auto& Pair : WidgetTagMap)
{
if (!Pair.Value) continue;
bool bShouldShow = TutorialManager->ShouldShowWidget(Pair.Key);
Pair.Value->SetVisibility(bShouldShow ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
// 高亮效果
bool bShouldHighlight = TutorialManager->ShouldHighlightWidget(Pair.Key);
if (bShouldHighlight)
{
// 添加高亮动画或材质效果
// PlayAnimation(HighlightAnim);
}
}
}
```
然后让 HomeUI 继承这个基类,在构造函数中填写映射:
```cpp
// HomeUIWidget.cpp
void UHomeUIWidget::NativeConstruct()
{
Super::NativeConstruct();
// 填写映射
WidgetTagMap.Add(TEXT("Btn_Sail"), Btn_Sail);
WidgetTagMap.Add(TEXT("Btn_Market"), Btn_Market);
WidgetTagMap.Add(TEXT("Btn_Shop"), Btn_Shop);
WidgetTagMap.Add(TEXT("Btn_Backpack"), Btn_Backpack);
}
```
---
## 五、特殊情况处理
### 1. 战斗特殊配置
不需要新的配置类,直接在战斗开始时检查:
```cpp
// AProjectFishGameMode.cpp
void AProjectFishGameMode::StartBattle()
{
UTutorialManager* Tutorial = GetGameInstance()->GetSubsystem<UTutorialManager>();
if (Tutorial && Tutorial->IsInTutorial())
{
int32 Step = Tutorial->GetCurrentStep();
// 第一场战斗步骤6
if (Step == 6)
{
// 使用特定的鱼配置
CurrentFish = LoadObject<UFishInfoConfigAsset>(..., TEXT("Tutorial_Fish_01"));
// 强制掉落
bForceDropItem = true;
ForcedDropItems.Add(TEXT("Item_Bass"));
}
// 第二场战斗步骤9
else if (Step == 9)
{
CurrentFish = LoadObject<UFishInfoConfigAsset>(..., TEXT("Tutorial_Fish_02"));
ForcedDropItems.Add(TEXT("Item_Bass"));
ForcedDropItems.Add(TEXT("Item_MagicBook"));
}
}
}
```
### 2. 地图单节点
```cpp
// FishingMapSubSystem.cpp
void UFishingMapSubSystem::GenerateMap()
{
UTutorialManager* Tutorial = GetGameInstance()->GetSubsystem<UTutorialManager>();
if (Tutorial && Tutorial->IsInTutorial() && Tutorial->GetCurrentStep() == 2)
{
// 只生成1个节点
AllNodes.Empty();
UFishingMapNode* Node = NewObject<UFishingMapNode>();
Node->NodeType = EMapNodeType::Battle;
AllNodes.Add(Node);
return;
}
// 正常生成
GenerateMapWithConfig(DefaultMapConfig);
}
```
### 3. 强制拾取奖励
```cpp
// RewardWidget.cpp
void URewardWidget::NativeConstruct()
{
Super::NativeConstruct();
UTutorialManager* Tutorial = GetGameInstance()->GetSubsystem<UTutorialManager>();
if (Tutorial && Tutorial->IsInTutorial())
{
int32 Step = Tutorial->GetCurrentStep();
if (Step == 7 || Step == 10) // 两场战斗后
{
// 隐藏关闭按钮,直到拾取完毕
Btn_Close->SetVisibility(ESlateVisibility::Collapsed);
bMustCollectAll = true;
}
}
}
void URewardWidget::OnAllItemsCollected()
{
if (bMustCollectAll)
{
Btn_Close->SetVisibility(ESlateVisibility::Visible);
// 显示提示:"点击关闭继续"
}
}
```
---
## 六、遮罩和高亮效果(简单实现)
### 遮罩WidgetWBP_TutorialMask
创建一个简单的UMG
```
Canvas Panel (全屏)
└─ Image (黑色透明度0.7)
```
在 TutorialManager 中显示/隐藏它。
### 高亮效果3种简单方案
**方案1边框发光**
```cpp
// 给按钮添加一个Border设置发光颜色
Btn_Sail->SetBorderBrush(HighlightBrush);
```
**方案2缩放动画**
```
创建UMG动画
0.0s -> Scale 1.0
0.5s -> Scale 1.1
1.0s -> Scale 1.0
循环播放
```
**方案3后处理材质**
给高亮Widget添加后处理效果最炫酷但稍复杂
---
## 七、完整流程示例
### 配置TutorialSteps在编辑器中
```cpp
TutorialSteps[0]:
StepID: 0
DialogueAsset: DA_Tutorial_Intro
VisibleWidgetTags: []
HintText: ""
TutorialSteps[1]:
StepID: 1
VisibleWidgetTags: [Btn_Sail]
HighlightWidgetTags: [Btn_Sail]
HintText: "点击【出航】按钮"
TutorialSteps[2]:
StepID: 2
VisibleWidgetTags: [MapNode_0]
HighlightWidgetTags: [MapNode_0]
HintText: "选择一个地点"
// ... 继续配置到步骤15
```
### 调用流程
```cpp
// 游戏启动时
void AMyGameInstance::Init()
{
Super::Init();
UTutorialManager* Tutorial = GetSubsystem<UTutorialManager>();
// 检查存档
bool bCompletedTutorial = GameInfoManager->HasCompletedTutorial();
if (!bCompletedTutorial)
{
Tutorial->StartTutorial();
}
}
// 出航按钮点击时
void UHomeUIWidget::OnSailButtonClicked()
{
UTutorialManager* Tutorial = GetGameInstance()->GetSubsystem<UTutorialManager>();
if (Tutorial && Tutorial->IsInTutorial() && Tutorial->GetCurrentStep() == 1)
{
// 完成步骤1进入步骤2
Tutorial->NextStep();
}
// 打开地图
OpenMapUI();
}
// 地图节点点击时
void UMapWidget::OnNodeClicked()
{
UTutorialManager* Tutorial = GetGameInstance()->GetSubsystem<UTutorialManager>();
if (Tutorial && Tutorial->IsInTutorial() && Tutorial->GetCurrentStep() == 2)
{
Tutorial->NextStep();
}
// 进入战斗
StartBattle();
}
```
---
## 八、总结
### 只需要做3件事
1. **创建 TutorialManager**1个类~200行代码
2. **给UI Widget设置Tag**在UMG编辑器中操作
3. **配置TutorialSteps表格**(在编辑器中填数据)
### 改造工作量:
- ✅ 新增代码2个文件TutorialManager + TutorialWidget基类
- ✅ UI改造继承基类或蓝图中加几行代码
- ✅ 特殊逻辑在现有代码中加if判断
- ✅ 总代码量:<500行
### 优势:
- 🟢 **极简**只有1个核心类
- 🟢 **直观**:配置就是一个表格
- 🟢 **灵活**可蓝图可C++
- 🟢 **低侵入**:不破坏现有结构
---
## 九、文件结构
```
Source/ProjectFish/Tutorial/
├── TutorialManager.h # 管理类(核心)
├── TutorialManager.cpp
├── TutorialWidget.h # UI基类可选
└── TutorialWidget.cpp
Content/UI/Tutorial/
└── WBP_TutorialMask.uasset # 遮罩Widget
```
就这么简单!🎉