C#/C# 미니실습

20230829 - 일지

temp-franc 2023. 8. 30. 23:26

과제 : 콘솔로 텍스트 게임 만들기.

 

1. 몬스터, 아이템의 정보를 .csv 파일로 따로 관리하기

 

  랜덤하게 몬스터를 젠하는 메서드를 구현해야 했다.

  이전까지는 일일히 객체의 정보를 입력하여 관리하였는데, 이번에는 외부 파일로 정보를 관리하는 것이 더 효율적일 것 같아서, 시도해보았다. 

 

 A. 현재 파일의 디렉토리 얻기

 

검색을 통해 다음과 같이 구현하였다.

public static string localPath = Directory.GetParent(Path.GetFullPath(@"..\Data\MonsterData.csv")).Parent.Parent.Parent.Parent.ToString();

 

Path.GetFullPath

 : 절대 경로 정보를 string 타입으로 반환

 

public static string GetFullPath (string path);

 

static void Main(string[] args)
{
    var fullPath = Path.GetFullPath(@"..");
    Console.WriteLine($"fullPath의 타입 : {fullPath.GetType().Name}");
    Console.WriteLine($"fullPath : {fullPath}");

    var memoPath = Path.GetFullPath(@"..memoPad.cs");
    Console.WriteLine($"fullPath의 타입 : {memoPath.GetType().Name}");
    Console.WriteLine($"fullPath : {memoPath}");
}

>

 

의 위치가 반환되길 기대했는데, bin 폴더 안의 파일의 경로를 반환하였다.

 

일단은, .Parent로 상위폴더로 직접 이동하는 식으로 원하는 경로를 반환하려 하였다.

Path.GetFullPath(@"..\Data\MonsterData.csv").Parent.Parent.Parent.Parent

그런데, Path.GetFullPath(@"..\Data\MonsterData.csv")) 의 반환값은 string 타입이기 때문에, Parent를 써서 상위 디렉토리를 구하려 했는데, 타입이 맞지 않아 에러가 발생했다.

 

Directory 클래스와 DirectoryInfo 클래스

 : Directory 클래스는 정적 메서드를 사용하여 문자열을 반환.

 : DirectoryInfo 클래스는 인스턴스화한 객체를 반환. 이 객체와 메서드를 통하여 디렉토리 관리 가능(ex. Delete())

 

 따라서, DirectoryInfo 클래스로 경로를 인스턴스화한 객체를 반환한 뒤, Parent 와 같은 메서드를 사용하여 Data 폴더를 갖고 있는 디렉토리까지 접근 가능하다.

 

static void Main(string[] args)
{
    DirectoryInfo currentDirInfo = new DirectoryInfo(Directory.GetCurrentDirectory());
    DirectoryInfo parentDirInfo = currentDirInfo.Parent.Parent.Parent.Parent;

    Console.WriteLine($"현재 디렉토리 (DirectoryInfo): {currentDirInfo.FullName}");
    Console.WriteLine($"부모*4 디렉토리 (parentDirInfo): {parentDirInfo.FullName}");
}

>

 

 원하는 디렉토리까지 접근한 후, ToString()으로 문자열로 반환.

 

!! 수정 코드

public class Pathes
{
    public static DirectoryInfo currentPath = new DirectoryInfo(Directory.GetCurrentDirectory());
    public static string localPath = currentPath.Parent.Parent.Parent.Parent.ToString();
    
    //... 생략 ...
}

 

 

B. 각 csv 파일의 Path 얻기

Path.Combine

  : 문자열 배열을 한 경로로 결합.

public static string Combine (params string[] paths);

 

public static string MonsterDataPath()
{
    var dataPath = @"Data";
    var fileName = @"MonsterData.csv";
    var fullPath = Path.Combine(localPath, dataPath, fileName);
    return fullPath;
}

 

 

2. 몬스터의 정보를 클래스에 적용시키기.

 

 A. 클래스와 필드에 대한 고민

 

 작업을 하면서, 몬스터 클래스에 지정한 필드는 다음과 같다.

internal class Monster : Character
{
    public int Level { get; }
    public int Hp { get; set; }
    public int Atk { get; }
    public int Def { get; }
    public int Gold { get; }
    public int Exp { get; }
    public string Name { get; }
    public string Chrd { get; }
    int _critical;

    public Monster(string name, string chrd, int level)
    {
        Name = name;
        Chrd = chrd;
        Level = level;
        Atk = level * 3;
        Def = level * 2;
        Hp = level * 20;
    }
}

 

이렇게 하면, 다양한 종류의 몬스터를 인스턴스화하여도, 같은 레벨이라면, 공격력, 방어력, Hp 가 같아 몬스터의 특색이 없어진다고 생각하였다.

static void Main(string[] args)
{
    Monster StrongRabbit = new Monster("우람한 토끼", "짐승", 3);
    Monster WeakRabbit = new Monster("비실비실한 토끼", "짐승", 3);

    Console.WriteLine($"우람한 토끼의 공격력 : {StrongRabbit.Atk}");
    Console.WriteLine($"비실비실한 토끼의 공격력 : {WeakRabbit.Atk}");

    Console.WriteLine($"우람한 토끼의 방어력 : {StrongRabbit.Def}");
    Console.WriteLine($"비실비실한 토끼의 방어력 : {WeakRabbit.Def}");

    Console.WriteLine($"우람한 토끼의 체력 : {StrongRabbit.Hp}");
    Console.WriteLine($"비실비실한 토끼 체력 : {WeakRabbit.Hp}");
}

>

 

 

그래서,  필드값을 컬럼으로 하여 각 몬스터의 정보를 csv 파일에 저장하고, 불러오려고 했다.

Monster 클래스는 생성자의 파라미터로 level을 받고 있기 때문에, level별로 정보를 csv 파일에 저장하면, 동일 몬스터에 대한 정보가 많아져 관리하기 어렵다고 생각했다.

  => level이 주어지면, 계산가능한 정보를 csv 파일에 저장하기로 하였다. (ex. 공격력 -> 공격계수)

이 때의 컬럼은 Monster 클래스의 필드와 달라지기 때문에, 컬럼과 동일한 필드를 지닌 MonsterInfo 클래스를 만들어 csv 파일의 정보를 MonsterInfo 인스턴스를 요소로 하는 배열로 관리하기로 하였다.

public class MonsterInfo
{
    public string Name { get; }
    public string Chrd { get; }
    public float MonAtkCoeff { get; }       // 공격계수
    public float MonDefCoeff { get; }       // 방어계수
    public int MonHPCoeff { get; }          // HP 계수
    public int Exp { get; }                 // 경험치 계수
    public int StageRank { get; }           // 스테이지별 등장하는 몬스터를 차별화하기 위해 추가한 인덱스
    public int DropGold { get; }            // 드롭하는 골드 계수
    
    public MonsterInfo(string name, string chrd, float monAtkCoeff, float monDefCoeff, int monHPCoeff, int exp, int dropGold, int stageRank)
    {
        Name = name;
        Chrd = chrd;
        MonAtkCoeff = monAtkCoeff;
        MonDefCoeff = monDefCoeff;
        MonHPCoeff = monHPCoeff;
        Exp = exp;
        DropGold = dropGold;
        StageRank = stageRank;
    }
    
    // ... 생략 ...

 

 

그림의 csv 파일을 MonsterInfo 인스턴스를 요소로 하는 배열로 만든 코드는 다음과 같다.

class MonsterInfo
{
    //... 생략...
    
    
    public static MonsterInfo[] GetMonsterInfo()
    {
        MonsterInfo[] AllMonsters = new MonsterInfo[] { };

        string fullPath = Pathes.MonsterDataPath();
        string[] monsterData = File.ReadAllLines(fullPath, Encoding.UTF8);
        string[] fieldNames = monsterData[0].Split(',');           // 컬럼 부분(첫줄)은 MonsterInfo의 필드 이름을 지니고 있음.

        for (int monIdx = 1; monIdx < monsterData.Length; monIdx++)
        {
            string[] monsterEach = monsterData[monIdx].Split(',');

            string name = monsterEach[Array.IndexOf(fieldNames, "Name")];
            string chrd = monsterEach[Array.IndexOf(fieldNames, "Chrd")];
            float monatkcoeff = float.Parse(monsterEach[Array.IndexOf(fieldNames, "MonAtkCoeff")]);
            float mondefcoeff = float.Parse(monsterEach[Array.IndexOf(fieldNames, "MonDefCoeff")]);
            int monHPcoeff = int.Parse(monsterEach[Array.IndexOf(fieldNames, "MonHPCoeff")]);
            int exp = int.Parse(monsterEach[Array.IndexOf(fieldNames, "Exp")]);
            int dropGold = int.Parse(monsterEach[Array.IndexOf(fieldNames, "DropGold")]);
            // 몬스터 스테이지 조정
            int stageRank = int.Parse(monsterEach[Array.IndexOf(fieldNames, "StageRank")]);

            MonsterInfo monsterDict = new MonsterInfo(name, chrd, monatkcoeff, mondefcoeff, monHPcoeff, exp, dropGold, stageRank);

            Array.Resize(ref AllMonsters, AllMonsters.Length + 1);
            AllMonsters[monIdx - 1] = monsterDict;
        }

        return AllMonsters;
    }

 

static 메서드

  : 클래스로부터 객체를 생성하지 않고 직접 호출하는 메서드.

 

Monster 클래스에서 몬스터 인스턴스를 초기화 할 때, MonsterInfo 인스턴스를 요소로 하는 배열에서 해당 Monster의 이름(Monster.Name)을 검색하여, 동일한 이름(MonsterInfo.Name)을 가지고 있는 MonsterInfo를 가지고와, 파라미터로 주어진 level 과의 계산을 수행하여, 필요한 필드값을 입력할 계획

=> 외부 클래스에서 객체 생성 없이 가져와야 함

=> GetMonsterInfo()를 public static 으로 선언.

 

File.ReadAllLines()

  : 텍스트 파일을 열고 파일의 모든 줄을 문자열 배열로 읽어 들인 다음 파일을 닫는 메서드

public static string[] ReadAllLines (string path, System.Text.Encoding encoding);

 

  : Encoding이 맞지 않는 경우, 글자가 깨지는 현상이 발생하여, csv 파일을 UTF-8로 인코딩한 후, 메서드 파라미터에도 파일에 적용된 UTF8 을 입력함.

string fullPath = Pathes.MonsterDataPath();
string[] monsterData = File.ReadAllLines(fullPath, Encoding.UTF8);

 

이후, Monster 클래스에  MonsterInfo.GetMonsterInfo() 메서드를 통하여, MonsterInfo 인스턴스를 요소로 하는 배열을 통해 생성자에서 필드값 초기화를 수행할 수 있게끔 생성자를 수정하였다.

internal class Monster : Character
{
    public int Level { get; }
    public int Exp { get; }
    public int Hp { get; set; }
    public int Atk { get; }
    public int Def { get; }
    public int Gold { get; }
    public string Name { get; }
    public string Chrd { get; }
    int _critical;
    public MonsterInfo[] MonsterInfoArray = MonsterInfo.GetMonsterInfo();
    
    public Monster(string name, string chrd, int level)
    {
        Level = level;
        Name = name;
        Chrd = chrd;
        MonsterInfo[] monsterInfo = MonsterInfoArray.Where(mon => mon.Name == name).ToArray(); 
        Atk = (int)(Level * monsterInfo[0].MonAtkCoeff);
        Def = (int)(Level * monsterInfo[0].MonDefCoeff);
        Hp = Level * monsterInfo[0].MonHPCoeff;
        Exp = Level * monsterInfo[0].Exp;
        Gold = Level * monsterInfo[0].DropGold;
        _critical = 20;
    }
    
    //... 생략....

 

 B. 활용

 

이후, Dungeon 클래스에서 몬스터의 임의 소환 기능을 구현하는 메서드 MonsterGen() 에 활용할 수 있었다.

 

class Dungeon
{
    private int _stage;
    public int Stage
    {
        get { return _stage; }
        set { _stage = value; }
    }
    private int _round;
    public int Round
    {
        get { return _round; }
        set { _round = value; }
    }
    //... 생략 ...
    
    public Monster[] BossMonsterGen()
    {
        Random random = new Random();
        int bossMonsterLevel = random.Next(Stage, Stage + 4);
        
        // MonsterInfo.GetMonsterInfo()를 통해 모든 몬스터의 MonsterInfo를 요소로 하는 배열 생성
        // Where을 통해 MonsterInfo.StageRank 와 현재 Dungeon.Stage를 비교하여 배열 제약
        MonsterInfo[] bossMonsterInfo = (Stage < 4) ? MonsterInfo.GetMonsterInfo().Where(mon => mon.StageRank < 0 && mon.StageRank > -Stage - 1).ToArray() : MonsterInfo.GetMonsterInfo().Where(mon => mon.StageRank == -Stage).ToArray();
        int bossIdx = random.Next(0, bossMonsterInfo.Length);
        
        // MonsterInfo에서 Name, Chrd, 해당 메서드에서 level을 파라미터로 새로운 Monster 클래스의 인스턴스 초기화.
        Monster bossMonster = new Monster(bossMonsterInfo[bossIdx].Name, bossMonsterInfo[bossIdx].Chrd, bossMonsterLevel);
        Monster[] getMonsterArray = new Monster[1];
        getMonsterArray[0] = bossMonster;

        return getMonsterArray;
    }