좀비 AI 기능을 구현하는 좀비 스크립트는 다음과 가능 기능을 구현해야 합니다.

LivingEntity에서 제공하는 기본 생명기능

외부에서 초기 능력치 초기화 가능

주기적으로 목표 위치를 찾아 경로갱신

피탄받았을때 효과 재생

트리거 콜라이더를 이용 감지된 적을 공격

사망시 추적 중지

사망시 사망효과 재생

 

  1. 좀비 필드
  2. public LayerMask whatIsTarget; // 추적 대상 레이어
  3. private NavMeshAgent navMeshAgent; // 경로계산 AI 에이전트
  4. public ParticleSystem hitEffect; // 피격시 재생할 파티클 효과
     public AudioClip deathSound; // 사망시 재생할 소리
     public AudioClip hitSound; // 피격시 재생할 소리
  5.     private Animator zombieAnimator; // 애니메이터 컴포넌트, 여기서는 사용안함
        private AudioSource zombieAudioPlayer; // 오디오 소스 컴포넌트, 효과음 재생을 위
        private Renderer zombieRenderer; // 렌더러 컴포넌트, 좀비 색을 변경시 사용
  6.     public float damage = 20f; // 공격력
        public float timeBetAttack = 0.5f; // 공격 간격
        private float lastAttackTime; // 마지막 공격 시점
  7. 추적할 대상이 존재하는지 알려주는 프로퍼티, targetEntity이고 죽지 않을 겨우만 true이므로 생명체를 체크할수 있음
// 추적할 대상이 존재하는지 알려주는 프로퍼티
private bool hasTarget  {
    get {
        // 추적할 대상이 존재하고, 대상이 사망하지 않았다면 true
        if (targetEntity != null && !targetEntity.dead)  {
            return true;
        }     
        return false; // 그렇지 않다면 false
    }
}

private void Awake() {}  사용할 컴포넌트들을 연결합니다.

public void Setup(ZombieData zombieData) {} 좀비의 능력치를 초기화 합니다.

private void Start( UpdatePath() ;) {  // 게임 오브젝트 활성화와 동시에 AI의 추적 루틴 시작 }

private void Update() {    // 추적 대상의 존재 여부에 따라 다른 애니메이션을 재생 }

private IEnumerator UpdatePath() { // 주기적으로 추적할 대상의 위치를 찾아 경로를 갱신 }

타겟도 이동중이므로 타겟이 존재한다면 타겟의 위치를 갱신한다.

                // 추적 대상 존재 : 경로를 갱신하고 AI 이동을 계속 진행
                navMeshAgent.isStopped = false;
                navMeshAgent.SetDestination(targetEntity.transform.position);

private IEnumerator UpdatePath() {
    // 살아있는 동안 무한 루프
    while (!dead)
    {
        if (hasTarget)
        {
            // 추적 대상 존재 : 경로를 갱신하고 AI 이동을 계속 진행
            navMeshAgent.isStopped = false;
            navMeshAgent.SetDestination(
                targetEntity.transform.position);
        }
        else
        {
            // 추적 대상 없음 : AI 이동 중지
            navMeshAgent.isStopped = true;

            // 20 유닛의 반지름을 가진 가상의 구를 그렸을때, 구와 겹치는 모든 콜라이더를 가져옴
            // 단, whatIsTarget 레이어를 가진 콜라이더만 가져오도록 필터링
            Collider[] colliders =
                Physics.OverlapSphere(transform.position, 20f, whatIsTarget);

            // 모든 콜라이더들을 순회하면서, 살아있는 LivingEntity 찾기
            for (int i = 0; i < colliders.Length; i++)
            {
                // 콜라이더로부터 LivingEntity 컴포넌트 가져오기
                LivingEntity livingEntity = colliders[i].GetComponent<LivingEntity>();

                // LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아있다면,
                if (livingEntity != null && !livingEntity.dead)
                {
                    // 추적 대상을 해당 LivingEntity로 설정
                    targetEntity = livingEntity;

                    // for문 루프 즉시 정지
                    break;
                }
            }
        }

        // 0.25초 주기로 처리 반복
        yield return new WaitForSeconds(0.25f);
    }
}

public override void OnDamage(float damage, }  // 데미지를 입었을때 실행할 처리

public override void Die() { // LivingEntity의 Die()를 실행하여 기본 사망 처리 실행}

private void OnTriggerStay(Collider other) {
        // 자신이 사망하지 않았으며,
        // 최근 공격 시점에서 timeBetAttack 이상 시간이 지났다면 공격 가능 }

Player를 선택하고Layer를 PLAYER로 바꿉니다. 모든 child에 적용할거냐는 질문에 No 합니다.


  Player를 Prefabs 폴더로 끌어다 프리팹으로 만듭니다. 

하이라키뷰에서 Enemy를 선택하고 public을 연결해줍니다.

 

Zombie.cs
0.01MB
Zombie Data.cs
0.00MB

'유니티좀비게임 > 생명과 좀비' 카테고리의 다른 글

좀비(Enemy) 오브젝트 준비  (0) 2023.05.03
네비게이션 시스템  (0) 2023.05.03
플레이어 체력 UI  (0) 2023.04.30
Event  (0) 2023.04.28
객체지향 다형성 LivingEntity  (0) 2023.04.28

하이라키뷰에서 Player를 선택하고 Ctrl-D를 눌러 복사하고 이름을 Enemy로 변경한다.

매터리얼을 하나 만들어 색을 바꿔준다.

컴포넌트는 Transform, Rigidbody, Capsule Collider만 남겨두고 나머지 스크립트는 전부 제거한다.

위치를 변경한다. (0,0,15)

 

Box Collider를 추가하고 Center(0,1,0.76) Size(0.5, 0.5, 0.5)로 변경한다. 좀비의 공격범위로 사용됩니다.

Audio Component를 추가하고 Play On Awake를 언체크 합니다.

Nav Mesh Agent 컴포넌트를 추가합니다.

Zombie 스크립트를 추가합니다.

 

Prefabs폴더의 BloodSparyEffect를 추가합니다. 피탄효과로 사용됩니다.

BloodSprayEffect.prefab
0.20MB

'유니티좀비게임 > 생명과 좀비' 카테고리의 다른 글

좀비 스크립트  (0) 2023.05.03
네비게이션 시스템  (0) 2023.05.03
플레이어 체력 UI  (0) 2023.04.30
Event  (0) 2023.04.28
객체지향 다형성 LivingEntity  (0) 2023.04.28

경로를 자동으로 계산하는 인공지능 좀비를 구현하기 위해 먼저 유니티 내비게이션 시스템을 살펴보고 좀비게임 오브젝트를 준비합니다.

 

하이라키에 큐브를 3x5=15쯤 만들고 배치합니다. 적이 쫒아 올때 장애물로 사용될겁니다.

Stage 빈폴더를 만들고 Plane과 만금 만든 큐브들을 끌어다 넣고 자식으로 만듭니다. Plane과 큐브들을 선택하고 인스펙트뷰에서 static으로 만듭니다. 

 

유니티는 한 위치에서 다른 위치로의 경로를 계산하고 실시간으로 장애물을 피하며 이동하는 인공지능을 만드는 네비게인션 시스템을 제공합니다. 시스템에 사용하는 오브젝트는 크게 다음 4가지 입니다.

  1. 네비메시 : 에이전트가 걸어 다닐 수 있는 표면
  2. 네비메시 에이전트 : 캐릭터 또는 컴포넌트
  3. 네비메시 장애물 : 에이전트의 경로를 막는 장애물
  4. 오프메시 링크 : 끊어진 내비메시 영역 사이를 잇는 연결 지점(뛰어넘을 수 있는 울타리나 타고 올라갈수있게)

네비메시 빌드

  1. 네비메시 굽기
    1. 네비게이션 창 열기 > Window > AI > Navigation
    2. 네비게이션 창에서 Bake 탭 클립, 잘 구어지면 다음과 같이 다닐수 있는 곳은 마스크가 생긴다.

 

'유니티좀비게임 > 생명과 좀비' 카테고리의 다른 글

좀비 스크립트  (0) 2023.05.03
좀비(Enemy) 오브젝트 준비  (0) 2023.05.03
플레이어 체력 UI  (0) 2023.04.30
Event  (0) 2023.04.28
객체지향 다형성 LivingEntity  (0) 2023.04.28

플레이어 캐릭터의 체력을 구현하기 전에 먼저 체력을 띄울 UI를 구현하겠습니다. 체력은 원형 슬라이더로 플레이어 캐릭터의 몸체에 표시됩니다.

  1. UI 슬라이더 준비
    1. 새 슬라이더 오브젝트 생성 : Create > UI > Slider
    2. 하이라키창에서 Cavas 게임오브젝트 선택
    3. Cavas 컴포넌트의 Render Mode를 Word Space로 변경 -> 플레이어 캐릭터를 따라 다녀야 하므로
    4. Canvas Scaler 컴포넌트의 Reference Pixels per Unit를 1로 변경 -> 깔끔하게 보임
  2. 캔버스의 위치와 크기 설정
    1. Canvas 게임 오브젝트를 Player Chracter 게임 오브젝트의 자식으로 만들기
    2. Canvas 게임 오브젝트 선택
    3. Rect Transform 컴포넌트의 위치를 0, 0.3, 0 Width와 height를 1로 변경
    4. Rect Transform 컴포넌트의 Rotation을 (90, 0, 0)으로 변경
  3. 슬라이더 크기 변경
    1. 하이라키창 Canvans 옆 펼치기 버튼을 Alt+클릭로 펼침
    2. Handle Slide Area 게임 오브젝트를 Delete
    3. Slider, Background, Fill Area, Fill 게임 오브젝트를 모두 선택
    4. 앵커 프리셋 클릭 > Alt를 누른채 우측 하단의 stretch클릭
  4. 슬라이더 컴포넌트 설정
    1. Slider게임 오브젝트 선택> 게임 오브젝트의 이름을 Health Slider로 변경
    2. Slider 컴포넌트에서 Interactable 체크 해제 - 상호작용 못하게
    3. Transition을 None으로 변경 - 색반응 못하게
    4. Max Value의 Value를 100으로 변경
  5. 슬라이더 배경 이미지 변경
    1. 하이라키창 Background게임 오브젝트 선택
    2. Image컴포넌트의 Source Image에 Health Circle 스프라이트 할당
    3. Image 컴포넌트의 칼라필드 클릭 > 알파를 30으로 변경
  6. 슬라이더 Fill이미지 변경
    1. Fill 게임 오브젝트 선택
    2. Image 컴포넌트의 Soruce Image에 Health Circle 스프라이트 할당
    3. Image 컴포넌트의 Color 필드 클릭> 컬러를 (255,0,0,150)으로 변경
    4. Image Type을 Filled로 변경
  7. PlayerHealth 스크립트
    1. LivingEntity의 생명체 기본 기능
    2. 체력이 변경되면 체력 슬라이더에 반영
    3. 공격받으면 피격 효과음 재생
    4. 사망시 플레이어의 다른 컴포넌트를 비활성화
    5. 사망시 사망효과음과 사망 애니메이션 재생
    6. 아이템을 감지하고 사용
  8. 스크립트 설명
    1. PlayerHealth 클래스는 LivingEntity를 상속합니다.
    2. healthSlider를 연결합니다.
    3. Awake() : 사용할 컴포넌트를 가져옵니다.
    4. LivingEntity로 상속된 메서드를 오버라이드 합니다.
    5. public override void OnEnable() : PlayerHealth컴포넌트가 활성화될 때마다 체력상태를 리셋하는 처리를 구현
      1. base.OnEnalbe()에 의해 부모메서드 health = startingHealth 가 먼저 실행됨
      2. 체력 슬라이드, Player, shotter등을 활성화합니다.
    6. public override void RestoreHealth()
      1. LivingEntity클래스의 ResotreHealth()를 오버라이드 합니다.
      2. 체력을 회복하는 처리는 이미 상속되었고 여기서는 슬라이더에 반영하는 처리를 추가합니다.
    7. public override void OnDamage()
      1. 상속되는 기능외에 효과음을 재생하고 체력 슬라이더를 갱신합니다.
    8. public override void Die()
      1. 상속되는 기능외에 사망 애니메이션과 효과음을 재생하고 체력 슬라이더를 비활성처리합니다.
    9. OnTriggerEnter()
      1. 트리거 충돌한 상대방 게임 오브젝트가 아이템인지 판단하고 아이템을 사용하는 처리
      2.  충돌한 객체를 IItem item으로 받아 item.Use()메서드로 느슨한 인터페이스 실행
private void OnTriggerEnter(Collider other) { 
    IItem item = other.GetComponent<IItem>(); 
    item.Use(gameObject);

PlayerHealth.cs 전체코드 Player게임오브젝트에 추가.

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

// 플레이어 캐릭터의 생명체로서의 동작을 담당
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)
        {
            // 충돌한 상대방으로 부터 Item 컴포넌트를 가져오기 시도
            IItem item = other.GetComponent<IItem>();

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

PlayerHealth 컴포넌트 설정

Audios.zip
3.71MB

 

 

'유니티좀비게임 > 생명과 좀비' 카테고리의 다른 글

좀비 스크립트  (0) 2023.05.03
좀비(Enemy) 오브젝트 준비  (0) 2023.05.03
네비게이션 시스템  (0) 2023.05.03
Event  (0) 2023.04.28
객체지향 다형성 LivingEntity  (0) 2023.04.28

이벤트를 연쇄 동작을 이끌어내는 사건입니다. 이벤트 자체는 어떤 일을 실행하지 않지만 이벤트가 발생하면 이벤트를 구독하는 처리들이 연쇄적으로 실행됩니다.

C#에서 이벤트를 구현하는 대표적인 방법은 델리게이트를 클래스 외부로 공하는 겁니다. 외부로 공개된 델리게이트는 클래스 외부의 메서드가 등록될 수 있는 명단이자 이벤트가 됩니다. 그리고 이벤트가 발동하면 이벤트에 등록된 메서드들이 모두 실행됩니다.

여기서 이벤트들을 듣고 있다가 실행되는 메서드들을 이벤트리스너라고 합니다. 이벤트 리스너를 이벤트에 등록하는 것을 이벤트를 구독한다고 합니다.

델리게이트 타입의 변수는 event 키워드를 붙여 선언할 수 있습니다. 어떤 델리게이트 변수를 event로 선언하면 클래스 외부에서는 해당 델리게이트를 실행할 수 없게 됩니다.

event를 사용하면 이벤트를 소유하지 않는 측에서 멋대로 이벤트를 발동하는 것을 막을 수 있습니다.

  1. OnEnable()
    1. LivingEntity의 OnEnable()메서드는 생명체의 상태를 리셋합니다. 사방상태를 flase()로 체력을 시작 체력값으로 초기화합니다. OnEnable()메서드는 LivingEntity가 활성화될 때 실행됩니다.
    2. OnEnable()은 virtual로 선언된 가상메서드이므로 자식 클래스에서 확장할 수 있습니다. 다만 자식 클래스에서 OnEnable()메서드를 접근 가능해야 확장 가능하므로 접근자를 private가 아닌 protected를 사용했습니다.
  2. OnDamage()
    1. 외부에서 LivingEntity()를 공격하는데 사용됩니다.
    2. 입력으로 받은 Damage만큼 health를 깎습니다. 0보다 작은 경우 Die()를 실행합니다.
    3. virtual로 선언되었으므로 자식 클래스에서 확장할 수 있습니다.
  3. RestoreHealth()
    1. 체력을 회복하는 메서드입니다.
    2. 죽지 않았다면 입력받은 회복량 newHealth만큼 현재 health를 증가 시킵니다.
    3. 가상메서드이므로 자식클래스에서 확장할 수 있습니다.
  4. Die()
    1. LivingEntity의 죽음을 구현
    2. onDeath() 이벤트를 발동하여 이벤트에 등록된 메서드를 실행합니다.

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;
    }
}

'유니티좀비게임 > 생명과 좀비' 카테고리의 다른 글

좀비 스크립트  (0) 2023.05.03
좀비(Enemy) 오브젝트 준비  (0) 2023.05.03
네비게이션 시스템  (0) 2023.05.03
플레이어 체력 UI  (0) 2023.04.30
객체지향 다형성 LivingEntity  (0) 2023.04.28

이장에서 다루는 내용

  • 다형성을 사용해 여러타입을 하나의 타입으로 다루기
  • 오버라이드를 사용해 부모 클래스의 기존메서드 확장하기
  • 이벤트를 사용해 견고한 커플링을 해소하고 코드를 간결하게 만들기
  • UI슬라이더 사용하기
  • 게임월드내부에 UI배치하기
  • 내비게이션 시스템을 사용해 인공지능 구현하기
  1. 다형성
    • 이장에서는 적 AI와 플레이어 캐릭터를 포함하여 생명체로 동작할 모든 클래스가 공유하는 기반 클래스 LivingEntity를 사용합니다. 생명체로 동작할 모든 클래스는 LivingEntity 클래스를 상속하고 그 위에 자신만의 기능을 추가합니다.
    • LivingEntity를 상속한 자식 클래스는 LivingEntity의 구현을 재사용할수 있을 뿐만 아니라 LivingEntyty타입으로 취급될 수 있습니다. 따라서 LivingEntity를 상속한 다양한 타입의 게임속 생명체를 LivingEntity 타입으로 일괄 처리 할 수 있습니다.
    • 이것은 다형성이라는 객체지향의 특징 덕분입니다. LivingEntity 클래스를 사용하기 전에 상속을 이해하고 다형성이 무엇인지 살펴보겠습니다. 그리고 다형성이 코드를 쉽고 간결하게 만드는 데 어떻게 도움을 주는지 알아보겠습니다.
  2. 상속관계에서의 다형성
    • 다형성을 문자 그대로 해석하면 여러형태입니다. c#에서 다형성은 자식 클래스 타입을 부모클래스 타입으로 다룰 수 있게 합니다.
    • 상속이란 부모클래스를 기반으로 자식클래스를 만드는 방법입니다. 이것은 현실에서 어떤 물체를 더 포괄적인 분류로 다루는 것에 비유할 수 있습니다. 예를 들어 동물이나 식물은 더 포괄적인 분류인 생명체로 다룰 수 있습니다.
    • class Monster{ public void Attack(); public float damage=100f;}라는 클래스가 있습니다.
      • class Orc : Monster { public void Cry()}, Monster를 상속받는 자식 클라스를 만들었습니다.
      • Orc orc; Monster monster = orc; 로 orc객체를 monster에 할당합니다. 변형이 아닙니다.
      • monster.Attack()는 문제 없지만 monster.Cry()는 에러입니다. monster클래스에는 Cry()가 없기 때문입니다.
    • 다형성을 사용한 패턴
      • class Dragon:Monster {public void Fly();}는 다른 자식클래스를 만듭니다.
      • 게임중 Dragon과 Orc의 객체를 많이 만듭니다.
      • Monster[] monsters = findObjectsOfType<Monsters>();
      • for(int i = 0;i<Monsters.Length;i++) monsters[i].damage += 10f;
      • 위와 같이 <Monster>로Dragon과 Orc객체들의 damage를 갱신할 수 있습니다.
  3. 오버라이드
    • 부모클래스에서 정의된 메서드를 자식클래스에서 재정의하는 걸 오버라이드라고 합니다.
    • Orc와 Dragon의 Attack()기술이 다를 경우 활용할수 있습니다.
    • class Monster{ public virtual void Attack(){Debug.Log("공격");}}이라고 할때
    • class Orc : Monster {public override void Attack() {base.Attack; 추가처리;} }오버라이드할 수 있습니다
      1. virtual 키워드로 지정된 메서드는 가상 메서드가 됩니다. 가상 메서드는 자식 클래스가 오버라이드 할 수 있도록 허용된 메서드 입니다. 자식 클래스는 override 키워드를 사용해 부모 클래스의 가상 메서드를 재정의 할 수 있습니다. 자식 객체가 연결된 부모 참조의 오버라이드된 메서드는 자식 메서드가 실행됩니다.
      2. Orc orc; Monster monster = orc; monster.Attack();  <- Orc에서 재정의된 Attack()가 실행
    • virtual로 지정된 메서드는 가상메서드가 됩니다. 자식클래스에서override를 허용해줍니다.
    • base를 사용해 부모의 원형 메서드로 접근할 수 있습니다.
    • class Dragon : Monster {public override void Attack() { base.Attack(); Debug.Log("추가"); } }
  4. 오버라이드 활용 : 다양한 자식 클래스의 같은 이름의 메서드를 실행하되 실제 처리는 각자 다르게 만들수 있습니다.
    • for(int i = 0;i<Monsters.Length;i++) monsters[i].Attack();
  5. LivingEntity기반 클래스
    • 체력, 피해, 사망기능,사망이벤트 제공
    • LivingEntity 클래스는 IDamageable을 상속하므로 OnDamage()메서드를 반드시 구현해야 합니다.
    • LivingEntity의 필드
      • startingHealth // 시작 체력
      • health { get; protected set; } // 현재 체력
      • public bool dead { get; protected set; } // 사망 상태
      • public event Action onDeath; // 사망시 발동할 이벤트
  6. Action
    1. Action 타입은 입력과 출력이 없는 메서드를 가리킬 수 있는 델리게이트입니다. 델리게이트는 대리자로 번역되며 메서드를 값으로 할당받을 수 있는 타입입니다.
    2. Action으로 지정된 객체는 +=를 이용해 메서드를 등록할 수 있습니다. 괄호없이 이름만 명시하면 됩니다. action += Func1(); action += Func2();
    3. 괄호를 붙이면 실행후 결과값이 들어가므로 에러입니다.
    4. Action 변수()를 실행하면 등록된 메서드들이 일괄 실행 됩니다.
    5.  
public class Cleaner : MonoBehaviour {
	Action OnClean;
    void Start() {
    	onClean += CleaningRoomA;  // += 로 메스드 등록
        onClean += CleaningRoomB;
      	}
    void Update() {
    	if (Input.GetMouseButtonDown(0)) {
        	onClean(); // CleaningRoomA()와 CleaningRoomB() 실행
        }
    }
    void CleaningRoomA() {
    	Debug.Log("A방 청소");
    }
    void CleaningRoomA() {
    	Debug.Log("A방 청소");
    }
}

 

'유니티좀비게임 > 생명과 좀비' 카테고리의 다른 글

좀비 스크립트  (0) 2023.05.03
좀비(Enemy) 오브젝트 준비  (0) 2023.05.03
네비게이션 시스템  (0) 2023.05.03
플레이어 체력 UI  (0) 2023.04.30
Event  (0) 2023.04.28

PlayerShooter 스크립트를 새로 만듭니다. 첨부된 파일을 Assets에 추가하셔도 됩니다.

PlayerShooter.cs
0.00MB

스크립트를 Player에 추가합니다. 플레이어에는 다음과 같이 3개의 스크립트가 추가되어 있고 다음과 같이 참조들이 연결되어 있어야 합니다.

GunShooter의 전체 스크립트입니다.

gun.Fire(),gun.Reload()가 있는 gun의 참조를 글로벌로 받아오고

입력을 받아올 playerInput컴포넌트를 playerInput 참조에 연결해줍니다.

 

Update() 에서 playerInput.fire가 참이면 gun.Fire()를 실행해 총알을 발사합니다. playerInput.fire가 참일경우 gun.Reload()를 실행합니다. Animatior가 있는 경우 애니메이션을 실행하나 여기서는 실행하지 않습니다. 마지막으로 UpdateUI()함수를 불러 UI를 업데이트 합니다. 아직 UI가 null이므로 실행하지는 않습니다.

using UnityEngine;

// 주어진 Gun 오브젝트를 쏘거나 재장전
// 알맞은 애니메이션을 재생하고 IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
public class PlayerShooter : MonoBehaviour {
    public Gun gun; // 사용할 총
    public Transform gunPivot; // 총 배치의 기준점


    private PlayerInput playerInput; // 플레이어의 입력
    //private Animator playerAnimator; // 애니메이터 컴포넌트

    private void Start() {
        // 사용할 컴포넌트들을 가져오기
        playerInput = GetComponent<PlayerInput>();
        //playerAnimator = GetComponent<Animator>();
    }

    private void OnEnable() {
        // 슈터가 활성화될 때 총도 함께 활성화
        gun.gameObject.SetActive(true);
    }

    private void OnDisable() {
        // 슈터가 비활성화될 때 총도 함께 비활성화
        gun.gameObject.SetActive(false);
    }

    private void Update() {
        // 입력을 감지하고 총 발사하거나 재장전
        if (playerInput.fire) {
            // 발사 입력 감지시 총 발사
            //Debug.Log("Player Shooter Input.fire");
            gun.Fire();
        } else if (playerInput.reload) {
            // 재장전 입력 감지시 재장전
            if (gun.Reload()) {
                // 재장전 성공시에만 재장전 애니메이션 재생
                //playerAnimator.SetTrigger("Reload");
                Debug.Log("Reload");
            }
        }

        // 남은 탄약 UI를 갱신
        //UpdateUI();
    }

    // 탄약 UI 갱신
    /*
    private void UpdateUI() {
        if (gun != null && UIManager.instance != null) {
            // UI 매니저의 탄약 텍스트에 탄창의 탄약과 남은 전체 탄약을 표시
            UIManager.instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);
        }
    }
    */

    // 애니메이터의 IK 갱신
}

'유니티좀비게임 > 총과슈터' 카테고리의 다른 글

GunData 스크립트  (0) 2023.04.28
총 게임오브젝트  (0) 2023.04.28
인터페이스  (0) 2023.04.28

Gun의 확장성의 문제

스크립터블 오브젝트가 필요한 이유를 이해하기 위해 Gun의 다양성 구현을 생각해 봅시다.

총마다 다음과 같은 특성이 있습니다.

총사운드, 공격력, 탄창용량, 연사력과 재장전시간등

이런 다른 특성을 가진 Gun을 관리하기 위해 수많은 프리팹을 만들어야 하고 런타임에 수치 데이터 변경이 어렵습니다. 이런 문제점을 해결하기 위해 Gun클래스에서 데이터에 해당하는 부분을 개별 클래스 GunData로 추출하여 일부 해결할 수 있습니다.

스크립터블 오브젝트

GunData오브젝트가 유니티 에디터에서 편집할 수 있는 형태로 존재해야 합니다. 씬위의 게임오브젝트가 아닌 형태로 존재해야 합니다. 씬위의 게임오브젝트는 해당 씬을 열어야 편집할 수 있기 대문입니다.

GunData는 단순한 데이터 컨테이너입니다. 따라서 MonoBehaviour를 상속받아서는 안됩니다. 스크립터블 오브젝트는 다음과 같은 경우 유용합니다.

여러 오브젝트가 공유하여 사용할 데이터를 에셋 형태로 분리

데이터를 유니티 인스펙터 창에서 편집 가능한 형태로 관리

GunData구현하기

Gundata 스크립트 열기

using UnityEngine;

public class GunData
{
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리

    public float damage = 25; // 공격력

    public int startAmmoRemain = 100; // 처음에 주어질 전체 탄약
    public int magCapacity = 25; // 탄창 용량

    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간
}

현재 GunData는 단순 클래스입니다. 이걸 스트립터블 오브젝트 타입상속하고 CreateAssetMenu()를 이용 에셋 생성메뉴에 추가하겠습니다.

using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/GunData",fileName = "Gun Data")]
public class GunData : ScriptableObject
{ 생략 }

GunData를 선택후 인스펙터뷰에서 Shot Clip, Reload Clip필드의 소스를 선택해 오디오클립을 지정해줍니다.

Audios.zip
3.71MB

1. Gun 스크립트 변수들

총의 상태를 나타내는 state변수가 있습니다. Gun의 상태는 Ready, Empty, Reloading중 하나입니다.

Gun 스크립트

이제 Gun스크립트를 Gun게임 오브젝트에 추가하고 스크립트를 완성합니다. 하이라키의 FirePosition, MuzzleFlashEffect와 ShellEjectEffect를 끌어다 연결해줍니다.

 

Gun게임 오브젝트 준비가 끝났습니다. 이제 총의 동작을 구현하는 Gun 스크립트를 Gun게임 오브젝트에 추가하고 스크립트를 분석해 보겠습니다.

Gun의 메서드

Awake() : 사용할 컴포넌트를 가져오고

OnEnable() : 총의 상태를 초기화

Fire() : 총을 발사를 시도

Shot() : 발사가능 상태에서만 발사, 안전하게 감싸는 껍떼기

ShotEffect(): 발사효과를 재생하고 탄알 궤적을 그립니다.

Reload() : r을 누르면 재장전이 가능한지 체크하고 다음 루팅을 Call합니다. 성공하면 true를 리턴

ReloadRoutine(): 실제 장전하는 곳

ShotEffect()와 ReloadRoutine()은 IEnumerator라는 코루틴메서드 타입을 반환

Gun의 필드

이제 Gun의 스크립트의 변수를 살펴봅시다.

public enum State {
        Ready, // 발사 준비됨
        Empty, // 탄창이 빔
        Reloading // 재장전 중
    }
public State state { get; private set; } // 현재 총의 상태 외부에서 변경 불가능

public Transform fireTransform; // 총알이 발사될 위치와 방향을 나타내는 변수 Fire Position과 연결

public ParticleSystem muzzleFlashEffect; // 총구 화염 효과

public ParticleSystem shellEjectEffect; // 탄피 배출 효과

private LineRenderer bulletLineRenderer; // 총알 궤적을 그리기 위한 렌더러

 

다음은 총소리를 위한 변수가 있습니다.

private AudioSource gunAudioPlayer; // 총 소리 재생기

음원및 총의 데이터는 Gundata로 따로 만들어 놓고 연결해 씁니다.

인스펙트창에서 연결해 줍니다.

public GunData gunData; // 총의 현재 데이터

 

 Awake()메서드

Gun 스크립트의 Awake()함수를 완성합니다.

private void Awake() {
    // 사용할 컴포넌트들의 참조를 가져오기
    gunAudioPlayer = GetComponent<AudioSource>();
    bulletLineRenderer = GetComponent<LineRenderer>();

    bulletLineRenderer.positionCount = 2; // 렌더러에서 사용할 점을 두개로 변경
    bulletLineRenderer.enabled = false; // 라인 렌더러를 비활성화, 스크립터에서 총을 쏠때만 나타나게 함
}

3. OnEnable()메써드

private void OnEnable() {
        ammoRemain = gunData.startAmmoRemain; // 전체 예비 탄약 양을 초기
        magAmmo = gunData.magCapacity; // 현재 탄창을 가득채우기
        state = State.Ready; // 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
        lastFireTime = 0; // 마지막으로 총을 쏜 시점을 초기화
    }

4. 코루틴

발사효과를 재생하는 ShotEffect()메서드는 시각효과를 재생하고 탄알 궤적을 그리기위해 라인렌더러를 켜서 선을 그린다음 라인렌더러를 꺼야 합니다. 이때 매우 짧은 시간동안 처리를 일시정지합니다. 따라서 라인렌더러를 끄고 켜는 대기시간이 필요합니다. 이때 코루틴이 사용됩니다.

유니티의 코루틴은 대기시간을 가질 수 있는 메서드입니다. 유니티에서 코루틴메서드는 IEnumerator타입을 반환해야하며, 처리가 일시 대기할 곳에 yield키워드를 명시해야합니다.

yield return new WaitForSecond(float 초); // 초단위로 쉬기

yield return null; // 한프레임 쉬기

코루틴은 StartCoroutine()메서드로 실행됩니다. 2가지 방법이 있습니다.

StartCoroutine(메서드());

StartCoroutine("메서드이름"); // 꼭 StopCroutine("메서드이름")으로 종료시켜야합니다.

5. ShotEffect()메서드

나중에 Shot()에서 불리워집니다.

private IEnumerator ShotEffect(Vector3 hitPosition) {
    muzzleFlashEffect.Play();  // 총구 화염 효과 재생
    shellEjectEffect.Play(); // 탄피 배출 효과 재생
    gunAudioPlayer.PlayOneShot(gunData.shotClip);  // 총격 소리 재생
    bulletLineRenderer.SetPosition(0, fireTransform.position); // 선의 시작점은 총구의 위치
    bulletLineRenderer.SetPosition(1, hitPosition);  // 선의 끝점은 입력으로 들어온 충돌 위치
    bulletLineRenderer.enabled = true;  // 라인 렌더러를 활성화하여 총알 궤적을 그린다
    yield return new WaitForSeconds(0.03f);   // 0.03초 동안 잠시 처리를 대기
    bulletLineRenderer.enabled = false;  // 라인 렌더러를 비활성화하여 총알 궤적을 지운다
}
6. Fire()메서드

State.Ready를 검사해 총을 발사 가능한 상태에서만 Shot()메서드가 실행되도록 합니다.

7. 레이캐스트

LineRenderer를 이용 광선을 그리기위해 총이 충돌하는 끝점을 알아야 합니다 .

충돌 처리를 위해 Ray라는 검사용 광선을 사용하며 충돌하면 RaycastHit 타입의 충돌정보가 생성됩니다.

충돌한 게임오브젝트 위치,표면의 방향등의 정보가 들어 있습니다.

8. Shot()메서드

레이캐스트를 이용해 총을 쏘고 총에 맞은 오브젝트를 찾아 데미지를 주는 Shot()메서드입니다.

RaycastHit hit; // 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너

Physics.Raycast(시작지점, 방향, out hit , 사정거리)

- 리턴값이 bool Raycast의 이외에 더 자세한 정보를 얻기위해 hit앞에 out 키워드를 사용하고 있습니다.

hit에 채워지는 정보는 origin(총구위치), direction(총구앞쪽방향), hitInfo(충돌정보), maxDistance(사정거리)

// 레이가 어떤 물체와 충돌한 경우 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도

IDamageable target =hit.collider.GetComponent<IDamageable>();

탄알이 맞은 위치는 hit.point, 표면의 방향은 hit.normal을 통해 알수 있습니다.

9.Reload()메서드

재장전을 시도하는 코루틴 메서드입니다.

yield return new WaitForSeconds()를 사용 재장정할 동안 state를 Reloading으로 고정시켜

다른 기능을 동작하지 않게 합니다.

10. Gun 컴포넌트 설정

하이라키의 Fire Position 게임오브젝트및 MuzzleFlashEffect, ShellEjectEffect게임오브젝트를 연결합니다.

GunData는 Scriptable Data에 있습니다. (이건 개정판 버전입니다)

Gun프리팹 갱신하기

인스펙터 상단 Overrides>Apply All 클릭

Gun.CS 전체코드

using System.Collections;
using UnityEngine;

// 총을 구현한다
public class Gun : MonoBehaviour {
    // 총의 상태를 표현하는데 사용할 타입을 선언한다
    public enum State {
        Ready, // 발사 준비됨
        Empty, // 탄창이 빔
        Reloading // 재장전 중
    }

    public State state { get; private set; } // 현재 총의 상태

    public Transform fireTransform; // 총알이 발사될 위치

    public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
    public ParticleSystem shellEjectEffect; // 탄피 배출 효과

    private LineRenderer bulletLineRenderer; // 총알 궤적을 그리기 위한 렌더러
    private AudioSource gunAudioPlayer; // 총 소리 재생기

    public GunData gunData; // 총의 현재 데이터
    
    private float fireDistance = 50f; // 사정거리

    public int ammoRemain = 100; // 남은 전체 탄약
    public int magAmmo; // 현재 탄창에 남아있는 탄약
    
    private float lastFireTime; // 총을 마지막으로 발사한 시점
    
    private void Awake() {
        // 사용할 컴포넌트들의 참조를 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        bulletLineRenderer = GetComponent<LineRenderer>();

        // 사용할 점을 두개로 변경
        bulletLineRenderer.positionCount = 2;
        // 라인 렌더러를 비활성화
        bulletLineRenderer.enabled = false;
    }

    private void OnEnable() {
        // 전체 예비 탄약 양을 초기화
        ammoRemain = gunData.startAmmoRemain;
        // 현재 탄창을 가득채우기
        magAmmo = gunData.magCapacity;

        // 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
        state = State.Ready;
        // 마지막으로 총을 쏜 시점을 초기화
        lastFireTime = 0;
    }

    // 발사 시도
    public void Fire() {
        // 현재 상태가 발사 가능한 상태
        // && 마지막 총 발사 시점에서 timeBetFire 이상의 시간이 지남
        if (state == State.Ready && Time.time >= lastFireTime + gunData.timeBetFire)
        {
            // 마지막 총 발사 시점을 갱신
            lastFireTime = Time.time;
            // 실제 발사 처리 실행
            Shot();
        }
    }

    // 실제 발사 처리
    private void Shot() {
        RaycastHit hit; // 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
        Vector3 hitPosition = Vector3.zero; // 총알이 맞은 곳을 저장할 변수
        // 레이캐스트(시작지점, 방향, 충돌 정보 컨테이너, 사정거리)
        if (Physics.Raycast(fireTransform.position,
            fireTransform.forward, out hit, fireDistance)){
            // 레이가 어떤 물체와 충돌한 경우
            // 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도
            IDamageable target =
                hit.collider.GetComponent<IDamageable>();

            // 상대방으로 부터 IDamageable 오브젝트를 가져오는데 성공했다면
            if (target != null){
                // 상대방의 OnDamage 함수를 실행시켜서 상대방에게 데미지 주기
                target.OnDamage(gunData.damage, hit.point, hit.normal);
            }

            // 레이가 충돌한 위치 저장
            hitPosition = hit.point;
        } else { // 레이가 다른 물체와 충돌하지 않았다면
            // 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용
            hitPosition = fireTransform.position +
                          fireTransform.forward * fireDistance;
        }

        // 발사 이펙트 재생 시작
        StartCoroutine(ShotEffect(hitPosition));

        // 남은 탄환의 수를 -1
        magAmmo--;
        if (magAmmo <= 0)
        {
            // 탄창에 남은 탄약이 없다면, 총의 현재 상태를 Empty으로 갱신
            state = State.Empty;
        }
    }

    // 발사 이펙트와 소리를 재생하고 총알 궤적을 그린다
    private IEnumerator ShotEffect(Vector3 hitPosition) {
        // 총구 화염 효과 재생
        muzzleFlashEffect.Play();
        // 탄피 배출 효과 재생
        shellEjectEffect.Play();

        // 총격 소리 재생
        gunAudioPlayer.PlayOneShot(gunData.shotClip);

        // 선의 시작점은 총구의 위치
        bulletLineRenderer.SetPosition(0, fireTransform.position);
        // 선의 끝점은 입력으로 들어온 충돌 위치
        bulletLineRenderer.SetPosition(1, hitPosition);
        // 라인 렌더러를 활성화하여 총알 궤적을 그린다
        bulletLineRenderer.enabled = true;

        // 0.03초 동안 잠시 처리를 대기
        yield return new WaitForSeconds(0.03f);

        // 라인 렌더러를 비활성화하여 총알 궤적을 지운다
        bulletLineRenderer.enabled = false;
    }

    // 재장전 시도
    public bool Reload() {
        if (state == State.Reloading ||
            ammoRemain <= 0 || magAmmo >= gunData.magCapacity)
        {
            // 이미 재장전 중이거나, 남은 총알이 없거나
            // 탄창에 총알이 이미 가득한 경우 재장전 할수 없다
            return false;
        }

        // 재장전 처리 시작
        StartCoroutine(ReloadRoutine());
        return true;
    }

    // 실제 재장전 처리를 진행
    private IEnumerator ReloadRoutine() {
        // 현재 상태를 재장전 중 상태로 전환
        state = State.Reloading;
        // 재장전 소리 재생
        gunAudioPlayer.PlayOneShot(gunData.reloadClip);

        // 재장전 소요 시간 만큼 처리를 쉬기
        yield return new WaitForSeconds(gunData.reloadTime);

        // 탄창에 채울 탄약을 계산한다
        int ammoToFill = gunData.magCapacity - magAmmo;

        // 탄창에 채워야할 탄약이 남은 탄약보다 많다면,
        // 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
        if (ammoRemain < ammoToFill)
        {
            ammoToFill = ammoRemain;
        }

        // 탄창을 채운다
        magAmmo += ammoToFill;
        // 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
        ammoRemain -= ammoToFill;

        // 총의 현재 상태를 발사 준비된 상태로 변경
        state = State.Ready;
    }
}

 

'유니티좀비게임 > 총과슈터' 카테고리의 다른 글

Player Shooter 스크립  (0) 2023.04.28
총 게임오브젝트  (0) 2023.04.28
인터페이스  (0) 2023.04.28

총과 총을 쏘는 슈터를 분리해 보겠습니다. 이유는 플레이어를 변경없이 총만 변경가능하기 때문입니다.

 

건(Gun)오브젝트 준비하기

플레이어에 총을 배치할 기준점이 될 자식 게임오브젝트 Pivot을 추가하겠습니다.

 

Gun  배치지점(Pivot) 생성

하이라키에서 플레이어를 선택하고  마우스 우클릭 > Create Empty 클릭

생성된 자식 게임 오브젝트의 이름을 Gun Pivot으로, 위치를 (0.5,0,0)로 변경

 

Gun 프리팹 생성

하이라키에서 +를 눌러 Create Empty> 이름을 Gun으로 변경,

Gun을 선택후 위를 우클릭후 3D Object>Cylinder 이름을 Barrel로 변경, Scale(0,2,0.5,0,2) , Rotation Y 90

 

Gun에 라인렌더러 추가하기

3. Gun에 탄알의 궤적을 그리기 위해 라인렌더러 추가하기

3.1 Gun 게임 오브젝트에 Line Renderer 컴포넌트 추가

3.2. Line Renderer 컴포넌트 체크 해제하여 비활성화

3.3 Positions 탭 펼치기>Size 0, Width를 0.02로 변경

3.4 Materials 탭 펼치기 > Element 0에 Bullet 머터리얼 할당 (Element 0 옆의 선택버튼 클릭) 선택창에서 Bullet을 선택 

Bullet.mat
0.00MB

3.5 Cast Shadows off로 변경, Receive Shadows 체크해제

 

라인 렌더러는 포지션필드에 지정된 점 사이를 이어 선을 그립니다. 지금은 선을 그리지 못하도록 Size를 0으로 하고 비활성화했습니다.

하이라키에서 Gun 오브젝트를 선택후 Add Component> Audio Source 추가 Play On Awake 언체크 Audio Clip연결

Gun Shoot.wav
0.16MB

Prefabs에서 MuzzleFlashEffect와 ShellEjectEffect를 끌어다 연결

MuzzleFlashEffect.prefab
0.11MB
ShellEjectEffect.prefab
0.21MB

완성된 Gun을 끌어다 프로젝트뷰에 놓으면 프리팹으로 만들어지고 하이라키의 이름이 파란색으로 변경된다.

Gun을 Gun Pivot의 자식으로 연결

'유니티좀비게임 > 총과슈터' 카테고리의 다른 글

Player Shooter 스크립  (0) 2023.04.28
GunData 스크립트  (0) 2023.04.28
인터페이스  (0) 2023.04.28

C# 인터페이스

클래스와 비슷하게 인터페이스는 메서드, 속성, 이벤트, 인덱서 등을 갖지만, 인터페이스는 이를 직접 구현하지 않고 단지 정의(prototype definition)만을 갖는다. 즉, 인터페이스는 추상 멤버(abstract member)로만 구성된 추상 Base 클래스(abstract base class)와 개념적으로 유사하다. 클래스가 인터페이스를 가지는 경우 해당 인터페이스의 모든 멤버에 대한 구현(implementation)을 제공해야 한다.

한 클래스는 하나의 Base 클래스만을 가질 수 있지만, 인터페이스는 여러 개를 가질 수 있다. 아래의 예를 보면, MyConnection 이라는 클래스는 Component 라는 하나의 Base 클래스와 IDbConnection, IDisposable이라는 2개의 인터페이스를 가지고 있음을 알 수 있다.
public class MyConnection : Component, IDbConnection, IDisposable
{
   // IDbConnection을 구현
   // IDisposable을 구현
}

C# 인터페이스의 정의

인터페이스는 C# 키워드 interface를 사용하여 정의한다. 인터페이스 정의 시에는 (메서드와 같은) 내부 멤버들에 대해 public과 같은 접근 제한자를 사용하지 않는다.
예를 들어, 아래 예제에서 CompareTo() 메서드 앞에 public 을 쓸 수 없다.


public interface IComparable
{
   // 멤버 앞에 접근제한자 사용 안함
   int CompareTo(object obj);
}
 

C# 인터페이스의 구현

C# 클래스가 인터페이스를 갖는 경우 인터페이스의 모든 멤버에 대한 구현을 제공해야 한다. C# 에서는 인터페이스로부터 직접 new를 사용하여 객체를 생성할 수 없다. 아래의 클래스는 IComparable이라는 인터페이스를 갖는 경우로서 IComparable.CompareTo() 메서드를 구현한 예이다.

public class MyClass : IComparable
{
   private int key;
   private int value;

   // IComparable 의 CompareTo 메서드 구현
   public int CompareTo(object obj)
   {
      MyClass target = (MyClass)obj;
      return this.key.CompareTo(target.key);
   }
}
 

C# 인터페이스의 사용

C# 실무에서 클래스와 인터페이스를 잘 정의하고 사용하는 것은 매우 중요하다. 비지니스를 객체지향 프로그래밍으로 디자인하고 구현하는데 가장 중요한 핵심이기 때문이다. 자연스럽게 .NET Framework도 상당히 많은 인터페이스를 구현했으며, 거의 모든 영역에서 흔히 사용되고 있다.

다음 코드는 IDbConnection이라는 인터페이스를 사용하는 예제이다. 이 코드에서 GetDbConnection() 메서드는 시스템의 구성파일로부터 DB타입과 Connection String을 받아와 해당 DB타입에 맞는 데이타베이스 Connection을 리턴한다. GetDbConnection()가 DB connection을 리턴할 때 IDbConnection을 리턴하고 있는데, 이 때문에 이 메서드를 사용하는 클라이언트에서는 어떤 DB를 사용하든지 상관없이 모든 DB 클래스에 공통적으로 구현된 IDbConnection 멤버들을(메서드, 속성등) 사용할 수 있게 된다.


public void Run()
{
   // 인터페이스 사용하기 때문에
   // 특정 DB Connection을 신경 쓸 필요가 없다
   IDbConnection dbCon = GetDbConnection();
   dbCon.Open();
   if (dbCon.State == ConnectionState.Open)
   {
      dbCon.Close();
   }
}

// IDbConnection 인터페이스를 리턴
public IDbConnection GetDbConnection()
{
   IDbConnection dbConn = null;
   string cn = ConfigurationManager.AppSettings["Connection"];
   switch (ConfigurationManager.AppSettings["DbType"])
   {
      case "SQLServer":
         dbConn = new SqlConnection(cn);
         break;
      case "Oracle":
         dbConn = new OracleConnection(cn);
         break;
      case "OleDB":
         dbConn = new OleDbConnection(cn);
         break;         
   }
   return dbConn;
}

인터페이스와 느슨한 커플링

게임속 플레이어는 다양한 오브젝트를 공격할 수 있습니다. 공격받은 오브젝트에 따라 반응이 서로 다릅니다. if문으로 일일히 타입을 검사하고 처리할 수 있습니다.

세련된 방법은 IDmageable 인터페이스를 사용 추상화하여 다룰수 있습니다.

인터페이스란 외부와 통신하는 공개 통로이며, 통로의 규격입니다. 인터페이스는 통로의 규격은 강제하지만 그 아래에 어떤일이 일어날지는 결정하지 않습니다.

IItem은 아이템 클래스들이 상속하는 인터페이스 입니다.(인터페이스앞에 I를 추가하는 것이 관례입니다.)

IItem.cs 스크립트를 다음과 같이 만듭니다.

using UnityEngine;

// 아이템 타입들이 반드시 구현해야하는 인터페이스
public interface IItem {
    // 입력으로 받는 target은 아이템 효과가 적용될 대상
    void Use(GameObject target);
}
Use() 메서드는 아이템을 사용하는 메서드입니다. 아이템을 사용할 대상을 GameObject타입으로 입력받습니다. 선언만 하고 구현은 자신을 상속하는 클래스에 맡깁니다. 외부에서 사용할수 있게 public으로 선언합니다.

IItem을 상속해서 만든 두 아이템을 클래스를 살펴봅시다.

public class AmmoPack : MonoBehavior, IItem {
    public int ammo = 30;
    public void Use(GameObject target) {
          Debug.Log("탄알이 증가했다 : "+ammo);
    }
}
public class HealthPack : MonoBehavior, IItem {
    public int health = 30;
    public void Use(GameObject target) {
          Debug.Log("체력을 회복했다 : "+ health);
    }
}

IItem을 상속한 클래스는 Use()메서드를 아이템의 역할에 따라 반드시 구현해야 합니다.

느슨한 커플링

IItem인터페이스를 상속한 클래스는 많아지고 충돌시 이들 모두를 검사하여 개별 처리하는 것보다

private void OnTriggerEnter(Collider other) {
    AmmoPack ammoPack = other.GetComponent<AmmoPack>();
    if(ammoPack != null) {
        ammoPack.Use();
    }
    HealthPack HealthPack = other.GetComponent<HealthPack>();
    if (HealthPack != null) {
        HealthPack.Use();
    }
}

 다음과 같이 IItem타입으로 처리가능합니다.

void OnTriggerenter(Collider other) {
    IItem item = other.GetComponent<IItem>();
    if(item != null) {
        item.Use();
    }
}

IDamageable : 공격당할 수 있는 모든 대상은 IDamageable인터페이스를 상속해야 합니다. IDamageable.cs 스크립트를 다음과 같이 만듭니다. 

using UnityEngine;
// 데미지를 입을 수 있는 타입들이 공통적으로 가져야 하는 인터페이스
public interface IDamageable {
    // 데미지를 입을 수 있는 타입들은 IDamageable을 상속하고 OnDamage 메서드를 반드시 구현해야 한다
    // OnDamage 메서드는 입력으로 데미지 크기(damage), 맞은 지점(hitPoint), 맞은 표면의 방향(hitNormal)을 받는다
    void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal);
}

'유니티좀비게임 > 총과슈터' 카테고리의 다른 글

Player Shooter 스크립  (0) 2023.04.28
GunData 스크립트  (0) 2023.04.28
총 게임오브젝트  (0) 2023.04.28

+ Recent posts