레트로의 유니티 게임프로그래밍 에센스

유니티 탑다운 슈터 게임 좀비서바이버 개발일지 6일차

막뇌 2023. 8. 21. 13:07

게임상 모든 생명체에게 적용되는 LivingEntity 클래스 만들기

 

이 장에서 진행할 내용

다형성을 사용해 여러 타입을 하나의 타입으로 다루는 과정을 게임내에서 구현합니다.

오버라이드를 사용해서 부모클래스의 맴버를 확장합니다.

이벤트를 사용해서 견고한 커플링을 해소하고 코드를 간결하게 만듭니다.

UI 슬라이더를 사용해봅니다.

게임월드 내부에 UI를 배치해봅니다.

내비게이션 시스템을 이용해서 인공지능을 구현해봅니다.

 

다형성

다형성이란 객체지향 패러다임의 특징중 하나로, 요약하자면 형태에 구애받지 않고 프로그래머가 원하는 '동작'에 집중할 수 있도록 공통된 녀석들을 하나로 묶는 것을 말합니다. 

이전에 가상함수 Virtual을 다루면서 다형성에 대해 기록한 포스팅이 있습니다.

가상함수 Virtual과 오버라이딩 (tistory.com)

 

LivingEntity 클래스

게임상에 살아움직이는 물체에 적용시킬 부모 클래스가 되겠습니다.

플레이어 캐릭터를 포함해서 적 AI 들은 공통 기능이 필요합니다.

- 체력을 가질것

- 체력 회복이 가능할 것

- 공격 받는것이 가능할것

- 살아있거나 죽거나 상태가 2개 있을것

 

public class LivingEntity : MonoBehaviour, IDamageable

인터페이스 IDamageable을 상속하네요.

protected virtual void OnEnable() {
        // 사망하지 않은 상태로 시작
        dead = false;
        // 체력을 시작 체력으로 초기화
        health = startingHealth;
    }

OnEnable 입니다. 다른 메서드들과는 다르게 protected 예약어로 자식들에게만 공유하고 있네요.

생명 주기를 담당하는 매서드는 외부에서 호출되어 의도치 않게 사용되는것을 막고자 한것으로 보입니다.

결과적으로 자식 클래스에서만 OnEnable 메서드를 재정의 할 수 있습니댜.

 

    public float startingHealth = 100f; // 시작 체력
    public float health { get; protected set; } // 현재 체력
    public bool dead { get; protected set; } // 사망 상태
    public event Action onDeath; // 사망시 발동할 이벤트

사용될 필드 입니다. health와 dead 는 역시 생명 주기에 관한 내용이므로 protected set 프로퍼티가 사용 되었습니다.

이 값을 수정하는 것은 자식클래스에서만 가능합니다.

 

Action 타입

 

action타입 은 입출력이 없는 메서드를 가리킬 수 있는 델리게이트 입니다.(delegate)

 

public class AB : MonoBehaviour
{
    Action actionObject;

    private void Start()
    {
        actionObject += A;
        actionObject += B;
    }

    private void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            actionObject();
        }
    }

    void A()
    {
        Debug.Log("A");
    }

    void B()
    {
        Debug.Log("B");
    }
}

actionObject는 Action타입의 필드입니다.

여기에 += 연산자를 사용해서 메소드 A와 B를 더해준 결과

actionObject()가 실행되면 A가 실행되고, 추가로 B가 실행되는 결과가 나오게 됩니다.

메소드 끼리 더할 수 있는 일종의 연산자 오버로딩이 되어있다고 봐야죠

메소드를 더할 때는 괄호를 붙이지 않고 이름만 써 주면 됩니다.

 

이벤트 (event)

이벤트는 도미노와 같이 연쇄적인 반응을 이끌어 낼 수 있는 트리거(Trigger) 입니다.

이벤트 자체는 그냥 발생할 뿐이지 어떤 동작을 실행하지 않습니다.

 

이벤트를 사용하게 되면 어떤 클래스에서 특정 사건(예: A메소드가 실행되었다) 이 발생 했을 때, 다른 클래스에서 그것을 감지하고 관련된 처리가 가능합니다.

이벤트 구현시에 필요한 요소를 2가지로 구분할 수 있는데, 이벤트 자체와, 이벤트 리스너(Event Listener)입니다.

언어 그대로 이해하면 됩니다. 이벤트리스너는 이벤트를 구독하고 있다가 이벤트가 발동하면 이벤트에 등록된 메서드가 모두 실행됩니다.

여기서 이벤트의 발동을 invoke 라고 합니다. 또, 이벤트를 담고있는 상자를 이벤트 컨테이너라고 합니다.

 

 

그래서 Action 이 왜 필요한가요?

Action은 견고한 커플링을 해소할 수 있습니다.

견고한 커플링이 뭔가요?

견고한 커플링은 클래스의 '구현'에 다른 클래스가 강하게 결합된 구조를 말합니다.

흔히 '하드 코딩' 이란 언어를 일상생활에서 한번쯤은 들어 보았을 것입니다. 여기에는 어느정도 같은 의미가 존재한다고 할 수 있겠네요.

견고한 커플링은 구현하기는 간단하지만 코드의 유지보수가 어렵다는 단점이 있습니다.

public class PlayerPoint : MonoBehaviour
{
    public ScoreBoard scoreBoard;
    public void Kill()
    {
        //킬 처리에 관한 메소드
        scoreBoard.UpdateBoard();
    }
}

public class ScoreBoard : MonoBehaviour
{
    public void UpdateBoard()
    {
        Debug.Log("점수판 업데이트");
    }
}

견고한 커플링의 예시입니다.

class가 두개 있죠. Player의 점수를 관리하는 class와 전체 점수를 관리하는 ScoreBoard 가 있다고 가정하겠습니다.

PlayerPoint 클래스에서는 Player에 관한것만 처리하면 그만입니다만, ScoreBoard 객체를 따로 생성하여 직접 사용하고 있습니다.

이때, ScoreBoard 에 유지보수가 발생하여 메서드이름이 변경된다거나 하면 PlayerPoint 까지 수정해야 하므로 유연하게 유지보수가 가능하다고 말하기 어렵습니다.

 

이를 이벤트와 Action 사용으로 변경하면 아래와 같이 수정 가능합니다.

public class PlayerPoint : MonoBehaviour
{
    public Action onKill;
    public void Kill()
    {
        //킬 처리에 관한 메소드
        onKill();
    }
}

public class ScoreBoard : MonoBehaviour
{
    private void Start()
    {
        PlayerPoint playerPoint = FindObjectOfType<PlayerPoint>();
        playerPoint.onKill += UpdateBoard;
    }
    public void UpdateBoard()
    {
        Debug.Log("점수판 업데이트");
    }
}

Action 타입의 onKill 자체가 이벤트 입니다. 이벤트가 발동하면서 연결된 이벤트 리스너 ScoreBoard 에서 UpdateBoard() 가 추가 된 onKill이 실행됩니다.

 

이벤트를 설치한 것 만으로 UpdateBoard 에 관련된 메소드를 외부에서 처리핸들링 않고 당사자 쪽으로 넘겼습니다.

이런식으로 견고한 커플링 문제를 해결한 것입니다.

 

event 예약어 사용

Action 객체를 선언하기 전에 event 예약어를 사용하면 재미있는 일이 벌어집니다.

public class PlayerPoint : MonoBehaviour
{
    public event Action onKill;
    public void Kill()
    {
        //킬 처리에 관한 메소드
        onKill();
    }
}

public class ScoreBoard : MonoBehaviour
{
    private void Start()
    {
        PlayerPoint playerPoint = FindObjectOfType<PlayerPoint>();
        playerPoint.onKill += UpdateBoard;
        //playerPoint.onKill();
    }
    public void UpdateBoard()
    {
        Debug.Log("점수판 업데이트");
    }
}

event를 추가 해준 모습입니다.

주석처리를 지워보면 컴파일 에러가 발생하는 것을 볼 수 있습니다.

그상태에서 event를 잠깐 지워보면 컴파일에러가 사라집니다.

event 를 사용했기에 이벤트가 이벤트 컨테이너에서 사용될 때는 예외이고 외부에서 사용시

+= 또는 -+ 의 왼쪽에만 사용할 수 있다고 나오죠.

즉, 이벤트에 등록된 메서드를 추가하거나 제거할 수는 있지만, 이벤트자체를 발동(invoke)시키는 것은 이벤트 컨테이너만이 할 수 있게 되는 것입니다.

 

base

virtual로 선언한 기반 클래스의 메서드를 파생클래스에서 다시 구현할 때, override 예약어를 사용하게 되는데요.

여기서 기반클래스 메소드를 그대로 사용하고자 할 때, base를 사용합니다.

public override void Die()

{

base.Die();

}

이렇게 하면 기반 클래스의 Die를 그대로 사용하겠다는 이야기이죠.