XR 플밍 - 6. 콘솔 프로젝트 [Project Exit] - 2일차 (4/9)

이형원·2025년 4월 9일
0

XR플밍

목록 보기
38/215

0. 들어가기에 앞서

어제자의 TODO 리스트로 정했던 것을 살펴 보자.

  1. 인벤토리 Open 기능 및 아이템 사용 기능 구현
  2. 탈출을 위한 아이템의 추가, 함정 등 새로운 오브젝트 및 맵 구성
  3. 본격적인 스토리 라인 구축
  4. 호감도 시스템 및 체력/정신력에 따른 엔딩 시스템 구현

결과적으로 말하자면, 이것 전부를 구현하기에는 시간이 많이 모자랄 것 같아서 실제 게임 내용에서 변동사항이 꽤 있었다.

  1. 인벤토리 Open 기능 및 아이템 사용 기능을 구현했다. 또한 소비 아이템처럼 사용 가능한 아이템(소모품)과 사용 불가능한 아이템(이벤트성 아이템) 조건을 걸어, 사용 가능한 아이템만 사용할 수 있는 기능을 추가하였다.
  2. 탈출을 위한 아이템을 추가하였다. 다만 함정 등 새로운 오브젝트 구현은 아직 못했으며, 개발 기간 상 생략될 수도 있다.
  3. 게임을 길게 만들 수는 없어서 조금 빈약한 스토리 라인이 되긴 했지만, 스토리 라인을 짜서 엔딩 루트를 구현했다.
  4. 사실상 지금까지의 진행 상황을 보면 거의 진행 불가다... 의외로 텍스트를 조건마다 만드는 게 상당한 노가다이고 시간이 많이 걸려서 이 부분은 빠지지 않을까 싶다.

꽤 시간 투자를 많이 했고 구현한 것도 제법 된다고 생각하지만, 의외로 결과물이 처참하기 그지없어서 조금 슬프다. 하지만 그래도 건진 게 꽤 있으니, 오늘자로 진행한 내용에 대해 기록해 보고자 한다.

1. 본격적인 스토리 라인

기본적으로 탈출 게임을 만들고 싶다는 생각에서 시작한 프로젝트이기 때문에 어느 정도 스토리라인을 잡아주었다.

스토리 라인 개요(최종)

플레이어인 P군과 NPC인 N양이 낯선 장소에서 깨어나게 된다.
P군과 N양은 면식이 있는 같은 대학 동기이며, 둘은 이 낯선 곳에서 같이 탈출하기로 한다.
하지만 조심스레 내려가는 아래에 낯선 남자가 있는데...?

이전에 작성했던 스토리라인에서 아주 조금 살을 붙인 정도이다. 사실 이야기 짓는 거에는 나름 자신이 있었는데 시간이 촉박하면 아이디어도 안 나오나 보다. (살려주세요 그냥 게임만이라도 완성하고 싶어요)
하지만 애초에 스토리에 중점을 두는 것보단 여러가지 코딩 및 시스템 구현 시도를 해 보는 것에 의의를 두기로 했다.

스토리 라인의 확정으로 추가된 항목으로, NPC - C군을 추가하기로 했다.

2. 인벤토리 열기와 아이템 사용의 구현

우선은 필수적으로 해야 했던 인벤토리 사용의 구현부터 하기로 했다.

인벤토리 사용 유틸을 구현하기 위해선, Stack의 개념이 들어가야 한다. 이유는, 실제 게임에서 인벤토리 창을 열 때의 예시를 생각해 보면 좋다.

  1. 인벤토리 창을 연다. (Stack.Push와 같은 기능)
  2. 인벤토리 창에서 어떤 아이템을 사용한다고 했을 때, 사용할 것인지 묻는 팝업이 나온다.(Stack.Push와 같은 기능)
  3. 아이템이 사용되고 해당 팝업이 닫힌다. (Stack.Pop와 같은 기능)
  4. 아이템을 버리는 행위를 했을 때, 아이템을 버리겠냐는 팝업이 나온다. (Stack.Push와 같은 기능)
  5. 아이템이 버려지고 해당 팝업이 닫힌다. (Stack.Pop와 같은 기능)
  6. 인벤토리를 닫으면서 팝업이 닫힌다. (Stack.Pop와 같은 기능)

이와 같이 작동하는 과정에 대해 생각해 보며 인벤토리의 열기를 구현해 보자.

먼저, 나는 인벤토리의 구현에 관해 아래와 같은 기능이 필요하다고 생각했다.

  1. 인벤토리를 I키를 눌러 실행시킨다. I를 누르면 인벤토리가 실행된다.
  2. 인벤토리가 나오면 보유한 아이템이 출력되고, 아래와 같은 세 가지 기능이 출력된다.
    2.1 아이템 정보 상세보기 기능
      2.1.1 아이템 상세보기는 아이템의 번호를 입력하고 해당 아이템의 정보를 출력한다.
      2.1.2 해당 아이템이 사용가능할 경우 사용할 지 선택지를 출력한다.
      2.1.3 해당 아이템을 사용하면 사라지고, 사용하지 않으면 그대로 남는다.
    2.2 아이템 버리기 기능
      2.2.1 아이템의 번호를 입력해 아이템이 있는지 확인하고, 버릴 건지 선택지를 출력한다.
      2.2.2 해당 아이템을 버리면 사라지고, 버리지 않으면 그대로 남는다.
    2.3 뒤로가기

이와 같이 조건을 작성하고, 코드를 작성하였다.

private Stack<string> stack = new Stack<string>();
private int selectIndex;

...

// 인벤토리가 열렸을 때, 해당 정보를 출력하기 위한 함수.
// 인벤토리 칸이 4칸으로 정해져 있기 때문에, 
// 인벤토리에 아이템이 있으면 아이템 이름을 출력, 아니면 비었음이라고 출력된다.
public void PrintAll()
{
    Console.WriteLine("===소지 중인 아이템===");
    Console.WriteLine();
    for (int i = 0; i < items.Length; i++)
    {
        if (items[i] != null)
        {
            Console.WriteLine($"{i + 1}. {items[i].name}");
        }
        else
        {
            Console.WriteLine($"{i + 1}. 비었음");
        }
    }
    Console.WriteLine();
}

// 인벤토리를 열었을 때 메뉴를 호출하면서 스택을 Push한다.
public void Open()
{
    stack.Push("Menu");

    while (stack.Count > 0)
    {
        Console.Clear();
        switch (stack.Peek())
        {
            case "Menu": Menu(); break;
            case "Detail": Detail(); break;
            case "DropMenu": DropMenu(); break;
        }
    }
}

// 메뉴가 출력하는 내용
private void Menu()
{
	// 아이템 출력
    PrintAll();
    
    // 선택지 출력
    Console.WriteLine("1. 상세 정보 확인하기");
    Console.WriteLine("2. 아이템 버리기");
    Console.WriteLine("X. 뒤로 가기");

    ConsoleKey input = Console.ReadKey(true).Key;
    switch (input)
    {
    	// 상세 정보 확인하기를 하면, 상세정보 출력이 Push되면서 해당 메뉴 호출
        case ConsoleKey.D1:
            stack.Push("Detail");
            break;
        // 아이템 버리기를 하면, 버리기 출력이 Push되면서 해당 메뉴 호출   
        case ConsoleKey.D2:
            stack.Push("DropMenu");
            break;
        // 뒤로가기를 누르면 스택이 Pop하면서 메뉴가 종료
        case ConsoleKey.X:
            stack.Pop();
            break;
    }
}
private void Detail()
{
    PrintAll();
    Console.WriteLine("상세 정보를 확인할 아이템을 선택해주세요.");
    Console.WriteLine("뒤로 가기를 하시려면 X를 눌러주세요.");

    ConsoleKey input = Console.ReadKey(true).Key;
    
    // 뒤로가기
    if (input == ConsoleKey.X)
    {
        stack.Pop();
    }
    else
    {
    	// 입력 문자열을 아스키 코드 번호로 형변환하여 계산함
        int select = (int)input - 49;
        
        // 잘못 입력한 경우 (인벤토리 범위 벗어남)
        if (select < 0 || select > 4)
        {
            Util.XKeyText("잘못 입력하였습니다.");
            Util.XKeyText("뒤로 가려면 X키를 눌러주세요.");
        }
        // 해당 번호에 아이템이 있을 경우
        else
        {
            if (items[select] != null)
            {
                Console.WriteLine();
                Console.WriteLine($"아이템명 : {items[select].name}");
                Console.WriteLine($"아이템 상세정보 : {items[select].description}");
                selectIndex = select;

                Item selectItem = items[selectIndex];
                
                // 아이템이 사용 가능한 아이템의 경우 - Item에 해당 속성을 부여
                if (items[selectIndex].isUsable)
                {
                    Console.WriteLine();
                    Console.WriteLine($"{items[selectIndex].name} 을/를 사용하시겠습니까? (y/n)");

                    ConsoleKey input2 = Console.ReadKey(true).Key;
                    switch (input2)
                    {
                        case ConsoleKey.Y:
                            Console.WriteLine();
                            selectItem.Use();
                            Console.WriteLine($"{items[selectIndex].name} 을/를 사용하였습니다.");
                            Util.XKeyText("뒤로 가려면 X키를 눌러주세요.");
                            items[selectIndex] = null;
                            stack.Pop();
                            break;
                        case ConsoleKey.N:
                            stack.Pop();
                            break;
                    }
                }
                // 아이템이 사용 불가능한 아이템의 경우 - Item에 해당 속성을 부여
                else
                {
                    Console.WriteLine();
                    Console.WriteLine("사용할 수 없는 아이템입니다.");
                    Console.WriteLine();
                    Util.XKeyText("뒤로 가려면 X키를 눌러주세요.");
                }
            }
        }
    }
}

// 아이템 버리기 메뉴
private void DropMenu()
{
    PrintAll();
    Console.WriteLine("버릴 아이템을 선택해주세요.");
    Console.WriteLine("뒤로 가기를 하시려면 X를 눌러주세요.");

    ConsoleKey input = Console.ReadKey(true).Key;
    
    // 뒤로가기
    if (input == ConsoleKey.X)
    {
        stack.Pop();
    }
    else
    {
        int select = (int)input - 49;
        if (select < 0 || select > 4)
        {
            Util.XKeyText("잘못 입력하였습니다.");
            Util.XKeyText("뒤로 가려면 X키를 눌러주세요.");
        }
        else
        {
        	// 아이템이 존재하고 버릴 수 있는 경우
            if (items[select] != null)
            {
                Console.WriteLine();
                Console.WriteLine($"{items[selectIndex].name} 을/를 버리시겠습니까? (y/n)");

                ConsoleKey input2 = Console.ReadKey(true).Key;
                switch (input2)
                {
                    case ConsoleKey.Y:
                        Console.WriteLine();
                        Console.WriteLine($"{items[selectIndex].name} 을/를 버렸습니다.");
                        Util.XKeyText("뒤로 가려면 X키를 눌러주세요.");
                        items[selectIndex] = null;
                        stack.Pop();
                        break;
                    case ConsoleKey.N:
                        stack.Pop();
                        break;
                }
            }
        }
    }
}

해당 내용은 강의 시간에서 시연해 준 방식이기는 하나, 막상 직접 해 보려니까 생각보다 구현하는 데에 생각을 많이 해야 했다.
다만 이미 한 번 해 본 것이라도 다시 구현해보니 스택이란 게 어떻게 작동하고, 인벤토리 열기 기능이 어떻게 구현되는 건지 훨씬 더 체감이 잘 되었다.

3. 새로운 아이디어, 매번 다른 대화 스크립트 출력하기

사실 오늘 작업 시간 중 대부분을 할애했던 것이 이 부분이 아니었을까 싶다.
1일차에서 맵 입장 시의 스크립트를 1회성으로 출력하기 위해 bool 변수를 사용했으나, 이건 대화 횟수가 많아지면 도저히 감당이 되지 않는 방식이지 않을까 싶었다.
그러다가 문득, 이런 생각이 들었다.

대화도 스크립트 1 -> 스크립트 2 -> 스크립트 3 순으로 출력되어야 하는 거니까 Queue의 선입선출을 사용하면 매번 다른 대화 스크립트를 구현할 수 있지 않을까?

사실은 Queue의 경우에는 턴제 전투를 가정한 상황에서 사용 가능할 거라는 조언이 있었다. 그래서 게임에 되도록 많은 기능을 구현해 보기 위해서 이 게임에다 전투를 구현해야 하나 싶기도 했었다. (하지만 제 게임 컨셉에 전투는 안 어울려서 굳이 이런 기능까지 만들고 싶지 않은 걸요)
만약 매번 다른 대화 스크립트를 큐로 구현할 수 있다면, 큐 기능을 쓰면서 내가 원래 의도했던 기능을 구현하고, 전투 씬을 개발할 시간을 빼는(?) 두 마리의 토끼를 잡는 셈이다.

매번 다른 대화를 출력하는 과정을 큐로 구현해 보자.
매커니즘 자체는 아래와 같이 돌아간다.

// 큐의 생성
Queue<int> talkLog = new Queue<int>();
public Ms_N(Vector2 position)
    : base(ConsoleColor.DarkYellow, 'N', position, false)
{
    name = "N양";
    isTalking = false;          
    unableToAct = false;
    
    // 큐는 NPC가 생성될 때 숫자 1, 2, 3을 입력한다.
    talkLog.Enqueue(1);
    talkLog.Enqueue(2);
    talkLog.Enqueue(3);
}

// 플레이어가 상호작용할 때 Talk가 실행된다.
public override void Interact(Player player)
{
    Talk();
}

public override void Talk()
{
	// 큐 스택이 1개 초과로 담아져 있을 경우
    if (talkLog.Count > 1)
    {
        switch (talkLog.Peek())
        {
        	// 첫 번째 텍스트로그를 출력하고, Dequeue로 숫자를 반환한다.
            // 이후 TalkLog1이 다시 출력되지 않게 된다. 
            case 1: TalkLog1(); talkLog.Dequeue(); break;
            
            // 두 번째 텍스트로그를 출력하고, Dequeue로 숫자를 반환한다.
            // 이후 TalkLog2는 다시 출력되지 않게 된다.
            case 2: TalkLog2(); talkLog.Dequeue(); break;
        }
    }
    
    // 큐의 스택이 하나만 남았을 경우, 마지막 대사를 반복 출력한다.
    else
    {
        TalkLog3();
    }
}

// N양과의 대화 내용 출력
private void TalkLog1()
{
    대화내용 1
}
private void TalkLog2()
{
    대화내용 2
}
private void TalkLog3()
{
    대화내용 3
}

또한 맵을 이동했을 때의 N양의 대화 내용이 달라져야 하는데, 이 부분에 대해서는 어떻게 구현할까 머리를 엄청 싸맸다. 온몸 비틀기를 하면서 N양 하나만 가지고 만들어볼까 하다가 결국 아래처럼 만들어버렸다.

그냥 맵2용 N양 NPC를 따로 만드는 편이 낫겠다는 결론이 났다. 어차피 당장 N양-1 의 대화 내용 자체도 엄청난 텍스트량이 있다 보니, 괜히 뭉쳐서 만드는 것보다는 층별로 N양을 구별하고 구현해서 각각의 대화 내용을 담아두는 편이 낫겠다는 생각이 들었다.

그 다음으로 C군 NPC의 구현 내용은, 다른 추가적인 내용을 다룬 뒤에 다뤄볼 생각이다.

4. 아이템 추가 구현 - 이벤트성 아이템

앞서 인벤토리 창의 구현 과정에서 사용 가능한 아이템과 불가능한 아이템을 구별하는 기능을 담았다고 했다. 이건 추가로 구현한 두 아이템 - 밧줄과 칼 때문이다.

붕대 같은 경우는 애초에 포션 용도로, HP를 회복하기 위한 용도로 준비한 아이템이다. 하지만 추가로 구현한 칼과 밧줄의 경우에는, C군 NPC와의 선택지에서 영향을 주는 아이템으로 설정하기 위해서, 사용 기능을 구현하지 않기로 했다.

이에 따라 변경된 내용은, 기존의 추상 클래스로 구현했던 Item 클래스를 일반 클래스로 바꾸고, Use를 필수적으로 구현해야 하는 함수에서 가상함수로 바꿨다. 또한 IsUsable bool 변수를 추가하여, 아이템의 사용 가능 여부를 기본적으로 설정하도록 했다.

위와 같이 내용을 변경하여 Use는 필수적으로 구현하지 않아도 되는 함수가 되었고, 아래와 같이 사용 시 효과가 발생하는 아이템과, 그저 소유의 목적으로 필요한 아이템 두 가지를 구현하였다.

(좌측 : 사용시 효과를 가지는 아이템        우측 : 사용 시의 효과는 없이 소유 목적의 아이템)

이와 같이 구분 지어 아이템의 소지 여부를 엔딩과 연관지을 수 있도록 구현할 것이다.

5. 엔딩 루트의 구현 - C군 NPC와의 대화 추가

새로운 NPC C군을 추가하였고, C군과의 대화를 중심으로 엔딩 루트를 만드는 것으로 구현해 보기로 했다. 앞서 NPC N양으로 Queue를 이용한 대화 출력을 성공시켜 봤으니, 이제 이걸 C군에게도 반영하면 된다.

문제는, 엔딩 분기점이다 보니 코드가 무진장 복잡하다. 이거를 그냥 bool 변수나 if문 같은 것으로만 구현했더라면 엄청나게 복잡했을 것이다. 그나마 Queue로 표현하려고 노력한 덕분에 코드를 조금이나마 간결하게 할 수 있었다.

public class Mr_C : NPC
{
    Queue<int> talkLog = new Queue<int>();
    private bool eventHappened = false;
    public Mr_C(Vector2 position)
        : base(ConsoleColor.Red, 'C', position, false)
    {
        name = "C군";
        isTalking = false;
        
        // 출력해야 하는 대화의 분기점이 6개이다.
        talkLog.Enqueue(1);
        talkLog.Enqueue(2);
        talkLog.Enqueue(3);
        talkLog.Enqueue(4);
        talkLog.Enqueue(5);
        talkLog.Enqueue(6);
    }

    public override void Interact(Player player)
    {
        Talk();
    }

	// 설정한 엔딩 자체는 3개이며, 2번 엔딩의 경우 텍스트 길이 상의 분량으로
    // 대화를 끊어서 출력하기로 함.
    public override void Talk()
    {
        if (talkLog.Peek() == 1)    // 대화 분기점
        {
            TalkLog1();
        }
        else if (talkLog.Peek() == 2)   // 1번 엔딩 반복
        {
            TalkLog5();
        }
        else if (talkLog.Peek() == 3)   // 2-1번 엔딩
        {
            TalkLog6();
        }
        else if (talkLog.Peek() == 4)   // 2-2번 엔딩
        {
            TalkLog7();
        }
        else if (talkLog.Peek() == 5)   // 2-3번 엔딩 반복
        {
            TalkLog8();
        }
        else if (talkLog.Peek() == 6)   // 3번 엔딩 반복
        {
            TalkLog9();
        }
    }

	// 대화 로그 1. 엔딩 분기점이 나뉘는 부분
    private void TalkLog1()
    {
        Console.SetCursorPosition(0, 14);
        Util.XKeyText("책상 위에 한 남자가 잠을 청하고 있습니다.");
        Util.XKeyText("남자는 곤히 잠에 든 듯 깨어날 기미가 보이지 않습니다.");
        Console.WriteLine();
        Util.XKeyText("지금이라면 도망치거나, 아니면 남자를 제압할 수 있을 것 같습니다.");
        Console.WriteLine();
        Console.WriteLine("1. 남자를 칼로 찌른다. ( 칼 소지품 필요 )");
        Console.WriteLine("2. 남자를 밧줄로 포박한다 ( 밧줄 소지품 필요 )");
        Console.WriteLine("3. 이대로 조용히 도망친다.");
        Console.WriteLine();
        input = Console.ReadKey(true).Key;
        
        // 각 선택지마다 다른 대화 로그로 이동하며, 각각의 결과를 출력
        switch (input)
        {
            case ConsoleKey.D1: TalkLog2(); break;	// 엔딩 1
            case ConsoleKey.D2: TalkLog3(); break;	// 엔딩 2
            case ConsoleKey.D3: TalkLog4(); break;	// 엔딩 3
            default: Console.WriteLine("잘못된 입력입니다."); break;
        }
        isTalking = false;
    }
    
    // 엔딩 1
    private void TalkLog2()
    {
        for (int i = 0; i < Game.Player.inventory.items.Length; i++)
        {
        	// 칼이 있을 경우 이벤트가 실행됨
            if (Game.Player.inventory.items[i] != null && Game.Player.inventory.items[i].name == "칼")
            {
                Util.XKeyText("당신을 칼을 들어 남자를 뒤에서 공격했습니다.");
                Util.XKeyText("N양이 당신을 채 막기도 전에 일어난 일입니다.");
                Console.WriteLine();
                Util.NPC_NText("꺄아아아악!");
                Console.WriteLine();
                Util.XKeyText("N양은 당신이 저지른 짓을 보고 충격을 받아 주저앉습니다.");
                Util.XKeyText("다른 방법을 택할 수는 없었을까요?");
                Util.XKeyText("당신은 무심하게 N양을 바라봅니다.");
                Console.WriteLine();
                Util.ZKeyText("대화를 종료하려면 Z키를 누르세요.");
                eventHappened = true;
                talkLog.Dequeue();	// 이후 Peek : 2 - 대화 로그 5로 이동

                isTalking = false;

                break;
            }                
        }
        
        // 칼이 없는 경우 게임오버
        if (eventHappened == false)
        {
            Util.XKeyText("당신에게는 남자를 제압할 칼이 없었습니다.");
            Util.XKeyText("당신과 남자는 몸싸움을 벌였고,");
            Util.XKeyText("당신보다 더 힘 쎈 남자에게 제압 당하면서 칼에 찔렸습니다.");
            Util.XKeyText("N양의 비명소리가 아득해지면서 당신은 정신을 잃습니다.");
            Game.GameOver("준비도 없이 남을 제압하려 들지 맙시다.");
        }

        isTalking = false;
    }
    
    // 엔딩 2
    private void TalkLog3()
    {
        for (int i = 0; i < Game.Player.inventory.items.Length; i++)
        {
            if (Game.Player.inventory.items[i] != null && Game.Player.inventory.items[i].name == "밧줄")
            {
                Util.XKeyText("당신은 남자의 등 뒤에서 양 팔을 강하게 붙잡아 포박합니다.");
                Util.XKeyText("남자는 갑작스런 공격에 몸부림을 칩니다.");
                Console.WriteLine();
                Util.NPC_CText("으윽, 뭐하는...!");
                Console.WriteLine();
                Util.XKeyText("위험천만한 몸싸움이 벌어졌지만,");
                Util.XKeyText("당신은 비몽사몽한 상태의 남자를 제압할 수 있었습니다.");
                // TODO 인벤토리에서 아이템 제거 진행
                Util.XKeyText("남자는 묶인 채로 주저앉습니다.");
                Console.WriteLine();
                Util.ZKeyText("대화를 종료하려면 Z키를 누르세요.");
                eventHappened = true;
                
                // 두 번 반환하여 대화 로그 6으로 넘어가게 설정
                talkLog.Dequeue();
                talkLog.Dequeue();	// 이후 Peek : 3 - 대화 로그 6으로 이동

                break;
            }                
        }
        if (eventHappened == false)
        {
            Util.XKeyText("당신에게는 남자를 제압할 밧줄이 없었습니다.");
            Util.XKeyText("당신과 남자는 몸싸움을 벌였고,");
            Util.XKeyText("당신보다 더 힘 쎈 남자에게 제압 당하면서 칼에 찔렸습니다.");
            Util.XKeyText("N양의 비명소리가 아득해지면서 당신은 정신을 잃습니다.");
            Game.GameOver("준비도 없이 남을 제압하려 들지 맙시다.");
        }
        isTalking = false;
    }
    
    // 엔딩 3
    private void TalkLog4()
    {
        Util.XKeyText("당신과 N양은, 괜히 남자를 건드리지 않고 가기로 합니다.");
        Util.XKeyText("남자는 당신들이 지나갈 때까지도 여전히 잠을 자고 있습니다.");
        Console.WriteLine();
        Util.ZKeyText("대화를 종료하려면 Z키를 누르세요.");
        
        // 
        talkLog.Dequeue();
        talkLog.Dequeue();
        talkLog.Dequeue();
        talkLog.Dequeue();
        talkLog.Dequeue();	// 이후 Peek : 6 - 대화 로그 9로 이동

        isTalking = false;
    }
    private void TalkLog5()
    {
        Console.SetCursorPosition(0, 14);
        Util.XKeyText("남자는 더 이상 움직이지 않습니다.");
        Console.WriteLine();
        Util.ZKeyText("대화를 종료하려면 Z키를 누르세요.");

        isTalking = false;
    }
    private void TalkLog6()
    {
        2-1 엔딩 내용

        talkLog.Dequeue(); // 이후 Peek : 6 - 대화 로그 7로 이동
        
        isTalking = false;
    }
    private void TalkLog7()
    {
        2-2 엔딩 내용

        talkLog.Dequeue();	// 이후 Peek : 7 - 대화 로그 8로 이동

        isTalking = false;
    }
    private void TalkLog8()
    {
        2-3 엔딩 내용 반복

        isTalking = false;
    }
    private void TalkLog9()
    {
        Console.SetCursorPosition(0, 14);
        Util.XKeyText("남자는 여전히 곤히 자고 있습니다.");
        Util.XKeyText("괜히 건들지 않는 편이 좋아 보입니다.");
        Console.WriteLine();
        Util.ZKeyText("대화를 종료하려면 Z키를 누르세요.");

        isTalking = false;
    }
}

이와 같이 Queue 를 이용하여 대화를 다르게 출력하고, 엔딩 루트를 구현했다.

5.1 오류의 발생과 수정과정

아직까지 구현해야 할 내용이 좀 더 남았지만, 이와 같이 대부분의 틀을 짜는 데까지도 생각보다 수월하지 않았다.

특히나 문제가 발생했던 부분이 아이템 소지 여부 판정 부분이었다.

이와 같이 조건이 길게 붙게 된 이유에는 다음과 같은 부분이 있다.

  1. 처음에는 칼이 있는지 여부만 찾으면 된다고 판단했다. 하지만 문제점이 발생한 게, 인벤토리가 꽉 차지 않았을 경우에 발생했다.

NullReferenceException - 즉 Null 값인 경우에 발생하는 오류이다. 이 문제가 왜 발생하는지 디버깅을 통해 알아보니, 인벤토리가 꽉 차지 않았을 경우, 아이템 배열 중에 Null 값이 있기 때문이었다.

코드 쪽으로 조건을 살펴 보면, 아래와 같이 조건을 전제로 만들었다.

// 인벤토리의 i 칸에 "칼"의 이름을 가진 아이템이 있는 경우

Game.Player.inventory.items[i].name == "칼"	

하지만 Null의 경우가 이 조건식 참조에 들어갈 경우, 이름이 애초에 존재하지 않는데 이름을 찾을 수 없다면서 게임이 터져버리는 것이다.

그래서 && 조건으로, Null인지 먼저 확인하고 이름을 검사하도록 만들었다.
(참고로 저 조건을 역순으로 바꿔도 게임이 터져버리니 유의하도록 하자)

5.2 그러면 아이템을 보유하지 않았을 경우를 조건으로 쓰려면

아이템을 소유했을 경우를 조건으로 쓸 때의 문제는 해결했지만, 아이템을 소유하지 않았을 경우에는 더 조건을 쓰기 어려워진다.

여기에서 사실 온갖 논리 오류들을 범하기도 했다. for문 안쪽에다가 else를 넣어 버리니까, 아이템을 가지고 있어도 없다고 출력되고, 바깥에다가 if문 걸고 했더니, 이벤트 다 출력되고 같이 출력되는 현상도 보고. 온갖 의도치 않은 상황을 다 겪고 나서 든 생각이, 차라리 이벤트가 발생했는지 여부를 판단하는 bool 변수를 두자는 생각이 들었다.

그래서 추가된 변수가 eventHappened 변수이다.

이와 같이 이벤트 발생 여부에 따라 구분하여 결과가 출력되도록 하였다.

6. 마무리와 수정해야 할 부분

수정해야 할 부분은 아래와 같다.

  • 죽음 엔딩은 지금 약간 출력 오류가 있는데, 엔딩 씬으로 만들어서 따로 교체를 해야 함
  • 현재 맵을 3개를 만들어 놨는데, 사실상 지금 C와의 엔딩 씬 구현 때문에 마지막 맵이 무쓸모가 된 상황이다. 과감하게 맵 3은 지울지, 활용할 지 고민

필수적으로 추가해야 하는 부분은 다음과 같다.

  • NPC - N양 대화 스크립트 - TalkLog2 중
  • NPC - C군 대화 스크립트 - TalkLog3 중

  • NPC - C군 대화 스크립트 - TalkLog7 중

우선은 위와 같이 게임의 완성도와 중요한 부분들을 해결하고, 도전 과제를 생각해보도록 하자.

profile
게임 만들러 코딩 공부중

0개의 댓글