Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
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
Archives
Today
Total
관리 메뉴

개발 달리기

내일배움캠프 9일차 - 텍스트 RPG 완성 본문

Unity

내일배움캠프 9일차 - 텍스트 RPG 완성

옹즤 2025. 4. 17. 22:49

1. 오늘 진행한 내용

- C# 문법 종합 5강까지 완강

- 텍스트 배틀

 


2. 학습하며 겪었던 문제점 & 에러

인벤토리 장비 탈부착 기능을 구현하는 과정에서 방식은 구체화를 해두었는데

그 방식을 구현할 코딩 지식이 부족해서 아무리 고민해도 방법이 나오지 않아 검색을 통해 구현했다.

 


3. 내일 학습 할 것은 무엇인지

텍스트 배틀 기능 추가

C# 문법 복습

 

4. 텍스트 배틀 제작

필수 구현 과제에 따라 처음에 직업 선택, 이름 선택

그리고 추가적으로 넣어둔 초반 기능 선택까지 넣어두었다.

        static void Main()
        {
            SetjobData();
            StartEquip();
            Title();
        }
        
                static void FirstScene() // 첫 씬
        {
            Console.Clear();
            ShowText("*이름없는 동굴*\n");
            DelKeyBuffer();
            Console.ReadKey(true);
            ShowText("너는 눈을 떴다.", 1000);
            ShowText("동굴 속 어딘가인 것 같다.", 1000);
            ShowText("\n(......)");
            DelKeyBuffer();
            Console.ReadKey(true);

            SelectJob();
        }
        static void SelectJob() // 직업 선택
        {
            while (true)
            {
                Console.Clear();
                ShowText("너는 어떠한 자였는가?\n", 500);
                ShowText("1. 전사\n2. 기사\n3. 도적\n");
                DelKeyBuffer();
                string input = Console.ReadLine();

                switch (input)
                {
                    case "1":
                        player.Job = "전사";
                        CopyStatsFromJob(player, jobData["전사"]);
                        break;
                    case "2":
                        player.Job = "기사";
                        CopyStatsFromJob(player, jobData["기사"]);
                        break;
                    case "3":
                        player.Job = "도적";
                        CopyStatsFromJob(player, jobData["도적"]);
                        break;
                    default:
                        ShowText("너는 다시 생각했다.");
                        DelKeyBuffer();
                        Console.ReadKey(true);
                        continue;
                }
                break;
            }
            SelectItem();
        }

무언가 기능을 한다 싶으면 일단 모두 메서드화 하여 하나의 메서드 기능이 끝나거나

행동을 선택할때 다음 메서드를 불러오는 식으로 전체적인 흐름을 구상했다.

장점은 굉장히 직관적이라는 것과 내가 이해하기 편하다는 것이고

단점은 코드가 너무 길어진다는 것이다.

하지만 현재 이 방법 외에 쌩초보 입장에서 더 좋고 효율적인 코드를 짤 수는 없는 단계다.

Main() 메서드에 Title() 메서드와 직업에 따른 고정 스탯이 부여되어있는 SetJobData() 메서드, 

초반 장착 아이템을 관리하는 StartEquip() 등 시작부터 3개의 메서드를 불러오고

본격적으로 Title() 메서드를 타면서 게임 흐름이 시작된다.

 

이후 선택지가 끝나면 다음 메서드 다음 메서드를 타는 식으로

플레이어 정보를 확정하게 된다.

연출적인 면에서는 일반적으로 C# 코드에서 지원하는 Colsole.WriteLine(); 을 사용하지 않고

내가 새롭게 메서드로 정의한 ShowText() 메서드를 사용했다.

        static void ShowText(string text, int delay = 0) // 대사 제어
        {
            Console.WriteLine(text);
            if (delay > 0)
            {
                Thread.Sleep(delay);
            }
        }
        static void DelKeyBuffer() // 키 입력 버퍼 지우기
        {
            while (Console.KeyAvailable)
            {
                Console.ReadKey(true);
            }
        }

ShowText는 delay 기능을 추가한 기능으로

텍스트를 출력 후 설정한 delay값에 따라 자동으로 다음 대사로 넘어가는 방식이다.

실제 과거 실무에서 대사 출력이 이와 비슷한 방식으로 짜여져있던게 생각나서 비슷하게 만들었다.

만약 실제 게임에서 비슷한 기능이 사용된다면 폰트 방식이나 대사 출력 위치 등

메서드에서 제어하는 변수값이 더 늘어날 것이다.

또한 텍스트 배틀을 제작하며 키입력을 막 하다보면 선입력이 남아있는 문제가 있어서

그것을 없애고자 DelKeyBuffer() 메서드를 만들어 선입력 초기화를 넣었다.

 

게임 시작 후 여기저기로 뻗어나가는 기능의 중심이 되는

멘더시움 도시는 Mendacium() 메서드에서 관리한다.

        static void Mendacium() // 멘더시움
        {
            while (true)
            {
                Console.Clear();
                ShowText("*도시 멘더시움*\n");
                DelKeyBuffer();
                Console.ReadKey(true);
                ShowText("무엇을 할 것인가?\n", 500);
                ShowText("1. 상태 보기\n2. 인벤토리 확인\n3. 상점\n4. 여관\n5. 이름없는 동굴\n", 500);
                DelKeyBuffer();
                string select = Console.ReadLine();

                switch (select)
                {
                    case "1":
                        Status();
                        break;
                    case "2":
                        Inventory();
                        break;
                    case "3":
                        Store();
                        break;
                    case "4":
                        Inn();
                        break;
                    case "5":
                        NamelessCave();
                        break;
                    default:
                        ShowText("너는 다시 생각했다.");
                        DelKeyBuffer();
                        Console.ReadKey(true);
                        continue;
                }
                break;
            }
        }

이곳에서 선택지에 따라 5가지의 메서드로 확장된다.

5가지 기능 구현 중 가장 어려웠던 부분은 '인벤토리 확인' 부분이다.

 

상태 보기 기능은 Player가 계속 가지고 있을 고유 정보들을

class 구조화 하여 그것을 그냥 불러오는 방식으로 만들었다.

    class Player // 플레이어 스테이터스 및 프로퍼티 구조화
    {
        public string Name;
        public string Job;
        public int HP;
        public int maxHP;
        public int Attack;
        public int Defense;
        public int Gold;
        public int Level;
        public int EXP;

        // 이벤트 프로퍼티
        public int CaveEvent;

        public void Setstate(int hp, int maxHp, int attack, int defense)
        {
            HP = hp;
            maxHP = maxHp;
            Attack = attack;
            Defense = defense;
            Gold = 500;
            Level = 1;
            EXP = 0;
            CaveEvent = 0;

            if (HP > maxHP)
            {
                HP = maxHP;
            }
        }
    }
    
            static void Status() // 스테이터스
        {
            Console.Clear();
            ShowText("<상태>\n");
            ShowText($"이름 : {player.Name}");
            ShowText($"레벨 : {player.Level}");
            ShowText($"직업 : {player.Job}");
            ShowText($"공격력 : {player.Attack}");
            ShowText($"방어력 : {player.Defense}");
            ShowText($"체력 : {player.HP} / {player.maxHP}");
            ShowText($"보유 골드 : {player.Gold}");
            ShowText("\n돌아간다.\n");
            DelKeyBuffer();
            Console.ReadKey(true);
            Console.ReadLine();

            Mendacium();
        }

코드는 길지만 매우 직관적이라 이해가 어렵지도 않고 비교적 대사 작성과 비슷한 수준으로 쉬웠다.

 

장비 확인 부분이 매우 어려웠는데

내가 이 텍스트 배틀에서 사용할 아이템을 먼저 Dictionary화를 한 후

그것을 리스트화를 다시 하여 장비 착용 유무에 따라

Dictionary에 저장된 각 아이템에 붙어있는 부가요소를 적용한다.

        static Dictionary<string, Equipment> equipData = new Dictionary<string, Equipment>() // 장비 데이터 (아이템 이름이 Dictionary에서 불러오는 키값)
        {
            { "검은 얼룩의 검", new Equipment { Name = "검은 얼룩의 검", Description = "깨어나보니 쥐고있던 검", AttackBonus = 0, DefenseBonus = 0, Slot = "무기" } },
            { "검게 얼룩진 옷", new Equipment { Name = "검은 얼룩진 옷", Description = "깨어나보니 입고있던 옷", AttackBonus = 0, DefenseBonus = 0, Slot = "몸통" } },
            { "일반적인 검", new Equipment { Name = "일반적인 검", Description = "특별한 점이 없는 일반적인 장검", AttackBonus = 5, DefenseBonus = 0, Slot = "무기", Gold = 1500 } },
            { "패링 대거", new Equipment { Name = "패링 대거", Description = "공격과 방어를 적절하게 할 수 있는 단검", AttackBonus = 2, DefenseBonus = 2, Slot = "무기", Gold = 1000 } },
            { "목제 방패", new Equipment { Name = "목제 방패", Description = "나무로 만들어진 흔한 방패", AttackBonus = 0, DefenseBonus = 1, Slot = "방패", Gold = 500 } },
            { "철 방패", new Equipment { Name = "철 방패", Description = "철로 만들어진 조금 무거운 방패", AttackBonus = 0, DefenseBonus = 2, Slot = "방패", Gold = 1000 } },
            { "레더 아머", new Equipment { Name = "레더 아머", Description = "단단한 동물의 가죽으로 만들어진 방어구", AttackBonus = 0, DefenseBonus = 2, Slot = "몸통", Gold = 1000 } },
            { "아이언 아머", new Equipment { Name = "아이언 아머", Description = "치명상도 막아주는 튼튼한 방어구", AttackBonus = 0, DefenseBonus = 3, Slot = "몸통", Gold = 1500 } },
            { "불온한 느낌의 검은 구슬", new Equipment { Name = "불온한 느낌의 검은 구슬", Description = "용도를 모르겠는 구슬", AttackBonus = 0, DefenseBonus = 0, Slot = "아이템", Gold = 10000 } }
        };
        static List<string> equippedItems = new List<string>(); // 장착 데이터

아이템 Dictionary를 다음과 같이 설정 후

Dictionary를 불러오기 위한 Key값을 아이템 명칭으로 설정해서

명칭을 사용하면 언제든지 Dictionary 내의 정보를 불러올 수 있게 만들었다.

        static void ShowEquipment() // 내 인벤토리 아이템 보여주는 기능
        {
            ShowText("<내 인벤토리>");

            List<string> equippable = inventory // 인벤토리에서
                .Where(item => equipData.ContainsKey(item)) // equipData 딕셔너리에 등록된 아이템 리스트화해서 가져옴
                .ToList();

            if (equippable.Count == 0)
            {
                ShowText("아이템 없음");
            }
            else
            {
                for (int i = 0; i < equippable.Count; i++)
                {
                    string ItemName = equippable[i]; // 리스트화 한 equipData아이템을 첫번째부터 출력
                    Equipment eq = equipData[ItemName];

                    ShowText($"{ItemName} : {eq.Description} / 공격력 + {eq.AttackBonus} / 방어력 + {eq.DefenseBonus}");
                }
            }
        }
        static void StartEquip()
        {
            List<string> inventory = new List<string>()
            {
                "검은 얼룩의 검",
                "검게 얼룩진 옷"
            };
            foreach (string itemName in inventory)
            {
                Equipment item = equipData[itemName];
                equippedItems.Add(itemName);
                player.Attack += item.AttackBonus;
                player.Defense += item.DefenseBonus;
            }
        }
        static void EquipMenu()
        {
            while (true)
            {
                Console.Clear();
                ShowText("<장비 확인>\n");

                List<string> equippable = inventory
                    .Where(item => equipData.ContainsKey(item))
                    .ToList();

                if (equippable.Count == 0)
                {
                    ShowText("아이템 없음");
                    return;
                }
                for (int i = 0; i < equippable.Count; i++)
                {
                    string ItemName = equippable[i];
                    Equipment item = equipData[ItemName];
                    bool isEquipped = equippedItems.Contains(ItemName);
                    string mark = isEquipped ? "[E]" : "";

                    ShowText($"{i + 1}. {mark} {ItemName} : {item.Description} / 공격력 + {item.AttackBonus} / 방어력 + {item.DefenseBonus}");
                }

                ShowText("\n너는 어떤 장비를 선택하려는가?\n", 1000);
                ShowText("0. 그만둔다.");
                DelKeyBuffer();
                string input = Console.ReadLine();

                if (input == "0")
                {
                    EquipManager();
                }

                if (int.TryParse(input, out int select) && select >= 1 && select <= equippable.Count)
                {
                    string selectedItem = equippable[select - 1];
                    Equipment item = equipData[selectedItem];

                    if (equippedItems.Contains(selectedItem))
                    {
                        equippedItems.Remove(selectedItem);
                        player.Attack -= item.AttackBonus;
                        player.Defense -= item.DefenseBonus;
                        ShowText("너는 선택한 장비를 해제했다.", 1000);
                        DelKeyBuffer();
                    }
                    else
                    {
                        equippedItems.Add(selectedItem);
                        player.Attack += item.AttackBonus;
                        player.Defense += item.DefenseBonus;
                        ShowText("너는 선택한 장비를 장착했다.", 1000);
                        DelKeyBuffer();
                    }
                }
                else
                {
                    ShowText("너는 다시 생각했다.", 1000);
                }
            }
        }

기능은 단순한데 이걸 코드로 만드려면 이렇게나 길어진다.

리스트화 하여 장비 착용 유무를 검사하여 착용이 되지 않았다면 [E] 표시와 함께 착용

착용 상태였다면 [E]를 제거하고 착용이 해제된다.

.where 부분이나 .ToList 부분은 아예 모르던 기능이라 이쪽 기능 구현은 검색을 통해 구현해서

100% 이해하지는 못했다.

 

인벤토리를 구현한 후 상점을 구현했다.

상점 구현은 비교적 쉬웠는데

일단 인벤토리를 구현할 때 썼던 기능 일부를 재활용해서 사용할 수 있었고

아이템을 여러개 선택해서 사는 방식이 아니라

1개씩 구매를 하고, 구매한 아이템은 다시 살 수 없게 만드는 방식으로 구현했다.

이렇게 아날로그 옛날 방식으로 구현하니 만들어야 할 기능이 좀 줄어서 오히려 수월했다.

        static void Store() // 상점
        {
            player.Gold = 10000;
            while (true)
            {
                Console.Clear();
                ShowText("*멘더시움 상점*\n");
                DelKeyBuffer();
                Console.ReadKey(true);
                ShowText("너는 상점에 들어갔다.", 1000);
                ShowText("무엇을 할 것인가?\n", 1000);
                ShowText($"<현재 골드 : {player.Gold}G>");

                for (int i = 0; i < shopItems.Count; i++)
                {
                    string itemName = shopItems[i];
                    Equipment item = equipData[itemName];
                    bool isAlreadyBought = purchasedItems.Contains(itemName);

                    string status = isAlreadyBought ? "(재고 없음)" : $"[{item.Gold}G]";
                    ShowText($"{i + 1}. {itemName} {status} : {item.Description} / 공격력 + {item.AttackBonus} / 방어력 + {item.DefenseBonus}");
                }
                ShowText("\n아이템 구매 (번호 선택)", 1000);
                ShowText("0. 그만둔다.");
                DelKeyBuffer();
                string input = Console.ReadLine();

                if (input == "0")
                {
                    ShowText("너는 인사를 하고 상점을 뒤로했다.", 1000);
                    Mendacium();
                }

                if (int.TryParse(input, out int select) && select >= 1 && select <= shopItems.Count)
                {
                    string selectedItem = shopItems[select - 1];

                    if (purchasedItems.Contains(selectedItem))
                    {
                        ShowText("이미 구매한 아이템이다.", 1000);
                        DelKeyBuffer();
                        continue;
                    }

                    Equipment item = equipData[selectedItem];

                    if (player.Gold < item.Gold)
                    {
                        ShowText("가진 골드가 부족하다.", 1000);
                        DelKeyBuffer();
                        continue;
                    }

                    player.Gold -= item.Gold;
                    inventory.Add(selectedItem);
                    purchasedItems.Add(selectedItem);

                    if (selectedItem == "불온한 느낌의 검은 구슬")
                    {
                        ShowText("......", 1000);
                        DelKeyBuffer();
                        Console.ReadKey(true);
                        Console.Clear();
                        ShowText("*멘더시움 상점*\n");
                        ShowText("\"자네...\"", 1500);
                        ShowText("상점 주인이 어두운 얼굴로 너에게 말을 건다.", 2000);
                        ShowText("\"정말로 그 가격으로 그걸 살 생각이오?\"", 1500);
                        ShowText("\"나야 상관없소만...\"", 1500);
                        ShowText("\"그 구슬 아름다우면서 뭔가 꺼림찍하단 말이지.\"", 1500);
                        ShowText("\"나라면 가까이하고 싶지 않소만, 보석으로써 가치는 있을거요.\"", 1500);
                        ShowText("\"애물단지를 사줘서 고맘소.\"\n", 1500);
                        DelKeyBuffer();
                        Console.ReadKey(true);
                        ShowText("너는 묘한 느낌을 받으며 구슬을 바라본다.", 1000);
                        ShowText("(......)", 1000);
                    }

                    ShowText($"너는 {item.Gold}G를 지불하여 {selectedItem}을 구매했다.", 1000);
                    DelKeyBuffer();
                    Console.ReadKey(true);
                }
                else
                {
                    ShowText("너는 다시 생각했다.", 1000);
                }
            }
        }

몇몇 대사로 인해 코드가 매우 길어보이지만 기능은 앞서 사용했던 기능들을 모두 활용한 것이다.

어차피 이 텍스트 배틀을 더 확장시킬 시간은 없지만, 혹시라도 만약 더 업그레이드를 한다면

확장성을 위해 세계관과 아이템 요소들, 주인공 설정등을 가볍게 짜놓고 시작해서

특정 아이템에 떡밥을 넣어둔다던지 등으로 조건을 걸어두었다.

 

여관 휴식 기능은 스테이터스를 보여주는 기능과 마찬가지로 매우 쉬운 기능이다.

        static void Inn() // 여관 휴식 기능
        {
            int cost = 100;

            while (true)
            {
                Console.Clear();
                ShowText("*멘더시움 여관*\n");
                DelKeyBuffer();
                Console.ReadKey(true);
                ShowText($"현재 체력 : {player.HP} / {player.maxHP}");
                ShowText($"현재 골드 : {player.Gold}G");
                ShowText("너는 여관에 들어갔다.\n", 1000);
                ShowText("너는 무엇을 할 것인가?\n", 1000);
                ShowText("1. 휴식한다. (100G)\n2. 그만둔다.\n");
                DelKeyBuffer();
                string select = Console.ReadLine();

                switch (select)
                {
                    case "1":
                        if (player.Gold >= cost)
                        {
                            player.Gold -= cost;
                            player.HP = player.maxHP;
                            ShowText("너는 여관에 들어가 잠시 쉴 방을 대여했다.", 1000);
                            ShowText("......", 2000);
                            ShowText("너는 몸이 완전히 회복된 것을 느낀다.", 1000);
                            Inn();
                        }
                        else
                        {
                            ShowText("너는 휴식을 위한 방을 빌리려고 했으나, 가진 골드가 부족했다.", 1000);
                            ShowText("너는 겸연쩍은 미소를 지으며 여관을 뒤로하고 돌아갔다.", 1000);
                            ShowText("(......)", 1000);
                            Mendacium();
                        }
                        break;
                    case "2":
                        Mendacium();
                        break;
                    default:
                        ShowText("너는 다시 생각했다.");
                        DelKeyBuffer();
                        Console.ReadKey(true);
                        continue;
                }
                break;
            }
        }

대사를 빼고 본다면 단순히 Player 구조체에서 HP, maxHP, Gold의 3개의 속성값을 가져와서

그에 맞춰 조건과 선언으로만 만들어졌다.

 

마지막 던전의 경우는 필수 구현 과제가 아니지만 일단 필수적으로 구현해야하는 기능들은

모두 구현한 상태이기에 던전과 전투 시스템, 몬스터 정보, 확률 요소 등

만들 수 있는 부분까지 만들고 마무리해볼 예정이다.