C#으로 만드는 텍스트 RPG (2)
두번 째 미션을 해결했다.
이번에도 우선 구현하는데 초점을 뒀고 어떻게하면 더 효율적인지 고민해봤다.
그럼에도 개선점이 있어보이지만 앞으로 유니티로 발전시키면서 개선해나갈 생각이다.
아이템기능을 추가하는 미션이었는데 리스트와 딕셔너리를 활용해 인벤토리, 아이템을 관리하는 기능을
만드는 것이었다.
미션 2: 리스트와 딕셔너리를 활용한 아이템
- 목표: List와 Dictionary를 활용해 데이터를 관리한다.
- 미션:
- Item 클래스를 만들고, 이름과 효과를 프로퍼티로 추가해라.
- 플레이어의 인벤토리를 관리하는 List<Item>을 생성해라.
- 아이템 사용 상태를 출력하는 코드를 작성해라.
아이템 4가지의 기획이다.
아이템 종류 기획
1. 체력 회복 물약
- 이름: 생명의 물약
- 효과: 플레이어의 체력을 50 회복.
- 설명: "마시자마자 체내에서 생명력이 솟아오르는 신비한 물약."
- 사용 상황: 전투 중 또는 탐험 도중 언제든 사용 가능.
2. 공격력 증가 물약
- 이름: 전사의 물약
- 효과: 플레이어의 공격력을 10 증가. 효과는 3턴 동안 지속.
- 설명: "전투의 열기를 불어넣어 당신의 힘을 극대화시킨다."
- 사용 상황: 전투 중 사용하여 강력한 몬스터를 상대할 때 유리.
3. 방어력 증가 물약
- 이름: 철벽의 물약
- 효과: 플레이어가 받는 피해를 3턴 동안 50% 감소.
- 설명: "몸을 철벽처럼 단단하게 만들어주는 마법의 물약."
- 사용 상황: 보스 전투처럼 강한 적의 공격을 견뎌야 할 때.
4. 랜덤 효과의 마법 주문서
- 이름: 혼돈의 주문서
- 효과: 랜덤으로 긍정적 또는 부정적 효과를 발생시킴.
- 긍정적 효과: 체력 전부 회복, 공격력 2배 증가 등.
- 부정적 효과: 체력 절반 감소, 방어력 감소 등.
- 설명: "혼돈의 힘을 다룰 준비가 되었는가? 예측할 수 없는 결과를 감수하라."
- 사용 상황: 절박한 상황에서 도박처럼 사용.
이것을 토대로 만들었다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading.Tasks;
namespace TextRpgCS
{
public abstract class Item
{
public string Name { get; protected set; }
public string Description { get; protected set; }
public virtual void Use()
{
Console.WriteLine($"{Name}아이템을 사용!");
}
};
public class HpPortion : Item
{
public HpPortion()
{
Name = "HP포션";
Description = "Hp 50을 회복합니다.";
}
public override void Use()
{
base.Use();
GameManager.Instance.Player.Hp += 50;
ItemManager.Instance.RemoveItem(Name);
}
};
// 여기서 사용, 사용종료 구현각각해서 아이ㅏ템매니저에서 Update로 계산
// 턴 개념이 있는 아이템은 이 클래스를 상속
public abstract class DurationItem : Item
{
public int Duration = 3;
public virtual void EndEffect() { Console.WriteLine($"{Name}아이템의 버프지속시간이 종료되었습니다."); }
}
public class AttackPotion : DurationItem
{
public AttackPotion()
{
Name = "공격력증가포션";
Description = "공격력을 3턴동안 10증가시킵니다.";
}
public override void Use()
{
base.Use();
// 세턴동안 진행
GameManager.Instance.Player.AttackPower += 10;
Item? it= ItemManager.Instance.RemoveItem(Name);
if(it!=null)
ItemManager.Instance.RegistItem(it);
}
public override void EndEffect()
{
//if (GameManager.Instance.Player == null) return;
GameManager.Instance.Player.AttackPower -= 10;
}
};
public class ShieldPotion : DurationItem
{
public ShieldPotion()
{
Name = "방어력증가포션";
Description = "3턴 동안 받는 피해가 50% 감소합니다.";
}
public override void Use()
{
base.Use();
// 3턴 동안 피해감소
GameManager.Instance.Player.DefenseRate -=0.5f;
Item? it = ItemManager.Instance.RemoveItem(Name);
if (it != null)
ItemManager.Instance.RegistItem(it);
}
public override void EndEffect()
{
GameManager.Instance.Player.DefenseRate += 0.5f;
}
};
public class RandomPortion : Item
{
public RandomPortion()
{
Name = "랜덤포션";
Description = "랜덤으로 긍정적 또는 부정적 효과를 발생시킴.\r\n\r\n" +
"긍정적 효과: 체력 전부 회복, 공격력 2배 증가.\r\n\r\n" +
"부정적 효과: 체력 절반 감소, 방어력 감소.";
}
public override void Use()
{
base.Use();
// 랜덤 효과 발생
Random random = new Random();
int randomValue = random.Next(0, 4);
switch(randomValue)
{
// hp 전부 회복
case 0:
GameManager.Instance.Player.Hp = GameManager.Instance.Player.MaxHp;
break;
// 공격력 2배
case 1:
GameManager.Instance.Player.AttackPower *= 2;
break;
// hp 절반으로 감소
case 2:
GameManager.Instance.Player.Hp /= 2;
break;
// 받는 데미지 2배
case 3:
GameManager.Instance.Player.DefenseRate *= 2;
break;
}
}
}
}
먼저 공통으로 필요한 기능들을 Item 추상화클래스로 만들었다. 그리고 관리가 편하게 모든 아이템들을 사용기능이 있어야하니 여기서 Use를 만들었다.
그리고 몇턴동안 효과가 유지되는 물약이 기획에 있었다. 이러한 것들 때문에 DurationPortion 이라는 추상화클래스가 하나 더 필요했다.
이 클래스를 상속받으면 몇턴동안 효과가 유지되는지와 끝났을 때 원래대로 돌려놓을 endEffect 함수를 구현하도록 만들었다.
따라서 몇턴동안 유지되는 물약들은 Duration 변수만큼 유지되고 Use로 사용 시, 효과가 적용되고 끝날 때, EndEffect함수로 돌아오게끔 구현했다.
랜덤포션은 랜덤한 변수로 스위치문을 이용해 구현했다.
다음은 아이템매니저이다. 여기서 인벤토리를 dictionary로 구현했고 턴을 계산하기 위해서 또 다른 Dictionary를 구현했다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TextRpgCS
{
public enum ItemType
{
HpPortion,
AttackPotion,
ShieldPotion,
RandomPortion
}
// TODO : 아이템에 아이디 부여해서 관리하는 방식이 더 나을 것 같음.
// ItemDataTableManager 같은것도 관리해서 하드코딩대신 사용하면 좋을듯.
public class ItemManager
{
public static ItemManager Instance { get; private set; }= new ItemManager();
// 아이템이름, 개수
public Dictionary<string,List<Item>> Inventory { get; set; }
public Dictionary<string, DurationItem> DurationItems { get; set; }
// 나중에 이거활용해서 개선할 수 있으면 하기. 인벤토리를 list가 아닌 개수로하거나
Dictionary<ItemType, Type> itemMap = new Dictionary<ItemType, Type>();
ItemManager()
{
Inventory = new Dictionary<string,List<Item>>();
DurationItems = new Dictionary<string, DurationItem>();
itemMap.Add(ItemType.HpPortion,typeof(HpPortion));
itemMap.Add(ItemType.AttackPotion, typeof(AttackPotion));
itemMap.Add(ItemType.ShieldPotion, typeof(ShieldPotion));
itemMap.Add(ItemType.RandomPortion, typeof(RandomPortion));
}
public void Update()
{
if (DurationItems == null)
{
Console.WriteLine("durationItems Null!");
return;
}
if (DurationItems.Count == 0) return;
List<string> RemoveItems = new List<string>();
foreach (var item in DurationItems)
{
item.Value.Duration--;
if(item.Value.Duration <= 0 )
{
item.Value.EndEffect();
RemoveItems.Add(item.Key);
}
}
foreach (var item in RemoveItems)
{
DurationItems.Remove(item);
}
}
#region 인벤토리 관련
public void AddItem(Item item)
{
if(Inventory.ContainsKey(item.Name)==false)
{
Inventory[item.Name] = new List<Item>();
}
Inventory[item.Name].Add(item);
}
public Item? RemoveItem(string itemName)
{
Item? it;
if(Inventory.ContainsKey(itemName) )
{
it = Inventory[itemName][0];
Inventory[itemName].RemoveAt(0);
if (Inventory[itemName].Count()==0)
Inventory.Remove(itemName);
}
else
{
Console.WriteLine($"Error! 없는 아이템 사용!{itemName}");
it = null;
}
return it;
}
public void PrintInventory()
{
Console.WriteLine("------인벤토리------");
foreach (var item in Inventory)
{
Console.WriteLine($"{item.Key} : {item.Value.Count}개");
}
}
#endregion
#region 사용버프아이템 관련
public void RegistItem(Item item)
{
DurationItem? di = item as DurationItem;
if (di == null)
{
Console.WriteLine("버프아이템 등록오류 : RegistItem");
return;
}
DurationItems[di.Name] = di;
}
public void PrintDurationItemList()
{
foreach(var item in DurationItems)
{
Console.WriteLine($"{item.Key} : {item.Value.Duration}턴 남음");
}
}
#endregion
public Item RandomCreateItem()
{
Random random = new Random();
ItemType randomType = (ItemType)random.Next(Enum.GetValues(typeof(ItemType)).Length);
Item item = (Item)Activator.CreateInstance(itemMap[randomType])!;
AddItem(item);
return item;
}
}
}
아이템 매니저 또한싱글톤으로 만들었다.
우선 Inventory 와 DurationItems 이다.
Inventory 는 string, List<Item> 으로 해당 이름을 키로하여 개수만큼 List로 저장했다.
string,int 처럼 개수만 저장하고 사용효과같은건 따로 불러오는 방식을생각했지만 어떻게구현해야할 지 감이안와서
쉬운방법인 List를 사용했다.
그리고 개수만 저장하면 나중에 장비아이템의 경우 고쳐야하기 때문에 지금 상황에서는 int가 더 좋지만 결국 확장성을 생각하면 list로 구현하는게 나을 것 같기도하다. 아니면 각 아이템에 아이디를 부여해서 아이디만 리스트로 저장하고 아이템데이터베이스 같은 것을 구축하는 것도 방법일 것 같다.
인벤토리의 기능은 목록을 보여주고 인벤토리에 추가, 삭제 기능이 있다.
DurationItems는 지속되는 버프효과를 위한 자료구조이다.
<string, DurationItem> 으로 버프아이템만 저장되게 하였다.
턴이 끝날 때마다 Update함수를 호출하면 for문을 통해 등록되어있는 물약의 턴수가 감소한다. 그리고 턴이 0이되면
목록에서 지우고 EndEffect 함수를 호출하는 방식이다.
Item.cs에 있는 하드코딩된 아이템설명이나 이름 등은 따로 텍스트관리매니저처럼 관리해도 좋을 것 같고, 아이템도 아이디기반으로 클라이언트에서 관리하고 데이터베이스를 이용하는게 효과적일 것 같기도하다.
앞으로 여러차례 개선해 나가야겠다.