본문 바로가기
스파르타 내배캠

스파르타 내배캠 Unity 3기 13일차

by LemongO 2024. 1. 10.

오늘은 야식으로 불닭 ㅋ

 

 

 

 

팀 프로젝트 기간에 들어왔고 내가 맡은 부분은 [전투 시작] 부분이었다.

요구사항을 찬찬히 보니 몬스터가 등장하는 부분이 있었고 기존 개인과제에서는 없었기에

몬스터를 추가하는 부분을 써보기로 했다.

 


Monster Class

몬스터의 정보를 총괄하는 Monster class 를 새로 만들어 보자.

 

  •  몬스터가 가지고 있어야 하는 정보
    1.  이름
    2.  레벨
    3.  체력
    4.  공격력
    5.  사망여부

먼저 내부에서 정보를 저장할 private 변수들을 생성해준다.

다만 몇 가지 차이점이 있다.

 

몬스터가 가지고 있는 정보 중 처음 생성 후, 변하지 않는 변수 [이름, 레벨, 공격력] 이 셋은 readonly 로 선언한다.

private readonly string  _name;        
private readonly int     _lv;
private readonly int     _atk;

 

공격을 받을 때 마다 변하는 [체력] 은 일반 private 변수로 선언한다.

private float   _hp;

 

[사망여부]를 알 수 있는 bool 변수는 public 프로퍼티 get; private set; 하나로 만든다.

public bool     IsDead  { get; private set; } = false;

 

나머지 네 변수도 외부에서 볼 수 있는 public 프로퍼티로 private 변수를 get 하지만

[이름, 레벨, 공격력]은 set 하지 않으므로 람다로 get 기능만 허용토록 한다.

public string   Name    => _name;
public int      Lv      => _lv;      
public int      Atk     => _atk;

 

Hp 프로퍼티는 런타임동안 몬스터가 데미지를 받으면 계속해서 변한다.

게다가 체력이 0 이하로 떨어지면 사망 상태가 되기에 프로퍼티의 set 부분에서 사망하도록 조건문을 쓰도록 하자.

public float    Hp 
{
    get => _hp;
    private set
    {
        _hp = value;
        if(Hp <= 0)
        {
            _hp = 0;
            IsDead = true;
        }
    }
}

 

그러나 이 Hp를 아무데서나 set 시키면 안 되니까 private으로 선언하고,

데미지를 받는 부분을 GetDamage 메서드를 통해 수행하도록 하자.

 

그런데 GetDamage는 몬스터 뿐만 아니라 플레이어 및 기타 데미지를 받을 수 있는 모든 객체의 공통사항이다.

그러니 GetDamage 메서드를 가지는 [ IDamagable ] interface를 생성하도록 하자.

// 데미지를 받을 수 있는 객체에게 상속할 인터페이스
internal interface IDamagable
{
    public void GetDamage(float damage);
}

 

그 후, Monster 클래스에서 상속 받고 해당 기능을 구현하자.

internal class Monster : IDamagable
{
    // 생성 시 할당 이후 변화 없는 변수 name, lv, atk 는 readonly로 선언.
    private readonly string  _name;        
    private readonly int     _lv;
    private readonly int     _atk;        

    public string   Name    => _name;
    public int      Lv      => _lv;      
    public int      Atk     => _atk;

    private float   _hp;
    /// <summary>
    /// get 시, Hp 반환
    /// set 시, Hp = value 값 및 0 이하 시 Hp = 0, Dead 메서드 호출
    /// </summary>
    public float    Hp 
    {
        get => _hp;
        private set
        {
            _hp = value;
            if(Hp <= 0)
            {
                _hp = 0;
                IsDead = true;
            }
        }
    }

    public bool     IsDead  { get; private set; } = false;

    public void GetDamage(float damage) => Hp -= damage;
}

 

그리고 몬스터를 어떤 방식으로 구별할지 고민하다가 enum 으로 구별하는것으로 결정했다.

Define 클래스에서 모든 enum을 담당하고 있었으므로 이곳에 추가 해주도록했다.

public enum MonsterType
{
    SkeletonWorrior,
    SkeletonArcher,
    SkeletonWizard,
    GoblinWorrior,
    GoblinArcher,
    GoblinWizard,
}

 

그리고 Monster 생성자를 통해 해당 타입에 따라 [이름, 데미지, 체력] 을 달리 해야한다.

데미지와 체력은 고정값으로 하기위해 Monster 클래스 내부에 readonly int[], float[] 를 사용해 할당 해주기로 했다.

레벨은 랜덤한 값을 받기위해 생성자의 매개변수로 할당하도록 하자.

private Define.MonsterType _type;
private readonly int[]      ATKs    = [5, 10, 15, 7, 12, 18];
private readonly float[]    HPs     = [15f, 10f, 5f, 20f, 15f, 10f];

public Monster(int lv, Define.MonsterType type)
{
    _lv     = lv;
    _atk    = ATKs[(int)type];
    _hp     = HPs[(int)type];
    _type   = type;
}

※ 공격력을 int / 체력을 float 로 설정했는데, 지금 생각해보니 반대로 해야 하는게 맞는거 같다. 내일 수정해야겠다.

 

이름은 한글로 표현하기 위해 switch문을 사용하였다.

private Define.MonsterType _type;
// 타입에 따라 부여할 공격력/체력 (고정값)
private readonly int[]      ATKs    = [5, 10, 15, 7, 12, 18];
private readonly float[]    HPs     = [15f, 10f, 5f, 20f, 15f, 10f];

public Monster(int lv, Define.MonsterType type)
{
    _lv     = lv;
    _atk    = ATKs[(int)type];
    _hp     = HPs[(int)type];
    _type   = type;

    // 몬스터 타입에 따라 이름 할당.
    switch (_type)
    {
        case Define.MonsterType.SkeletonWorrior:
            _name = $"Lv.{Lv}  스켈레톤 전사";
            break;
        case Define.MonsterType.SkeletonArcher:
            _name = $"Lv.{Lv}  스켈레톤 궁수";
            break;
        case Define.MonsterType.SkeletonWizard:
            _name = $"Lv.{Lv}  스켈레톤 마법사";
            break;
        case Define.MonsterType.GoblinWorrior:
            _name = $"Lv.{Lv}  고블린 전사";
            break;
        case Define.MonsterType.GoblinArcher:
            _name = $"Lv.{Lv}  고블린 궁수";
            break;
        case Define.MonsterType.GoblinWizard:
            _name = $"Lv.{Lv}  고블린 마법사";
            break;
    }            
}

 

마지막으로 인 게임 상에서 보여줄 몬스터의 정보들은 ShowText 메서드를 사용하였고,

이를 각 메뉴에 따라 달리 보여야 했기에 오버로드를 하여 사용처를 두 개로 분할하였다.

// 인 게임 상에서 출력할 Text 함수, 오버로딩으로 전투씬 전,후 구별 사용
public void ShowText()
{
    if (IsDead)
    {
        Console.ForegroundColor = ConsoleColor.DarkGray;
        Console.WriteLine($"{Name}  \tDead");
        Console.ResetColor();
    }                
    else
        Console.WriteLine($"{Name}  \tHP {Hp}");
}
public void ShowText(int index)
{
    if (IsDead)
    {
        Console.ForegroundColor = ConsoleColor.DarkGray;
        Console.WriteLine($"({index}) {Name}  \tDead");
        Console.ResetColor();
        return;
    }
    Console.WriteLine($"({index}) {Name}  \tHP {Hp}");
}

 

아래는 Monster 클래스의 전체 코드이다.

internal class Monster : IDamagable
{
    // 생성 시 할당 이후 변화 없는 변수 name, lv, atk 는 readonly로 선언.
    private readonly string  _name;        
    private readonly int     _lv;
    private readonly int     _atk;        

    public string   Name    => _name;
    public int      Lv      => _lv;      
    public int      Atk     => _atk;

    private float   _hp;
    /// <summary>
    /// get 시, Hp 반환
    /// set 시, Hp = value 값 및 0 이하 시 Hp = 0, Dead 메서드 호출
    /// </summary>
    public float    Hp 
    {
        get => _hp;
        private set
        {
            _hp = value;
            if(Hp <= 0)
            {
                _hp = 0;
                IsDead = true;
            }
        }
    }

    public bool     IsDead  { get; private set; } = false;

    private Define.MonsterType _type;
    // 타입에 따라 부여할 공격력/체력 (고정값)
    private readonly int[]      ATKs    = [5, 10, 15, 7, 12, 18];
    private readonly float[]    HPs     = [15f, 10f, 5f, 20f, 15f, 10f];
}

 

 

이제 몬스터를 인게임 상에서 만들어 주도록한다.

몬스터가 생성되는 시기는 던전에서 전투를 시작했을 때 이다.

 

  • 몬스터가 생성되기 전 미리 정해야 하는 값
    1. 스폰 되는 몬스터의 수
    2. 레벨
    3. 몬스터 타입

몬스터의 수는 Random().Next() 를 사용하여 임의적으로 1~4마리가 스폰되도록 설정하고

int spawnCount 지역변수에 할당한다.

 

그리고 몬스터들의 정보를 담을 배열 Monster[] monsters 도 만들도록 한다.

 

만들어진 배열에 새로운 몬스터들을 생성한다. 이 때, 각 개체마다 레벨과 타입이 다르므로 

for문 내부에 랜덤 레벨 및 타입을 선언 후 몬스터를 생성한다.

레벨은 임의적으로 lv 1~5 까지 범위로 설정한다.

int spawnCount = new Random().Next(1, 5);
Monster[] monsters = new Monster[spawnCount];
for (int i = 0; i < monsters.Length; i++)
{
    int randLv = new Random().Next(1, 6);
    int randomType = new Random().Next(0, Enum.GetValues(typeof(MonsterType)).Length);

    monsters[i] = new Monster(randLv, (MonsterType)randomType);                
}

 

그 후 Monster 클래스에서 만들어 둔 ShowText 메서드를 이용해 몬스터들의 정보를 콘솔창에 출력 해주도록 하자.

for (int i = 0; i < monsters.Length; i++)
    monsters[i].ShowText();

오버로드로 추가한 기능은 공격할 대상 선택 메뉴 진입시에 보여주는 것이다. 다음과 같이 보이게 된다.

for (int i = 0; i < monsters.Length; i++)
    monsters[i].ShowText(i + 1);