尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity任务系统架构设计:基于委托与ScriptableObject的解耦实践

Unity任务系统架构设计:基于委托与ScriptableObject的解耦实践 1. 项目概述为什么我们需要一个“委托式”任务系统在Unity游戏开发中任务系统几乎是所有RPG、开放世界、模拟经营乃至许多动作冒险游戏的核心骨架。它驱动着玩家的目标引导着游戏流程承载着叙事和奖励。然而很多开发者在初次构建任务系统时往往会陷入一个困境代码高度耦合任务逻辑与UI、数据、条件判断、奖励发放等模块纠缠在一起导致系统难以扩展、难以测试设计师想要调整一个任务目标程序员就得跟着改代码。这就是“委托实现”这个标题背后真正的痛点。它不是一个简单的“如何用C#委托Delegate写个任务列表”的教程而是指向一种更高级、更优雅的架构思想——策略模式Strategy Pattern与事件驱动Event-Driven的结合体其核心是利用C#委托或更广义的“委托对象”如ScriptableObject来解耦任务逻辑的“定义”与“执行”。想象一下一个任务“收集10个苹果”。传统做法可能是在Quest类里有一个UpdateProgress方法里面硬编码了检查玩家背包里苹果数量的逻辑一旦满足条件就调用CompleteQuest然后可能又硬编码了发放金币和经验值的逻辑。这种写法每增加一种新的任务类型如“击杀5只狼”、“到达某个地点”你就要修改Quest类添加新的条件判断和奖励逻辑代码会迅速膨胀成一个难以维护的“巨类”。而“委托实现”的思路是将“检查是否完成”这个行为抽象出来变成一个独立的、可插拔的“策略”。Quest类本身不关心具体怎么检查它只持有一个“完成条件检查器”一个委托或一个策略对象。同样任务奖励的发放也被抽象成另一个独立的“奖励执行器”。这样Quest类就变成了一个纯粹的容器它只管理任务的状态未接取、进行中、已完成和持有这些策略的引用。当需要判断任务是否完成时它只需调用持有的“检查器委托”当需要发放奖励时就调用“奖励器委托”。这种架构带来的好处是革命性的高可扩展性要新增一种任务类型比如“与NPC对话3次”你无需修改任何现有任务相关的核心代码只需创建一个新的“对话次数检查器”策略对象并在编辑器中将其赋给任务即可。设计师友好利用Unity的ScriptableObject你可以将各种检查器和奖励器做成可配置的资源文件。策划人员可以在Unity编辑器里像搭积木一样通过拖拽不同的ScriptableObject资源来组合出复杂的任务逻辑无需程序员介入。高可测试性每个策略对象如“击杀怪物检查器”都是独立的、功能单一的类可以很容易地进行单元测试。逻辑复用“收集10个苹果”和“收集5个铁矿”可以共用同一个“物品收集检查器”只是配置的参数物品ID和数量不同。接下来我将带你从零开始构建一个基于这种“委托/策略模式”思想的任务系统。我们会深入核心设计拆解每一个技术细节并分享在实际项目中积累的宝贵经验和避坑指南。2. 核心架构设计解耦的艺术构建一个健壮的任务系统第一步不是写代码而是设计清晰的架构。我们的目标是实现高度的模块化和解耦。整个系统可以划分为以下几个核心层次2.1 数据层ScriptableObject作为配置载体这是系统的基石。我们将大量使用ScriptableObject来存储静态的任务数据。为什么不直接用普通的C#类因为ScriptableObject是Unity的资产Asset拥有诸多优势编辑器可视化所有字段都可以在Inspector窗口中显示和编辑对策划极其友好。资源管理可以作为.asset文件保存在项目中方便版本管理和批量操作。运行时引用MonoBehaviour可以持有对ScriptableObject实例的引用实现数据与逻辑的分离。我们会创建以下几种核心的ScriptableObjectQuestDataSO任务的本体数据容器。包含任务ID、名称、描述、任务步骤列表等。但它不包含任何游戏逻辑。QuestStepSO任务步骤的数据容器。描述一个步骤的目标如“收集”、“击杀”。TaskConditionSO抽象基类这就是我们的“委托对象”或“策略对象”。它定义了一个IsConditionMet(QuestContext context)的抽象方法。具体的条件检查逻辑由其子类实现如CollectItemConditionSO,KillEnemyConditionSO,ReachLocationConditionSO。TaskRewardSO抽象基类同理定义GrantReward(QuestContext context)抽象方法。子类如ExperienceRewardSO,CurrencyRewardSO,ItemRewardSO。设计要点QuestDataSO通过序列化字段持有TaskConditionSO和TaskRewardSO的引用数组。这意味着在编辑器中你可以为一个任务配置多个完成条件和多个奖励只需拖拽对应的ScriptableObject资源即可。2.2 逻辑层MonoBehaviour作为运行时管理器数据是静态的需要动态的逻辑来驱动。这一层主要由MonoBehaviour构成负责在游戏运行时实例化任务、追踪进度、响应事件。QuestManager单例或服务类管理所有已接取和可接取的任务实例。它维护一个Dictionarystring, Quest键是任务ID。它还负责从QuestDataSO创建运行时任务对象。Quest类纯C#类任务的运行时实例。它包含对QuestDataSO的引用以及当前步骤索引、各步骤的完成状态等运行时数据。它的核心方法是UpdateProgress()该方法会遍历当前步骤的所有TaskConditionSO调用其IsConditionMet方法并根据结果更新状态。QuestContext结构体这是一个关键设计。当调用IsConditionMet或GrantReward时我们需要传递一些上下文信息比如是哪个玩家触发的、当前游戏状态等。将其封装成一个QuestContext结构体进行传递比传递一堆散乱的参数要清晰和灵活得多也便于未来扩展。2.3 通信层基于ScriptableObject的事件通道Event Channel这是连接任务系统与游戏其他模块如背包系统、战斗系统、场景系统的桥梁。我们绝不能让QuestManager直接去调用InventoryManager.GetItemCount()这又会产生耦合。解决方案是使用事件驱动。我们创建一系列GameEventSO例如ItemCollectedEventSO,EnemyKilledEventSO,LocationReachedEventSO。这些也是ScriptableObject它们内部维护着一个C#事件ActionT。当玩家拾取物品时InventoryManager会触发RaiseItemCollectedEventSO事件并传递物品ID和数量。QuestManager或专门的事件监听器会监听Subscribe这些事件。当事件触发时它会遍历所有进行中的任务通知它们“有一个ItemCollectedEvent事件发生了你们检查一下自己的条件是否满足。”每个TaskConditionSO如CollectItemConditionSO在检查时会从传入的QuestContext中获取到相关的事件数据并与自身配置的物品ID和数量进行比对。这种设计实现了彻底的解耦。战斗系统不知道任务系统的存在它只是在怪物死亡时广播一个事件。任务系统监听这个事件并做出反应。任何模块都可以广播或监听事件系统间的协作变得清晰而灵活。3. 关键实现细节与代码剖析理论讲完了我们进入实战环节。让我们深入代码看看这些模块是如何具体实现的。3.1 定义核心委托对象TaskConditionSO这是整个系统的“心脏”。我们先从抽象基类开始。using UnityEngine; public abstract class TaskConditionSO : ScriptableObject { [TextArea] public string conditionDescription; // 给策划看的条件描述如“收集10个苹果” /// summary /// 检查此条件是否满足 /// /summary /// param namecontext任务上下文包含触发事件的相关数据/param /// returns是否满足条件/returns public abstract bool IsConditionMet(QuestContext context); /// summary /// 获取当前进度描述用于UI显示例如“3/10” /// /summary public virtual string GetProgressDescription(QuestContext context) { return conditionDescription; } }接下来实现一个具体的收集物品条件[CreateAssetMenu(fileName NewCollectCondition, menuName Quest System/Conditions/Collect Item)] public class CollectItemConditionSO : TaskConditionSO { public string targetItemId; // 目标物品的ID public int requiredAmount 1; // 需要收集的数量 private int currentAmount 0; // 注意这里不能直接存进度 public override bool IsConditionMet(QuestContext context) { // 重要context应该包含从ItemCollectedEvent传递过来的数据 if (context.EventData is ItemCollectedEventData itemData) { if (itemData.ItemId targetItemId) { // 这里有一个关键设计决策进度存储在哪里 // 方案A存储在Condition对象自身如this.currentAmount。问题ScriptableObject是资产所有引用共享同一份数据会导致进度串扰 // 方案B存储在Quest的运行时实例中。这是正确做法。 // 因此IsConditionMet不应该直接修改自身状态而是返回一个“检查结果”。 // 进度的累加应由Quest类在接收到事件后在专门的进度字典中进行。 return true; // 仅表示“此次事件匹配条件” } } // 也可能需要检查玩家当前背包总数这需要另一种事件或直接查询尽量用事件 return false; } public override string GetProgressDescription(QuestContext context) { // 从context中获取该任务该条件的当前进度由QuestManager维护 int current context.GetConditionProgress(this); return ${conditionDescription} ({current}/{requiredAmount}); } }关键陷阱与解决方案注意ScriptableObject的共享状态问题这是新手最容易踩的坑。CollectItemConditionSO是一个.asset文件。如果游戏中有两个任务都使用了同一个CollectItemConditionSO资产比如都是“收集苹果”并且你在该SO内部用currentAmount字段存储进度那么当一个任务完成收集后currentAmount被修改另一个任务的进度也会突然变成已完成因为所有引用指向的是同一个对象实例。正确做法进度数据必须存储在运行时对象中例如Quest类内部的一个DictionaryTaskConditionSO, int用来记录每个条件当前的完成量。TaskConditionSO本身只提供配置需要多少和判断逻辑如何算满足。3.2 构建事件通信系统首先定义事件通道的基类using UnityEngine; using UnityEngine.Events; public abstract class GameEventSOT : ScriptableObject { // 使用UnityEvent便于在编辑器中配置响应但这里我们主要用C# Action // 为了解耦我们同时支持两种方式 [System.NonSerialized] // 防止Unity序列化我们需要在运行时重建 public UnityEventT unityEventResponse new UnityEventT(); // C#事件性能更好更适用于脚本间通信 [System.NonSerialized] public System.ActionT OnEventRaised; public void RaiseEvent(T eventData) { // 先调用UnityEvent方便编辑器内连线 unityEventResponse?.Invoke(eventData); // 再调用C# Action OnEventRaised?.Invoke(eventData); } // 提供一个注册方法方便管理 public void RegisterListener(System.ActionT action) { OnEventRaised action; } public void UnregisterListener(System.ActionT action) { OnEventRaised - action; } } // 具体的事件数据类 public struct ItemCollectedEventData { public string ItemId; public int Amount; public GameObject Collector; // 收集者 } // 具体的事件通道资产 [CreateAssetMenu(fileName ItemCollectedEvent, menuName Quest System/Events/Item Collected)] public class ItemCollectedEventSO : GameEventSOItemCollectedEventData { }在物品收集的逻辑处触发事件public class Inventory : MonoBehaviour { // 在Inspector中拖入赋值 public ItemCollectedEventSO itemCollectedEvent; public void AddItem(string itemId, int amount) { // ... 实际添加物品的逻辑 ... // 触发事件 if (itemCollectedEvent ! null) { itemCollectedEvent.RaiseEvent(new ItemCollectedEventData { ItemId itemId, Amount amount, Collector this.gameObject }); } } }3.3 实现任务管理器与运行时任务QuestManager是大脑它监听所有游戏事件并更新对应的任务。public class QuestManager : MonoBehaviour { public static QuestManager Instance { get; private set; } // 所有可接取任务的配置 public ListQuestDataSO allQuestData; // 运行时任务字典 private Dictionarystring, Quest activeQuests new Dictionarystring, Quest(); // 需要监听的事件通道在Inspector中拖入 public ItemCollectedEventSO itemCollectedEvent; public EnemyKilledEventSO enemyKilledEvent; // ... 其他事件 // 任务进度改变的事件用于通知UI更新 public UnityEventQuest onQuestProgressChanged; private void Awake() { if (Instance ! null Instance ! this) { Destroy(this); return; } Instance this; } private void OnEnable() { // 注册事件监听 if (itemCollectedEvent ! null) itemCollectedEvent.OnEventRaised OnItemCollected; if (enemyKilledEvent ! null) enemyKilledEvent.OnEventRaised OnEnemyKilled; } private void OnDisable() { // 注销监听防止内存泄漏 if (itemCollectedEvent ! null) itemCollectedEvent.OnEventRaised - OnItemCollected; if (enemyKilledEvent ! null) enemyKilledEvent.OnEventRaised - OnEnemyKilled; } private void OnItemCollected(ItemCollectedEventData data) { // 构建上下文 var context new QuestContext { EventData data, Trigger data.Collector }; // 遍历所有进行中的任务通知它们更新 foreach (var quest in activeQuests.Values) { quest.UpdateProgress(context); } } // 接取任务 public void AcceptQuest(string questId) { if (activeQuests.ContainsKey(questId)) { Debug.LogWarning($Quest {questId} is already active.); return; } var questData allQuestData.Find(q q.questId questId); if (questData null) { Debug.LogError($Quest data not found for ID: {questId}); return; } var newQuest new Quest(questData); activeQuests.Add(questId, newQuest); // 初始化任务开始追踪 newQuest.Start(); onQuestProgressChanged?.Invoke(newQuest); } // 其他方法提交任务、放弃任务等... }最后是核心的Quest运行时类[System.Serializable] public class Quest { public QuestDataSO Data { get; private set; } public QuestState State { get; private set; } public int CurrentStepIndex { get; private set; } // 存储每个条件Condition的当前进度 public DictionaryTaskConditionSO, int ConditionProgress { get; private set; } public Quest(QuestDataSO data) { Data data; State QuestState.NotStarted; CurrentStepIndex 0; ConditionProgress new DictionaryTaskConditionSO, int(); // 初始化进度字典 foreach (var step in Data.steps) { foreach (var condition in step.conditions) { if (!ConditionProgress.ContainsKey(condition)) { ConditionProgress[condition] 0; } } } } public void Start() { if (State ! QuestState.NotStarted) return; State QuestState.InProgress; // 可以在这里触发任务开始事件 } public void UpdateProgress(QuestContext context) { if (State ! QuestState.InProgress) return; var currentStep Data.steps[CurrentStepIndex]; bool stepCompleted true; // 检查当前步骤的所有条件 foreach (var condition in currentStep.conditions) { // 调用委托对象进行检查 if (condition.IsConditionMet(context)) { // 条件满足更新进度 // 注意这里需要根据事件数据来增加进度例如收集了1个苹果进度1 // 这需要更精细的事件数据设计例如ItemCollectedEventData里包含数量。 // 简单实现每次匹配事件进度1。 ConditionProgress[condition] Mathf.Min(ConditionProgress[condition] 1, condition.requiredAmount); } // 判断该条件是否最终完成 if (ConditionProgress[condition] condition.requiredAmount) { stepCompleted false; } } if (stepCompleted) { CompleteCurrentStep(); } } private void CompleteCurrentStep() { // 发放步骤奖励如果有 var currentStep Data.steps[CurrentStepIndex]; foreach (var reward in currentStep.rewards) { reward.GrantReward(new QuestContext()); // 需要更丰富的上下文 } CurrentStepIndex; if (CurrentStepIndex Data.steps.Count) { CompleteQuest(); } else { // 进入下一步可以触发事件 } } private void CompleteQuest() { State QuestState.Completed; // 发放最终奖励 foreach (var reward in Data.finalRewards) { reward.GrantReward(new QuestContext()); } // 触发任务完成事件 QuestManager.Instance?.OnQuestCompleted(this); } } public enum QuestState { NotStarted, InProgress, Completed, Failed }4. 编辑器扩展与工作流优化一个强大的系统离不开好用的工具。为了让策划能高效工作我们必须对Unity编辑器进行扩展。4.1 自定义QuestDataSO的Inspector界面默认的ScriptableObject Inspector虽然能用但不够直观。我们可以使用CustomEditor来创建一个更友好的界面。using UnityEditor; using UnityEngine; [CustomEditor(typeof(QuestDataSO))] public class QuestDataSOEditor : Editor { private SerializedProperty questIdProp; private SerializedProperty questNameProp; private SerializedProperty stepsProp; private void OnEnable() { questIdProp serializedObject.FindProperty(questId); questNameProp serializedObject.FindProperty(questName); stepsProp serializedObject.FindProperty(steps); } public override void OnInspectorGUI() { serializedObject.Update(); EditorGUILayout.LabelField(任务基础信息, EditorStyles.boldLabel); EditorGUILayout.PropertyField(questIdProp); EditorGUILayout.PropertyField(questNameProp); EditorGUILayout.Space(10); EditorGUILayout.LabelField(任务步骤, EditorStyles.boldLabel); // 使用ReorderableList来管理步骤列表支持拖拽排序 if (GUILayout.Button(添加新步骤)) { stepsProp.arraySize; } for (int i 0; i stepsProp.arraySize; i) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); var stepProp stepsProp.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(stepProp.FindPropertyRelative(stepDescription)); // 显示条件列表 var conditionsProp stepProp.FindPropertyRelative(conditions); EditorGUILayout.LabelField(完成条件, EditorStyles.miniBoldLabel); EditorGUI.indentLevel; for (int j 0; j conditionsProp.arraySize; j) { EditorGUILayout.PropertyField(conditionsProp.GetArrayElementAtIndex(j), GUIContent.none); } if (GUILayout.Button( 添加条件, EditorStyles.miniButton)) { conditionsProp.arraySize; } EditorGUI.indentLevel--; // 显示奖励列表 var rewardsProp stepProp.FindPropertyRelative(rewards); EditorGUILayout.LabelField(步骤奖励, EditorStyles.miniBoldLabel); EditorGUI.indentLevel; for (int j 0; j rewardsProp.arraySize; j) { EditorGUILayout.PropertyField(rewardsProp.GetArrayElementAtIndex(j), GUIContent.none); } if (GUILayout.Button( 添加奖励, EditorStyles.miniButton)) { rewardsProp.arraySize; } EditorGUI.indentLevel--; if (GUILayout.Button(删除此步骤, EditorStyles.miniButton)) { stepsProp.DeleteArrayElementAtIndex(i); break; // 删除后退出循环避免索引错误 } EditorGUILayout.EndVertical(); EditorGUILayout.Space(5); } serializedObject.ApplyModifiedProperties(); } }4.2 创建资源菜单与快速创建工具为了让策划快速创建各种条件和奖励我们需要在Assets/Create菜单中添加对应的选项。这通过CreateAssetMenu属性实现正如前面代码示例中的[CreateAssetMenu(fileName NewCollectCondition, menuName Quest System/Conditions/Collect Item)]。更进一步可以创建一个编辑器窗口用于批量创建或管理任务链。using UnityEditor; using UnityEngine; public class QuestSystemEditorWindow : EditorWindow { [MenuItem(Tools/Quest System/Quest Editor)] public static void ShowWindow() { GetWindowQuestSystemEditorWindow(任务编辑器); } private void OnGUI() { GUILayout.Label(任务系统管理, EditorStyles.boldLabel); if (GUILayout.Button(创建新任务配置)) { var questData ScriptableObject.CreateInstanceQuestDataSO(); questData.questId $QUEST_{System.Guid.NewGuid().ToString().Substring(0, 8)}; questData.questName 新任务; string path EditorUtility.SaveFilePanelInProject(保存任务配置, NewQuest, asset, 请选择保存位置); if (!string.IsNullOrEmpty(path)) { AssetDatabase.CreateAsset(questData, path); AssetDatabase.SaveAssets(); EditorGUIUtility.PingObject(questData); } } if (GUILayout.Button(验证所有任务配置)) { ValidateAllQuests(); } } private void ValidateAllQuests() { // 查找所有QuestDataSO检查ID是否重复条件/奖励引用是否为空等 // 这是一个非常实用的工具能在开发早期发现配置错误 var allQuestGuids AssetDatabase.FindAssets(t:QuestDataSO); HashSetstring idSet new HashSetstring(); foreach (var guid in allQuestGuids) { string path AssetDatabase.GUIDToAssetPath(guid); var quest AssetDatabase.LoadAssetAtPathQuestDataSO(path); if (idSet.Contains(quest.questId)) { Debug.LogError($发现重复的任务ID: {quest.questId} 位于 {path}); } else { idSet.Add(quest.questId); } // 进一步检查每个步骤的条件和奖励是否有效... } Debug.Log($验证完成共检查{allQuestGuids.Length}个任务。); } }工作流建议在项目中建立清晰的文件夹结构例如Assets/ ├─ Scripts/ │ ├─ QuestSystem/ │ │ ├─ Core/ │ │ ├─ Conditions/ │ │ ├─ Rewards/ │ │ ├─ Events/ │ │ └─ Editor/ ├─ Data/ │ ├─ Quests/ │ ├─ Conditions/ │ ├─ Rewards/ │ └─ Events/将脚本和对应的ScriptableObject资产分开存放便于管理和资源加载。5. 高级应用、性能优化与疑难排查当基础系统搭建完毕后我们会面临更复杂的实际需求。这里分享一些进阶技巧和常见问题的解决方案。5.1 复杂条件与复合条件简单的“收集10个苹果”很容易但如果任务是“在雨天夜晚且装备了‘幸运戒指’的情况下收集10个‘月光苹果’”呢我们需要支持条件的逻辑组合。解决方案创建复合条件委托对象。public enum ConditionLogic { All, Any } [CreateAssetMenu(menuName Quest System/Conditions/Compound Condition)] public class CompoundConditionSO : TaskConditionSO { public ConditionLogic logic ConditionLogic.All; public ListTaskConditionSO subConditions; public override bool IsConditionMet(QuestContext context) { if (subConditions null || subConditions.Count 0) return true; switch (logic) { case ConditionLogic.All: foreach (var cond in subConditions) { if (!cond.IsConditionMet(context)) return false; } return true; case ConditionLogic.Any: foreach (var cond in subConditions) { if (cond.IsConditionMet(context)) return true; } return false; default: return false; } } public override string GetProgressDescription(QuestContext context) { // 可以返回一个组合描述或者简单返回自己的描述 return conditionDescription; } }这样策划就可以在编辑器中创建一个CompoundConditionSO并在其中拖入多个子条件甚至嵌套其他复合条件自由组合出复杂的逻辑。5.2 任务依赖与任务链任务A完成后才能解锁任务B。这是非常常见的需求。实现方案在QuestDataSO中增加一个prerequisiteQuestIds前置任务ID的字符串列表。在QuestManager.AcceptQuest方法中接取前检查所有前置任务是否都处于QuestState.Completed状态。更复杂的依赖如“完成A或B任意一个后可接取C”可以通过创建一个专门的QuestUnlockConditionSO来实现它本身也是一个TaskConditionSO但专门用于判断任务是否可接取。QuestManager在提供可接任务列表时会检查这些解锁条件。5.3 性能优化考量事件监听优化QuestManager监听了所有游戏事件并在每个事件触发时遍历所有进行中的任务。如果同时有上百个任务可能会有性能问题。优化A按事件类型过滤为每个任务步骤的条件列表记录它关心的事件类型如ItemCollected,EnemyKilled。在QuestManager中维护一个DictionarySystem.Type, ListQuest的映射。当ItemCollectedEvent触发时只通知那些关注物品收集事件的任务。这需要额外的数据结构来维护。优化B条件自检在TaskConditionSO中增加一个bool ListensToEvent(GameEventSO event)方法。QuestManager在分发事件时先询问条件“你关心这个事件吗”不关心就直接跳过。这减少了不必要的IsConditionMet调用。ScriptableObject的加载大量ScriptableObject资产会导致游戏启动或场景加载时资源加载压力增大。可以考虑使用Addressables或AssetBundle进行异步加载和按需加载。进度存储的序列化玩家的任务进度需要保存。DictionaryTaskConditionSO, int不能直接被Unity的JsonUtility或PlayerPrefs序列化因为键是UnityEngine.Object。解决方案在保存时将字典转换为一个可序列化的结构例如ListSerializedConditionProgress其中包含conditionGUID资产的GUID和progress。加载时再根据GUID反向查找对应的TaskConditionSO资产。5.4 常见问题与调试技巧问题1任务进度不更新。检查点1确认事件是否被正确触发。在GameEventSO.RaiseEvent方法开头加一个Debug.Log看事件是否被调用。检查点2确认QuestManager是否正确监听了该事件。检查Inspector中事件通道的引用是否赋值。检查点3在TaskConditionSO.IsConditionMet方法中打日志检查传入的QuestContext数据是否正确条件判断逻辑是否合理。检查点4检查任务的State是否为InProgress。问题2使用同一个Condition资产不同任务的进度串了。这就是前面提到的共享状态陷阱。确保进度数据存储在Quest实例的ConditionProgress字典中而不是TaskConditionSO资产内部。问题3策划配置的任务在游戏中不显示或无法接取。检查点1该任务的QuestDataSO是否被添加到QuestManager的allQuestData列表中检查点2任务的前置条件prerequisiteQuestIds是否已满足检查点3接取任务的代码如与NPC对话是否被正确执行并调用了QuestManager.AcceptQuest问题4在编辑器模式下修改了ScriptableObject资产但游戏运行时没变化。ScriptableObject资产在Play模式下被修改后如果未启用Editor设置中的“Enter Play Mode Options”下的“Reload Domain”和“Reload Scene”修改可能不会被重置。或者修改的是磁盘上的资产但内存中的实例未被标记为脏dirty。可以尝试在修改后调用EditorUtility.SetDirty(so)并AssetDatabase.SaveAssets()仅限编辑器代码。调试工具创建一个简单的QuestDebugUI在游戏画面中显示所有进行中任务的ID、状态和当前步骤的进度描述。这对于快速验证任务逻辑至关重要。构建一个基于委托和ScriptableObject的任务系统初期需要投入更多设计时间但带来的长期收益是巨大的——清晰的架构、高度的灵活性、以及策划与程序之间顺畅的协作界面。它不仅仅是一个任务系统更是一种适用于Unity中许多复杂游戏系统如技能系统、Buff系统、对话系统的模块化设计范本。当你熟悉这套模式后你会发现构建和扩展游戏功能变得前所未有的高效和愉悦。
返回列表