[TIL] C# - day 25

뭉크의 개발·2023년 8월 22일
1

C# - Camp

목록 보기
9/18
post-thumbnail

🐧 들어가기 앞서

[ 완 ]
아이템 정보를 클래스 / 구조체로 활용하기
아이템 정보를 배열로 관리하기
아이템 추가하기
콘솔 꾸미기
인벤토리 크기 맞춤
인벤토리 정렬하기
상점 - 아이템 구매
상점 - 아이템 판매
장착 개선

[ 미완 ]
던전입장
휴식기능
레벨업기능
게임 저장하기


생각보다 많이 구현했다.

수정 및 추가 함수만 설명!


🐧 스파르타 던전 구현

1. Main(string args[])

콘솔 창 크기를 변화하는 코드다.

계속해서 크기를 늘려주는게 번거로워 추가했다.

// witdht, height
            Console.SetWindowSize(200, 50);

2. InitItemDatabase()

상점을 구현하기 위해 새로운 아이템 데이터베이스 리스트를 작성했었는데,

서로 연동하는게 굉장히 번거로웠다. 예를 들어 코드는 아래와 같았는데,

static void InitItemDatabase()
        {
            _itemsInDatabase.Add(new ItemData(0, "낡은 검", 2, 0, "쉽게 볼 수 있는 낡은 검입니다.", 20));
            _itemsInDatabase.Add(new ItemData(1, "천 갑옷", 0, 2, "질긴 천을 덧대어 제작한 낡은 갑옷입니다.", 20));
            _itemsInDatabase.Add(new ItemData(2, "헤라클레스의 곤봉", 5, 0, "이 곤봉은 12가지 과업을 대비해서 갖고 다녀야합니다.", 50));
            _itemsInDatabase.Add(new ItemData(3, "포세이돈의 삼지창", 10, 0, "이 삼지창을 쥐면 바다를 다스릴 수 있다는 소문 때문에 선원들이 탐내는 무기입니다.", 100));
            _itemsInDatabase.Add(new ItemData(4, "트리스메기투스의 지팡이", 30, 0, "미지의 세계, 아틀란티스로 갈 수 있는 열쇠입니다.", 300));
        }

static void InitShopItemDatabase()
        {
            _shopItems.Add(new ItemData(5, "무한의 검", 15, 0, "끝없는 힘을 가진 검", 150));
            _shopItems.Add(new ItemData(6, "신령의 갑옷", 0, 15, "신비한 힘이 깃든 갑옷", 150));
        }



해당하는 인덱스가 서로 달라서,
입력해야하는 인덱스는 6인데,1을 입력해야 사라졌다.
리스트가 서로 매칭이 되지 않았던 것!


그래서 다시 생각해보니, 아이템 데이터 베이스에서

플레이어가

소유하고 있으면 -> Inventory

소유하고 있지 않으면 -> Store

이렇게 구성하면 아이템 DB 메서드는 하나로 구성 가능하다.

마지막에 bool값이 추가되었다.

static void InitItemDatabase()
        {
            _itemsInDatabase.Add(new ItemData(0, "낡은 검", 2, 0, "쉽게 볼 수 있는 낡은 검입니다.", 200, true));
            _itemsInDatabase.Add(new ItemData(1, "천 갑옷", 0, 2, "질긴 천을 덧대어 제작한 낡은 갑옷입니다.", 150, true));
            _itemsInDatabase.Add(new ItemData(2, "헤라클레스의 곤봉", 5, 0, "이 곤봉은 12가지 과업을 대비해서 갖고 다녀야합니다.", 500, false));
            _itemsInDatabase.Add(new ItemData(3, "포세이돈의 삼지창", 10, 0, "이 삼지창을 쥐면 바다를 다스릴 수 있습니다.", 1000, false));
            _itemsInDatabase.Add(new ItemData(4, "신령의 갑옷", 0, 15, "신비한 힘이 깃든 갑옷", 1300, false));
            _itemsInDatabase.Add(new ItemData(5, "트리스메기투스의 지팡이", 30, 0, "미지의 세계, 아틀란티스로 갈 수 있는 열쇠입니다.", 3000, false));
        }

2.1 ItemData Class

위 설명처럼 마지막에 _isPlayerOwned를 추가헀다.

public class ItemData
    {
        public int ItemId;
        public bool IsItemEquipped;
        public bool IsPlayerOwned;
        public string ItemName;
        public int ItemAtk;
        public int ItemDef;
        public string ItemComm;
        public int ItemPrice { get; set; }

        public ItemData(int _itemId, string _itemName, int _itemAtk, int _itemDef, string _itemComm, int _itemPrice, bool _isPlayerOwned)
        {
            ItemId = _itemId;
            ItemName = _itemName;
            ItemAtk = _itemAtk;
            ItemDef = _itemDef;
            ItemComm = _itemComm;
            ItemPrice = _itemPrice;
            IsItemEquipped = false;
            IsPlayerOwned = _isPlayerOwned;
        }
    }

3. MainGameScene()

상점 추가로 인한 3번째 버튼이 생겼다.

static void MainGameScene()
        {
            Console.Title = "스파르타 던전";
            SetConsoleColor(ConsoleColor.Red);
            Console.WriteLine("Sparta Dungeon Game!");
            Console.ResetColor();
            SetConsoleColor(ConsoleColor.Cyan);
            Console.Write($"{_playerStat.Name} ");
            Console.ResetColor();
            Console.WriteLine("님, 스파르타 마을에 오신것을 환영합니다!\n");
            Console.WriteLine("이곳에서 던전으로 돌아가기 전 활동을 할 수 있습니다.\n");
            Console.WriteLine("0. 게임 종료");
            Console.WriteLine("1. 상태 보기");
            Console.WriteLine("2. 인벤토리");
            Console.WriteLine("3. 상점");
            Console.WriteLine(" ");
            int input = CheckValidAction(0, 3);

            switch (input)
            {
                case 0:
                    Environment.Exit(0);
                    break;
                case 1:
                    DisplayPlayerState();
                    break;
                case 2:
                    DisplayPlayerInventory();
                    break;
                case 3:
                    DisplayItemShop();
                    break;
            }
        }

4. DisplayItemShop()

아이템을 구매 / 판매 할 수 있는 상점을 출력하는 메서드다.

특별한 것은 없고, 중간에 FormatAndPad 메서드가 추가 되었는데, 정렬을 도와준다.

0번 - 메인으로
1번 - 구매
2번 - 판매

순으로 진행된다.

static void DisplayItemShop()
        {
            Console.Clear();
            Console.Title = "상점";
            Console.WriteLine("[보유 골드]\n");
            Console.WriteLine($"{_playerStat.Gold} G\n");
            Console.WriteLine("[상점 아이템 목록]\n");

            for (int i = 0; i < _itemsInDatabase.Count; i++)
            {
                ItemData _shopItem = _itemsInDatabase[i];
                string _itemName = FormatAndPad(_shopItem.ItemName, 17);
                string _itemComm = FormatAndPad(_shopItem.ItemComm, 40);

                if (!_shopItem.IsPlayerOwned)
                {
                    Console.WriteLine($"{i + 1} | {_itemName} | {_itemComm} | 구매 가격 : {_shopItem.ItemPrice} G");
                }
            }
            Console.WriteLine(" ");
            Console.WriteLine("2. 아이템 판매");
            Console.WriteLine("1. 아이템 구매");
            Console.WriteLine("0. 나가기");

            int _input = CheckValidAction(0, 2);

            switch (_input)
            {
                case 0:
                    Console.Clear();
                    MainGameScene();
                    break;
                case 1:
                    Console.Clear();
                    BuyManagementItemShop();
                    break;
                case 2:
                    Console.Clear();
                    SellManagementItemShop();
                    break;
            }
        }

4.1 BuyManagementItemShop()

아이템을 구매하는 메서드다.

여기서 부터 꼬리로 따라 붙는 메서드가 많다.

현재 소지한 금액을 보여주며, 아이템을 판매 / 소지로 구분했다.

해당하는 아이템의 인덱스를 입력하면, 아이템 구매가 가능하다.

static void BuyManagementItemShop()
        {
            Console.Clear();
            Console.Title = "상점 - 아이템 구매";
            Console.WriteLine("[보유 골드]\n");
            Console.WriteLine($"{_playerStat.Gold} G\n");
            Console.WriteLine("[상점 아이템 목록]\n");

            for (int i = 0; i < _itemsInDatabase.Count; i++)
            {
                ItemData _shopItem = _itemsInDatabase[i];
                string _itemName = FormatAndPad(_shopItem.ItemName, 17);
                string _itemComm = FormatAndPad(_shopItem.ItemComm, 40);

                if (!_shopItem.IsPlayerOwned)
                {
                    Console.WriteLine($"{i + 1} | {_itemName} | {_itemComm} | 구매 가격 : {_shopItem.ItemPrice} G");
                }
            }
            Console.WriteLine("");
            Console.WriteLine("[인벤토리 아이템 목록]\n");

            for (int i = 0; i < _itemsInDatabase.Count; i++)
            {
                ItemData _inventoryItem = _itemsInDatabase[i];
                if (_inventoryItem.IsPlayerOwned)
                {
                    Console.WriteLine($"{i + 1} | {_inventoryItem.ItemName} | 판매 가격 :{_inventoryItem.ItemPrice * 0.8} G | 소지중");
                }
            }

            Console.WriteLine("");
            Console.WriteLine("0. 나가기\n");

            Console.WriteLine("아이템의 고유 번호를 입력해주세요.");
            int _input = CheckValidAction(0, _itemsInDatabase.Count);

            if (_input == 0)
            {
                Console.Clear();
                DisplayItemShop();
            }
            else
            {
                // 입력한 번호에 해당하는 아이템의 인덱스 계산
                int _itemIndex = _input - 1;

                if (!_itemsInDatabase[_itemIndex].IsPlayerOwned)
                {
                    BuyItem(_itemIndex); // 아이템을 구매
                }
                // 상점 목록을 다시 출력
                BuyManagementItemShop();
            }
        }

4.1.1 BuyItem()

아이템을 실질적으로 구매하는 메서드다.
해당하는 아이템 인덱스가 들어오면,
소지한 골드가 해당 아이템의 가격과 같거나, 많을 때
플레이어의 골드를 차감하고 아이템을 소지했다(true)로 변경한다.

만약 금액이 부족하다면, 골드가 부족하다 출력한다.

콘솔이 계속 클리어되는 순서라 함수가 실행될 때 글이 나오지 않았다.

따라서
사용자가 키를 입력하기 전 까지
넘어가지 않게 했다.

 static void BuyItem(int _itemIndex)
        {
            ItemData _selectedShopItem = _itemsInDatabase[_itemIndex];

            if (_playerStat.Gold >= _selectedShopItem.ItemPrice)
            {
                _playerStat.Gold -= _selectedShopItem.ItemPrice;
                _selectedShopItem.IsPlayerOwned = true;
                Console.WriteLine("");
                Console.WriteLine($"{_selectedShopItem.ItemName}을(를) 구매하였습니다.\n");
            }
            else
            {
                Console.WriteLine("");
                Console.WriteLine("골드가 부족합니다.");
            }
            Console.WriteLine("아무 키나 입력하세요...\n"); // 사용자의 입력을 기다림
            Console.ReadKey(); // 아무 키나 입력할 때까지 대기
        }

4.2 SellManagementItemShop()

이름이 점점 길어진다...


아이템을 판매하는 콘솔 창이다.
아이템을 판매하면 80% 가격으로 돌려받는다.
구매와 별 다른점은 없지만, 가장 마지막에
아이템을 플레이어가 소유하고 있다 (IsPlayerOwned == ture) 일 때만
아이템을 판매할 수 있다.

static void SellManagementItemShop()
        {
            Console.Clear();
            Console.Title = "상점";
            Console.WriteLine("[보유 골드]\n");
            Console.WriteLine($"{_playerStat.Gold} G\n");
            Console.WriteLine("[상점 아이템 목록]\n");

            for (int i = 0; i < _itemsInDatabase.Count; i++)
            {
                ItemData _shopItem = _itemsInDatabase[i];
                string _itemName = FormatAndPad(_shopItem.ItemName, 17);
                string _itemComm = FormatAndPad(_shopItem.ItemComm, 40);

                if (!_shopItem.IsPlayerOwned)
                {
                    Console.WriteLine($"{i + 1} | {_itemName} | {_itemComm} | 구매 가격 : {_shopItem.ItemPrice} G");
                }
            }
            Console.WriteLine("");
            Console.WriteLine("[인벤토리 아이템 목록]\n");

            for (int i = 0; i < _itemsInDatabase.Count; i++)
            {
                ItemData _inventoryItem = _itemsInDatabase[i];
                if (_inventoryItem.IsPlayerOwned)
                {
                    Console.WriteLine($"{i + 1} | {_inventoryItem.ItemName} | 판매 가격 :{_inventoryItem.ItemPrice * 0.8} G | 소지중");
                }
            }

            Console.WriteLine("");
            Console.WriteLine("0. 나가기\n");

            Console.WriteLine("아이템의 고유 번호를 입력해주세요.");
            int _input = CheckValidAction(0, _itemsInDatabase.Count);

            if (_input == 0)
            {
                Console.Clear();
                DisplayItemShop();
            }
            else
            {
                // 입력한 번호에 해당하는 아이템의 인덱스 계산
                int _itemIndex = _input - 1;

                if (_itemsInDatabase[_itemIndex].IsPlayerOwned == true)
                {
                    Sell_Item(_itemIndex); // 아이템을 판매
                }
                // 상점 목록을 다시 출력
                SellManagementItemShop();
            }
        }

4.2.1 Sell_Item()

콘솔에서 SellItem()을 하니 l과 I가 정말 똑같았다.
여기서도 그런데, 조심해야한다.
buyItem()메서드와 조금 다르다.

우선 장착한 아이템을 판매한다면, 장착한 아이템을 해제 한 뒤 판매를 진행한다.
판매는 원래 가격의 80%만 반영된다.
그리고 UpdatePlayerStats()를 통해 플레이어의 스탯을 변경한다.

static void Sell_Item(int _itemIndex)
        {
            ItemData _selectedShopItem = _itemsInDatabase[_itemIndex];
            if (_selectedShopItem.IsPlayerOwned == true)
            {
                // 장착된 아이템 해제 후 판매가 진행되어야 한다.
                if (_selectedShopItem.IsItemEquipped)
                {
                    ToggleEquip(_selectedShopItem); // 아이템 장착 해제
                }

                double _sellRet = _selectedShopItem.ItemPrice * 0.8;
                _playerStat.Gold += (int)_sellRet;
                _selectedShopItem.IsPlayerOwned = false;
                Console.WriteLine($"{_selectedShopItem.ItemName}을(를) 판매하였습니다.\n");

                UpdatePlayerStats();
            }
            Console.WriteLine("아무 키나 입력하세요...\n"); // 사용자의 입력을 기다림
            Console.ReadKey(); // 아무 키나 입력할 때까지 대기
        }

5. DisplayPlayerInventory()

코드 중간에 FormatAndPad와 IsplayerOwned를 판단하는 문법이 추가되었다.

만약 플레이어가 소지하고 있지 않으면 (IsPlayerOwend == false)

플레이어 인벤토리에 출력하지 않는다.

static void DisplayPlayerInventory()
        {
            Console.Clear();
            Console.Title = "인벤토리";
            Console.WriteLine("[인벤토리]");
            Console.WriteLine("보유 중인 아이템을 관리할 수 있습니다.\n");
            Console.WriteLine("[아이템 목록]");

            string _itemEquipped;
            for (int i = 0; i < _itemsInDatabase.Count; i++)
            {
                ItemData _item = _itemsInDatabase[i];

                if (!_item.IsPlayerOwned)
                {
                    continue; // IsPlayerOwned가 false면 출력하지 않고 다음 아이템으로 넘어감
                }

                _itemEquipped = _item.IsItemEquipped ? "[E] " : "";

                string _itemName = FormatAndPad(_item.ItemName, 17);
                string _itemComm = FormatAndPad(_item.ItemComm, 30);

                Console.Write($"{_item.ItemId + 1} | ");
                if (_item.IsItemEquipped)
                {
                    SetConsoleColor(ConsoleColor.Yellow);
                }
                Console.Write($"{_itemEquipped}");
                Console.ResetColor();
                Console.Write($"{_itemName}");
                DisplayAtkOrDef(_item);
                Console.WriteLine($" {_itemComm} ");
            }
            Console.WriteLine(" ");
            Console.WriteLine("1. 장착 관리");
            Console.WriteLine("0. 나가기");

            int _input = CheckValidAction(0, 1);

            switch (_input)
            {
                case 0:
                    Console.Clear();
                    MainGameScene();
                    break;
                case 1:
                    Console.Clear();
                    ManagementPlayerInventory();
                    break;
            }
        }

5.1 ManagementPlayerInventory()

DisplayPlayerInventory()와 거의 비슷하다.
마지막에 ToggleEquip가 추가되었는데, 여기서 장착 / 해제 및 중복 착용을 관리한다.

else if (_input > 0 && _input <= _itemsInDatabase.Count)
            {
                ItemData _selectedItem = _itemsInDatabase[_input - 1];
                ToggleEquip(_selectedItem);
                ManagementPlayerInventory();
            }

5.1.1 ToggleEquip()

플레이어가 아이템을 장착 / 해제 / 중복 아이템 관리를 하는 메서드다.

만약 중복 아이템(무기를 착용했는데, 무기가 또 입력)이 입력된다면
기존 장착된 아이템을 해제 한다.
후에 입력된 아이템을 추가하여 장착한다.

중간에 주석 부분은 아래에서 추가 설명!

 static void ToggleEquip(ItemData _item)
        {
            bool _isAtkItemEquipped = IsAtkItemEquipped();
            bool _isDefItemEquipped = IsDefItemEquipped();

            if (_item.ItemAtk > 0 && _isAtkItemEquipped && !_item.IsItemEquipped)
            {
                for (int i = _playerEquippedItems.Count - 1; i >= 0; i--)
                {
                    ItemData item = _playerEquippedItems[i];
                    if (item.IsItemEquipped && item.ItemAtk > 0)
                    {
                        item.IsItemEquipped = false;
                        _playerEquippedItems.RemoveAt(i);
                        break;
                    }
                }
            }

            // 중복 방어구 아이템 제거 후 장착
            if (_item.ItemDef > 0 && _isDefItemEquipped && !_item.IsItemEquipped)
            {
                for (int i = _playerEquippedItems.Count - 1; i >= 0; i--)
                {
                    ItemData item = _playerEquippedItems[i];
                    if (item.IsItemEquipped && item.ItemDef > 0)
                    {
                        item.IsItemEquipped = false;
                        _playerEquippedItems.RemoveAt(i);
                        break;
                    }
                }
                //foreach (var item in _playerEquippedItems)
                //{
                //    if (item.IsItemEquipped && item.ItemDef > 0)
                //    {
                //        item.IsItemEquipped = false;
                //        _playerEquippedItems.Remove(item);
                //        break;
                //    }
                //}
            }

            _item.IsItemEquipped = !_item.IsItemEquipped;

            if (_item.IsItemEquipped)
            {
                _playerEquippedItems.Add(_item);
            }
            else
            {
                _playerEquippedItems.Remove(_item);
            }
            UpdatePlayerStats();
        }

6. FormatAndPad()

여러가지 인벤토리, 상점 등 아이템의 목록의 길이를 같게 해주기 위해 추가한 메서드다.

텍스트의 길이를 계산하고, _width에서 최대 길이를 정해준다.

남은 공간은 최대 길이 - 텍스트 길이 로 계산되어

좌, 우 공간에 삽입한다.
좌측은 남은 공간 / 2
우측은 남은 공간 - 좌측 공간
이다.

여기서 오류가 조금 발생했었다.
초기에 작성한 코드는

static string FormatAndPad(string _text, int _width)
        {
            int _remainingSpace = _width - _text.Length;
            if(_remainingSpace <= 0)
            {
                return _text;
            }
            else
            {
                int _leftPadding = _remainingSpace / 2;
                int _rightPadding = _remainingSpace - _leftPadding;
                string _formattedText = new string (' ', _leftPadding) + _text + new string(' ',  _rightPadding);
                return _formattedText;
            }
        }

이런 식이었는데,
아마 알다 싶이 공백 2개가 문자 하나와 같은 길이다. (폰트와 환경에 따라 달라질 수 있다.)


static string FormatAndPad(string _text, int _width)
        {
            int _remainingSpace = _width - _text.Length;
            if(_remainingSpace <= 0)
            {
                return _text;
            }
            else
            {
                int _leftPadding = _remainingSpace / 2;
                int _rightPadding = _remainingSpace - _leftPadding;
                string _formattedText = new string (' ', 2 * _leftPadding) + _text + new string(' ',  2 * _rightPadding);
                return _formattedText;
            }
        }

아무튼

 string _formattedText = new string (' ', 2 * _leftPadding) + _text + new string(' ',  2 * _rightPadding);

이렇게 남은 공간에 * 2 를 해주었다. 공백을 늘릴수는 없으니까.(char)

정렬!

7. 🎇ToggleEquip() 추가 설명

✔ Reference

https://www.techiedelight.com/ko/remove-elements-from-list-while-iterating-csharp/

  1. foreach
foreach (var item in _playerEquippedItems)
                {
                    if (item.IsItemEquipped && item.ItemDef > 0)
                    {
                        item.IsItemEquipped = false;
                        _playerEquippedItems.Remove(item);
                        break;
                    }
                }
  1. for
if (_item.ItemDef > 0 && _isDefItemEquipped && !_item.IsItemEquipped)
            {
                for (int i = _playerEquippedItems.Count -1; i >= 0; i--)
                {
                    ItemData item = _playerEquippedItems[i];
                    if (item.IsItemEquipped && item.ItemDef > 0)
                    {
                        item.IsItemEquipped = false;
                        _playerEquippedItems.RemoveAt(i);
                        break;
                    }
                }

둘 다 같은 동작을 하는 함수다. 그러나 진행 방식이 다르다.

  • foreach
    처음부터 인덱스를 순회한다. 이는 인덱스가 새로 재정렬 될 수 있다.
    예를 들어, 다음 아이템을 검사하기 전에 현재 아이템 다음 인덱스 아이템을 검사하여 오류가 발생할 수 있다.
    또한, 컬렉션을 순차적으로 순회하면 컬렉션 크기가 줄어 순회 중 오류가 생길 수 있다.

  • for
    따라서 역순으로 컬렉션을 순회하는 방식을 for문을 이용해 삭제 작업을 처리할 수 있다.
    역순으로 진행하면, 컬렉션의 크기가 줄어도 삭제 아이템 인덱스에 영향을 끼치지 않는다.
    즉, 삭제를 해도 아이템 현재 아이템 인덱스가 변하지 않는다.

자세한 내용은 링크를 타고가면 예시 코드와 함께 쉽게 이해할 수 있다!


🐧 실행 결과

아이템 구매





아이템 판매



아이템 중복 장착 관리


착용 중인 아이템 판매



🐧 내일 할 일

던전 입장
휴식 기능
레벨업 기능
게임 저장하기

남았다.
아무래도 저장은 어려울 듯 하고, 최대한 던전을 빠르게 구현하고
UI를 수정해야겠다.

생각보다 코드가 길어져서 보기가 너무 힘들다!

0개의 댓글