Project02/ProjectFish/Docs/TutorialSystemDesign_Simple.md

620 lines
14 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.

# 新手引导系统设计(简化版)
## 一、核心思路
**一句话概括**:用一个 `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
```
就这么简单!🎉