[내일배움캠프 2주차] ConsoleRPG 코드리뷰 - 1

하얀요니콘·2025년 7월 11일
0

엄청나게 많은 양이 될것 같은 이번주 개인과제 코드리뷰를 진행한다.

개인과제 개요

Console로 진행하는 TEXT RPG를 제작하시오.

자 일단 이게 RPG이긴 한데, 이번주 구현 내용은 실제 플레이 내용은 존재하지 않는다. 다만 스텟, 아이템, 인벤토리, 던전(자동) 등 다양한 기능을 구현하는것이 목적이였다.

우선 Console RPG Github 의 Readme에 약간이라도 적어놨으니, 구조는 이를 참고하면서 보면 된다. 그러면 하나하나 보며 진행하도록 하겠다.

코드리뷰

Root

Program.cs

실제 구동이 되는 Main함수가 존재하지만, 구현 내용은 별로 없다. 게임 내용을 GameManager에서 관리를 전부 해 주기 때문에, Main에서는 게임이 구동되기 위한 GameManger만 호출해주면 할일이 끝난다.

Item.cs

아이템에 대한 정보를 저장하는 struct가 포함되어있다. Enum을 활용하여 아이템의 타입(ItemType)과, 오르는 스텟(StatsType)에 관한 타입을 만들어주었다.
Item은 struct구조이다. (왜냐하면 Item내부의 스킬같은 것이 없으니, 실제로는 값들만 저장하기 때문이다.)

  • ItemID : 아이템 순서가 섞여도 구분이 가능하도록 ID를 매겨준다. 나중에 설명할 CATEGORY에 인덱스라고 보면 된다.
  • ItemName, ItemDescription : 아이템에 대한 이름과 설명을 저장한다.
  • ItemT : 아이템이 어느 부위에 장착되는지를 타입으로 설정한다.
  • ItemPrice : 아이템의 가격이다. 나중 상점에서 구매 혹은 판매를 위해 존재한다.
  • ItemStats : 아이템이 하나이상을 스텟을 가질 수 있어, 배열을 사용하여 (StatsType, int) 인 세트를 받는다. Dictionary로 구현도 가능했을 듯 하지만, 일단...

그 후 생성자 하나가 존재하고, 다음과 같이 카테고리가 존재한다.

public static readonly Item[] ITEM_CATEGORY

이는 struct 내부에서 선언해준 딕셔너리로, 앞으로 아이템들을 매번 스텟을 새로 만들지 않고, ID를 통해 정보를 여기서 끌어올 것이다. (아직 강화요소가 없어서 가능하다.)

Shop.cs

상점 Class이다. 상점에는 상점에 있을 아이템을 위한 인벤토리, 그리고 아이템 구매 위주로 작업 해 주었다.

  • ShopItems : 자유롭게 설정이 가능하도록 List로 만들어 주었다. (나중에 상점을 따로 만들어 줄것을 생각해서) 그리고 생성자에서 List내에 아이템을 채우는데, 아직은 ITEM_CATEGORY에 있는 모든 아이템을 넣도록만 생성을 시켜주었다.
  • ShowShopItems(bool) : 상점 아이템들을 출력해주는 메소드이다. bool은 앞에 숫자를 출력할지 (입력을 받기위해) 혹은 '-'을 출력할지 (입력을 받지 않을 경우) 이다.
  • BuyShopItem(int, Player) : 상점에서 아이템을 구매하는 로직이다. 해당 리스트의 위치를 받아, 그를 Player에서 넘겨주는 방식이다. BuyShopItem에서의 아이템 ID를 Player에서 GainItem을 통해 넣어주어, 실제로 플레이어가 아이템을 로드할 시 ITEMCATEGORY에서 로드시킨다.

AutoDungeon.cs

자동던전이다. 실제로는 데이터만 저장하므로 Struct를 활용하였다. 각각의 정보를 사용하고, 위의 ITEM_CATEGORY처럼 이곳에서 public static readonly AUTODUNGEON_CATEGORY를 생성해 주었다.

Entity

Entity.cs

엔티티의 특성을 가지는 모든 곳에 사용할 Abstract Class이다. 지금은 Player밖에 없지만, 나중 적 구현을 생각하고 미리 참조할 Abstract Parent Class를 만들어 주어 사용하였다.

이름, 레벨, 공격력, 방어력, 최대체력, 최소체력과 골드를 기본으로 가진다.

  • ShowEntityInfo() : 엔티티의 정보를 출력하는 Abstract 매소드이다.

Player.cs

실제 게임 요소의 핵심인 Player의 정보를 저장하는 클래스이다. Entity를 상속하여 사용하며, 몇가지 추가 메서드를 구현하였다.

<변수>

  • PlayerClassType : 플레이어의 직업을 나타내는 Enum이다.
  • playerClassT : 위 타입을 가지는 플레이어 정보이다.
  • List<(int, bool)> Inventory : 인벤토리에 있는 아이템이다. int는 ItemID만 받고, bool은 이 장비를 착용하였는지 확인하는 boolean값이다.
  • Dictionary<ItemType, int?> EquippedItems : 착용하고 있는 아이템을 저장하는 Dict이다. 어찌보면 각 아이템이 부위가 정해져 있으니, ItemType에는 하나의 아이템만 장착 가능하고, int는 "인벤토리의 인덱스"를 가져온다 (ItemID가 아니다.)
  • Dictionary<StatsType, int> PlayerItemStats : 각 장착한 장비를 파악하여 아이템에서 얻은 스텟들이다. 이또한 올라갈 스텟이 정해져 있어 Dict로 사용하였다.
  • ClearedLevels : 레벨업에 필요한 int이다.

<메서드>

  • 저장로직
readonly static string path = "PlayerData.dat";
//Data Management
        public bool TryRead()
        {
            if (!File.Exists(path))
                return false;
            return ReadData();
        }

        //Data Management
        public void SaveData()
        {
            if(File.Exists(path))
                { File.Delete(path); }
            FileStream fs = new FileStream(path, FileMode.Create);
            StreamWriter writer = new StreamWriter(fs);
            writer.WriteLine(this.name);
            writer.WriteLine(this.playerClassT);
            writer.WriteLine(this.level);
            writer.WriteLine(this.atk);
            writer.WriteLine(this.def);
            writer.WriteLine(this.maxHp);
            writer.WriteLine(this.curHp);
            writer.WriteLine(this.gold);
            writer.WriteLine("Inventory");
            for(int i = 0; i < Inventory.Count; i++)
                writer.WriteLine(Inventory[i].Item1.ToString() + " " + Inventory[i].Item2.ToString());
            writer.WriteLine("\\Inventory");

            writer.WriteLine("EquippedItems");
            for (int i = 0; i < EquippedItems.Count; i++)
                writer.WriteLine(EquippedItems.ElementAt(i).Value.ToString());
            writer.WriteLine("\\EquippedItems");

            writer.WriteLine("PlayerItemStats");
            for (int i = 0; i < PlayerItemStats.Count; i++)
                writer.WriteLine(PlayerItemStats.ElementAt(i).Value.ToString());
            writer.WriteLine("\\PlayerItemStats");

            writer.WriteLine(this.ClearedLevels);
            writer.Close();
        }
        
        //Data Management
        public bool ReadData()
        {
            if (!File.Exists(path))
                return false;
            FileStream fs = new FileStream(path, FileMode.Open ,FileAccess.Read); 
            StreamReader reader = new StreamReader(fs);

            this.name = reader.ReadLine();
            Enum.TryParse(reader.ReadLine(), out PlayerClassType Type);
            this.playerClassT = Type;
            this.level = int.Parse(reader.ReadLine());
            this.atk = float.Parse(reader.ReadLine());
            this.def = int.Parse(reader.ReadLine());
            this.maxHp = int.Parse(reader.ReadLine());
            this.curHp = int.Parse(reader.ReadLine());
            this.gold = int.Parse(reader.ReadLine());
            if (reader.ReadLine() != "Inventory")
                return false;
            while (true)
            {
                string t = reader.ReadLine();
                if (t == "\\Inventory")
                    break;
                string[] items = t.Split(' ');
                Inventory.Add((int.Parse(items[0]), bool.Parse(items[1])));
            }
            if (reader.ReadLine() != "EquippedItems")
                return false;
            for (int i = 0; i < EquippedItems.Count; i++)
            {
                string t = reader.ReadLine();
                if (t == "")
                    EquippedItems[EquippedItems.ElementAt(i).Key] = null;
                else
                    EquippedItems[EquippedItems.ElementAt(i).Key] = int.Parse(t);
            }
            if (reader.ReadLine() != "\\EquippedItems")
                return false;
            if(reader.ReadLine() != "PlayerItemStats")
                return false;
            for (int i = 0; i < PlayerItemStats.Count; i++)
            {
                string t = reader.ReadLine();
                EquippedItems[EquippedItems.ElementAt(i).Key] = int.Parse(t);
            }
            if (reader.ReadLine() != "\\PlayerItemStats")
                return false;
            this.ClearedLevels = int.Parse(reader.ReadLine());
            reader.Close();
            return true;

        }
             //DataManagement
        public void ResetPlayer()
        {
            if (!File.Exists(path))
                return;
            File.Delete(path);
        }

뭔가 엄청 긴데... 노가다 해서 데이터를 하나씩 넣어줘서 그렇다.
이때 FileStream과 C#에 있는 StreamReader, StreamWriter을 활용하였다.

StreamReader, StreamWriter은 FileStream을 받아 데이터를 읽어주는 역활을 한다.

  • 초기화
private void _init()
        { 
            this.level = 1;
            this.atk = 10.0f;
            this.def = 5;
            this.maxHp = 100;
            this.curHp = this.maxHp;
            this.gold = 1500;
            if (GameManager.IsDebug)
                this.gold = 10000;
            Inventory = new List<(int itemID, bool isEquipped)>();
            PlayerItemStats = new Dictionary<StatsType, int>
            {
                {StatsType.Attack, 0 },
                {StatsType.Defense, 0},
                {StatsType.HealthPoint, 0}
            };
            _SetInitStats();
        }

        private void _SetInitStats()
        {
            PlayerItemStats[StatsType.Attack] = 0;
            PlayerItemStats[StatsType.Defense] = 0;
            PlayerItemStats[StatsType.HealthPoint] = 0;
            foreach (int? index in EquippedItems.Values)
            {
                if (index == null)
                    continue;
                foreach ((StatsType, int) t in Item.ITEM_CATEGORY[Inventory[(int)index].Item1].ItemStats)
                {
                    if (t.Item2 == 0)
                        continue;
                    PlayerItemStats[t.Item1] += t.Item2;
                }
            }
            
            //Set Current Hp not larger than MaxHp + Item;
            if (this.curHp > this.maxHp + PlayerItemStats[StatsType.HealthPoint])
                this.curHp = this.maxHp + PlayerItemStats[StatsType.HealthPoint];
        }

이는 플레이어 생성 시 초기화를 해주는 생성자 외 _init을 따로 구현한 내용들이다. _SetInitStats는 아이템들의 정보를 받아 아이템 스텟을 다시 계신해준다.

너무 길어져서 다른거 눈여겨볼 것들은... SetItemEquipped에서의 아이템 처리정도가 될것이다.
아이템의 장착 상태에 따라 업데이트 해주고, 반드시 인벤토리가 줄어드므로, Equipped ITem에서 인덱스 관리를 추가로 해 줘야 하는점이 눈여겨 볼 점이다.

다른 메서들들은 보면 간단하지만, 출력이 대다수고, 체력을 조정하는 GainGold와 GainHeal, 그리고 아이템 추가하는 AddItem과 판매하는 SellItem이 있다.

오늘은 늦어져서 여기까지 TIL작성하도록 한다.

profile
코딩공부용

0개의 댓글