작성
·
392
0
안녕하세요 선생님
강의를 열심히 따라 듣고있던 중 세이브 & 로드 구현하고 테스트하는 와중 에러메세지 때문에 몇일 고민하다가 도저히 안되겠어서 글 남깁니다
어딘가 오타가있는건 아닌지 꼼꼼히 확인해봤는데 제눈엔 도저히 찾질 못하겠습니다 ㅜㅜ
우선 에러메세지는 다음과 같습니다
세이브 테스트 오브젝트에 스크립트도 잘 부착하였구요
스크립터블 오브젝트도 강의와 똑같이 만들고 부착하였습니다
어느 스크립트가 문제인지 몰라 Quest, QuestSystem, QuestSystemSaveTest
세가지 스크립트 작성하여 등록하겠습니다
Quest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
public enum QuestState
{
Inactive,
Running,
Complete,
Cancel,
WaitingForCompletion // 퀘스트 완료시 자동으로 완료되는게 아닌, 사용자가 퀘스트 완료를 눌러야 완료되는 상태
}
[CreateAssetMenu(menuName = "Quest/Quest", fileName ="Quest_")]
public class Quest : ScriptableObject
{
#region Event
public delegate void TaskSuccessChangeHandler(Quest quest, Task task, int currentSuccess, int prevSuccess);
public delegate void CompleteHandler(Quest quest);
public delegate void CancelHandler(Quest quest);
public delegate void NewTaskGroupHandler(Quest quest, TaskGroup currentTaskGroup, TaskGroup prevTaskGroup);
#endregion
[SerializeField]
private Category category;
[SerializeField]
private Sprite icon;
[Header("Text")]
[SerializeField]
private string codeName;
[SerializeField]
private string displayName;
[SerializeField, TextArea]
private string description;
[Header("Task")]
[SerializeField]
private TaskGroup[] taskGroups;
[Header("Reward")]
[SerializeField]
private Reward[] rewards;
[Header("Condition")]
[SerializeField]
private Condition[] acceptionConditions;
[SerializeField]
private Condition[] cancelConditions;
[Header("Option")]
[SerializeField]
private bool useAutoComplete;
[SerializeField]
private bool isCancelable; // 캔슬 불가능한 퀘스트를 위한 변수
[SerializeField]
private bool isSaveable;
private int currentTaskGroupIndex;
#region 프로퍼티들
public Category Category => category;
public Sprite Icon => icon;
public string CodeName => codeName;
public string DisplayName => displayName;
public string Description => description;
public QuestState State { get; private set; }
public TaskGroup CurrentTaskGroup => taskGroups[currentTaskGroupIndex];
public IReadOnlyList<TaskGroup> TaskGroups => taskGroups;
public IReadOnlyList<Reward> Rewards => rewards;
public bool IsRegistered => State != QuestState.Inactive;
public bool IsCompletable => State == QuestState.WaitingForCompletion;
public bool IsComplete => State == QuestState.Complete;
public bool IsCancel => State == QuestState.Cancel;
public virtual bool IsCancelable => isCancelable && cancelConditions.All(x=>x.IsPass(this));
public bool IsAcceptable => acceptionConditions.All(x => x.IsPass(this));
public virtual bool IsSaveable => isSaveable;
#endregion
public event TaskSuccessChangeHandler onTaskSuccessChanged;
public event CompleteHandler onCompleted;
public event CancelHandler onCanceled;
public event NewTaskGroupHandler onNewTaskGroup;
// Awake 역할의 함수로 Quest가 System에 등록되면 실행될 함수
public void OnRegister()
{
// 퀘스트를 중복 등록했을경우 에러 발생
Debug.Assert(!IsRegistered, "This quest has already been registered.");
foreach (var taskGroup in taskGroups)
{
taskGroup.Setup(this);
foreach (var task in taskGroup.Tasks)
task.onSuccessChanged += OnSuccessChanged;
}
State = QuestState.Running;
CurrentTaskGroup.Start();
}
public void ReceiveReport(string category, object target, int successCount)
{
Debug.Assert(IsRegistered, "This quest has already been registered.");
Debug.Assert(!IsCancel, "this quest has been canceled");
if (IsComplete)
return;
CurrentTaskGroup.ReceiveReport(category, target, successCount);
if (CurrentTaskGroup.IsAllTaskComplete)
{
if (currentTaskGroupIndex + 1 == taskGroups.Length) // 다음 taskGroup이 없다면
{
State = QuestState.WaitingForCompletion;
if (useAutoComplete)
Complete();
}
else
{
var prevTaskGroup = taskGroups[currentTaskGroupIndex++];
prevTaskGroup.End();
CurrentTaskGroup.Start();
onNewTaskGroup?.Invoke(this, CurrentTaskGroup, prevTaskGroup);
}
}
else
State = QuestState.Running;
}
public void Complete()
{
CheckIsRunning();
foreach (var taskGroup in taskGroups)
taskGroup.Complete();
State = QuestState.Complete;
foreach (var reward in rewards)
{
reward.Give(this);
}
onCompleted?.Invoke(this);
onTaskSuccessChanged = null;
onCompleted = null;
onCanceled = null;
onNewTaskGroup = null;
}
public virtual void Cancel()
{
CheckIsRunning();
Debug.Assert(IsCancelable, "this quest can't be canceled");
State = QuestState.Cancel;
onCanceled?.Invoke(this);
}
public Quest Clone()
{
var clone = Instantiate(this);
clone.taskGroups = taskGroups.Select(x => new TaskGroup(x)).ToArray();
return clone;
}
public QuestSaveData ToSaveData()
{
return new QuestSaveData
{
codeName = codeName,
state = State,
taskGroupIndex = currentTaskGroupIndex,
taskSuccessCounts = CurrentTaskGroup.Tasks.Select(x => x.CurrentSuccess).ToArray()
};
}
public void LoadFrom(QuestSaveData saveData)
{
State = saveData.state;
currentTaskGroupIndex = saveData.taskGroupIndex;
for (int i = 0; i < currentTaskGroupIndex; i++)
{
var taskGroup = taskGroups[i];
taskGroup.Start();
taskGroup.Complete();
}
for (int i = 0; i < saveData.taskSuccessCounts.Length; i++)
{
CurrentTaskGroup.Start();
CurrentTaskGroup.Tasks[i].CurrentSuccess = saveData.taskSuccessCounts[i];
}
}
private void OnSuccessChanged(Task task, int currentSuccess, int prevSuccess)
=> onTaskSuccessChanged?.Invoke(this, task, currentSuccess, prevSuccess);
[Conditional("UNITY_EDITOR")] // 유니티 에디터로 시행할때만 작동하도록 함
private void CheckIsRunning()
{
Debug.Assert(IsRegistered, "This quest has already been registered.");
Debug.Assert(!IsCancel, "this quest has been canceled");
Debug.Assert(!IsComplete, "this quest has already been completed");
}
}
QuestSystem.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using Newtonsoft.Json.Linq;
public class QuestSystem : MonoBehaviour
{
#region Save Path
private const string kSaveRootPath = "questSystem";
private const string kActiveQuestsSavePath = "activeQuests";
private const string kCompletedQuestsSavePath = "completedQuests";
private const string kActiveAchievementsSavePath = "activeAchievements";
private const string kCompletedAchievementsSavePath = "completedAchievements";
#endregion
#region Events
public delegate void QuestRegisteredHandler(Quest newQuest);
public delegate void QuestCompletedHandler(Quest quest);
public delegate void QuestCanceledHandler(Quest quest);
#endregion
private static QuestSystem instance;
private static bool isApplicationQuitting;
public static QuestSystem Instance
{
get
{
if(!isApplicationQuitting && instance == null)
{
instance = FindObjectOfType<QuestSystem>();
if(instance == null)
{
instance = new GameObject("Quest System").AddComponent<QuestSystem>();
DontDestroyOnLoad(instance.gameObject);
}
}
return instance;
}
}
private List<Quest> activeQuests = new List<Quest>();
private List<Quest> completedQuests = new List<Quest>();
private List<Quest> activeAchievements = new List<Quest>();
private List<Quest> completedAchievements = new List<Quest>();
private QuestDatabase questDatabase;
private QuestDatabase achievementDatabase;
public event QuestRegisteredHandler onQuestRegistered;
public event QuestCompletedHandler onQuestCompleted;
public event QuestCanceledHandler onQuestCanceled;
public event QuestRegisteredHandler onAchievementRegistered;
public event QuestCompletedHandler onAchievementCompleted;
public IReadOnlyList<Quest> ActiveQuests => activeQuests;
public IReadOnlyList<Quest> CompletedQuests => completedQuests;
public IReadOnlyList<Quest> ActiveAchievements => activeAchievements;
public IReadOnlyList<Quest> CompletedAchievements => completedAchievements;
private void Awake()
{
questDatabase = Resources.Load<QuestDatabase>("QuestDatabase");
achievementDatabase = Resources.Load<QuestDatabase>("AchievementDatabase");
if(!Load()) // 저장된 데이터가 없다면 업적에 등록
{
foreach (var achievement in achievementDatabase.Quests)
Register(achievement);
}
}
private void OnApplicationQuit()
{
isApplicationQuitting = true;
Save();
}
public Quest Register(Quest quest)
{
var newQuest = quest.Clone();
if(newQuest is Achievement)
{
newQuest.onCompleted += OnAchievementCompleted;
activeAchievements.Add(newQuest);
newQuest.OnRegister();
onAchievementRegistered?.Invoke(newQuest);
}
else
{
newQuest.onCompleted += OnQuestCompleted;
newQuest.onCanceled += OnQuestCanceled;
activeQuests.Add(newQuest);
newQuest.OnRegister();
onQuestRegistered?.Invoke(newQuest);
}
return newQuest;
}
// 내부용
private void ReceiveReport(List<Quest> quests, string category, object target, int successCount)
{
foreach (var quest in quests.ToArray())
quest.ReceiveReport(category, target, successCount);
}
//외부용
public void ReceiveReport(string category, object target, int successCount)
{
ReceiveReport(activeQuests, category, target, successCount);
ReceiveReport(activeAchievements, category, target, successCount);
}
public void ReceiveReport(Category category, TaskTarget target, int successCount)
=> ReceiveReport(category.CodeName, target.Value, successCount);
// 수업자료 보고 추가한 메서드
public void CompleteWaitingQuests()
{
foreach (var quest in activeQuests.ToList())
{
if (quest.IsCompletable)
quest.Complete();
}
}
public bool ContainsInActiveQuests(Quest quest) => activeQuests.Any(x => x.CodeName == quest.CodeName);
public bool ContainsInCompletedQuests(Quest quest) => completedQuests.Any(x => x.CodeName == quest.CodeName);
public bool ContainsInActiveAchievements(Quest quest) => activeAchievements.Any(x => x.CodeName == quest.CodeName);
public bool ContainsInCompletedAchievements(Quest quest) => completedAchievements.Any(x => x.CodeName == quest.CodeName);
private void Save()
{
var root = new JObject();
root.Add(kActiveQuestsSavePath, CreateSaveDatas(activeQuests));
root.Add(kCompletedQuestsSavePath, CreateSaveDatas(completedQuests));
root.Add(kActiveAchievementsSavePath, CreateSaveDatas(activeAchievements));
root.Add(kCompletedAchievementsSavePath, CreateSaveDatas(completedAchievements));
PlayerPrefs.SetString(kSaveRootPath, root.ToString());
PlayerPrefs.Save();
}
private bool Load()
{
if (PlayerPrefs.HasKey(kSaveRootPath))
{
var root = JObject.Parse(PlayerPrefs.GetString(kSaveRootPath));
LoadSaveDatas(root[kActiveQuestsSavePath], questDatabase, LoadActiveQuest);
LoadSaveDatas(root[kCompletedQuestsSavePath], questDatabase, LoadCompletedQuest);
LoadSaveDatas(root[kActiveAchievementsSavePath], achievementDatabase, LoadActiveQuest);
LoadSaveDatas(root[kCompletedAchievementsSavePath], achievementDatabase, LoadCompletedQuest);
return true;
}
else return false;
}
private JArray CreateSaveDatas(IReadOnlyList<Quest> quests)
{
var saveDatas = new JArray();
foreach (var quest in quests)
{
if(quest.IsSaveable)
saveDatas.Add(JObject.FromObject(quest.ToSaveData())); // SaveData를 Json형태로 변환하여 JsonArray에 넣어줌
}
return saveDatas;
}
private void LoadSaveDatas(JToken datasToken, QuestDatabase database, System.Action<QuestSaveData, Quest> OnSuccess)
{
var datas = datasToken as JArray;
foreach (var data in datas)
{
var saveData = data.ToObject<QuestSaveData>();
var quest = database.FindQuestBy(saveData.codeName);
OnSuccess.Invoke(saveData, quest);
}
}
private void LoadActiveQuest(QuestSaveData saveData, Quest quest)
{
var newQuest = Register(quest);
newQuest.LoadFrom(saveData);
}
private void LoadCompletedQuest(QuestSaveData saveData, Quest quest)
{
var newQuest = quest.Clone();
newQuest.LoadFrom(saveData);
if (newQuest is Achievement)
completedAchievements.Add(newQuest);
else
completedQuests.Add(newQuest);
}
#region Callback
private void OnQuestCompleted(Quest quest)
{
activeQuests.Remove(quest);
completedQuests.Add(quest);
onQuestCompleted?.Invoke(quest);
}
private void OnQuestCanceled(Quest quest)
{
activeQuests.Remove(quest);
onQuestCanceled?.Invoke(quest);
Destroy(quest, Time.deltaTime);
}
private void OnAchievementCompleted(Quest achievement)
{
activeAchievements.Remove(achievement);
completedAchievements.Add(achievement);
onAchievementCompleted?.Invoke(achievement);
}
#endregion
}
QuestSystemSaveTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class QuestSystemSaveTest : MonoBehaviour
{
[SerializeField]
private Quest quest;
[SerializeField]
private Category category;
[SerializeField]
private TaskTarget target;
private void Start()
{
var questSystem = QuestSystem.Instance;
if(questSystem.ActiveQuests.Count == 0)
{
Debug.Log("Register");
var newQuest = questSystem.Register(quest);
}
else
{
questSystem.onQuestCompleted += (quest) =>
{
Debug.Log("Complete");
PlayerPrefs.DeleteAll();
PlayerPrefs.Save();
};
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
QuestSystem.Instance.ReceiveReport(category, target, 1);
}
}
이상입니다..
답변 1
0
수강해주셔서 감사합니다.
Error Stack을 쫓아가보면 QuestSystem 92번째 줄 newQuest.OnRegister() Code에서 Error가 나고 있는데요, 보시면 Quest의 Type이 Achivement입니다.
즉, 현재 Error를 내고 있는건 보여주신 Quest가 아니라 AchivementDatabase에 등록해두신 어떤 Achviement에서 Error가 나고 있는 겁니다. 그 부분을 확인해보시면 될 것 같습니다.
Erorr의 정확한 이유는 등록해두신 Achivement의 taskGroup에 아무런 taskGroup도 존재하지 않아서 Quest Script에 있는
public TaskGroup CurrentTaskGroup => taskGroups[currentTaskGroupIndex];
이 Code가 존재하지 않는 배열 Index에 접근하고 있다는 Out Of Bounce Error를 내고 있습니다.
생각치도 못한 곳에서 Error가 발생할 수 있는만큼 너무 오래 고민하지 마시고, 도움이 필요하시면 언제든지 질문 글을 올려주세요.
감사합니다.
맞아요 선생님... AchievementDatabase에 Task등록을 안해서 발생한 오류였습니다 퀘스트만 생각하고 업적을 생각못했네요ㅜㅜ 정말 감사합니다!