# 新手引导系统设计(简化版) ## 一、核心思路 **一句话概括**:用一个 `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 VisibleWidgetTags; // 高亮的Widget标签 UPROPERTY(EditAnywhere, BlueprintReadWrite) TArray 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 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 ``` ### 方式2:C++中统一处理 创建一个简单的基类: ```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 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(); 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(); if (Tutorial && Tutorial->IsInTutorial()) { int32 Step = Tutorial->GetCurrentStep(); // 第一场战斗(步骤6) if (Step == 6) { // 使用特定的鱼配置 CurrentFish = LoadObject(..., TEXT("Tutorial_Fish_01")); // 强制掉落 bForceDropItem = true; ForcedDropItems.Add(TEXT("Item_Bass")); } // 第二场战斗(步骤9) else if (Step == 9) { CurrentFish = LoadObject(..., 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(); if (Tutorial && Tutorial->IsInTutorial() && Tutorial->GetCurrentStep() == 2) { // 只生成1个节点 AllNodes.Empty(); UFishingMapNode* Node = NewObject(); Node->NodeType = EMapNodeType::Battle; AllNodes.Add(Node); return; } // 正常生成 GenerateMapWithConfig(DefaultMapConfig); } ``` ### 3. 强制拾取奖励 ```cpp // RewardWidget.cpp void URewardWidget::NativeConstruct() { Super::NativeConstruct(); UTutorialManager* Tutorial = GetGameInstance()->GetSubsystem(); 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); // 显示提示:"点击关闭继续" } } ``` --- ## 六、遮罩和高亮效果(简单实现) ### 遮罩Widget(WBP_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(); // 检查存档 bool bCompletedTutorial = GameInfoManager->HasCompletedTutorial(); if (!bCompletedTutorial) { Tutorial->StartTutorial(); } } // 出航按钮点击时 void UHomeUIWidget::OnSailButtonClicked() { UTutorialManager* Tutorial = GetGameInstance()->GetSubsystem(); if (Tutorial && Tutorial->IsInTutorial() && Tutorial->GetCurrentStep() == 1) { // 完成步骤1,进入步骤2 Tutorial->NextStep(); } // 打开地图 OpenMapUI(); } // 地图节点点击时 void UMapWidget::OnNodeClicked() { UTutorialManager* Tutorial = GetGameInstance()->GetSubsystem(); 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 ``` 就这么简单!🎉