TIL 0104 TextRPG - 3(개인)

강성원·2024년 1월 4일
0

TIL 오늘 배운 것

목록 보기
9/70

오늘은 장비 장착 개선, 인벤토리 클래스 구현, 던전 입장 기능 구현을 했다.

1. 장비 장착 개선

기존에는 무기, 방어구, 방패 종류에 상관 없이 모두 장착되게 했었고

변경 후에는 무기, 방어구, 방패 슬롯에 종류에 맞는 장비 한 개씩만 장착되게 했다.

1-1. 장비 슬롯

public enum ItemType
{
    Weapon,
    Armor,
    Shield,
}

public class Player
{
        
	public Item[] slots;

	public Player()
	{
		slots = new Item[3];
        ,,,생략,,,
    }
}

길이 3개의 Item 클래스 배열을 만들어준다. 이는 장비 장착 부위(슬롯)이다.
열거형 "ItemType"과 같이 쓰일 것이다.

1-2 장비 장착

public class Player
{
	,,,생략,,,
    
  public void Equip(Item item)
  {
      if (slots[(int)item.Type] == item)     //슬롯에 있는 장비와 같은 장비라면
      {
          Unequip(slots[(int)item.Type]);
          return;
      }


      if (null != slots[(int)item.Type])      //이미 슬롯이 차있다면
          Unequip(slots[(int)item.Type]);     //슬롯에 있는 장비 해제 메서드 호출

      slots[(int)item.Type] = item;           //새 장비로 슬롯 채움
      item.Equip(this);                       //새 장비 장착
  }

  public void Unequip(Item item)
  {
      slots[(int)item.Type] = null;           //슬롯 비우기
      item.Unequip(this);                     //아이템의 장착 해제 메서드 호출
  }
}

아이템을 전달해서 Equip 메서드나 Unequip 메서드를 호출한다.
아이템의 슬롯은 아이템의 타입으로 찾을 수 있다.

  • 슬롯에 장착한 아이템과 전달 받은 아이템이 같으면 Unequip 메서드를 호출해서 해당 장비를 해제한다.
  • 슬롯이 이미 차있다면 해당 슬롯의 아이템에 대해 Unequip을 호출해 해제한다.
    그 후에 슬롯에 새로운 아이템을 할당하고 장착한다.

2. 인벤토리 클래스 구현

과제는 아니었지만 인벤토리를 관리하는 장면이 있어서 클래스화 시키는게 나을거라고 생각해서 구현했다.

2-1. 인벤토리 구현 기본 데이터

무슨 말을 써야할지 몰라서 기본 데이터라고 써놨다.ㅎㅎ

public enum InventoryState
{
    Main,
    Equip,
}

public class Inventory
{
    InventoryState state;		//1. 인벤토리 상태
    
    private List<Item> items; 	//2. 아이템을 가지고 있을 리스트
    
    public Player player;
    public Inventory(Player player) //3. 플레이어 의존성 주입
    { 
        items = new List<Item>();
        state = InventoryState.Main;
        this.player = player;
    }
}
  1. 열거형으로 인벤토리 상태를 정의해서 현재 상태에 따라서 다른 화면을 보여주고 알맞는 로직을 진행하도록 했다.

  2. 플레이어가 장착할 아이템은 이 리스트에 저장하도록 했다.
    접근은 프로퍼티와 메서드를 사용했고 위에서는 생략함.

  3. 의존성 주입 형식을 채택해서 플레이어의 객체를 넘겨받아 사용한다.

2-2. 인벤토리 상태에 따른 출력 / 진행

ShowInventory() 메서드

public void ShowInventory()
{
    //출력
    Console.Clear();
    ,,, 방패 그림 출력 생략,,,

    switch(state)
    {
        case InventoryState.Main:
            Console.WriteLine("============= 인벤토리 =============");
            Console.WriteLine("보유 중인 아이템을 관리할 수 있습니다.");
            Console.WriteLine("===================================");
            Console.WriteLine("\n[아이템 목록]");
            break;
        case InventoryState.Equip:
            Console.WriteLine("======= 인벤토리 - 장착 관리 =======");
            Console.WriteLine("보유 중인 아이템을 관리할 수 있습니다.");
            Console.WriteLine("===================================");
            Console.WriteLine("\n[아이템 목록]");
            break;
    }
}
  • 현재 상태에 맞춰서 다르게 출력시킴

ShowItemList() 메서드

public void ShowItemList()
{
    int index = 0; //아이템 넘버링

    switch(state) 
    { 
        case InventoryState.Main:
            foreach (Item item in items)
            {
                string itemInfo = "- ";

                if (item.IsEquip)
                    itemInfo += "[E]";

                itemInfo += $"{item.Name}\t";

                if (item.Type == ItemType.Weapon)
                    itemInfo += $"| 공격력 +{(item as Weapon).Attack}| ";
                else if (item.Type == ItemType.Armor)
                    itemInfo += $"| 방어력 +{(item as Armor).Defence}| ";
                else if (item.Type == ItemType.Shield)
                    itemInfo += $"| 방어력 +{(item as Shield).Defence}| ";

                itemInfo += item.Desc;

                Console.WriteLine(itemInfo);
            }
            break;

        case InventoryState.Equip:
            foreach (Item item in items)
            {
                string itemInfo = $"- {++index} ";

                if (item.IsEquip)
                    itemInfo += "[E]";

                itemInfo += $"{item.Name}\t";

                if (item.Type == ItemType.Weapon)
                    itemInfo += "| 공격력 | ";
                else
                    itemInfo += "| 방어력 | ";

                itemInfo += item.Desc;

                Console.WriteLine(itemInfo);
            }
            break;
    }
}

인벤토리에 있는 아이템 리스트를 출력한다.

  • switch의 case만 집중하면 된다.

  • Main인 경우에는 장비의 목록이 출력된다.
    아이템의 타입에 맞춰서 "공격력","방어력"이 출력된다.

  • Equip인 경우에는 번호로 아이템을 선택할 수 있게 넘버링을 해준다.
    그 외는 Main과 같다.

ShowInventoryMenu() 메서드

public void ShowInventoryMenu()
{
    switch (state)
    {
        case InventoryState.Main:
            Console.WriteLine("\n1. 장착 관리");
            Console.WriteLine("0. 나가기\n"); //마을로

            Console.WriteLine("원하시는 행동을 입력해주세요.");
            Console.Write(">>");
            break;
        case InventoryState.Equip:
            Console.WriteLine("\n0. 나가기\n"); //인벤토리로

            Console.WriteLine("원하시는 행동을 입력해주세요.");
            Console.Write(">>");
            break;
        default:
            break;
    }
}

인벤토리 하단에 나오는 선택 메뉴이다.

Main에서는 1. 장착 관리 / 0. 나가기(마을로) 메뉴가 보이고

Equip에서는 0. 나가기(인벤토리로) 메뉴만 보인다.

SelectMenu(string playerInput, ref Scenes scenes) 메서드

public void SelectMenu(string playerInput, ref Scenes scenes)
{
    int index = 0; // 아이템 인덱싱

    if(state == InventoryState.Main)
    {
        switch (playerInput)
        {
            case "0": // 마을로 나가기
                scenes = Scenes.Town;
                break;
            case "1": // 인벤토리 장착 화면으로
                state = InventoryState.Equip;
                break;
        }
    }
    else if(state == InventoryState.Equip)
    {
        switch (playerInput)
        {
            case "0":
                state = InventoryState.Main;
                break;

            default:
                //입력 값이 숫자 and 1 이상 and 인벤토리 총 수 보다 작음
                if (int.TryParse(playerInput, out index) && 0 < index && index <= items.Count)
                {
                    player.Equip(items[--index]); //내 인벤토리에 있는 장비 장착
                }
                else
                {
                    WrongInput();
                }
                break;

        }
    }
}

로직을 진행하는 메서드이다.

  • Main상태에는 입력 값에 따라서 scenes를 Town으로 바꾸거나
    인벤토리의 상태를 Equip으로 바꾼다.

  • Equip상태에서는 입력 값에 맞는 장비를 착용하거나
    인벤토리의 상태를 Main으로 바꾼다.

2-3. 인벤토리의 메서드 사용

위에서 설명한 메서드들은 메인 로직에서 호출만 된다.

    class Game
    {
        Scenes scenes = Scenes.Town;
        public Player player;
        public Store store;
        public Inventory inventory;
        public Dungeon dungeon;

        public Game()
        {
            player = Player.Instance;
            store = new Store();
            inventory = player.inventory;
            dungeon = new Dungeon(player);
        }

        public void Process()
        {
            switch (scenes)
            {
                ,,,생략,,,

                case Scenes.Inventory:
                    Inventory();
                    break;

                ,,,생략,,,
            }
        }
        
        ,,,수 많은 생략,,,
        
        private void Dungeon()
        {
            //입력 데이터
            string playerInput = "";

            dungeon.ShowDungeon();

            dungeon.ShowDungeonResult();

            dungeon.ShowDungeonMenu();

            playerInput = Console.ReadLine();

            dungeon.SelectMenu(playerInput, ref scenes);
        }
    }

위에 Inventory메서드에서 호출만 하면 위에 사진 처럼 출력되고 작동한다.

2-999. StackOverflow 문제


3. 던전 입장 기능 구현

3-1 던전 입장 기본 데이터

public enum DungeonState 		//1. 던전 상태
{
    Main,
    Clear,
    Failure
}

public enum DungeonDifficulty	//2. 난이도
{
    Easy,
    Normal,
    Hard,
}

public class Dungeon
{
    public Player player;
    DungeonState state; 		//1. 던전 상태
    private int[] recommendedDefense = { 5, 11, 17 };  //2. 난이도 별 권장 방어력

   
    int hpSave;					//3. 정보 저장
    int goldSave;
    string difficultySave = "";

    public Dungeon(Player player)
    {
        state = DungeonState.Main;
        this.player = player;
        hpSave = player.Hp;
        goldSave = player.Gold;
    }
  1. 던전도 마찬가지로 현재 상태에 따라서 출력과 로직을 진행하게 했다.

  2. 열거형으로 난이도를 정의하고 난이도 별 권장 방어력을 정수형 배열로 만들었다.
    난이도 열거형이 권장 방어력의 인덱스 역할을 해줄 것이다.

  3. 클리어/실패 했을 때 이전 체력/재화를 보여주기 위해 선언한 변수들이다.

3-2. 던전 상태에 따른 출력 / 진행

ShowDungeon() 메서드

public void ShowDungeon()
{
    Console.Clear();
    ,,,대문짝만한 대문 생략,,,

    switch (state)
    {
        case DungeonState.Main:
            Console.WriteLine("=============== 던전 입장 ===============");
            Console.WriteLine(" 던전으로 들어가기전 활동을 할 수 있습니다.");
            Console.WriteLine("========================================");
            break;
        case DungeonState.Clear:
            Console.WriteLine("============== 클리어 성공 ==============");
            Console.WriteLine("               축하합니다!               ");
            Console.WriteLine($"   {difficultySave} 던전을 클리어 하였습니다.");
            Console.WriteLine("========================================");
            break;
        case DungeonState.Failure:
            Console.WriteLine("============== 클리어 실패 ==============");
            Console.WriteLine($"   {difficultySave} 던전 클리어에 실패했습니다...");
            Console.WriteLine("========================================");
            break;
        default:
            break;
    }
}

현재 던전 상태에 따라서 메인인지, 클리어했는지, 실패했는지를 알 수 있다.

ShowDungeonResult() 메서드

public void ShowDungeonResult() //던전 도전 성공, 실패 시에 뜨는 화면
{
    switch (state)
    {
        case DungeonState.Main: //아무런 출력 없음
            break;
        case DungeonState.Clear:
            Console.WriteLine("[탐험 결과]");
            Console.WriteLine($"[체력 {hpSave} -> {player.Hp}]");
            Console.WriteLine($"[Gold {goldSave} -> {player.Gold}]");
            break;
        case DungeonState.Failure:
            Console.WriteLine("[탐험 결과]");
            Console.WriteLine($"[체력 {hpSave} -> {player.Hp}]");
            Console.WriteLine("아무런 보상도 얻지 못했습니다.");
            break;
    }
}

Main 상태에서는 아무것도 뜨지 않고 성공, 실패 시에만 탐험 결과가 출력된다.

ShowDungeonMenu() 메서드

public void ShowDungeonMenu()
{
    switch (state)
    {
        case DungeonState.Main:
            Console.WriteLine("1. 쉬운 던전\t\t 방어력 5 이상 권장");
            Console.WriteLine("2. 일반 던전\t\t 방어력 11 이상 권장");
            Console.WriteLine("3. 어려운 던전\t\t 방어력 17 이상 권장");
            Console.WriteLine("0. 나가기");
            break;
        case DungeonState.Clear:
        case DungeonState.Failure:
            Console.WriteLine("\n0. 나가기");
            break;
    }

    Console.WriteLine("원하시는 행동을 입력해주세요.");
    Console.WriteLine(">>>");
}

Main인 경우 던전 난이도를 선택하는 메뉴가 나오고 나머지는 나가는 메뉴만 뜬다.

SelectMenu(string playerInput, ref Scenes scenes) 메서드

public void SelectMenu(string playerInput, ref Scenes scenes)
{
    int index = 0; //던전 난이도 선택용
    if (state == DungeonState.Main)
    {
        switch (playerInput)
        {
            case "0": // 마을로 나가기
                scenes = Scenes.Town;
                break;
            case "1": // 쉬운 던전
                EnterDungeon(DungeonDifficulty.Easy);
                break;
            case "2": // 일반 던전
                EnterDungeon(DungeonDifficulty.Normal);
                break;
            case "3": // 어려운 던전
                EnterDungeon(DungeonDifficulty.Hard);
                break;
        }
    }
    //클리어 or 실패라서 던전 메인으로 가는 선택지만 있음
    else if (state == DungeonState.Clear || state == DungeonState.Failure)
    {
        switch (playerInput)
        {
            case "0": // Main으로 돌아가기
                state = DungeonState.Main;
                break;
        }
    }
}

입력 값에 따라서 분기가 나뉜다.

  • Main인 경우 입력 값에 맞는 난이도의 던전에 들어가는 함수를 호출하도록 했다.

  • 나머지는 Main상태로 되돌리도록 했다.

EnterDungeon(DungeonDifficulty difficulty) 메서드

public void EnterDungeon(DungeonDifficulty difficulty) //던전 결과 뱉는 로직
{
    Random random = new Random();

    int num = random.Next(1, 101);

    int damaged = 0;
    int reward = 0;

    //결과 화면에 쓰일 데이터 저장
    hpSave = player.Hp;
    goldSave = player.Gold;
    if (difficulty == DungeonDifficulty.Easy)
        difficultySave = "쉬운";
    else if (difficulty == DungeonDifficulty.Easy)
        difficultySave = "보통";
    else if (difficulty == DungeonDifficulty.Hard)
        difficultySave = "어려운";

    if ((recommendedDefense[(int)difficulty] > player.Defence) && (num <= 40)) //권장 방어력 미만 && 40퍼 당첨
    {
        ,,,생략,,,
        //체력 절반으로 떨어트리고 내쫓음
    }
    else // 권장 방어력 이상 or 40퍼 미당첨
    {
        //데미지 계산
        damaged = random.Next(20, 35);//받은 데미지 저장(표시 용도)
        damaged += recommendedDefense[(int)difficulty] - player.Defence; //권장 방어력 - 내 방어력
        player.Hp -= damaged;

        //보상 계산
        int playerAttack = (int)player.Attack + (int)player.AttackBonus;
        switch (difficulty)
        {
            ,,,생략,,,
            //난이도에 따라 보상이 달라짐
        }

        //플레이어가 죽었는지 확인
        if (player.IsDead())
        {
            player.Hp = 0;
            state = DungeonState.Failure;
            return;
        }

        state = DungeonState.Clear;
        player.Gold += reward;
    }
}
  • 권장 방어력 미만이면 확률적으로 체력 절반 잃고 던전 클리어 실패함

  • 그게 아니라면 클리어 성공하고 데미지 입고 보상을 얻는다.

  • 플레이어의 Hp가 0이되면 무조건 실패한다.

3-3. 던전 메서드 사용

인벤토리처럼 메인에서 위 메서드들을 호출만 한다.

private void Dungeon()
{
    //입력 데이터
    string playerInput = "";

    dungeon.ShowDungeon();

    dungeon.ShowDungeonResult();

    dungeon.ShowDungeonMenu();

    playerInput = Console.ReadLine();

    dungeon.SelectMenu(playerInput, ref scenes);
}

4. 개발 중 발생한 문제들

4-1. List로 선언한 변수를 반환해주면 값이 아니라 "참조"로 반환해준다.

아마 List도 힙에 생성된 자료구조를 참조하는 형식인가보다.
나중에 쓸 일이 많을텐데 꽤 중요한 내용을 일찍 깨달은 기분이다.

4-2. CS0053 오류

A 클래스의 접근 제한자와 A클래스 인스턴스의 접근 제한자가 서로 맞지 않으면 발생하는 오류다.
이 부분은 여러 종류의 접근 제한자에 대한 공부와 함께 정리해봐야겠다.

4-3. StackOverflowException

짧은 글로는 전부 설명하기 힘들고 간단히 말하면 비정적 객체와 정적 객체(싱글톤)의 생성자 호출이 꼬여서 정적 객체의 생성자가 무한 호출되는 문제를 겪었다.
이 문제의 해결에 대해서는 나중에 좀 더 자세히 써볼 생각이다 (메모 해놔서 다행이다.)

profile
개발은삼순이발

0개의 댓글