744 lines
26 KiB
Markdown
744 lines
26 KiB
Markdown
# 38 · NPC 任务链系统(Quest System)
|
||
|
||
> **命名空间** `BaseGames.Quest`
|
||
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
|
||
> **依赖** `BaseGames.Core.Events` · `BaseGames.Dialogue`(InteractableNPC、RelationshipManager)· `BaseGames.World`(SaveManager)· `BaseGames.Player`(PlayerStats)
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [系统总览](#1-系统总览)
|
||
2. [QuestSO — 任务数据](#2-questso--任务数据)
|
||
3. [QuestObjectiveSO — 目标类型](#3-questobjectiveso--目标类型)
|
||
4. [QuestManager — 核心管理器](#4-questmanager--核心管理器)
|
||
5. [QuestGiver — 任务 NPC](#5-questgiver--任务-npc)
|
||
6. [任务分支与条件](#6-任务分支与条件)
|
||
7. [好感度集成](#7-好感度集成)
|
||
8. [任务日志 UI](#8-任务日志-ui)
|
||
9. [SaveData 集成](#9-savedata-集成)
|
||
10. [事件频道](#10-事件频道)
|
||
11. [编辑器友好设计](#11-编辑器友好设计)
|
||
|
||
---
|
||
|
||
## 1. 系统总览
|
||
|
||
任务链系统负责追踪 **NPC 委托型任务**(探索交互/委托/支线)的进度与状态,与好感度系统、对话条件系统深度集成,是非线性世界叙事的骨架。
|
||
|
||
```
|
||
任务链系统职责:
|
||
├─ QuestSO → 任务定义(名称/目标链/奖励/分支条件)
|
||
├─ QuestObjectiveSO → 单步目标(与谁对话/击败谁/收集什么/到达哪里)
|
||
├─ QuestManager → 运行时追踪所有任务状态,响应游戏事件推进进度
|
||
├─ QuestGiver → 扩展 InteractableNPC,负责发布/完成任务、切换对话
|
||
└─ QuestLogUI → 任务日志界面(暂停菜单中的"任务"页)
|
||
```
|
||
|
||
**设计原则**:
|
||
- 任务状态为只进不退:`Unavailable → Available → Active → Completed / Failed`
|
||
- 每个目标是独立 SO,可在多个任务之间共享
|
||
- 任务进度完全事件驱动,QuestManager 只监听事件频道,不主动轮询
|
||
- 任务完成奖励通过 `RewardHandler` 分发,不在 QuestManager 中直接操作 PlayerStats
|
||
|
||
---
|
||
|
||
## 2. QuestSO — 任务数据
|
||
|
||
```csharp
|
||
[CreateAssetMenu(menuName = "Quest/Quest")]
|
||
public class QuestSO : ScriptableObject
|
||
{
|
||
[Header("标识")]
|
||
public string questId; // 唯一 ID,如 "Quest_FindMushroom"
|
||
public string displayName;
|
||
[TextArea(2, 6)]
|
||
public string description;
|
||
public Sprite icon; // 任务日志图标(可选)
|
||
|
||
[Header("目标链")]
|
||
public QuestObjectiveSO[] objectives; // 按顺序执行,全部完成 = 任务可交完
|
||
|
||
[Header("分支(可选)")]
|
||
public QuestBranch[] branches; // 任务完成后的后续任务(多选一)
|
||
|
||
[Header("前置条件")]
|
||
public string[] prerequisiteQuestIds; // 所有前置任务必须已完成才可接取
|
||
public int minAffinityToAccept; // 发布该任务的 NPC 好感度门槛(0 = 无限制)
|
||
|
||
[Header("奖励")]
|
||
public RewardSO reward; // 奖励 SO(见下方)
|
||
|
||
[Header("配置")]
|
||
public bool canFail; // 是否有失败条件(默认 false)
|
||
public QuestObjectiveSO failCondition; // 若 canFail = true,此目标触发 = 任务失败
|
||
}
|
||
|
||
[Serializable]
|
||
public class QuestBranch
|
||
{
|
||
public string conditionQuestId; // 若该任务已完成 → 走此分支(空 = 默认分支)
|
||
public QuestSO nextQuest; // 解锁的后续任务
|
||
public string npcDialogueKey; // 触发的 NPC 对话 key(切换 NPC 台词)
|
||
}
|
||
```
|
||
|
||
### RewardSO(奖励数据)
|
||
|
||
```csharp
|
||
[CreateAssetMenu(menuName = "Quest/Reward")]
|
||
public class RewardSO : ScriptableObject
|
||
{
|
||
public int geo; // Geo 奖励
|
||
public int soulBonus; // 灵魂槽扩展(+max)
|
||
public ItemSO[] items; // 物品奖励(工具/护符等)
|
||
public int affinityBonus; // 完成后对发布 NPC 好感度增量
|
||
public string unlockDialogueKey;// 解锁 NPC 新台词集合 key
|
||
public AbilityType unlockedAbility; // 若非 None → 解锁玩家能力
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. QuestObjectiveSO — 目标类型
|
||
|
||
所有目标继承 `QuestObjectiveSO` 抽象类:
|
||
|
||
```csharp
|
||
public abstract class QuestObjectiveSO : ScriptableObject
|
||
{
|
||
[Header("目标描述(日志显示)")]
|
||
public string objectiveText; // e.g. "与旅人 Aldric 对话"
|
||
public bool isOptional; // 可选目标(完成有额外奖励,不完成不影响主线)
|
||
|
||
/// <summary>
|
||
/// 向 QuestManager 注册需要监听的事件频道。
|
||
/// QuestManager 在激活目标时调用此方法。
|
||
/// </summary>
|
||
public abstract void RegisterListeners(IQuestObjectiveListener listener);
|
||
public abstract void UnregisterListeners(IQuestObjectiveListener listener);
|
||
|
||
/// <summary>
|
||
/// 返回是否已完成(数据持久化状态,供存档读取后还原)
|
||
/// </summary>
|
||
public abstract bool EvaluateCompletion(QuestObjectiveState state);
|
||
}
|
||
|
||
public interface IQuestObjectiveListener
|
||
{
|
||
void OnObjectiveTriggered(QuestObjectiveSO objective);
|
||
}
|
||
```
|
||
|
||
### 3.1 内置目标类型
|
||
|
||
#### TalkToNPCObjective
|
||
|
||
```csharp
|
||
[CreateAssetMenu(menuName = "Quest/Objectives/TalkToNPC")]
|
||
public class TalkToNPCObjective : QuestObjectiveSO
|
||
{
|
||
public string targetNpcId; // 目标 NPC 的 npcId
|
||
|
||
public override void RegisterListeners(IQuestObjectiveListener listener)
|
||
=> DialogueManager.Instance.OnNPCDialogueCompleted += id =>
|
||
{
|
||
if (id == targetNpcId)
|
||
listener.OnObjectiveTriggered(this);
|
||
};
|
||
|
||
public override void UnregisterListeners(IQuestObjectiveListener listener)
|
||
=> DialogueManager.Instance.OnNPCDialogueCompleted -= id =>
|
||
{
|
||
if (id == targetNpcId)
|
||
listener.OnObjectiveTriggered(this);
|
||
};
|
||
|
||
public override bool EvaluateCompletion(QuestObjectiveState state)
|
||
=> state.completed;
|
||
}
|
||
```
|
||
|
||
#### DefeatEnemyObjective
|
||
|
||
```csharp
|
||
[CreateAssetMenu(menuName = "Quest/Objectives/DefeatEnemy")]
|
||
public class DefeatEnemyObjective : QuestObjectiveSO
|
||
{
|
||
public string enemyTypeId; // 目标敌人类型(空 = 任意敌人)
|
||
public int requiredCount; // 需击败数量
|
||
|
||
// 通过 OnEnemyDefeated 事件频道监听
|
||
public override void RegisterListeners(IQuestObjectiveListener listener) { /* ... */ }
|
||
public override void UnregisterListeners(IQuestObjectiveListener listener) { /* ... */ }
|
||
|
||
public override bool EvaluateCompletion(QuestObjectiveState state)
|
||
=> state.progressCount >= requiredCount;
|
||
}
|
||
```
|
||
|
||
#### CollectItemObjective
|
||
|
||
```csharp
|
||
[CreateAssetMenu(menuName = "Quest/Objectives/CollectItem")]
|
||
public class CollectItemObjective : QuestObjectiveSO
|
||
{
|
||
public string itemId;
|
||
public int requiredAmount;
|
||
|
||
// 通过 OnCollectiblePickedUp 事件频道监听
|
||
}
|
||
```
|
||
|
||
#### ReachAreaObjective
|
||
|
||
```csharp
|
||
[CreateAssetMenu(menuName = "Quest/Objectives/ReachArea")]
|
||
public class ReachAreaObjective : QuestObjectiveSO
|
||
{
|
||
public string sceneId; // 目标场景名(进入即完成)
|
||
public string triggerZoneId; // 或场景内特定 TriggerZone 的 ID
|
||
|
||
// 通过 OnRoomEntered / 自定义 TriggerZone 事件监听
|
||
}
|
||
```
|
||
|
||
#### UseSkillObjective
|
||
|
||
```csharp
|
||
[CreateAssetMenu(menuName = "Quest/Objectives/UseSkill")]
|
||
public class UseSkillObjective : QuestObjectiveSO
|
||
{
|
||
public string skillId; // 如 "Parry" / "Tool_Slingshot"
|
||
public int requiredCount; // 使用次数(演示用技能的任务)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. QuestManager — 核心管理器
|
||
|
||
```csharp
|
||
namespace BaseGames.Quest
|
||
{
|
||
public class QuestManager : MonoBehaviour, IQuestObjectiveListener
|
||
{
|
||
[Header("事件频道—订阅")]
|
||
[SerializeField] StringEventChannelSO _onNPCDialogueCompleted;
|
||
[SerializeField] StringEventChannelSO _onEnemyDefeated;
|
||
[SerializeField] StringEventChannelSO _onCollectiblePickedUp;
|
||
[SerializeField] StringEventChannelSO _onRoomEntered;
|
||
[SerializeField] StringEventChannelSO _onSkillUsed;
|
||
|
||
[Header("事件频道—发布")]
|
||
[SerializeField] QuestEventChannelSO _onQuestStarted;
|
||
[SerializeField] QuestEventChannelSO _onQuestCompleted;
|
||
[SerializeField] QuestEventChannelSO _onQuestFailed;
|
||
[SerializeField] VoidEventChannelSO _onObjectiveUpdated;
|
||
|
||
// 运行时数据
|
||
readonly Dictionary<string, QuestRuntimeState> _quests = new();
|
||
|
||
// ──────────────── 生命周期 ─────────────────────────────────────────
|
||
|
||
void Awake()
|
||
{
|
||
// 从 SaveData 恢复状态
|
||
LoadFromSave();
|
||
}
|
||
|
||
void OnEnable()
|
||
{
|
||
_onNPCDialogueCompleted.OnEventRaised += HandleNPCDialogue;
|
||
_onEnemyDefeated.OnEventRaised += HandleEnemyDefeated;
|
||
_onCollectiblePickedUp.OnEventRaised += HandleCollectible;
|
||
_onRoomEntered.OnEventRaised += HandleRoomEntered;
|
||
_onSkillUsed.OnEventRaised += HandleSkillUsed;
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
_onNPCDialogueCompleted.OnEventRaised -= HandleNPCDialogue;
|
||
_onEnemyDefeated.OnEventRaised -= HandleEnemyDefeated;
|
||
_onCollectiblePickedUp.OnEventRaised -= HandleCollectible;
|
||
_onRoomEntered.OnEventRaised -= HandleRoomEntered;
|
||
_onSkillUsed.OnEventRaised -= HandleSkillUsed;
|
||
}
|
||
|
||
// ──────────────── 核心接口 ─────────────────────────────────────────
|
||
|
||
/// <summary>NPC 调用:接受任务</summary>
|
||
public bool TryAcceptQuest(QuestSO quest, string giverNpcId)
|
||
{
|
||
if (_quests.ContainsKey(quest.questId))
|
||
return false; // 已接取或已完成
|
||
|
||
if (!ArePrerequisitesMet(quest))
|
||
return false;
|
||
|
||
var state = new QuestRuntimeState(quest, giverNpcId);
|
||
_quests[quest.questId] = state;
|
||
ActivateCurrentObjective(state);
|
||
|
||
_onQuestStarted.Raise(quest);
|
||
SaveToData();
|
||
return true;
|
||
}
|
||
|
||
/// <summary>NPC 调用:完成任务(交任务时)</summary>
|
||
public bool TryCompleteQuest(string questId)
|
||
{
|
||
if (!_quests.TryGetValue(questId, out var state)) return false;
|
||
if (!state.IsReadyToComplete) return false;
|
||
|
||
state.Status = QuestStatus.Completed;
|
||
UnregisterObjectiveListeners(state);
|
||
|
||
_onQuestCompleted.Raise(state.Quest);
|
||
RewardHandler.Instance.GrantReward(state.Quest.reward, state.GiverNpcId);
|
||
|
||
// 解锁分支后续任务
|
||
var branch = SelectBranch(state.Quest);
|
||
if (branch?.nextQuest != null)
|
||
MakeQuestAvailable(branch.nextQuest.questId);
|
||
|
||
SaveToData();
|
||
return true;
|
||
}
|
||
|
||
// ──────────────── IQuestObjectiveListener ──────────────────────────
|
||
|
||
public void OnObjectiveTriggered(QuestObjectiveSO objective)
|
||
{
|
||
foreach (var (id, state) in _quests)
|
||
{
|
||
if (state.Status != QuestStatus.Active) continue;
|
||
var current = state.CurrentObjective;
|
||
if (current != objective) continue;
|
||
|
||
// 推进进度
|
||
state.AdvanceProgress();
|
||
if (state.IsCurrentObjectiveComplete())
|
||
state.MoveToNextObjective();
|
||
|
||
if (state.IsReadyToComplete)
|
||
_onObjectiveUpdated.Raise(); // 通知 UI 任务可交完
|
||
else
|
||
_onObjectiveUpdated.Raise(); // 通知 UI 进度更新
|
||
|
||
SaveToData();
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ──────────────── 内部辅助 ──────────────────────────────────────────
|
||
|
||
bool ArePrerequisitesMet(QuestSO quest)
|
||
{
|
||
foreach (var id in quest.prerequisiteQuestIds)
|
||
if (!IsQuestCompleted(id)) return false;
|
||
return true;
|
||
}
|
||
|
||
void ActivateCurrentObjective(QuestRuntimeState state)
|
||
{
|
||
state.CurrentObjective?.RegisterListeners(this);
|
||
}
|
||
|
||
void UnregisterObjectiveListeners(QuestRuntimeState state)
|
||
{
|
||
state.CurrentObjective?.UnregisterListeners(this);
|
||
}
|
||
|
||
QuestBranch SelectBranch(QuestSO quest)
|
||
{
|
||
if (quest.branches == null || quest.branches.Length == 0) return null;
|
||
foreach (var b in quest.branches)
|
||
if (!string.IsNullOrEmpty(b.conditionQuestId) && IsQuestCompleted(b.conditionQuestId))
|
||
return b;
|
||
// 返回默认分支(conditionQuestId 为空的最后一个)
|
||
return Array.Find(quest.branches, b => string.IsNullOrEmpty(b.conditionQuestId));
|
||
}
|
||
|
||
bool IsQuestCompleted(string questId)
|
||
=> _quests.TryGetValue(questId, out var s) && s.Status == QuestStatus.Completed;
|
||
|
||
void MakeQuestAvailable(string questId) { /* 在 _availableQuestIds 集合中标记,供 QuestGiver 查询 */ }
|
||
|
||
// ──────────────── 事件处理器(转发到各 Objective)──────────────────
|
||
|
||
void HandleNPCDialogue(string npcId) => BroadcastEvent("NPC", npcId);
|
||
void HandleEnemyDefeated(string typeId) => BroadcastEvent("Enemy", typeId);
|
||
void HandleCollectible(string itemId) => BroadcastEvent("Item", itemId);
|
||
void HandleRoomEntered(string sceneId) => BroadcastEvent("Room", sceneId);
|
||
void HandleSkillUsed(string skillId) => BroadcastEvent("Skill", skillId);
|
||
|
||
void BroadcastEvent(string category, string id)
|
||
{
|
||
// 将事件转发给当前所有激活目标,让目标自行判断是否匹配
|
||
foreach (var state in _quests.Values)
|
||
{
|
||
if (state.Status != QuestStatus.Active) continue;
|
||
state.CurrentObjective?.OnEvent(category, id, this);
|
||
}
|
||
}
|
||
|
||
void LoadFromSave() { /* 从 SaveManager.Instance.GetQuestData() 重建 _quests */ }
|
||
void SaveToData() { SaveManager.Instance.SetQuestData(_quests.Values); }
|
||
}
|
||
}
|
||
```
|
||
|
||
### QuestRuntimeState(运行时状态容器)
|
||
|
||
```csharp
|
||
public class QuestRuntimeState
|
||
{
|
||
public QuestSO Quest { get; }
|
||
public string GiverNpcId { get; }
|
||
public QuestStatus Status { get; set; }
|
||
|
||
int _objectiveIndex;
|
||
readonly int[] _progressCounts; // 每个目标的当前计数(用于 Count 类目标)
|
||
|
||
public QuestObjectiveSO CurrentObjective
|
||
=> _objectiveIndex < Quest.objectives.Length
|
||
? Quest.objectives[_objectiveIndex]
|
||
: null;
|
||
|
||
public bool IsReadyToComplete
|
||
=> _objectiveIndex >= Quest.objectives.Length && Status == QuestStatus.Active;
|
||
|
||
public QuestRuntimeState(QuestSO quest, string giverNpcId)
|
||
{
|
||
Quest = quest;
|
||
GiverNpcId = giverNpcId;
|
||
Status = QuestStatus.Active;
|
||
_objectiveIndex = 0;
|
||
_progressCounts = new int[quest.objectives.Length];
|
||
}
|
||
|
||
public void AdvanceProgress() => _progressCounts[_objectiveIndex]++;
|
||
public bool IsCurrentObjectiveComplete()
|
||
=> CurrentObjective?.EvaluateCompletion(GetState(_objectiveIndex)) ?? true;
|
||
|
||
public void MoveToNextObjective()
|
||
{
|
||
CurrentObjective?.UnregisterListeners(null); // 注销旧目标
|
||
_objectiveIndex++;
|
||
CurrentObjective?.RegisterListeners(null); // 注册新目标
|
||
}
|
||
|
||
QuestObjectiveState GetState(int idx) => new QuestObjectiveState
|
||
{
|
||
completed = _progressCounts[idx] > 0,
|
||
progressCount = _progressCounts[idx],
|
||
};
|
||
}
|
||
|
||
public struct QuestObjectiveState
|
||
{
|
||
public bool completed;
|
||
public int progressCount;
|
||
}
|
||
|
||
public enum QuestStatus
|
||
{
|
||
Unavailable, // 前置条件未满足
|
||
Available, // 可接取(NPC 有 ! 标志)
|
||
Active, // 进行中
|
||
Completed, // 已完成
|
||
Failed, // 已失败
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. QuestGiver — 任务 NPC
|
||
|
||
`QuestGiver` 继承 `InteractableNPC`,管理该 NPC 可以发布/完成的任务列表:
|
||
|
||
```csharp
|
||
namespace BaseGames.Quest
|
||
{
|
||
public class QuestGiver : InteractableNPC
|
||
{
|
||
[Header("任务")]
|
||
[SerializeField] QuestSO[] _offeredQuests; // 该 NPC 可发布的任务(按顺序尝试)
|
||
[SerializeField] string _npcId; // 与 Dialogue/Relationship 系统对齐
|
||
|
||
protected override void Interact_Internal(Transform player)
|
||
{
|
||
var pendingComplete = GetQuestReadyToComplete();
|
||
if (pendingComplete != null)
|
||
{
|
||
// 优先结算可完成的任务
|
||
PlayQuestCompleteDialogue(pendingComplete);
|
||
QuestManager.Instance.TryCompleteQuest(pendingComplete.questId);
|
||
return;
|
||
}
|
||
|
||
var available = GetAvailableQuest();
|
||
if (available != null)
|
||
{
|
||
// 提供新任务
|
||
PlayQuestAcceptDialogue(available);
|
||
QuestManager.Instance.TryAcceptQuest(available, _npcId);
|
||
return;
|
||
}
|
||
|
||
// 无任务 → 播放普通对话(含好感度对应台词)
|
||
base.Interact_Internal(player);
|
||
}
|
||
|
||
QuestSO GetQuestReadyToComplete()
|
||
{
|
||
foreach (var q in _offeredQuests)
|
||
if (QuestManager.Instance.IsReadyToComplete(q.questId))
|
||
return q;
|
||
return null;
|
||
}
|
||
|
||
QuestSO GetAvailableQuest()
|
||
{
|
||
foreach (var q in _offeredQuests)
|
||
if (QuestManager.Instance.CanAccept(q.questId))
|
||
return q;
|
||
return null;
|
||
}
|
||
|
||
void PlayQuestAcceptDialogue(QuestSO quest) =>
|
||
DialogueManager.Instance.Play(quest.questId + "_Accept");
|
||
|
||
void PlayQuestCompleteDialogue(QuestSO quest) =>
|
||
DialogueManager.Instance.Play(quest.questId + "_Complete");
|
||
}
|
||
}
|
||
```
|
||
|
||
**NPC 头顶图标规则**(由 `QuestIconController` 驱动):
|
||
|
||
| 状态 | 图标 | 颜色 |
|
||
|------|------|------|
|
||
| 可接新任务 | `!`(感叹号)| 黄色 |
|
||
| 玩家有可交付的任务 | `?`(问号)| 橙色 |
|
||
| 进行中(追踪中)| `⋯`(省略号)| 灰色 |
|
||
| 无任务 | 无图标 | — |
|
||
|
||
---
|
||
|
||
## 6. 任务分支与条件
|
||
|
||
### 分支示例:《寻找蘑菇》
|
||
|
||
```
|
||
Quest_FindMushroom
|
||
objectives:
|
||
[0] CollectItemObjective { itemId = "Item_GlowMushroom", requiredAmount = 3 }
|
||
[1] TalkToNPCObjective { targetNpcId = "NPC_Herbalist" }
|
||
branches:
|
||
[0] conditionQuestId = "Quest_DefeatForestBoss" → nextQuest = Quest_SpecialBrew
|
||
[1] conditionQuestId = ""(默认) → nextQuest = Quest_PoisonTip
|
||
```
|
||
|
||
`SelectBranch()` 在 `TryCompleteQuest` 时执行,根据当前存档状态选择一个分支解锁后续任务。
|
||
|
||
### 失败条件示例:《守护商队》
|
||
|
||
```
|
||
Quest_EscortCaravan
|
||
canFail = true
|
||
failCondition = DefeatEnemyObjective { enemyTypeId = "NPC_Caravan_Merchant", requiredCount = 1 }
|
||
(商队 NPC 被击败时任务失败)
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 好感度集成
|
||
|
||
任务系统与 §13 `RelationshipManager` 深度整合:
|
||
|
||
| 时机 | 行为 |
|
||
|------|------|
|
||
| 接取任务 | `RelationshipManager.AddAffinity(npcId, +5)` |
|
||
| 完成任务 | `RelationshipManager.AddAffinity(npcId, reward.affinityBonus)` |
|
||
| 好感度解锁新任务 | `QuestSO.minAffinityToAccept` 门槛检查 |
|
||
| 任务失败 | `RelationshipManager.AddAffinity(npcId, -10)` |
|
||
| 完成任务后 NPC 对话 | 切换至 `reward.unlockDialogueKey` 对应的台词集合 |
|
||
|
||
```csharp
|
||
// RewardHandler 在发放奖励时同步更新好感度
|
||
void GrantReward(RewardSO reward, string npcId)
|
||
{
|
||
PlayerStats.Instance.AddGeo(reward.geo);
|
||
PlayerStats.Instance.AddMaxSoul(reward.soulBonus);
|
||
|
||
foreach (var item in reward.items)
|
||
InventoryManager.Instance.AddItem(item);
|
||
|
||
if (reward.affinityBonus != 0)
|
||
RelationshipManager.Instance.AddAffinity(npcId, reward.affinityBonus);
|
||
|
||
if (!string.IsNullOrEmpty(reward.unlockDialogueKey))
|
||
DialogueManager.Instance.UnlockDialogueSet(npcId, reward.unlockDialogueKey);
|
||
|
||
if (reward.unlockedAbility != AbilityType.None)
|
||
PlayerStats.Instance.UnlockAbility(reward.unlockedAbility);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 任务日志 UI
|
||
|
||
暂停菜单中的"任务"标签页:
|
||
|
||
```
|
||
┌─ 任务日志 ─────────────────────────────────────────────┐
|
||
│ [进行中] [已完成] │
|
||
│ ─────────────────────────────────────────────────── │
|
||
│ ► 寻找蘑菇 ⋯ 进行中 │
|
||
│ 目标:将 发光蘑菇 交给 草药师 Aldric │
|
||
│ 进度:[✓] 收集 3/3 发光蘑菇 │
|
||
│ [ ] 与 Aldric 对话 │
|
||
│ ─────────────────────────────────────────────────── │
|
||
│ ► 守护商队 ✓ 已完成 │
|
||
└────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
```csharp
|
||
public class QuestLogUI : MonoBehaviour
|
||
{
|
||
[SerializeField] QuestEventChannelSO _onQuestStarted;
|
||
[SerializeField] QuestEventChannelSO _onQuestCompleted;
|
||
[SerializeField] VoidEventChannelSO _onObjectiveUpdated;
|
||
|
||
[SerializeField] QuestEntryWidget _entryPrefab;
|
||
[SerializeField] Transform _activeContainer;
|
||
[SerializeField] Transform _completedContainer;
|
||
|
||
void OnEnable()
|
||
{
|
||
_onQuestStarted.OnEventRaised += OnQuestStarted;
|
||
_onQuestCompleted.OnEventRaised += OnQuestCompleted;
|
||
_onObjectiveUpdated.OnEventRaised += Refresh;
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
_onQuestStarted.OnEventRaised -= OnQuestStarted;
|
||
_onQuestCompleted.OnEventRaised -= OnQuestCompleted;
|
||
_onObjectiveUpdated.OnEventRaised -= Refresh;
|
||
}
|
||
|
||
void Refresh()
|
||
{
|
||
// 从 QuestManager 获取当前所有任务状态,重建 UI 条目
|
||
ClearContainer(_activeContainer);
|
||
ClearContainer(_completedContainer);
|
||
|
||
foreach (var state in QuestManager.Instance.GetAllQuests())
|
||
{
|
||
var entry = Instantiate(_entryPrefab,
|
||
state.Status == QuestStatus.Completed ? _completedContainer : _activeContainer);
|
||
entry.Bind(state);
|
||
}
|
||
}
|
||
|
||
void OnQuestStarted(QuestSO _) => Refresh();
|
||
void OnQuestCompleted(QuestSO _) => Refresh();
|
||
|
||
void ClearContainer(Transform t)
|
||
{
|
||
foreach (Transform c in t)
|
||
Destroy(c.gameObject);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. SaveData 集成
|
||
|
||
```json
|
||
"quests": {
|
||
"active": [
|
||
{
|
||
"questId": "Quest_FindMushroom",
|
||
"giverNpcId": "NPC_Herbalist",
|
||
"objectiveIndex": 1,
|
||
"progressCounts": [3, 0]
|
||
}
|
||
],
|
||
"completed": ["Quest_EscortCaravan", "Quest_OldManLetter"],
|
||
"failed": [],
|
||
"availableQuestIds": ["Quest_SpecialBrew"]
|
||
}
|
||
```
|
||
|
||
```csharp
|
||
// SaveManager 扩展
|
||
public void SetQuestData(IEnumerable<QuestRuntimeState> states)
|
||
{
|
||
_saveData.quests.active = states
|
||
.Where(s => s.Status == QuestStatus.Active)
|
||
.Select(s => s.ToSaveData()).ToList();
|
||
_saveData.quests.completed = states
|
||
.Where(s => s.Status == QuestStatus.Completed)
|
||
.Select(s => s.Quest.questId).ToList();
|
||
WriteDirty();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 事件频道
|
||
|
||
| 频道资产 | 类型 | 发布方 | 主要订阅方 |
|
||
|---------|------|--------|----------|
|
||
| `OnQuestStarted.asset` | `QuestEventChannelSO` | `QuestManager` | `QuestLogUI`(添加条目)、`HUD`(屏幕提示)|
|
||
| `OnQuestCompleted.asset` | `QuestEventChannelSO` | `QuestManager` | `QuestLogUI`(移至完成列)、`AchievementManager` |
|
||
| `OnQuestFailed.asset` | `QuestEventChannelSO` | `QuestManager` | `QuestLogUI`、`DialogueManager`(触发失败台词)|
|
||
| `OnObjectiveUpdated.asset` | `VoidEventChannelSO` | `QuestManager` | `QuestLogUI`(刷新进度)、`HUD`(可交付提示)|
|
||
|
||
```csharp
|
||
[CreateAssetMenu(menuName = "Events/QuestEventChannel")]
|
||
public class QuestEventChannelSO : ScriptableObject
|
||
{
|
||
public event Action<QuestSO> OnEventRaised;
|
||
public void Raise(QuestSO quest) => OnEventRaised?.Invoke(quest);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 编辑器友好设计
|
||
|
||
### QuestManager Inspector(Play Mode)
|
||
|
||
```
|
||
┌─ QuestManager ─────────────────────────────────────────────┐
|
||
│ 进行中任务: │
|
||
│ ├─ Quest_FindMushroom [目标 1/2] 进度: 3/3 │
|
||
│ └─ Quest_GuardAldric [目标 0/1] 进度: ─ │
|
||
│ 已完成: 3 失败: 0 │
|
||
│ [强制完成选中任务] [重置选中任务] [解锁任务 ▼] │
|
||
└────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 新增任务 SOP(零代码)
|
||
|
||
1. `Create → Quest/Quest` → 填写 `questId`、`displayName`、`objectives`
|
||
2. 为每个目标 `Create → Quest/Objectives/{Type}`
|
||
3. 将任务 SO 拖入目标 NPC 的 `QuestGiver._offeredQuests`
|
||
4. 若有奖励:`Create → Quest/Reward`,填写 Geo / Item / Affinity
|
||
|
||
### 对话文件命名规范
|
||
|
||
```
|
||
Assets/Data/Dialogue/{QuestId}_Accept.asset ← 接任务台词
|
||
Assets/Data/Dialogue/{QuestId}_Complete.asset ← 完成任务台词
|
||
Assets/Data/Dialogue/{QuestId}_InProgress.asset ← 进行中每次对话台词
|
||
Assets/Data/Dialogue/{QuestId}_Failed.asset ← 失败台词(可选)
|
||
```
|