엄청나게 많은 양이 될것 같은 이번주 개인과제 코드리뷰를 진행한다.
Console로 진행하는 TEXT RPG를 제작하시오.
자 일단 이게 RPG이긴 한데, 이번주 구현 내용은 실제 플레이 내용은 존재하지 않는다. 다만 스텟, 아이템, 인벤토리, 던전(자동) 등 다양한 기능을 구현하는것이 목적이였다.
우선 Console RPG Github 의 Readme에 약간이라도 적어놨으니, 구조는 이를 참고하면서 보면 된다. 그러면 하나하나 보며 진행하도록 하겠다.
실제 구동이 되는 Main함수가 존재하지만, 구현 내용은 별로 없다. 게임 내용을 GameManager에서 관리를 전부 해 주기 때문에, Main에서는 게임이 구동되기 위한 GameManger만 호출해주면 할일이 끝난다.
아이템에 대한 정보를 저장하는 struct가 포함되어있다. Enum을 활용하여 아이템의 타입(ItemType)과, 오르는 스텟(StatsType)에 관한 타입을 만들어주었다.
Item은 struct구조이다. (왜냐하면 Item내부의 스킬같은 것이 없으니, 실제로는 값들만 저장하기 때문이다.)
그 후 생성자 하나가 존재하고, 다음과 같이 카테고리가 존재한다.
public static readonly Item[] ITEM_CATEGORY
이는 struct 내부에서 선언해준 딕셔너리로, 앞으로 아이템들을 매번 스텟을 새로 만들지 않고, ID를 통해 정보를 여기서 끌어올 것이다. (아직 강화요소가 없어서 가능하다.)
상점 Class이다. 상점에는 상점에 있을 아이템을 위한 인벤토리, 그리고 아이템 구매 위주로 작업 해 주었다.
자동던전이다. 실제로는 데이터만 저장하므로 Struct를 활용하였다. 각각의 정보를 사용하고, 위의 ITEM_CATEGORY처럼 이곳에서 public static readonly AUTODUNGEON_CATEGORY를 생성해 주었다.
엔티티의 특성을 가지는 모든 곳에 사용할 Abstract Class이다. 지금은 Player밖에 없지만, 나중 적 구현을 생각하고 미리 참조할 Abstract Parent Class를 만들어 주어 사용하였다.
이름, 레벨, 공격력, 방어력, 최대체력, 최소체력과 골드를 기본으로 가진다.
실제 게임 요소의 핵심인 Player의 정보를 저장하는 클래스이다. Entity를 상속하여 사용하며, 몇가지 추가 메서드를 구현하였다.
<변수>
<메서드>
readonly static string path = "PlayerData.dat";
//Data Management
public bool TryRead()
{
if (!File.Exists(path))
return false;
return ReadData();
}
//Data Management
public void SaveData()
{
if(File.Exists(path))
{ File.Delete(path); }
FileStream fs = new FileStream(path, FileMode.Create);
StreamWriter writer = new StreamWriter(fs);
writer.WriteLine(this.name);
writer.WriteLine(this.playerClassT);
writer.WriteLine(this.level);
writer.WriteLine(this.atk);
writer.WriteLine(this.def);
writer.WriteLine(this.maxHp);
writer.WriteLine(this.curHp);
writer.WriteLine(this.gold);
writer.WriteLine("Inventory");
for(int i = 0; i < Inventory.Count; i++)
writer.WriteLine(Inventory[i].Item1.ToString() + " " + Inventory[i].Item2.ToString());
writer.WriteLine("\\Inventory");
writer.WriteLine("EquippedItems");
for (int i = 0; i < EquippedItems.Count; i++)
writer.WriteLine(EquippedItems.ElementAt(i).Value.ToString());
writer.WriteLine("\\EquippedItems");
writer.WriteLine("PlayerItemStats");
for (int i = 0; i < PlayerItemStats.Count; i++)
writer.WriteLine(PlayerItemStats.ElementAt(i).Value.ToString());
writer.WriteLine("\\PlayerItemStats");
writer.WriteLine(this.ClearedLevels);
writer.Close();
}
//Data Management
public bool ReadData()
{
if (!File.Exists(path))
return false;
FileStream fs = new FileStream(path, FileMode.Open ,FileAccess.Read);
StreamReader reader = new StreamReader(fs);
this.name = reader.ReadLine();
Enum.TryParse(reader.ReadLine(), out PlayerClassType Type);
this.playerClassT = Type;
this.level = int.Parse(reader.ReadLine());
this.atk = float.Parse(reader.ReadLine());
this.def = int.Parse(reader.ReadLine());
this.maxHp = int.Parse(reader.ReadLine());
this.curHp = int.Parse(reader.ReadLine());
this.gold = int.Parse(reader.ReadLine());
if (reader.ReadLine() != "Inventory")
return false;
while (true)
{
string t = reader.ReadLine();
if (t == "\\Inventory")
break;
string[] items = t.Split(' ');
Inventory.Add((int.Parse(items[0]), bool.Parse(items[1])));
}
if (reader.ReadLine() != "EquippedItems")
return false;
for (int i = 0; i < EquippedItems.Count; i++)
{
string t = reader.ReadLine();
if (t == "")
EquippedItems[EquippedItems.ElementAt(i).Key] = null;
else
EquippedItems[EquippedItems.ElementAt(i).Key] = int.Parse(t);
}
if (reader.ReadLine() != "\\EquippedItems")
return false;
if(reader.ReadLine() != "PlayerItemStats")
return false;
for (int i = 0; i < PlayerItemStats.Count; i++)
{
string t = reader.ReadLine();
EquippedItems[EquippedItems.ElementAt(i).Key] = int.Parse(t);
}
if (reader.ReadLine() != "\\PlayerItemStats")
return false;
this.ClearedLevels = int.Parse(reader.ReadLine());
reader.Close();
return true;
}
//DataManagement
public void ResetPlayer()
{
if (!File.Exists(path))
return;
File.Delete(path);
}
뭔가 엄청 긴데... 노가다 해서 데이터를 하나씩 넣어줘서 그렇다.
이때 FileStream과 C#에 있는 StreamReader, StreamWriter을 활용하였다.
StreamReader, StreamWriter은 FileStream을 받아 데이터를 읽어주는 역활을 한다.
private void _init()
{
this.level = 1;
this.atk = 10.0f;
this.def = 5;
this.maxHp = 100;
this.curHp = this.maxHp;
this.gold = 1500;
if (GameManager.IsDebug)
this.gold = 10000;
Inventory = new List<(int itemID, bool isEquipped)>();
PlayerItemStats = new Dictionary<StatsType, int>
{
{StatsType.Attack, 0 },
{StatsType.Defense, 0},
{StatsType.HealthPoint, 0}
};
_SetInitStats();
}
private void _SetInitStats()
{
PlayerItemStats[StatsType.Attack] = 0;
PlayerItemStats[StatsType.Defense] = 0;
PlayerItemStats[StatsType.HealthPoint] = 0;
foreach (int? index in EquippedItems.Values)
{
if (index == null)
continue;
foreach ((StatsType, int) t in Item.ITEM_CATEGORY[Inventory[(int)index].Item1].ItemStats)
{
if (t.Item2 == 0)
continue;
PlayerItemStats[t.Item1] += t.Item2;
}
}
//Set Current Hp not larger than MaxHp + Item;
if (this.curHp > this.maxHp + PlayerItemStats[StatsType.HealthPoint])
this.curHp = this.maxHp + PlayerItemStats[StatsType.HealthPoint];
}
이는 플레이어 생성 시 초기화를 해주는 생성자 외 _init을 따로 구현한 내용들이다. _SetInitStats는 아이템들의 정보를 받아 아이템 스텟을 다시 계신해준다.
너무 길어져서 다른거 눈여겨볼 것들은... SetItemEquipped에서의 아이템 처리정도가 될것이다.
아이템의 장착 상태에 따라 업데이트 해주고, 반드시 인벤토리가 줄어드므로, Equipped ITem에서 인덱스 관리를 추가로 해 줘야 하는점이 눈여겨 볼 점이다.
다른 메서들들은 보면 간단하지만, 출력이 대다수고, 체력을 조정하는 GainGold와 GainHeal, 그리고 아이템 추가하는 AddItem과 판매하는 SellItem이 있다.
오늘은 늦어져서 여기까지 TIL작성하도록 한다.