TIL 0103 TextRPG - 2(개인) / 리팩토링

강성원·2024년 1월 3일
0

TIL 오늘 배운 것

목록 보기
8/70
post-thumbnail

오늘 개발 포스트 엄청 엄청 길다.
그만큼 고생한 것이겠지..
다만 남의 코드 보고 바로 이해하는 사람은 없다는게 문제다...

이번 텍스트 RPG에는 [필수 구현]항목이 있고 [추가 구현]항목(선택)이 있다.
필수 구현은 모두 마쳤고 오늘 구현한 상점 판매 기능과 상점의 리팩토링을 구현하였다.

오늘 개발한 내용은 대략 이렇다.

  1. 상점의 상태를 열거형으로 Main, Buy, Sell로 나누었고, 현재 상점 상태에 따라서 실행되는 로직을 다르게 했다.

  2. 원래 하나의 함수에 다 넣어놓은 상점의 로직을 세분화해서 메서드로 만들고 필요한 곳에서 필요한 부분만 호출하도록 했다.
    각 메서드는 상점의 상태를 분기로 해서 내부 로직을 실행한다.

  3. 플레이어 객체가 다른 클래스에서 많이 쓰여서 플레이어 객체를 싱글톤으로 운영하기로 했다.


1. [상점에서 판매] 기능 추가

상점에서 구매하는 기능에 이어서 플레이어가 아이템을 판매하는 기능을 추가했다.

일단 보여지는 것은 이렇게 했다.
Store 클래스에는 열거형으로 상점의 상태를 출력하는 함수를 두었다.
상점의 상태에는 Main, Buy, Sell 이렇게 3가지의 형태가 있다.
위 사진은 Sell일 때 상점에서 출력하는 화면이다.
밑에 코드들도 위에 사진을 기준으로 설명함.

1-1. 클래스 Store의 HandleStore 메서드

public void HandleStore(string playerInput, ref Scenes scenes)
{
    int num = 0;

    if(state == StoreState.Main)// 상점 상태 Main
    {
        ,,,생략,,,
    }
    else if (state == StoreState.Buy)// 상점 상태 Buy
    {
        ,,,생략,,,
    }
    else if (state == StoreState.Sell)// 상점 상태 Sell
    {
        switch (playerInput)
        {
            case "0": // 상점으로 나가기
                scenes = Scenes.Store;
                state = StoreState.Main;
                break;

            default:
                if (int.TryParse(playerInput, out num) && 0 < num && num <= player.Inventory.Count)
                {
                    player.Sell(player.Inventory[--num]);
                }
                else
                {
                    WrongInput();
                }
                break;
        }
    }

}

사진 기준 1을 입력하면 맨 첫 번째 아이템인 "낡은 직검"이 판매된다.

1-2. Player 클래스의 Sell 메서드

namespace TextRPG
{
    public class Player
    {
        
        ,,,생략,,,
        
        public void Sell(Item item)
        {
            //85퍼 가격에 판매
            Gold += (int)(item.Price * 0.85);
            //판매시 장착 해제 -> 장착 개선기능에 영향 미칠 듯 하다.

            //아이템 벗음
            item.Equip(this);

            //아이템에 세팅 전부 false
            item.Isbought = false;
            item.IsEquip = false;
            
            //인벤토리에서 제외함
            int index = Inventory.IndexOf(item);
            Inventory.RemoveAt(index);
        }
    }
}

판매하는 기능은 Player에 Sell메서드에 구현해놓았다.

  • 우선 아이템의 가격의 85퍼센트가 플레이어에게 들어온다.
  • item에 구현해놓은 Equip함수를 호출한다.
  • item의 구매, 장착 상태를 false로 되돌린다.
  • 플레이어의 인벤토리(List)에서 아이템을 제외시킨다.

1-3. Weapon 클래스의 Equip 메서드

public class Weapon : Item
{
    ,,,생략,,,

    public override void Equip(Player player)
    {
        //이 장비 장착중 아닐 때
        if(!this.IsEquip)
        {
            this.IsEquip = true;
            player.AttackBonus += this.Attack;
        }
        //이 장비 장착중일 때
        else
        {
            this.IsEquip = false;
            player.AttackBonus -= this.Attack;
        }
    }
}

Equip 메서드는 Item 클래스를 상속한 Weapon, Armor, Shield 모두 가지고있다.

  • 장비의 장착 상태가 false라면 true로 바꾼 뒤 매개변수로 받은 플레이어의 보너스 공격력(방어력)에 장비의 공격력(방어력)을 추가해준다.
  • 반대로 장착 상태가 true라면 false로 바꾸고 보너스 공격력(방어력)을 빼준다.

위 과정들을 거치면 상점에서도 구매 가능 상태가 되고, 인벤토리에서도 사라지게 된다.


2. 상점 리팩토링

리팩토링 한다고 상점 관련 코드들을 다 뒤집어 엎었다.
이렇게 된 이상 아마 나머지 추가기능들의 구현은 미뤄두고 구현한 것에 대한 리팩토링에 더 신경쓰지 않을까 싶다.

2-1. 상점 기능 모듈화

처음에는 상점을 Game 클래스의 Store 메서드에서 전부 처리했었다.
아래처럼 모든 기능을 함수 안에 다 넣었었다.

게임 클래스
{
	Store메서드()
    {
    	- 메인 화면 출력
        - 판매 목록 출력
        
        - 사용자 입력에 따라 진행되는 로직
    }
}

상점은 메인, 구매, 판매에 따라서 보여지는 화면도 달라서 실제로는 위에보다도 훨씬 복잡했다.
안되겠다 싶어서 Store 클래스를 만들어서 아이템 관리, 출력, 로직 메서드로 나누어주었다.

2-2. 상점 상태

상점은 메인, 구매, 판매에 따라서 보여지는 화면이 달라야한다.
고민을 했지만 제일 쉬운 방법은 열거형이었다. 우선 열거형으로 상점의 상태를 정의해줬다.

public enum StoreState
{
    Main = 1,
    Buy = 2,
    Sell = 3,
}

2-3. 출력

상점 기본 출력

상점 상태에 따라서 상점이 어떤 상태인지 알 수 있도록 함수를 만들었다.

public void ShowStore()
{
    //출력
    Console.Clear();
    Console.WriteLine(
        ":##*                             .-:    \r\n" +
        "-@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%.  \r\n" +
        "-@@@######%@@@##########@@@######@@@#   \r\n" +
        ".++=   :===+++==========+++===:   .     \r\n" +
        "       *@@%++*@@@@@@@@@@+++%@@+         \r\n" +
        "       *@@@@@@@@@@@@@@@@@@@@@@+         \r\n" +
        "       *@@@@@%%%%%%%%%%%%@@@@@+         \r\n" +
        "       *@@@@:            =@@@@+         \r\n" +
        "       *@@@@+============*@@@@+         \r\n" +
        "       *@@@@@@@@@@@@@@@@@@@@@@+         \r\n" +
        "       *@@@@=............+@@@@+         \r\n" +
        "       *@@@@-............=@@@@+         \r\n" +
        "       *@@@@@@@@@@@@@@@@@@@@@@+         \r\n" +
        "       *@@@@@@@@@@@@@@@@@@@@@@+         \r\n" +
        "       :*%@@@@@@@@@@@@@@@@@@%+:         \r\n" +
        "           :=*%@@@@@@@@%*=:             \r\n" +
        "                @@@@@@                  \r\n");

    switch (state)
    {
        case StoreState.Main:
            Console.WriteLine("================= 상점 =================");
            Console.WriteLine("필요한 아이템을 얻을 수 있는 상점입니다.");
            Console.WriteLine("========================================");
            break;
        case StoreState.Buy:
            Console.WriteLine("========== 상점 - 아이템 구매 ==========");
            Console.WriteLine("필요한 아이템을 얻을 수 있는 상점입니다.");
            Console.WriteLine("========================================");
            break;
        case StoreState.Sell:
            Console.WriteLine("========== 상점 - 아이템 판매 ==========");
            Console.WriteLine("필요한 아이템을 얻을 수 있는 상점입니다.");
            Console.WriteLine("========================================");
            break;
        default:
            
            break;
    }
}

아이템 리스트 출력

코드 먼저

public void ShowItemList()
{
    Console.WriteLine("\n[아이템 목록]");
    int index = 0;
    switch (state)
    {
        
        case StoreState.Main: //상점 메인에 보이는 아이템 리스트
            foreach (Item item in items)
            {
                string itemInfo = $"- {item.Name} | ";

                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;

                itemInfo += item.Isbought ? "| 구매 완료" : $"| {item.Price} G";

                Console.WriteLine(itemInfo);
            }
            break;
        case StoreState.Buy: //상점 - 구매하기에서 보이는 아이템 리스트
            index = 0;
            foreach (Item item in items)
            {
                ,,,메인이랑 비슷해서 생략,,,
            }
            break;
        case StoreState.Sell: //상점 - 판매하기에서 보이는 내 아이템 리스트
            index = 0;
            foreach (Item item in player.Inventory)
            {
                string itemInfo = $"- {++index} {item.Name} | ";

                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;

                itemInfo += $"| {item.Price} G";

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

}
  • 메인과 구매 상태에서는 상점의 아이템 리스트에 있는 친구들을 쫙 다 출력해준다.
    구매 된 애들은 "구매 완료"를 띄워준다.
  • 판매 상태에서는 플레이어 인벤토리의 아이템들을 전부 출력해준다.

상점 메뉴 출력

현재 상태에 따라서 나오는 메뉴도 다르다.

public void ShowStoreHandle()
{
    switch(state)
    {
        case StoreState.Main:
            Console.WriteLine();
            Console.WriteLine("1. 아이템 구매");
            Console.WriteLine("2. 아이템 판매");
            Console.WriteLine("0. 나가기\n");
            Console.WriteLine("원하시는 행동을 입력해주세요.");
            Console.Write(">>");
            break;

        ,,,생략,,,
    }    
}

중요한 것 없어서 생략

2-4. 상점 로직

사용자의 입력과 현재 상점의 상태에 따라서 로직을 수행한다.

public void SelectMenu(string playerInput, ref Scenes scenes)
{
    int num = 0;

    if(state == StoreState.Main)// 상점 상태 Main
    {
        switch (playerInput)
        {
            case "0": // 마을로 나가기
                scenes = Scenes.Town;
                state = StoreState.Main;
                break;
            case "1": // 구매 화면
                state = StoreState.Buy;
                break;
            case "2": // 판매 화면
                state = StoreState.Sell;
                break;
        }
    }
    else if (state == StoreState.Buy)// 상점 상태 Buy
    {
        switch (playerInput)
        {
            case "0": // 상점으로 나가기
                scenes = Scenes.Store;
                state = StoreState.Main;
                break;

            default:
                if (int.TryParse(playerInput, out num) && 0 < num && num <= items.Count)
                {
                    player.Buy(items[--num]);
                }
                else
                {
                    WrongInput();
                }
                break;
        }
    }
    else if (state == StoreState.Sell)// 상점 상태 Sell
    {
        switch (playerInput)
        {
            case "0": // 상점으로 나가기
                scenes = Scenes.Store;
                state = StoreState.Main;
                break;

            default:
                if (int.TryParse(playerInput, out num) && 0 < num && num <= player.Inventory.Count)
                {
                    player.Sell(player.Inventory[--num]);
                }
                else
                {
                    WrongInput();
                }
                break;
        }
    }

}
  • Main 상태에서는 입력에 따라 [마을로 나가기] , [상태 Buy로 변경] , [상태 Sell로 변경] 을 진행한다.
  • Buy 상태에서는 [상태 Main으로 변경] , [아이템 구매] 를 진행한다.
  • Sell 상태에서는 [상태 Main으로 변경] , [아이템 판매] 를 진행한다.

2-5. 위 함수들 사용

위의 함수들을 사용하여 아래처럼 상점 기능을 Store의 메서드 호출로만 작성할 수 있게 됐다.

private void Store()
{
    //데이터
    string playerInput = "";
    
    int index = 0;

    store.ShowStore();

    Console.WriteLine("\n[보유 골드]");
    Console.WriteLine($"{player.Gold} G");

    store.ShowItemList();

    store.ShowStoreHandle();

    playerInput = Console.ReadLine();

    store.SelectMenu(playerInput, ref scenes);

}

3. 여러군데에서 플레이어가 쓰임

싱글톤

플레이어가 굉장히 다른 곳에서 쓰이는 상황이 잦아졌다.
처음에는 의존성 주입으로 다른 객체들의 생성자에 플레이어 인스턴스를 넣어줄까 했다.
하지만 객체 생성 시기를 맞추고 하는 문제가 머리가 아프고 시간이 너무 많이걸려서 작은 규모에서 간단하게 쓸 수 있는 싱글톤을 채택했다.
싱글 플레이 게임이기 때문에 Player를 싱글톤으로 운영하는 것이 합리적으로 보였다.

 public class Player
 {
     private static Player instance;
     public static Player Instance
     { get 
         {
             if (instance == null)
                 instance = new Player();
             return instance;
         }
     }
  }
  • 정적 Player 참조 변수를 만들어준다.
  • 프로퍼티를 통해 변수에 접근하면 새로운 Player 인스턴스를 변수에 할당해주고 리턴해준다.

힙 메모리에 정적 인스턴스가 존재하게된다.


오랜만에 엔진 없이 간단한 게임을 만드려니까 신경 쓸 부분도 꽤 있어서 시간 가는 줄 몰랐다.
WinAPI 독학할 때 배운 게임 구조가 많은 도움이 됐으리라고 본다.

profile
개발은삼순이발

0개의 댓글