'마녀:귀찮아!' DevLog 00
_
오늘부터 개발일지를 쓰기로 마음먹었다.
내 생각이나 개발 과정보다는 생각 정리, 자료 정리에 집중할 것 같다.
오늘은 퀘스트 시스템을 기획 해보기로 했다.
퀘스트 시스템 기획
참고 1: 쿠키런 킹덤 - 곰젤리 열차
쿠키런 킹덤에는 곰젤리 열차라는 건물이 있다.
곰젤리 열차는 아래 기능을 한다.
일정시간마다
열차가 역에 들어온다.- 열차마다 서로다른
아이템을 요구
한다. - 열차에 아이템을
납품하면
, 열차가 역을 떠난다. - 떠난 열차는
랜덤한 보상
을 가지고 돌아온다. - 반복
납품의 난이도가 어려울수록 더 좋은 보상을 준다.
컨텐츠를 진행하면 열차의 수가 늘어난다.
참고 2: 시즌 패스
최근 몇 년간, 여러 게임에서 사용하기 시작한 시스템이다.
시간을 특정 주기를 나눠 시즌으로 구분시키고, 아래 기능을 제공한다.
- 퀘스트를 준다.
- 퀘스트를 완료하면 포인트를 준다.
- 포인트를 일정량 모으면 레벨이 늘어난다.
- 레벨마다 보상을 받을 수 있다.
재화를 주고 티켓을 구매하면, 레벨마다 추가적인 보상을 얻을 수 있다.
퀘스트는 일일, 주간, 랜덤 등 다양하게 주어진다.
참고 3
특정 스토리나 주제를 가지고 쭉 이어지는 퀘스트 (Like RPG, 아이러브커피)
일퀘, 주간퀘 (Like RPG, 원신)
스까보자
- 의뢰를 해결하면 보상과 의뢰 경험치를 받음
- 의뢰 경험치를 모아 의뢰 레벨을 올릴 수 있음
- 의뢰 레벨이 높아질수록, 높은 등급의 의뢰를 받을 수 있고, 여러 의뢰가 해금됨
의뢰 레벨을 임의로 조정하여, 원하는 의뢰를 집중하여 받을 수 있도록
- 의뢰 종류
- 단순 의뢰: 전달, 대화, 호위, 아이템 요청, 던전 클리어, 몹 처치, 상호작용
- 스토리 의뢰 (메인, 서브, 확장팩): 스토리, 대화, 컷씬 + 단순 의뢰
일단 만들어보자
쿠키런 킹덤의 곰젤리 열차 같은 기능을 먼저 만들어보자.
근데 마녀의 집에는 열차가 없다.
주변 마을에 있는 의뢰 게시판에서 의뢰를 찾아오기로 하자.
의뢰를 찾아오고, 아이템을 전달하는 일은 모두 인형이 대신 해줄 것이다.
인형은 플레이어블 캐릭터지만,
동시에 클래시오브클랜의 장인과 같은 역할도 해줄 것이다.
분재 게임에서 많이 본 것 같은 시스템이다.
어쨌든,
거느리고 있는 인형을 관리하는 시스템과 UI가 필요하다.
이를 먼저 만들어보자.
인형 관리
사실 인형 관리 UI는 이미 있다.
하지만 던전에 들어갈 인형을 선택하는 기능, 인형의 장비를 선택하는 기능만 있는 상태다.
여기서 더 필요한 기능은 아래와 같다.
- 거느리는 인형의 수를 표시
- 거느리는 인형 Slot 나열
갑자기 생각났는데,
플레이어블 캐릭터는 아니지만, 일은 할 수 있는 인형이 있으면 좋을 것 같다.
더미 인형
이라고 부르면 될 것 같다.
추후 이런 더미 인형에 혼을 집어넣어서,
플레이어블 캐릭터로 진화시키는 식으로 만들면 좋을 것 같다.
- 더미 인형
더미 인형
1
2
3
4
5
public class GameData
{
// ~
public int dummyDollCount = 1;
}
기존 인형 정보를 저장하는 것처럼, 더미 인형도 정보를 저장할 필요가 있다.
이때, 더미 인형은 모두 똑같은 모습으로 만들고 개성을 주지 않을 것이기 때문에,
기존 인형처럼 인형이 착용하고 있는 장비 같은 추가 정보를 저장할 필요가 없다.
그래서 단순히 GameData
클래스에 dummyDollCount
변수를 추가했다.
모든 인형을 실체화시켜 맵에 돌아다니게 하고 싶긴 한데, 당장 필요하진 않아서 패스.
거느리는 인형 표시
거느리고 있는 인형은 어떻게 표시할까?
모든 인형 정보를 가져와서 인벤토리 형식으로 만들고,
거느리고 있는 인형만 활성화시키기로 하자.
새로 UIDollBuffer: UIDataBuffer<Doll>
클래스를 만들었다.
UIDataBuffer<T>
는 특정 컨테이너에 있는 요소들을 UISlot
에 할당시켜 보여주는, 내가 만든 클래스다.
이를 상속 받고, 더미 인형과 함께 거느리고 있는 인형만 표시하도록 Override했다.
위 작업은 금방 끝났는데,
기존 인형 관리 UI에 있던 기능들을 옮기고 수정하느라 시간을 좀 먹었다.
기존에는 던전에 들어갈 인형 정보 하나만 UI로 보여줬다.
그래서 Prev/Next 버튼을 누르면 selectedDollIndex
데이터를 수정하고, dollDic[selectedDollIndex]
로 인형 데이터를 가져와 UI를 갱신했다.
하지만 여러 인형을 리스트로 나열하게 되면서, 던전에 들어갈 인형이 아닌 다른 인형의 정보도 UI에 띄울 수 있어야 했다.
그래서 UI에서 마지막으로 선택한 인형의 Index
를 기준으로 UI를 띄우고, 표시된 인형을 던전에 들어갈 인형으로 선택할 수 있는 버튼을 새로 하나 만들었다.
UI는 추후 더 꾸며보기로.
거느리는 인형 수 표시를 하기 위해서
클래시오브클랜 장인처럼, 여기서도 거느리고 있는 인형 수를 표시하면 좋을 것 같다.
목적은 현재 일을 시킬 수 있는 인형 수가 얼마나 있고, 일을 하고 인형 수는 얼마나 있는지 알려주는 것.
여기서 좀 고민스러워지는게 있다.
어떤 인형이 어떤 일을 하고 있는지 알고있어야 하는데, 요건 어떻게 저장할까?
더미인형만 일을 한다고 가정한다면, 일 정보를 따로 저장하면 된다.
더미인형의 모습은 모두 똑같기 때문에, 일 정보와 더미인형을 단순히 인덱스 순서대로 매칭시키면 된다.
하지만 플레이어블 캐릭터도 일하는 것이 가능하게 만들고 싶다.
플레이어블 캐릭터가 일을 한다고 가정한다면, 일 정보와 플레이어블 캐릭터의 정보를 매칭시켜서 저장해야 한다.
똑같은 플레이어블 캐릭터를 여러 기 가질 수 없기 때문에, 플레이어블 캐릭터의 ID와 일 정보를 매칭시키면 된다.
둘 다 일을 할 수 있게 하려면..
생각해보니 그냥 더미인형 따로, 플레이어블 캐릭터 따로 저장하면 되네 !
일 정보 저장
일 정보를 먼저 만들어보자.
일의 특징은 아래와 같을 것이다.
- 시간과 이벤트 정보를 가지고 있다.
- 인게임 시간이 지나면서, 자동으로 완료도가 진행된다.
- 완료도가 100% 진행되면 완료되며, 이벤트가 발생한다.
음, 이벤트를 어떻게 저장하고 처리할지가 고민스럽다.
Action 같은 걸 직렬화 시킬 수는 없을 것 같고, 단순히 WorkType
Enum을 만들어서 하드코딩 해보기로 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public enum WorkType
{
CompleteQuest
}
public class Work
{
public WorkType workType;
public int value;
public float time;
public float curTime = 0;
public Work(WorkType workType, int value, float time)
{
this.workType = workType;
this.value = value;
this.time = time;
}
public void Tick(float tick)
{
curTime += tick;
if (IsCompleted)
{
curTime = time;
}
}
public bool IsCompleted => curTime >= time;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void TickWorks()
{
foreach (KeyValuePair<int, List<Work>> work in Works)
{
for (int i = work.Value.Count - 1; i >= 0; i--)
{
work.Value[i].Tick(TimeManager.Tick);
if (work.Value[i].IsCompleted)
{
switch (work.Value[i].workType)
{
case WorkType.CompleteQuest:
SOManager.Instance.QuestDataBuffer.RuntimeItems[work.Value[i].value].Complete();
break;
default:
throw new ArgumentOutOfRangeException();
}
work.Value.RemoveAt(i);
}
}
}
}
코드가 이쁘진 않다.
하드코딩한 일 이벤트를 따로 함수로 빼면 좀 괜찮아질 것 같다.
Work
를 Struct으로 만들면 TickWorks
에서 각 Work
를 Tick시키는 부분 때문에 더 지저분해져서, Class로 만들었다.
Dictionary<int, List<Work>>
로 일들을 관리한다.
Key값으로 인형의 ID를, Value값으로 해당 인형의 일 정보를 저장한다.
works를 플레이어블 인형 용 Dictionary<int, Work>
, 더미 인형 용List<Work>
로 구분시킬까하다가, 데이터 저장/불러오기가 복잡해질 것 같아서 Dictionary
하나로 통일시켰다.
그래서 인형이 한 번에 하나의 일을 할 수 있음에도 일 정보를 List
로 저장한다.
똑같은 ID를 가지는 더미인형의 일을 한 번에 처리하기 위함이다.
AddWork 같은 함수에서 플레이어블 인형은 일을 하나만 할 수 있도록 별개의 처리를 해야하긴 하지만,
그건 플레이어블 인형과 더미 인형 컨테이너를 분리시켜도 마찬가지라 논외로 둔다.
거느리는 인형 수 표시
이를 바탕으로 거느리는 인형 수를 표시해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private IEnumerator UpdateUI()
{
WaitForSeconds wait = new(.1f);
while (true)
{
int workableDollCount = SOManager.Instance.Dolls.RuntimeItems.Count;
foreach (Doll doll in SOManager.Instance.Dolls.RuntimeItems)
{
if (doll.ID == Doll.DUMMY_ID)
continue;
if (workManager.DoseDollHaveWork(doll.ID))
workableDollCount--;
}
if (workManager.Works.ContainsKey(Doll.DUMMY_ID))
workableDollCount -= workManager.Works[Doll.DUMMY_ID].Count;
text.text = $"{workableDollCount}/{SOManager.Instance.Dolls.RuntimeItems.Count} 인형";
yield return wait;
}
}
테스트 함수를 만들고 테스트 해보자.
아래는 더미인형이 10초의 일을 하도록 하는 테스트코드다.
1
2
3
4
5
[ContextMenu(nameof(TestWork))]
public void TestWork()
{
WorkManager.AddWork(Doll.DUMMY_ID, new Work(WorkType.CompleteQuest, 0, 10));
}
잘 작동한다.
데이터 저장/불러오기도 잘 동작한다.
일반적인 SNG 류의 게임은 오프라인에도 시간이 흐르지만, 이 게임에서는 Cult of the Lamb 처럼 게임에 접속해 있을 때만 시간이 흐르게 할 것이다.
마을 의뢰
이제 마을 의뢰를 만든다.
처음부터 마을 의뢰를 가지고 시작하는 것은 어색할 것이다.
주변 마을에서 온 NPC를 만들고, 마을 의뢰를 하나 받도록 하면 좋을 것 같다.
NPC로부터 퀘스트 받기
NPC가 퀘스트를 주기 위해서는, 어떤 퀘스트를 줄지 알고 있어야한다.
NPC 스크립터블 오브젝트에 List<Quest>
를 새로 추가하고,
NPC UI를 갱신할 때, NPC가 퀘스트를 가지고 있으면 선택지로 보여준다.
퀘스트 선택지를 선택했을 때,
만약 퀘스트를 받지 않은 상태라면 받고,
만약 퀘스트를 받았지만 완료하지 못한 상태라면 무시,
만약 퀘스트를 받고 완료한 상태라면 보상을 받도록 한다.
이런 상태를 표현하기 위해 퀘스트 스크립터블 오브젝트에 QuestType
을 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void SelectQuest(int index)
{
Quest quest = curNPCData.Quests[index];
switch (quest.QuestState)
{
case QuestState.Locked:
quest.Unlock();
break;
case QuestState.Unlocked:
// 대화
break;
case QuestState.Completed:
// 보상
break;
default:
throw new ArgumentOutOfRangeException();
}
UIManager.Instance.SetOverlay(MPanelType.None);
}
다만, 마을 의뢰는 기획 상 NPC에게 직접 보상을 받는 방식이 아니다.
때문에 case QuestState.Completed
블럭안에 추가적인 조건을 달아줄 필요가 있다.
1
2
3
4
5
public enum QuestType
{
Normal,
VillageQuest
}
1
2
3
4
5
6
case QuestState.Completed:
if (quest.QuestType != QuestType.VillageQuest)
{
// 보상
}
break;
아직 모든 case가 내부 구현은 안됐지만, 전체적인 흐름은 만들어졌다.
추후, 개선된 대화 시스템을 만들면서 함께 개선시킬 것.
퀘스트가 잘 받아지는 걸 확인했으니, 이제 의뢰 UI에서 마을 의뢰를 완료하는 기능을 만들어보자.
마을 의뢰 완료하기
To Be Continued..
메모
메이플스토리 NPC처럼, 퀘스트 상황을 머리 위 아이콘으로 알 수 있으면 좋을 것 같다.