유니티/게임만들기

16장. 좀비 서바이벌(Action, base, 오버라이드, 플레이어 체력 UI 슬라이더)

MJ_119 2024. 4. 16. 01:13

- 다형성을 사용해 여러 타입을 하나의 타입으로 다루기

- 오버라이드를 사용해 부모 클래스의 기존 메서드 확장하기

- 이벤트를 사용해 견고한 커플링을 해소하고 코드를 간결하게 만들기

- UI 슬라이더 사용하기

- 게임 월드 내부에 UI 배치하기

- 내비게이션 시스템을 사용해 인공지능 구현하기

 

@ 다형성

- 이 장에서는 적 AI인 좀비와 플레이어 캐릭터를 포함하여 생명체로 동작할 모든 클래스가 공유하는 기반 클래스 LivingEntity를 사용함.

- 생명체로 동작할 모든 클래스는 LivingEntity 클래스를 상속하고 그 위에 자신만의 기능을 추가함.

 - LivingEntity를 상속한 자식 클래스는 LivingEntity의 구현을 재사용할 수 있을 뿐만 아니라 LivingEntity타입으로 취급될 수 있음.

 - C#에서 다형성은 자식 클래스 타입을 부모 클래스 타입으로 다룰 수 있게 함.

 - 자식 클래스는 부모 클래스의 필드와 메서드를 가지고 있음.

 

- FindObjectsOfType () 메서드는 씬에서 명시한 타입의 모든 오브젝트를 찾아 배열로 반환함.

 

@ 오버라이드(override)

 - 같은 이름의 메서드가 서로 다른 방식으로 동작하게 할 수 있음.

 - 부모 클래스에서 작성한 메서드를 자식 클래스에서 재정의 하는것.

 

 - virtual 키워드로 지정된 메서드는 가상 메서드가 됨, 가상 메서드는 자식 클래스가 오버라이드를 할 수 있게 허용됨.

 

- base.Attack(); => 부모 클래스인 Monster의 Attack() 메서드를 실행함. base 키워드는 부모 클래스를 지칭하며, base를 사용해 오버라이드가 되기 전의 원형 메서드로 접근할 수 있음.

 

 - base.Attack(); 을 사용하지 않았다면 Orc에서 Moster의 Attack() 메서드를 확장하는 것이 아니라 바닥부터 새롭게 만드는 것이 됨.

 

 

Monster[] monsters = FindObjectsOfType<Monster>();
for( int i = 0; i < monsters.Length; i++)
{
	monsters[i].Attack();
}

 -> Attack() 메서드로 여러타입의 몬스터가 동시에 공격을 실행하되 각자 공격 대사는 다르게 출력하는 예시.

 

@ LivingEntity 기반 클래스 

 - 공통기능 : 체력, 체력회복, 공격을 받을 수 있음, 살거나 죽을 수 있음

using System;
using UnityEngine;

// 생명체로서 동작할 게임 오브젝트들을 위한 뼈대를 제공
// 체력, 데미지 받아들이기, 사망 기능, 사망 이벤트를 제공
public class LivingEntity : MonoBehaviour, IDamageable {
    public float startingHealth = 100f; // 시작 체력
    public float health { get; protected set; } // 현재 체력
    public bool dead { get; protected set; } // 사망 상태
    public event Action onDeath; // 사망시 발동할 이벤트

    // 생명체가 활성화될때 상태를 리셋
    protected virtual void OnEnable() {
        // 사망하지 않은 상태로 시작
        dead = false;
        // 체력을 시작 체력으로 초기화
        health = startingHealth;
    }

    // 데미지를 입는 기능
    public virtual void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal) {
        // 데미지만큼 체력 감소
        health -= damage;

        // 체력이 0 이하 && 아직 죽지 않았다면 사망 처리 실행
        if (health <= 0 && !dead)
        {
            Die();
        }
    }

    // 체력을 회복하는 기능
    public virtual void RestoreHealth(float newHealth) {
        if (dead)
        {
            // 이미 사망한 경우 체력을 회복할 수 없음
            return;
        }

        // 체력 추가
        health += newHealth;
    }

    // 사망 처리
    public virtual void Die() {
        // onDeath 이벤트에 등록된 메서드가 있다면 실행
        if (onDeath != null)
        {
            onDeath();
        }

        // 사망 상태를 참으로 변경
        dead = true;
    }
}

 

 - IDamageable을 상속하므로 OnDamage() 메서드를 반드시 구현해야 함.

 

 - protected set : 클래스 외부에서는 접근 불가능하지만 자식 클래스에서는 접근 가능.

 

 - Action 타입은 입력과 출력이 없는 메서드를 가리킬 수 있는 델리게이트(delegate)임. 델리게이트는 '대리자'로 번역되며 메서드를 값으로 할당받을 수 있는 타입임.

 

 - Action, Func 공부 참고

https://www.youtube.com/watch?v=7H3MHXfFkhI

 

 

@ OnEnable()

 : 생명체의 상태를 리셋. 사망 상태를 false, 체력을 시작 체력값으로 초기화함.

 - virtual로 가상메서드이므로 자식 클래스에서 확장 가능.

 - 자식클래스에서 OnEnable() 메서드를 접근 가능해야 확장이 가능해서 접근자를 private 말고 protected를 사용함.

 

 

 

@ 플레이어 체력 UI

 - 체력은 원형 슬라이더로 플레이어 캐릭터의 몸체에 표시

 

 - UGUI의 캔버스는 게임 화면을 기준으로 UI를 배치함. 하지만 체력 슬라이더는 3D 공간에서 캐릭터를 따라다녀야 함.

 그러므로 캔버스 컴포넌트의 렌더러 모드를 전역공간(world space)으로 변경.

 이 경우 캔버스와 그 위의 UI 게임 오브젝트들은 3D 게임 월드에 배치되며, 캔버스 게임 오브젝트는 일반적인 오브젝트처럼 게임 월드 상의 위치, 회전, 크기를 가지게 됨.

 

- 단위당 레퍼런스 픽셀(Reference Pixels per Unit) : UI 스프라이트의 픽셀 크기와 게임 월드의 유닛 크기가 대응되는 비율을 결정함. 이 값은 UI의 스프라이트 화질에 영향을 줌.

 

 

- 하이어라키창에서 캔버스를 캐릭터아래에 넣고 위치와 로테이션값, 레퍼런스 픽셀을 변경해줌. 그러면 캐릭터 아래에 캔버스가 위치하는것을 확인할 수 있음.

 

@ Handle Slide Area 오브젝트 : 슬라이더의 손잡이를 그림. 필요없으면 삭제할 것

 

 

 

- 앵커 프리셋을 사용해 슬라이더와 슬라이더의 배경을 그리는 UI 오브젝트의 크기를 캔버스에 맞춰 잡아 늘림.

 - Alt를 누른 상태로 우측 하단의 stretch 클릭.

 

 

- 슬라이더의 이름을 알맞게 변경해주고, 위와 같이 값들을 체크 및 변경해준다.

- UGUI에서 Interactable UI 컴포넌트는 상호작용이 가능한 필드를 가짐. Interactable이 체크된 경우 사용자가 클릭이나 드래그 등을 이용해 UI 오브젝트와 상호작용할 수 있음. 체크를 해제해야 사용자가 슬라이더를 움직일 수 없음.

 

- Transition : UI와 상호작용 시 일어나는 시각 피드백을 설정.

예를들어 Color Tint이면 UI요소에 마우스를 가져다 대거나 클릭하면 색이나 투명도가 잠시 변함.

 

 

 - 백그라운드를 선택 후 소스 이미지에 알맞은 이미지를 넣고 컬러의 알파값을 30으로 변경

 

 

- Fill에서도 마찬가지로 소스 이미지와 컬러랑 알파값을 변경 후 이미지 타입을 Filled로 변경.

 

@ PlayerHealth 스크립트

 - 기능들 : LivingEntity의 생명체 기본 기능

 - 체력이 변경되면 체력 슬라이더에 반영

 - 공격 받으면 피격 효과음 재생

 - 사망 시 플레이어의 다른 컴포넌트 비활성화

 - 사망 시 사망 효과음, 사망 애니메이션 재생

 - 아이템을 감지하고 사용

 

using UnityEngine;
using UnityEngine.UI; // UI 관련 코드
using UnityEngine.UIElements;

// 플레이어 캐릭터의 생명체로서의 동작을 담당
public class PlayerHealth : LivingEntity {
    public Slider healthSlider; // 체력을 표시할 UI 슬라이더

    public AudioClip deathClip; // 사망 소리
    public AudioClip hitClip; // 피격 소리
    public AudioClip itemPickupClip; // 아이템 습득 소리

    private AudioSource playerAudioPlayer; // 플레이어 소리 재생기
    private Animator playerAnimator; // 플레이어의 애니메이터

    private PlayerMovement playerMovement; // 플레이어 움직임 컴포넌트
    private PlayerShooter playerShooter; // 플레이어 슈터 컴포넌트

    private void Awake() {
        // 사용할 컴포넌트를 가져오기
        playerAnimator = GetComponent<Animator>();
        playerAudioPlayer = GetComponent<AudioSource>();

        playerMovement = GetComponent<PlayerMovement>();
        playerShooter = GetComponent<PlayerShooter>();
    }

    protected override void OnEnable() {
        // LivingEntity의 OnEnable() 실행 (상태 초기화)
        base.OnEnable();

        // 체력 슬라이더 활성화
        healthSlider.gameObject.SetActive(true);

        // 체력 슬라이더의 최댓값을 기본 체력값으로 변경
        healthSlider.maxValue = startingHealth;

        // 체력 슬라이더의 값을 현재 체력값으로 변경
        healthSlider.value = health;

        // 플레이어 조작을 받는 컴포넌트 활성화
        playerMovement.enabled = true;
        playerShooter.enabled = true;
    }

    // 체력 회복
    public override void RestoreHealth(float newHealth) {
        // LivingEntity의 RestoreHealth() 실행 (체력 증가)
        base.RestoreHealth(newHealth);

        // 갱신된 체력으로 체력 슬라이더 갱신
        healthSlider.value = health;
    }

    // 데미지 처리
    public override void OnDamage(float damage, Vector3 hitPoint, Vector3 hitDirection) {
        if (!dead)
        {
            // 사망하지 않은 경우에만 효과음 재생
            playerAudioPlayer.PlayOneShot(hitClip);
        }
        
        // LivingEntity의 OnDamage() 실행(데미지 적용)
        base.OnDamage(damage, hitPoint, hitDirection);

        // 갱신된 체력으로 체력 슬라이더 갱신
        healthSlider.value = health;


    }

    // 사망 처리
    public override void Die() {

        // LivingEntity의 Die() 실행(사망 적용)
        base.Die();

        // 체력 슬라이더 비활성화
        healthSlider.gameObject.SetActive(false);

        // 사망음 재생
        playerAudioPlayer.PlayOneShot(deathClip);

        // 애니메이터의 Die 트리거를 발동시켜 사망 애니메이션 재생
        playerAnimator.SetTrigger("Die");

        // 플레이어 조작을 받는 컴포넌트 비활성화
        playerMovement.enabled = false;
        playerShooter.enabled = false;

    }

    private void OnTriggerEnter(Collider other) {
        // 아이템과 충돌한 경우 해당 아이템을 사용하는 처리

        // 사망하지 않은 경우에만 아이템 사용 가능

        if(!dead)
        {
            // 충돌한 상대방으로부터 IItem 컴포넌트 가져오기 시도
            IItem item = other.GetComponent<IItem>();

            // 충돌한 상대방으로부터 IItem 컴포넌트 가져오기 성공했다면
            if( item != null )
            {
                // Use 메서드를 실행하여 아이템 사용
                item.Use(gameObject);
                // 아이템 습득 소리 재생
                playerAudioPlayer.PlayOneShot(itemPickupClip);
            }
        }
    }
}

 

 - PlayerMovement와 PlayerShooter 컴포넌트를 PlayerHealth 스크립트에서 변수에 할당하고 관리하는 이유는 플레이어 캐릭터가 사망한 경우 캐릭터가 움직이거나 총을 쏠 수 없도록 조작에 맞춰 동작하는 컴포넌트들을 비활성화 하기 위해서.

 

@ OnEnable() 메서드

 - PlayerHealth 컴포넌트가 활성화될 때마다 체력 상태를 리셋하는 처리를 구현함.

 - LivingEntity 클래스의 OnEnable() 메서드를 오버라이드하여 구현.

 

@ OnDamage() 메서드

 : 먼저 피격 사운드를 재생. 단, 캐릭터가 이미 사망한 상태에서 공격을 받았을때 효과음이 재생되지 않도록 if 문으로 사망하지 않은 경우에만 효과음을 재생하도록 함.

그다음 LivingEntity의 OnDamage() 메서드를 실행해 데미지를 적용함.

마지막으로 데미지가 적용되어 변경된 체력을 체력 슬라이더에 반영함.

 

@ OnTriggerEnter() 메서드

 : 트리거 충돌한 상대방 게임 오브젝트가 아이템인지 판단하고 아이템을 사용하는 처리를 구현.

 

@ 'Slider' is an ambiguous reference between 'UnityEngine.UI.Slider' and 'UnityEngine.UIElements.Slider' 오류가 나서

7번째 줄을 < public UnityEngine.UI.Slider healthSlider; // 체력을 표시할 UI 슬라이더 >로 수정함