취미로 게임만드는 사람이 쓴 글입니다.
실무적이지 않은 것도 포함될 수 있다보니 내용을 '참고'해주면 감사하겠습니다.
소개하기
저는 csv 파일에 대화를 저장하고 분기점을 만들고 선택하고, 퀘스트를 저장할 수 있도록 구현했습니다.
작동 방식을 간략하게 설명하면 다음과 같습니다.
1. 대화를 시작합니다.
2. csv에서 [이벤트]에 해당하는 대화를 시작합니다.
3. 모든 이벤트에 진입하기 전에 플레이어가 진입조건(entry)을 만족하는지 확인합니다. 만족하지 않는다면 '24>D'처럼 표시되어있다면 D [이벤트]로 넘어가게 됩니다.
4. 만약 대화 옵션이 GoTo라면 대화가 끝나면 [다음대사]에 해당하는 [이벤트]로 이동하여 대화를 계속합니다.
5. 만약 대화 옵션이 End라면 다음번 대화시에 [다음대사]에 해당하는 [이벤트]부터 시작한다는 저장하고 대화를 끝냅니다.
예를 들어 위 예시에서 나온 대화를 분석하면,
(1) A [이벤트] 진입, 조건 확인: ok, 대화시작
(2) # 표시가 있는 곳은 선택지를 의미하는 것으로 "옆 마을로 가서 구해볼게:24"라고 적혀있으므로(그림에선 잘려서 안 보임) 진입조건(entry) 24번을 얻게 됩니다.
(3) 대화 옵션이 GoTo이므로 C [이벤트]로 이동, 조건 확인: (2)에서 진입조건 24 획득했으니 ok, 대화시작,
(4) 물병을 먹으면 진입조건(entry) 39번을 얻게 됩니다.(저는 ItemData.csv 파일을 따로 만들어서 아이템들의 진입조건을 따로 관리 했습니다.)
(5) 대화 (3)의 옵션이 'End, 다음 대화는 B'이므로 B [이벤트] 진입, 조건 확인: ok, 대화시작
...
위와 같이 할 수 있습니다.
이것을 코드로 어떻게 구현하고 CSV파일을 어떻게 만들고 저장하는지까지 다루겠습니다.
구현하기
CSV파일의 작성법은 가장 마지막에 다룰 것이기에 아래와 같이 파일이 저장되었다고 합시다.
이벤트 이름,대상,진입 조건,캐릭터,대화,스프라이트 모션,다음 대사 지정:End가 있을 때만 유효함,#,#
A:8,NPC,,NPC,물약을 구해줄 수 있니?,NPC_Curious,,, ,,,Crist,시도해볼게,Crist_Unsure,,, ,,,NPC,정말?,NPC_Curious,,, ,,,Crist,...,Crist_Unsure,,옆 마을로 가서 구해볼게:24,아니야..어려울 것 같아:56 GoTo,,,,,,C,, B:9,NPC,39&25>C,NPC,고마워!,NPC_Thank,,, End,,,,,,F,,
C:10,NPC,24>D,NPC,부탁해~,NPC_Smile1,,, End,,,,,,B,,
D:11,NPC,56>X,NPC,...,NPC_Normal,,, End,,,,,,F,,
F:16,NPC,120>C,NPC,지진이 났어,NPC_Surprised,,, End,,,,,,G,,
X:200,NPC,,NPC,에러,NPC_Surprised,,, End,,,,,,X,,
뭐...그냥 보기에는 무서워 보이지만, 엑셀로 열면 표랑 다를 게 없는 정보입니다.
csv파일을 다루기 전에 필요한 구조체와 열거형, 상수들을 정의하고 가겠습니다.
#region csv separator
const int EVENT_NAME = 0;
const int TARGET = 1;
const int ENTRY = 2;
const int CHARACTER = 3;
const int CONVERSATION_LINE = 4;
const int CHARACTER_EMOTION = 5;
const int NEXT_CONVERSATION = 6;
#endregion
위 상수들은 CSV파일을 읽는 데에 사용되는 것들입니다. 파일을 읽은 후에는 사용되지 않습니다.
public enum EndType
{
End,
GoTo
}
아까 이벤트에는 2가지 대화 옵션이 있었죠? GoTo와 End가 있었는데, 열거형으로 정의해주었습니다.
public struct ConversationLine
{
public string character; // 말하는 캐릭터
public string spriteMotion; // 스프라이트 모션(얼굴)
public string line; // 대사
public KeyValuePair<string, int>[] choices; // (있는 경우) 초이스(분기점)
public bool isChoice; // 초이스(분기점)인지 여부
}
CSV파일을 읽을 때 한 줄에 해당하는 내용을 담는 구조체입니다.
위에 예시에선
"NPC,물약을 구해줄 수 있니?,NPC_Curious"-> 이 한 줄에 해당합니다.
즉 A [이벤트]에는 4개의 ConversationLine이 있는 것입니다.
public struct ConversationData
{
public KeyValuePair<string, int> eventName; // [이벤트] '이름:id' 이름은 사실상 사람이 작성하기 편하라고 둔 것이고, id 위주로 프로그램을 작성함
public List<ConversationLine> conversationLines; // ConversationLine(한 대사에 대한 정보를 담고 있음)의 리스트
public List<int> entryCondition; // 이 대화에 진입하기 위한 조건(entry)
public string alternativeEntry; // 대화에 진입하지 못한 경우 가게될 대화
public EndType endType; // 대화 옵션 종류
public string nextEvent; // 다음 이벤트
public string target; // 누구와 대화하는지
}
하나의 [이벤트]를 담는 구조체입니다. 즉, 이것들이 여러개가 있어야 하나의 게임이 만들어지겠죠
파일이 있다면 읽는 것부터 시작해야죠, 아래처럼 읽어들입니다.
아래 방식은 말 그대로 csv를 직접 '읽는' 방식이라 이보다 쉽게 구현할 수 있을 것 같습니다.
https://answers.unity.com/questions/782965/reading-data-from-a-csv-file.html
아직은 제대로 개발을 안해서... 나중에 만들어보겠습니다
void Awake()
{
// 아래의 함수를 사용하여 씬이 전환되더라도 선언되었던 인스턴스가 파괴되지 않는다.
DontDestroyOnLoad(gameObject);
string csvText = csvFile.text.Substring(0, csvFile.text.Length - 1);
csvText = csvText.Replace("\r", "");
string[] rows = csvText.Split(new char[] { '\n' });
int index = 1;
bool isEnd = false;
conversationData = new List<ConversationData>();
ConversationData conversationParagraph = new ConversationData();
conversationParagraph.conversationLines = new List<ConversationLine>();
// ,로 분리 시작
do
{
if (isEnd)
{
conversationParagraph = new ConversationData();
conversationParagraph.conversationLines = new List<ConversationLine>();
isEnd = false;
}
string line = rows[index]; // A:8,,NPC,물약을 구해줄 수 있니?,NPC_Curious,,,,, 부분
if (string.IsNullOrEmpty(line)) { continue; }
string[] parts = line.Split(new char[] { ',' });
ConversationLine conversationLine = new ConversationLine();
for (int j = 0; j < parts.Length; j++)
{
switch (j)
{
case EVENT_NAME: // 대화 이벤트의 이름, 번호 또는 끝을 알리는 부분
if (string.IsNullOrEmpty(parts[j])) { }
else if (parts[j] == "End") { conversationParagraph.endType = EndType.End; isEnd = true; conversationParagraph.nextEvent = parts[NEXT_CONVERSATION]; }
else if (parts[j] == "GoTo") { conversationParagraph.endType = EndType.GoTo; isEnd = true; conversationParagraph.nextEvent = parts[NEXT_CONVERSATION]; }
else
{
var temp = parts[j].Split(new char[] { ':' });
conversationParagraph.eventName = new KeyValuePair<string, int>(temp[0], int.Parse(temp[1]));
}
break;
case TARGET: // 대상 조사
if (string.IsNullOrEmpty(parts[j])) { }
else
{
conversationParagraph.target = parts[j];
}
break;
case ENTRY: // 엔트리 컨디션을 조사하는 부분
if (string.IsNullOrEmpty(parts[j])) { conversationParagraph.entryCondition = null; }
else
{
var temp = parts[j].Split(new char[] { '>' });
conversationParagraph.entryCondition = temp[0].Split(new char[] { '&' }).Select(int.Parse).ToList();
conversationParagraph.alternativeEntry = temp[1];
}
break;
case CHARACTER: // 캐릭터 이름
if (string.IsNullOrEmpty(parts[j])) { Debug.LogError("이름이 비어있음!" + index + "인덱스"); }
else
{
conversationLine.character = parts[j];
}
break;
case CONVERSATION_LINE:
conversationLine.line = parts[j];
break;
case CHARACTER_EMOTION:
conversationLine.spriteMotion = parts[j];
break;
case NEXT_CONVERSATION: // 위쪽 End, GoTo에서 실행함
//if (!string.IsNullOrEmpty(parts[j])) { }
break;
default:
if (!string.IsNullOrEmpty(parts[j]))
{
ConversationLine choiceLine = new ConversationLine();
choiceLine.character = parts[CHARACTER];
choiceLine.isChoice = true;
choiceLine.spriteMotion = parts[CHARACTER_EMOTION];
choiceLine.choices = new KeyValuePair<string, int>[2];
for (int k = j; k < parts.Length; k++)
{
if (!string.IsNullOrEmpty(parts[j]))
{
var temp = parts[k].Split(new char[] { ':' });
choiceLine.choices[k - j] = new KeyValuePair<string, int>(temp[0], int.Parse(temp[1]));
}
}
conversationParagraph.conversationLines.Add(choiceLine);
goto End; // be aware of goto and keep following the flow of code
}
break;
}
if (isEnd)
{
break;
}
}
if (!string.IsNullOrEmpty(conversationLine.line))
{
conversationLine.isChoice = false;
conversationParagraph.conversationLines.Add(conversationLine);
}
// GOTO for breaking double loop
End:
if (isEnd)
{
conversationData.Add(conversationParagraph);
}
index++;
} while (index <= rows.Length - 1);
}
꽤 깁니다. 천천히 따라가 보죠
사실 노가다가 전부인 코드입니다.
// 작성중
'개발 > [C#] Unity' 카테고리의 다른 글
[Unity] 2.5d 어드벤쳐 게임 #개발일지 2022/09/06 (0) | 2022.09.06 |
---|---|
[Unity] 2.5d 어드벤쳐 게임 #개발일지 2022/09/04 (0) | 2022.09.04 |