ZirAjs
ZirAjs' blog
ZirAjs
전체 방문자
오늘
어제
  • 분류 전체보기
    • 고딩 공부
      • 물리학Ⅰ
      • 자작 문제집
      • 자작 문제집 정오표
    • 개발
      • [C#] WPF
      • [C#] Unity
      • [Python]
      • [C++] QT
      • [Java|Kotlin] minecraft plu..
    • Books

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

  • 완성된 프로그램 목록

인기 글

태그

  • 멀티엔딩
  • 모의고사
  • 수특변형
  • 문제 자르기
  • 양자 컴퓨터 원리와 수학적 기초
  • Unity
  • 자리배치
  • 문제 추출
  • c#
  • RPG
  • 대화구현
  • 분기
  • 2.5
  • iCon
  • 학력평가
  • 쯔꾸르
  • 헌법의 기초
  • 분기점
  • 대화
  • 수능
  • 물리1
  • QT
  • WPF
  • Isometric

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
ZirAjs

ZirAjs' blog

[Unity] RPG 대화 구현 (csv 이용)
개발/[C#] Unity

[Unity] RPG 대화 구현 (csv 이용)

2022. 9. 7. 00:52

취미로 게임만드는 사람이 쓴 글입니다.

실무적이지 않은 것도 포함될 수 있다보니 내용을 '참고'해주면 감사하겠습니다.



소개하기


저는 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
    '개발/[C#] Unity' 카테고리의 다른 글
    • [Unity] 2.5d 어드벤쳐 게임 #개발일지 2022/09/06
    • [Unity] 2.5d 어드벤쳐 게임 #개발일지 2022/09/04
    ZirAjs
    ZirAjs
    ZirAjs의 블로그입니다. 과학, 수학, 개발에 관심이 있습니다

    티스토리툴바