[ 완 ]
아이템 정보를 클래스 / 구조체로 활용하기
아이템 정보를 배열로 관리하기
아이템 추가하기
콘솔 꾸미기
인벤토리 크기 맞춤
인벤토리 정렬하기
상점 - 아이템 구매
상점 - 아이템 판매
장착 개선
[ 미완 ]
던전입장
휴식기능
레벨업기능
게임 저장하기
생각보다 많이 구현했다.
수정 및 추가 함수만 설명!
콘솔 창 크기를 변화하는 코드다.
계속해서 크기를 늘려주는게 번거로워 추가했다.
// witdht, height
Console.SetWindowSize(200, 50);
상점을 구현하기 위해 새로운 아이템 데이터베이스 리스트를 작성했었는데,
서로 연동하는게 굉장히 번거로웠다. 예를 들어 코드는 아래와 같았는데,
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));
}
위 설명처럼 마지막에 _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번째 버튼이 생겼다.
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;
}
}
아이템을 구매 / 판매 할 수 있는 상점을 출력하는 메서드다.
특별한 것은 없고, 중간에 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;
}
}
아이템을 구매하는 메서드다.
여기서 부터 꼬리로 따라 붙는 메서드가 많다.
현재 소지한 금액을 보여주며, 아이템을 판매 / 소지로 구분했다.
해당하는 아이템의 인덱스를 입력하면, 아이템 구매가 가능하다.
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();
}
}
아이템을 실질적으로 구매하는 메서드다.
해당하는 아이템 인덱스가 들어오면,
소지한 골드가 해당 아이템의 가격과 같거나, 많을 때
플레이어의 골드를 차감하고 아이템을 소지했다(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(); // 아무 키나 입력할 때까지 대기
}
이름이 점점 길어진다...
아이템을 판매하는 콘솔 창이다.
아이템을 판매하면 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();
}
}
콘솔에서 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(); // 아무 키나 입력할 때까지 대기
}
코드 중간에 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;
}
}
DisplayPlayerInventory()와 거의 비슷하다.
마지막에 ToggleEquip가 추가되었는데, 여기서 장착 / 해제 및 중복 착용을 관리한다.
else if (_input > 0 && _input <= _itemsInDatabase.Count)
{
ItemData _selectedItem = _itemsInDatabase[_input - 1];
ToggleEquip(_selectedItem);
ManagementPlayerInventory();
}
플레이어가 아이템을 장착 / 해제 / 중복 아이템 관리를 하는 메서드다.
만약 중복 아이템(무기를 착용했는데, 무기가 또 입력)이 입력된다면
기존 장착된 아이템을 해제 한다.
후에 입력된 아이템을 추가하여 장착한다.
중간에 주석 부분은 아래에서 추가 설명!
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();
}
여러가지 인벤토리, 상점 등 아이템의 목록의 길이를 같게 해주기 위해 추가한 메서드다.
텍스트의 길이를 계산하고, _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)
정렬!
https://www.techiedelight.com/ko/remove-elements-from-list-while-iterating-csharp/
foreach (var item in _playerEquippedItems)
{
if (item.IsItemEquipped && item.ItemDef > 0)
{
item.IsItemEquipped = false;
_playerEquippedItems.Remove(item);
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
처음부터 인덱스를 순회한다. 이는 인덱스가 새로 재정렬 될 수 있다.
예를 들어, 다음 아이템을 검사하기 전에 현재 아이템 다음 인덱스 아이템을 검사하여 오류가 발생할 수 있다.
또한, 컬렉션을 순차적으로 순회하면 컬렉션 크기가 줄어 순회 중 오류가 생길 수 있다.
for
따라서 역순으로 컬렉션을 순회하는 방식을 for문을 이용해 삭제 작업을 처리할 수 있다.
역순으로 진행하면, 컬렉션의 크기가 줄어도 삭제 아이템 인덱스에 영향을 끼치지 않는다.
즉, 삭제를 해도 아이템 현재 아이템 인덱스가 변하지 않는다.
자세한 내용은 링크를 타고가면 예시 코드와 함께 쉽게 이해할 수 있다!
던전 입장
휴식 기능
레벨업 기능
게임 저장하기
남았다.
아무래도 저장은 어려울 듯 하고, 최대한 던전을 빠르게 구현하고
UI를 수정해야겠다.
생각보다 코드가 길어져서 보기가 너무 힘들다!