이전 포스팅에서 더이상 컨텐츠를 개발하지 않고 마무리 하기로 했으나, Rpg 게임에서 퀘스트 시스템을 만들지 않는건 예의가 아닌것 같아 퀘스트까지만 만들기로 했다.
퀘스트 시스템 흐름 분석
퀘스트를 획득하는 시스템을 구상할 때 흐름을 분석해보자.
- 클라이언트 플레이어가 퀘스트 Npc에게 퀘스트를 수락 받음.
- 서버쪽으로 플레이어가 퀘스트를 수락했다고 보냄.
- 실제로 그 Npc가 해당 퀘스트를 가지고 있는지 검증(위치 포함)
- 문제가 없다면 서버쪽 플레이어에게 퀘스트를 추가.
- 그리고 클라이언트 플레이어에게 정상적으로 퀘스트가 추가됐으니 알려줌.
- 클라이언트 플레이어의 퀘스트 매니저에 추가하고 퀘스트 UI 등을 업데이트 해줌.
요런 흐름을 가지고 있다.
그렇다면 퀘스트 진행사항을 추적하는 경우는 어떻게 할까?
- 각 퀘스트 타입에 맞는 상황 연출 됨.(몬스터 사망 or 아이템 획득 or 특정 장소 입장 etc...)
- 예를 들어 몬스터가 사망했을 경우, 최종 타격자가 가지고 있는 퀘스트를 가져옴.
- 가져온 퀘스트에서 해당 내용의 퀘스트를 가지고 있는지 확인 후 있다면 업데이트 해줌.
- 업데이트가 됐다면 클라이언트 플레이어에게 업데이트 된 내용을 알려줌.
이러한 방식으로 진행하는 것이 가장 깔끔하긴 할 것 같다. 물론 퀘스트가 기하급수적으로 많을 경우 모든 퀘스트를 for문을 돌아야하기 때문에 시간복잡도가 많이 상승하겠지만, 그럴 경우를 방지해 각 퀘스트 타입에 맞는 퀘스트만 검증할 것이다.
마지막으로 퀘스트 클리어의 흐름이다.
- 클라이언트 플레이어가 Npc를 찾아가 클리어 요청
- 클라이언트 측에서도 실제 그 퀘스트를 가지고 있으며 클리어 했는지 확인.
- 확인했다면 서버쪽에 클리어 패킷 보냄.
- 서버쪽에서도 실제 퀘스트를 플레이어가 가지고 있고 npc가 가지고 있고 클리어 했는지 검증(패킷 변조)
- 잘못된 사항이 없다면 퀘스트에 따른 보상 지급
- 또한 클리어된 퀘스트 내용을 클라이언트에게 보냄.
- 클라이언트 플레이어의 퀘스트 UI등을 업데이트함.
3가지 경우 전반적으로 서버쪽에서 퀘스트를 추가하거나 진행사항을 업데이트하거나 퀘스트를 클리어하거나 등 주요 작업을 진행하고 그 결과를 클라이언트쪽에 통보하는 식이다.
QuestData.json
역시 Quest의 내용을 전부 다 유니티에서 작업하기에는 어렵기때문에 기존과 마찬가지로 Json파일을 통해 Quest의 내용을 저장하자.
너무 길기 때문에 일부분만 보여주겠다.
이런식으로 각종 정보들을 쉽게 관리한다.
여기서 퀘스트 타입은 임시적으로 Battle, Collection, Enter로만 구성되어 있다.
QuestManager.cs
클라이언트에서 Quest들을 관리하기 위한 클래스이다. 물론 서버쪽에도 똑같이 구성되어 있다.
QuestList에서 QuestType을 나눠 관리해 앞서 말한 속도를 줄이는 방식을 택했다.
using Data;
using Google.Protobuf.Protocol;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class QuestManager
{
Dictionary<int, Quest>[] QuestList = new Dictionary<int, Quest>[(int)QuestType.MaxCount];
Dictionary<int, Quest> FinishedQuest = new Dictionary<int, Quest>();
public void Init()
{
for(int i = 0; i < QuestList.Length; i ++)
{
QuestList[i] = new Dictionary<int, Quest>();
}
}
public void AddQuest(Quest quest)
{
QuestList[(int)quest.QuestType].Add(quest.TemplateId, quest);
}
public void RemoveQuest(Quest quest)
{
QuestList[(int)quest.QuestType].Remove(quest.TemplateId);
}
public Quest GetQuest(int id, QuestType questType)
{
QuestList[(int)questType].TryGetValue(id, out Quest quest);
return quest;
}
public List<Quest> GetAllQuest()
{
List<Quest> quests = new List<Quest>();
foreach (var quest in QuestList)
{
quests.AddRange(quest.Values.ToList());
}
return quests;
}
public List<Quest> GetAllFinishQuest()
{
List<Quest> quests = FinishedQuest.Values.ToList();
return quests;
}
public bool CheckClearQuest(int id, QuestType questType)
{
return QuestList[(int)questType].TryGetValue(id, out Quest quest) && quest.IsFinish;
}
public bool CheckIsFinishQuest(int id)
{
return FinishedQuest.TryGetValue(id,out Quest value);
}
public void Clear()
{
foreach (var quest in QuestList)
{
quest.Clear();
}
}
public void FinishQuest(Quest quest)
{
FinishedQuest.TryAdd(quest.TemplateId, quest);
}
}
Quest.cs
MakeQuest를 통해서 templateId만 입력하면 Quest를 생성해서 사용할 수 있다. 그리고 각종 퀘스트 타입에 따라 다르게 Update를 함으로서 퀘스트 진행사항을 다르게 구현할 수 있다.
using Data;
using Google.Protobuf.Protocol;
using System;
using System.Collections.Generic;
using System.Text;
public class Quest
{
public int TemplateId { get; private set; }
public QuestType QuestType { get; private set; }
public int DemandLevel { get; private set; }
public int DemandQuest { get; private set; }
public bool IsRepeated { get; private set; }
public QuestReward Reward { get; private set; }
public string QuestName { get; private set; }
public bool IsFinish { get; set; }
public Quest(QuestType questType)
{
QuestType = questType;
}
public static Quest MakeQuest(int id)
{
Quest quest = null;
if (Managers.Data.QuestDict.TryGetValue(id, out QuestData questData) == false)
return null;
switch (questData.questType)
{
case QuestType.Battle:
quest = new BattleQuest(questData);
break;
case QuestType.Collection:
quest = new CollectionQuest(questData);
break;
case QuestType.Enter:
quest = new EnterQuest(questData);
break;
}
if (quest != null)
{
quest.TemplateId = questData.id;
quest.IsRepeated = questData.isRepeated;
quest.DemandQuest = questData.demandQuest;
quest.DemandLevel = questData.demandLevel;
quest.Reward = questData.reward;
quest.QuestName = questData.questTitle;
quest.IsFinish = false;
}
return quest;
}
}
public class BattleQuest : Quest
{
public List<BattleQuestGoals> goals;
public Dictionary<int, int> countDict = new Dictionary<int, int>();
public BattleQuest(QuestData questData) : base(QuestType.Battle)
{
if (questData == null)
return;
countDict.Clear();
goals = ((BattleQuestData)questData).goals;
foreach (var goal in goals)
countDict.Add(goal.enemyId, 0);
}
public bool Update(BattleQuestGoals questGoals)
{
foreach (var goal in goals)
{
if (goal.enemyId == questGoals.enemyId)
{
if (!countDict.ContainsKey(goal.enemyId)) return false;
countDict[goal.enemyId] += questGoals.count;
CheckQuestClear();
return true;
}
}
return false;
}
public void CheckQuestClear()
{
foreach (var goal in goals)
{
if (!countDict.ContainsKey(goal.enemyId)) return;
if (goal.count > countDict[goal.enemyId])
{
IsFinish = false;
return;
}
}
IsFinish = true;
}
}
public class CollectionQuest : Quest
{
public List<CollectionQuestGoals> goals;
public Dictionary<int, int> countDict = new Dictionary<int, int>();
public CollectionQuest(QuestData questData) : base(QuestType.Collection)
{
if (questData == null)
return;
goals = ((CollectionQuestData)questData).goals;
countDict.Clear();
foreach (var goal in goals)
countDict.Add(goal.collectionId, 0);
}
public bool Update(CollectionQuestGoals questGoals)
{
foreach (var goal in goals)
{
if (goal.collectionId == questGoals.collectionId)
{
if (!countDict.ContainsKey(goal.collectionId)) return false;
countDict[goal.collectionId] += questGoals.count;
CheckQuestClear();
return true;
}
}
return false;
}
public void CheckQuestClear()
{
foreach (var goal in goals)
{
if (!countDict.ContainsKey(goal.collectionId)) return;
if (goal.count > countDict[goal.collectionId])
{
IsFinish = false;
return;
}
}
IsFinish = true;
}
}
public class EnterQuest : Quest
{
public int goals;
public int cur = -1;
public EnterQuest(QuestData questData) : base(QuestType.Enter)
{
if (questData == null)
return;
goals = ((EnterQuestData)questData).goals;
}
public bool Update(int goal)
{
cur = goal;
CheckQuestClear();
return true;
}
public void CheckQuestClear()
{
if (cur == goals)
IsFinish = true;
else
IsFinish = false;
}
}
퀘스트 UI
퀘스트 UI는 굉장히 작업할게 많다. 전체적인 모습은 다음과 같다.
들어간게 없어서 이게 뭐지 싶을 수 있지만 밑의 영상을 보면 알 것이다. QuestManager로 부터 플레이어가 가지고 있는 Quest를 전부 불러와 클리어 상태, 진행사항, 완료된 퀘스트 등 분리하여 띄우게 된다.
using Data;
using DG.Tweening;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class UI_Quest : UI_Base
{
bool _init = false;
List<UI_Quest_Item> questItems = new List<UI_Quest_Item>();
public UI_Quest_Item currentItem = null;
bool currentTab = true;
enum GameObjects
{
DetailBackground,
QuestContent
}
enum Texts
{
QuestFullNameText,
QuestDemandLevelText,
QuestLocationText,
QuestDetailText,
QuestClearText
}
enum Images
{
QuestNpcImage,
QuestClearItemImage,
}
enum Buttons
{
ProgressBtn,
FinishBtn,
ExitButton,
CloseDetailBtn,
}
public override void Init()
{
BindObject(typeof(GameObjects));
BindImage(typeof(Images));
BindText(typeof(Texts));
BindButton(typeof(Buttons));
GetButton((int)Buttons.ProgressBtn).gameObject.BindEvent(ViewProgressQuest);
GetButton((int)Buttons.FinishBtn).gameObject.BindEvent(ViewFinishQuest);
GetObject((int)GameObjects.DetailBackground).SetActive(false);
GetButton((int)Buttons.ExitButton).gameObject.BindEvent((e) => {
var ui = Managers.UI.SceneUI as UI_GameScene; ui.CloseUI("UI_Quest");
});
GetButton((int)Buttons.CloseDetailBtn).gameObject.BindEvent((e) => {
Managers.Sound.Play("ButtonClick");
CloseQuestDetailUI();
});
questItems.Clear();
_init = true;
RefreshUI();
}
public void ViewProgressQuest(PointerEventData point)
{
GetButton((int)Buttons.ProgressBtn).GetComponent<Image>().sprite = Managers.Resource.Load<Sprite>("UI/Content/name_bar2");
GetButton((int)Buttons.FinishBtn).GetComponent<Image>().sprite = Managers.Resource.Load<Sprite>("UI/Content/name_bar3");
if (currentTab == false)
{
CloseQuestDetailUI(); Managers.Sound.Play("ButtonClick");
}
List<Quest> quests = Managers.Quest.GetAllQuest();
GetObject((int)GameObjects.QuestContent).GetComponent<RectTransform>().sizeDelta = new Vector2(0, (quests.Count + 1) * 40);
QuestListUI(quests);
currentTab = true;
}
public void ViewFinishQuest(PointerEventData point)
{
GetButton((int)Buttons.FinishBtn).GetComponent<Image>().sprite = Managers.Resource.Load<Sprite>("UI/Content/name_bar2");
GetButton((int)Buttons.ProgressBtn).GetComponent<Image>().sprite = Managers.Resource.Load<Sprite>("UI/Content/name_bar3");
if (currentTab == true)
{
CloseQuestDetailUI(); Managers.Sound.Play("ButtonClick");
}
List<Quest> quests = Managers.Quest.GetAllFinishQuest();
GetObject((int)GameObjects.QuestContent).GetComponent<RectTransform>().sizeDelta = new Vector2(0, (quests.Count + 1) * 40);
QuestListUI(quests);
currentTab = false;
}
public void QuestListUI(List<Quest> quests)
{
Transform parent = GetObject((int)GameObjects.QuestContent).transform;
questItems.Clear();
foreach (Transform child in parent.transform)
Destroy(child.gameObject);
if (quests == null) return;
foreach (Quest quest in quests)
{
GameObject go = Managers.Resource.Instantiate("UI/SubItem/UI_Quest_Item", parent);
UI_Quest_Item questItem = go.GetComponent<UI_Quest_Item>();
questItem.Setting(quest, this);
questItems.Add(questItem);
if (currentItem != null && questItem._quest.TemplateId == currentItem._quest.TemplateId)
currentItem = questItem;
}
}
public void OpenQuestDetailUI(QuestData questData, UI_Quest_Item item)
{
if (currentItem != null && currentItem != item)
currentItem.ResetColor();
currentItem = item;
currentItem.SetColor();
GetText((int)Texts.QuestFullNameText).text = questData.questTitle;
GetText((int)Texts.QuestDemandLevelText).text = $"레벨 {questData.demandLevel}이상";
GetText((int)Texts.QuestLocationText).text = questData.questLocationString;
GetImage((int)Images.QuestNpcImage).sprite = Managers.Resource.Load<Sprite>(questData.questNpcIconPath);
GetText((int)Texts.QuestDetailText).text = questData.questDetailString;
if (questData.questItemIconPath == null)
GetImage((int)Images.QuestClearItemImage).color = new Color(1, 1, 1, 0);
else
{
GetImage((int)Images.QuestClearItemImage).color = new Color(1, 1, 1, 1);
GetImage((int)Images.QuestClearItemImage).sprite = Managers.Resource.Load<Sprite>(questData.questItemIconPath);
}
switch (item._quest.QuestType)
{
case Google.Protobuf.Protocol.QuestType.Battle:
BattleQuest bq = (BattleQuest)item._quest;
if(bq != null)
{
int id = ((BattleQuestData)questData).goals[0].enemyId;
GetText((int)Texts.QuestClearText).text = $"{questData.goalText} {bq.countDict[id]} / {((BattleQuestData)questData).goals[0].count}";
}
break;
case Google.Protobuf.Protocol.QuestType.Collection:
CollectionQuest cq = (CollectionQuest)item._quest;
if (cq != null)
{
int id = ((CollectionQuestData)questData).goals[0].collectionId;
GetText((int)Texts.QuestClearText).text = $"{questData.goalText} {cq.countDict[id]} / {((CollectionQuestData)questData).goals[0].count}";
}
break;
case Google.Protobuf.Protocol.QuestType.Enter:
EnterQuestData enterQuestData = (EnterQuestData)questData;
GetText((int)Texts.QuestClearText).text = enterQuestData.goalText;
break;
}
GetObject((int)GameObjects.DetailBackground).SetActive(true);
//Dotween
GetObject((int)GameObjects.DetailBackground).transform.DOLocalMoveX(435f, 0.15f).SetEase(Ease.OutExpo);
}
public void CloseQuestDetailUI()
{
if (currentItem != null)
currentItem.ResetColor();
currentItem = null;
GetObject((int)GameObjects.DetailBackground).transform.DOLocalMoveX(65f, 0.15f)
.SetEase(Ease.OutExpo)
.OnComplete(
() => { GetObject((int)GameObjects.DetailBackground).SetActive(false); }
);
}
public void RefreshUI()
{
if (_init == false) return;
if (currentItem != null )
{
if(currentTab == true)
ViewProgressQuest(null);
else
ViewFinishQuest(null);
currentItem.ClickQuest(null);
currentItem.SetColor();
}
else
if (currentTab == true)
ViewProgressQuest(null);
else
ViewFinishQuest(null);
}
public void ResetCurrentItem(Quest quest)
{
if (currentItem._quest.TemplateId == quest.TemplateId)
CloseQuestDetailUI();
}
}
using Data;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class UI_Quest_Item : UI_Base
{
public Quest _quest;
QuestData _questData;
UI_Quest parent;
bool _init = false;
enum Buttons
{
QuestDetailBtn
}
enum Texts
{
QuestNameText
}
enum Images
{
FinishImage
}
public override void Init()
{
BindImage(typeof(Images));
BindText(typeof(Texts));
BindButton(typeof(Buttons));
GetButton((int)Buttons.QuestDetailBtn).gameObject.BindEvent(ClickQuest);
_init = true;
RefesthUI();
}
public void ClickQuest(PointerEventData data)
{
if (parent != null)
parent.OpenQuestDetailUI(_questData, this);
Managers.Sound.Play("ButtonClick");
}
public void SetColor()
{
if (GetButton((int)Buttons.QuestDetailBtn) != null)
GetButton((int)Buttons.QuestDetailBtn).GetComponent<Image>().color = Util.HexColor("#391010");
}
public void ResetColor()
{
if (GetButton((int)Buttons.QuestDetailBtn) != null)
GetButton((int)Buttons.QuestDetailBtn).GetComponent<Image>().color = Util.HexColor("#262021");
}
public void Setting(Quest quest, UI_Quest uiQuest)
{
if (Managers.Data.QuestDict.TryGetValue(quest.TemplateId, out QuestData questData) == false) return;
_quest = quest;
_questData = questData;
parent = uiQuest;
RefesthUI();
}
public void RefesthUI()
{
if (_quest == null || _init == false) return;
GetText((int)Texts.QuestNameText).text = _questData.questTitle;
if(_quest.IsFinish)
GetImage((int)Images.FinishImage).sprite = Managers.Resource.Load<Sprite>("Textures/Quest/Finish");
else
GetImage((int)Images.FinishImage).sprite = Managers.Resource.Load<Sprite>("Textures/Quest/Progress");
}
}
퀘스트 대화 UI
퀘스트 대화 UI도 마찬가지로 Npc와 대화했을 경우 내가 선택한 퀘스트의 정보를 넘겨주고 그 값을 띄우는 형식으로 구성된다. Npc와의 대화도 spaceBar를 이용해 할 수 있고 대화창을 넘기는 것도 역시 spaceBar이다.
using Data;
using DG.Tweening;
using Google.Protobuf.Protocol;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class UI_QuestDialogue_Popup : UI_Popup
{
List<UI_QuestList_Item> QuestList = new List<UI_QuestList_Item>();
QuestNpc npc = null;
List<QuestData> QuestDatas = new List<QuestData>();
QuestData selectQuest = null;
Tweener tweener = null;
bool _init = false;
bool QuestTrigger = false;
bool QuestEnd = false;
enum GameObjects
{
Content,
ScrollView
}
enum Texts
{
DialogueText,
NpcNameText
}
enum Buttons
{
YesBtn,
NoBtn
}
public override void Init()
{
BindObject(typeof(GameObjects));
BindButton(typeof(Buttons));
BindText(typeof(Texts));
GetButton((int)Buttons.YesBtn).gameObject.BindEvent((e) => { AcceptQuest(); });
GetButton((int)Buttons.NoBtn).gameObject.BindEvent((e) => { DeclineQuest(); });
GetButton((int)Buttons.YesBtn).gameObject.SetActive(false);
GetButton((int)Buttons.NoBtn).gameObject.SetActive(false);
GetText((int)Texts.DialogueText).text = "";
_init = true;
selectQuest = null;
RefreshUI();
}
bool QuestAccept = false;
public void AcceptQuest()
{
GetText((int)Texts.DialogueText).text = "";
GetButton((int)Buttons.YesBtn).gameObject.SetActive(false);
GetButton((int)Buttons.NoBtn).gameObject.SetActive(false);
QuestTrigger = false;
QuestEnd = false;
QuestAccept = false;
GetText((int)Texts.DialogueText).DOText(selectQuest.questAcceptString, selectQuest.questAcceptString.Length * 0.02f).OnComplete(() => { QuestEnd = true; QuestAccept = true; });
}
public void DeclineQuest()
{
GetText((int)Texts.DialogueText).text = "";
GetButton((int)Buttons.YesBtn).gameObject.SetActive(false);
GetButton((int)Buttons.NoBtn).gameObject.SetActive(false);
QuestTrigger = false;
GetText((int)Texts.DialogueText).DOText(selectQuest.questRefuseString, selectQuest.questRefuseString.Length * 0.02f).OnComplete(() => { QuestEnd = true; });
}
public void Setting(QuestNpc questNpc, List<QuestData> questDatas)
{
npc = questNpc;
QuestDatas = questDatas;
RefreshUI();
}
public void RefreshUI()
{
if (_init == false || npc == null)
return;
QuestList.Clear();
foreach (Transform child in GetObject((int)GameObjects.Content).transform)
Destroy(child.gameObject);
foreach (var quest in QuestDatas)
{
if ((quest.demandQuest != 0 && Managers.Quest.CheckIsFinishQuest(quest.demandQuest) == false) ||
quest.demandLevel > Managers.Object.MyPlayer.Stat.Level) continue;
GameObject go = Managers.Resource.Instantiate("UI/SubItem/UI_QuestList_Item", GetObject((int)GameObjects.Content).transform);
UI_QuestList_Item questItem = go.GetComponent<UI_QuestList_Item>();
questItem.Setting(quest, this);
QuestList.Add(questItem);
}
if(QuestList.Count <= 0)
{
GetObject((int)GameObjects.ScrollView).SetActive(false);
GetText((int)Texts.DialogueText).DOText(npc.npcData.questNonString, npc.npcData.questNonString.Length * 0.02f);
}
GetText((int)Texts.NpcNameText).text = npc.NpcName.text;
}
int count = 0;
public void ClickQuest(QuestData quest)
{
selectQuest = quest;
QuestList.Clear();
foreach (Transform child in GetObject((int)GameObjects.Content).transform)
Destroy(child.gameObject);
GetObject((int)GameObjects.ScrollView).SetActive(false);
Managers.Object.MyPlayer.QuestTrigger = true;
count = 0;
ShowText(count++);
}
public void Update()
{
if (QuestTrigger == true && Input.GetKeyDown(KeyCode.Space))
{
ShowText(count++);
}
else if(QuestEnd == true && Input.GetKeyDown(KeyCode.Space))
{
QuestEnd = false;
if(QuestClear == true)
{
// 클리어 패킷
C_ClearQuest clearQuestPacket = new C_ClearQuest();
clearQuestPacket.NpcId = npc.Id;
clearQuestPacket.QuestId = selectQuest.id;
clearQuestPacket.QuestType = selectQuest.questType;
Managers.Network.Send(clearQuestPacket);
}
else if(QuestAccept == true)
{
// 퀘스트 수락 패킷
C_AddQuest addQuestPacekt = new C_AddQuest();
addQuestPacekt.NpcId = npc.Id;
addQuestPacekt.QuestId = selectQuest.id;
Managers.Network.Send(addQuestPacekt);
}
Managers.Object.MyPlayer.QuestTrigger = false;
npc.CloseNpc();
}
}
public void ShowText(int cnt)
{
if (cnt >= selectQuest.questDescription.Count) return;
QuestTrigger = true;
tweener?.Kill();
TextMeshProUGUI text = GetText((int)Texts.DialogueText);
text.text = "";
string npcText = selectQuest.questDescription[cnt];
if(npcText == null)
{
Debug.LogError("non script");
return;
}
tweener = text.DOText(npcText, npcText.Length * 0.02f).SetEase(Ease.Linear).OnComplete(() =>
{
if(cnt == selectQuest.questDescription.Count - 1)
{
GetButton((int)Buttons.YesBtn).gameObject.SetActive(true);
GetButton((int)Buttons.NoBtn).gameObject.SetActive(true);
}
});
}
bool QuestClear = false;
public void QuestFinish(QuestData quest)
{
GetText((int)Texts.DialogueText).text = ""; QuestList.Clear();
foreach (Transform child in GetObject((int)GameObjects.Content).transform)
Destroy(child.gameObject);
Managers.Object.MyPlayer.QuestTrigger = true;
GetObject((int)GameObjects.ScrollView).SetActive(false);
GetButton((int)Buttons.YesBtn).gameObject.SetActive(false);
GetButton((int)Buttons.NoBtn).gameObject.SetActive(false);
selectQuest = quest;
QuestTrigger = false;
QuestEnd = false;
QuestClear = false;
GetText((int)Texts.DialogueText).DOText(quest.questClearString, quest.questClearString.Length * 0.02f).OnComplete(() => { QuestEnd = true; QuestClear = true; });
}
public void QuestNotYetFinish(QuestData quest)
{
GetText((int)Texts.DialogueText).text = ""; QuestList.Clear();
foreach (Transform child in GetObject((int)GameObjects.Content).transform)
Destroy(child.gameObject);
Managers.Object.MyPlayer.QuestTrigger = true;
GetObject((int)GameObjects.ScrollView).SetActive(false);
GetButton((int)Buttons.YesBtn).gameObject.SetActive(false);
GetButton((int)Buttons.NoBtn).gameObject.SetActive(false);
QuestTrigger = false;
GetText((int)Texts.DialogueText).DOText(quest.questNonClearString, quest.questNonClearString.Length * 0.02f).OnComplete(() => { QuestEnd = true; });
}
}
using Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class UI_QuestList_Item : UI_Base
{
QuestData QuestData;
UI_QuestDialogue_Popup dialogue_Popup;
bool _init = false;
enum Texts
{
QuestNameText
}
enum Images
{
Image
}
public override void Init()
{
BindText(typeof(Texts));
BindImage(typeof(Images));
GetText((int)Texts.QuestNameText).gameObject.BindEvent((e) =>
{
GetText((int)Texts.QuestNameText).fontStyle = TMPro.FontStyles.Underline;
}, Define.UIEvent.PointerEnter);
GetText((int)Texts.QuestNameText).gameObject.BindEvent((e) =>
{
GetText((int)Texts.QuestNameText).fontStyle = TMPro.FontStyles.Normal;
}, Define.UIEvent.PointerExit);
GetText((int)Texts.QuestNameText).gameObject.BindEvent((e) =>
{
Quest quest = Managers.Quest.GetQuest(QuestData.id, QuestData.questType);
if(quest == null)
{
dialogue_Popup.ClickQuest(QuestData);
return;
}
if (Managers.Quest.CheckClearQuest(QuestData.id, QuestData.questType))
{
// 퀘스트를 클리어 경우
dialogue_Popup.QuestFinish(QuestData);
}
else
{
// 아직 퀘스트를 클리어하지 못했을 경우
dialogue_Popup.QuestNotYetFinish(QuestData);
}
});
_init = true;
RefreshUI();
}
public void Setting(QuestData questData, UI_QuestDialogue_Popup popup)
{
QuestData = questData;
dialogue_Popup = popup;
RefreshUI();
}
public void RefreshUI()
{
if (_init == false || QuestData == null) return;
GetText((int)Texts.QuestNameText).text = QuestData.questTitle;
if (Managers.Quest.CheckClearQuest(QuestData.id, QuestData.questType))
{
GetImage((int)Images.Image).color = Util.HexColor("#15A55C");
}
else
{
GetImage((int)Images.Image).color = Util.HexColor("#A51516");
}
}
}
퀘스트 추가하기(서버, 클라)
위의 대화창 스크립트에서 보면 알겠지만 중간에 C_AddQuest 패킷을 보내는걸 볼 수 있다.
그 결과 서버에서는 이렇게 동작한다.
public void HandleAddQuest(Player player, C_AddQuest addQuestPacket)
{
if (player == null) return;
if (_npc.TryGetValue(addQuestPacket.NpcId, out Npc npc) == false) return;
QuestNpc questNpc = npc as QuestNpc;
if (questNpc == null || questNpc.QuestList == null) return;
if(questNpc.QuestList.Contains(addQuestPacket.QuestId) == false)
{
Console.WriteLine($"잘못된 npc 접근입니다. 접근자 : {player.Info.Name}");
}
Quest quest = Quest.MakeQuest(addQuestPacket.QuestId);
if(quest == null)
{
Console.WriteLine("존재하지 않는 퀘스트 입니다.");
}
player.QuestInven.AddQuest(quest);
// DB에도 저장해야함.
// 클라 통보
S_AddQuest addQuestOk = new S_AddQuest();
addQuestOk.QuestId = addQuestPacket.QuestId;
player.Session.Send(addQuestOk);
}
여러 검증을 하고 Quest를 추가해준 다음 클라에 통보해준다.
public static void S_AddQuestHandler(PacketSession session, IMessage packet)
{
S_AddQuest addQuestPacket = (S_AddQuest)packet;
Quest quest = Quest.MakeQuest(addQuestPacket.QuestId);
if (quest == null) return;
Managers.Quest.AddQuest(quest);
UI_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
if (gameSceneUI != null)
{
gameSceneUI.QuestUI.RefreshUI();
}
}
통보 받으면 Update 해준다.
퀘스트 클리어(서버, 클라)
서버에서 클리어 요청 패킷이 오면 이렇게 동작한다.
public void HandleClearQuest(Player player, C_ClearQuest clearQuestPacket)
{
if (player == null) return;
if (_npc.TryGetValue(clearQuestPacket.NpcId, out Npc npc) == false) return;
QuestNpc questNpc = npc as QuestNpc;
if (questNpc == null || questNpc.QuestList == null) return;
if (questNpc.QuestList.Contains(clearQuestPacket.QuestId) == false)
{
Console.WriteLine($"잘못된 npc 접근입니다. 접근자 : {player.Info.Name}");
}
Quest quest = player.QuestInven.GetQuest(clearQuestPacket.QuestId, clearQuestPacket.QuestType);
if (quest == null)
{
Console.WriteLine("존재하지 않는 퀘스트 입니다.");
return;
}
if (player.QuestInven.CheckQuestClear(quest.TemplateId, quest.QuestType) == true)
{
player.QuestInven.RemoveQuest(quest);
player.QuestInven.FinishQuest(quest);
// 보상 지급
QuestReward questReward = quest.Reward;
if (questReward != null)
{
if(questReward.exp > 0)
player.RewardExp(questReward.exp);
if(questReward.money > 0)
{
C_AddItem addItem = new C_AddItem();
addItem.TemplateId = 1000;
addItem.Count = questReward.money;
addItem.IsBuy = false;
HandleAddItem(player, addItem);
}
if(questReward.itemId != -1)
{
C_AddItem addItem = new C_AddItem();
addItem.TemplateId = questReward.itemId;
addItem.Count = 1;
addItem.IsBuy = false;
HandleAddItem(player, addItem);
}
}
S_ClearQuest clearQuestOk = new S_ClearQuest();
clearQuestOk.QuestId = quest.TemplateId;
clearQuestOk.QuestType = quest.QuestType;
player.Session.Send(clearQuestOk);
}
else
{
Console.WriteLine("퀘스트가 완료되지 않았습니다");
}
}
통보 받은 클라이언트는
public static void S_ClearQuestHandler(PacketSession session, IMessage packet)
{
S_ClearQuest clearQuestPacket = (S_ClearQuest)packet;
Quest quest = Managers.Quest.GetQuest(clearQuestPacket.QuestId, clearQuestPacket.QuestType);
if (quest == null) return;
Managers.Quest.RemoveQuest(quest);
Managers.Quest.FinishQuest(quest);
UI_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
if (gameSceneUI != null)
{
gameSceneUI.QuestUI.ResetCurrentItem(quest);
gameSceneUI.QuestUI.RefreshUI();
}
}
역시나 마찬가지로 Update만 해준다.
퀘스트 진행사항 처리
퀘스트 진행사항은 오로지 클라는 패킷을 건네 받았을 때만 처리가 가능하다.(치팅 불가)
그렇기 때문에 예를 들어 몬스터가 죽었을 경우 서버에서
public override void OnDead(GameObject attacker)
{
if (Room == null)
return;
State = CreatureState.Dead;
S_Die diePacket = new S_Die();
diePacket.ObjectId = Id;
diePacket.AttackerId = attacker.Id;
Room.Broadcast(Pos, diePacket);
Player player = attacker as Player;
if (player != null)
{
player.QuestInven.UpdateQuestProgress(QuestType.Battle, new BattleQuestGoals { enemyId = TemplateId, count = 1 });
}
ItemDrop(attacker);
attacker.RewardExp(Stat.Exp);
Room.PushAfter(5000, DieEvent);
}
이렇게 QuestInven을 UpdateQuestProgress를 통해 Update해준다.
그리고 Update에 성곡하면 그 결과를 클라이언트에게 통보한다.
public void UpdateQuestProgress<T>(QuestType questType, T questGoals)
{
var list = QuestList[(int)questType];
foreach (var quest in list.Values)
{
if (quest.IsFinish) continue;
switch (quest)
{
case BattleQuest battleQuest when questGoals is BattleQuestGoals battleGoals:
if (battleQuest.Update(battleGoals))
{
S_QuestChangeValue changeValue = new S_QuestChangeValue();
changeValue.QuestId = battleQuest.TemplateId;
changeValue.QuestType = QuestType.Battle;
changeValue.TemplateId = battleGoals.enemyId;
changeValue.Count = battleGoals.count;
changeValue.IsFinish = battleQuest.IsFinish;
player?.Session.Send(changeValue);
}
break;
case CollectionQuest collectionQuest when questGoals is CollectionQuestGoals collectionGoals:
if (collectionQuest.Update(collectionGoals))
{
S_QuestChangeValue changeValue = new S_QuestChangeValue();
changeValue.QuestId = collectionQuest.TemplateId;
changeValue.QuestType = QuestType.Collection;
changeValue.TemplateId = collectionGoals.collectionId;
changeValue.Count = collectionGoals.count;
changeValue.IsFinish = collectionQuest.IsFinish;
player?.Session.Send(changeValue);
}
break;
case EnterQuest enterQuest when questGoals is int enterGoals:
if (enterQuest.Update(enterGoals))
{
S_QuestChangeValue changeValue = new S_QuestChangeValue();
changeValue.QuestId = enterQuest.TemplateId;
changeValue.QuestType = QuestType.Enter;
changeValue.IsFinish = enterQuest.IsFinish;
player?.Session.Send(changeValue);
}
break;
}
}
}
통보 받은 클라이언트 역시 Update 해준다.
public static void S_QuestChangeValueHandler(PacketSession session, IMessage packet)
{
S_QuestChangeValue questChangeValue = (S_QuestChangeValue)packet;
Quest quest = Managers.Quest.GetQuest(questChangeValue.QuestId, questChangeValue.QuestType);
if (quest == null) return;
switch (quest.QuestType)
{
case QuestType.Battle:
{
BattleQuest battleQuest = (BattleQuest)quest;
battleQuest?.Update(new Data.BattleQuestGoals() { enemyId = questChangeValue.TemplateId, count = questChangeValue.Count });
battleQuest.IsFinish = questChangeValue.IsFinish;
}
break;
case QuestType.Collection:
{
CollectionQuest collectionQuest = (CollectionQuest)quest;
collectionQuest?.Update(new Data.CollectionQuestGoals() { collectionId = questChangeValue.TemplateId, count = questChangeValue.Count });
collectionQuest.IsFinish = questChangeValue.IsFinish;
}
break;
case QuestType.Enter:
{
EnterQuest enterQuest = (EnterQuest)quest;
enterQuest.IsFinish = questChangeValue.IsFinish;
}
break;
}
if(quest.IsFinish == true)
{
UI_SceneConfirm_Popup go = Managers.Resource.Instantiate("UI/Popup/UI_SceneConfirm_Popup").GetComponent<UI_SceneConfirm_Popup>();
go.Setting($"<color=green>(퀘스트)</color>\n\n{quest.QuestName} <color=green>완료</color>");
}
UI_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
if (gameSceneUI != null)
{
gameSceneUI.QuestUI.RefreshUI();
}
}
결과
다음 포스팅은 이제 퀘스트 들을 DB에 저장해서 로그인할 때 불러오는 작업을 해보자.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 3D] 유저간 채팅 시스템 구현하기 (0) | 2024.08.14 |
---|---|
[Unity 3D] 퀘스트 DB에 저장하기 (0) | 2024.08.14 |
[Unity 3D] 각종 사운드 추가 (0) | 2024.07.29 |
[Unity 3D] 보스 드래곤의 패턴 만들기 (0) | 2024.07.29 |
[Unity 3D] 보스 원정대 꾸리기 + 보스 컷신 (3) | 2024.07.11 |