620 lines
14 KiB
Markdown
620 lines
14 KiB
Markdown
|
|
# 新手引导系统设计(简化版)
|
|||
|
|
|
|||
|
|
## 一、核心思路
|
|||
|
|
|
|||
|
|
**一句话概括**:用一个 `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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 方式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<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);
|
|||
|
|
// 显示提示:"点击关闭继续"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、遮罩和高亮效果(简单实现)
|
|||
|
|
|
|||
|
|
### 遮罩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<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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
就这么简单!🎉
|