Unity - 이벤트 및 실습

땡구의 개발일지·2025년 4월 18일

Unity마스터

목록 보기
11/78
post-thumbnail

C#델리게이트이벤트가 유니티 에서는 어떤식으로 구현되어 있는지 알아보자

기존의 C# 이벤트 방식

public event Action OnDied;

private void Dead()
{

}
void Main()
{
    OnDied += Dead;
    OnDied.Invoke();
}

유니티 이벤트

  • 유니티에서는 C#의 이벤트를 편하게 구현할 수 있게 기능을 제공한다

UnityEngine.Events

  • 네임스페이스 UnityEngine.Events를 추가해야된다
using UnityEngine.Events;

UnityEvent

  • 유니티에서 이벤트를 쓰려면 UnityEvent를 사용한다. 클래스로 구현된 이벤트다
using UnityEngine.Events;

public class eventTester : MonoBehaviour
{
    public GameObject go;
    public GameObject go1;
    public UnityEvent myEvent;

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.A))
        {
            myEvent.Invoke();
        }
    }
}
  • 이벤트를 조건에 맞춰서 실행하려면 동일하게 Invoke()를 쓴다

사용법

  • 먼저 작성한 컴포넌트를 게임 오브젝트에 추가한다
  • 드래그 & 드랍으로 함수를 실행할 게임 오브젝트를 등록할 수 있다
  • + 버튼, - 버튼으로 함수를 추가, 삭제 할 수 있다

함수 추가

  • No Fuction을 클릭해서 수행할 함수를 선택할 수 있다

  • 유니티는 마우스로 이벤트에 함수를 추가할 수 있다

사용처

  • 게임의 UI 에서 많이 쓰인다

유니티이벤트 매개변수

  • 델리게이트 제네릭 액션과 비슷하게 매개변수를 넣으면, 매개변수 타입이 동일한 함수를 추가,삭제할 수 있다
public UnityEvent<int> myEvent;
  • 다만 다른 점이라면 int로 지정했던 매개변수를 유니티 에디터 창에서 자동으로 float로도 넣을 수 있다. 그 반대도 마찬가지. 플레이 중에도 매개변수 값을 바꿔가면서 결과를 실시간으로 볼 수있다
  • 유니티이벤트C#액션이다. 발생하는 것을 목표로 하기 때문이다

단점

  • 매개변수에 자율성이 있지만, 직렬화 기술의 한계로 인터페이스, 추상클래스, 일반화 클래스는 담을 수 없다

  • 에디터에서 추가하는 경우이기 때문에, 스크립트 에서는 어디서 추가가 되었는지 확인이 안됨. 즉, 디버깅이 더 어려움

  • 아무래도 마우스 클릭으로 게임 만드는 것보다 스크립트로 만드는게 편해지는 시점이 온다. 그런 때가 오면 유니티이벤트가 번거롭게 느껴진다

    유니티액션

  • UnityAction은 델리게이트 제네릭의 Action과 완벽히 동일하다

  • 이것은 기존에 유니티가 여러 언어를 지원하면서 옵저버 패턴으로 자체적으로 구현한 Action이다. 이후 C#으로 정착하면서 생긴 사소한 헤프닝이다

    public event UnityAction uAction;
    public event Action action;

실습

실습 1) 연사와 차지 발사가 가능한 탱크 구현

  • 연사 기능

    • Space 키를 누르는 중에는 일정한 간격으로 탄환을 발사하는 기능을 구현한다
    • Space 키를 누르지 않을 때는 경우 연사 기능을 멈추고 탄환을 발사하지 않는다
  • 차징 기능

    • Space 키를 누르는 중에 탄환을 멀리 날리기 위한 차징을 하는 기능을 구현한다
    • Space 키를 누르던 시간에 따라 탄환의 속도를 더욱 높게 날릴 수 있도록 구현한다.
  • 위 두 기능은 둘 다 업데이트에서가 아닌 코루틴으로 구현한다
public class coroutineShooter : MonoBehaviour
{
    // 기본 발사 세팅
    public GameObject bulletPrefab;
    public Transform muzzlePoint;
    private GameObject bulletOut;
    [Range(1, 50)]
    public float fireSpeed;

    // 연사, 차지 전환
    private bool fireMode = false;

    // 연사 발사 기능
    private Coroutine rapidCoroutine;
    [SerializeField] float rapidFireSpeed;

    // 버튼 연타 막기 위한 쿨타임
    [SerializeField] float coolTime;
    private WaitForSeconds rapidCoolTime;

    // 차지 기능
    private Coroutine chargeCoroutine;
    [SerializeField] float chargeSpeed;
    private WaitForSeconds chargingSpeedDelay;
    private float bulletSpeed;

    private void Awake()
    {
        rapidCoolTime = new WaitForSeconds(coolTime);
        chargingSpeedDelay = new WaitForSeconds(chargeSpeed);
    }
    void Update()
    {

        // 연사 공격
        if (Input.GetKeyDown(KeyCode.Space) && rapidCoroutine == null && fireMode == false)
        {
            rapidCoroutine = StartCoroutine(FireRoutine());
        }
        if (Input.GetKeyUp(KeyCode.Space) && rapidCoroutine != null && fireMode == false)
        {
            StopCoroutine(rapidCoroutine);
            rapidCoroutine = null;
        }

        // 차지 공격
        if (Input.GetKeyDown(KeyCode.Space) && fireMode == true)
        {
            if (chargeCoroutine == null)
            {
                chargeCoroutine = StartCoroutine(ChargeRoutine());
            }
        }

        // 공격 모드 전환 : C키
        if (Input.GetKeyUp(KeyCode.C))
        {
            if (chargeCoroutine != null)
            {
                StopCoroutine(chargeCoroutine);
                chargeCoroutine = null;
            }
            else if(rapidCoroutine != null)
            {
                StopCoroutine(rapidCoroutine);
                rapidCoroutine = null;
            }
            ChangeFireForm();
        }
    }
    void Fire()
    {
        bulletOut = Instantiate(bulletPrefab, muzzlePoint.position, muzzlePoint.rotation);
        Rigidbody rig = bulletOut.GetComponent<Rigidbody>();
        rig.velocity = muzzlePoint.forward * fireSpeed;
    }
    void Fire(float bulletSpeed)
    {
        bulletOut = Instantiate(bulletPrefab, muzzlePoint.position, muzzlePoint.rotation);
        Rigidbody rig = bulletOut.GetComponent<Rigidbody>();
        rig.velocity = muzzlePoint.forward * bulletSpeed;
    }

    // 연사 코루틴
    IEnumerator FireRoutine()
    {
        WaitForSeconds delay = new WaitForSeconds(rapidFireSpeed);
        while (true)
        {
            Fire();
            yield return delay;
        }
    }

    // 차지 코루틴
    IEnumerator ChargeRoutine()
    {
        float timer = 0;
        while (true)
        {
            timer += Time.deltaTime*fireSpeed;
            yield return null;

            if (Input.GetKeyUp(KeyCode.Space)||Input.GetKeyDown(KeyCode.C))
            {
                break;
            }
        }
        // mathf : 수학 함수 기능
        bulletSpeed = Mathf.Clamp(fireSpeed + timer, fireSpeed, fireSpeed * 5);
        Fire(bulletSpeed);
        chargeCoroutine = null;
    }

    // 연사, 차지 전환
    void ChangeFireForm()
    {
        fireMode = !fireMode;
        if (fireMode)
        {
            Debug.Log("차지 공격으로 전환!");
        }
        else if (!fireMode)
        {
            Debug.Log("연사 공격으로 전환!");
        }
    }
}
  • 연사차지 둘 다 하나의 컴포넌트로 구현했다. C키를 누르면 발사 방식이 변경된다
  • 연사, 차지는 bool형 변수 fireMode가 결정한다. C 키를 누르면 true에서 false로, false에서 true로 바뀐다.

고찰

  • 만약 발사 폼이 3개 이상이면 switch 조건문을 쓰거나, 각 발사 방식 마다 컴포넌트로 따로 작성한 다음, 이벤트로 컴포넌트들을 연결해 C키로 지금처럼 변경하는 식으로 해야 될 것 같다.

실습 2) 이벤트 활용

  • 트리거 충돌체로 탱크가 진입할 수 있는 구역을 구현(자기장과 같은 구역)
  • 구역에는 여러 몬스터를 배치하여 대기 시킨다
  • 구역 진입시 구역은 이벤트를 발생시킨다
  • 이벤트에 반응하여 구역에 있는 모든 몬스터들이 탱크를 향해 이동할 수 있도록 구현한다

설계

  1. 트리거 충돌체 구형을 월드에 설치.
  2. 트리거 충돌체의 OnTriggerEnter()이벤트.Invoke()
  3. 레이캐스트에서 만들었던 몬스터추적 컴포넌트를 재활용 해서, 레이캐스트 부분들을 삭제하고 트리거 되면 플레이어를 쫒아가는 식으로 한다.
  4. 구역 바깥으로 나가면 다시 제자리로 가게 구현해본다. 트리거 충돌체의 OnTriggerExit()에 구현한다.

코드 구현

문제 발생

1. 몬스터 추적 컴포넌트에 플레이어를 넣을 수가 없다...

  • 탱크를 프리팹으로 만들어서 해결했다. 그냥 탱크를 프로젝트 프리팹 폴더에다 드래그&드랍 하면 알아서 프리팹으로 만들어준다

    2. 어떻게 해도 함수 실행이 안된다...

  • 플레이어의 정보를 몬스터들에게 이벤트로 넣어줄라고 암만 노력해도 안됐는데, 알고보니 프리팹에다가 함수를 적용해도 씬에 배치된 몬스터들에게는 적용이 되지 않는거였다. 몬스터 한 마리 한 마리 다 따로 함수를 실행시켜야 했던 것이다. 만약 프리팹으로 플레이어 정보를 넣거나, 변수를 변경 시키려면 참조타입으로 해야된다

3. 그래도 안된다... 너무하네 증말. 알고보니 몬스터 게임오브젝트의 스크립트의 함수로 선택을 해야한다

  • 즉, 몬스터 게임오브젝트의 스크립트를 먼저 대상으로 지정하고, 그 스크립트의 함수를 실행한다고 추가해야된다. 몬스터 게임오브젝트도 특정해야되고, 스크립트 또한 특정한 다음 함수를 실행해야 되는거다

4. 플레이어의 초기 위치 정보로만 추적한다

  • 이를 해결하기 위해 업데이트 마다 플레이어 위치를 바꿔줘야 한다

  • 아예 함수를 입력 받는 부분에 플레이어 오브젝트를 넣을 수 있게 구현했다. 그리고 스크립트로 플레이어의 포지션을 따라 갈 수 있게 target = player.transform 했다.

해결

  • 정상적으로 목표대로 모든 동작이 수행된다. 아래는 사용된 컴포넌트들과 그 스크립트 들이다

플레이어

  • 플레이어는 기존 코드들을 그대로 사용했다. 토씨하나 안달라졌다

전기장

  • 트리거 역할을 하는 전기장
public class magneticFieldEvent : MonoBehaviour
{
    public UnityEvent magEvent;
    public GameObject player;
    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            magEvent.Invoke();
        }
    }
    private void OnTriggerExit(Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            magEvent.Invoke();
        }
    }
}
  • 이벤트 실행 용도로만 쓰였다

몬스터

public class MonsterTraceEvent : MonoBehaviour
{
    public Transform target;
    
    float moveSpeed;

    private Vector3 defaultPosition;
    private Quaternion defaultRotation;
    public bool playerInMagField;

    private void Awake()
    {
        defaultPosition = transform.position;
        defaultRotation = transform.rotation;
        moveSpeed = 3f;
        playerInMagField = false;
    }

    void Update()
    {
        
        if (playerInMagField && target != null)
        {
            TracePlayer(target);
        }
        else
        {
            BackToDefault();
            if ((transform.position - defaultPosition).magnitude < 0.1)
            {
                transform.rotation = defaultRotation;
            }
        }
    }

    public void PlayerSettings(GameObject player)
    {
        target = player.transform;
        playerInMagField = !playerInMagField;
    }

    public void TracePlayer(Transform target)
    {
        transform.position = Vector3.MoveTowards(transform.position, target.position, moveSpeed * Time.deltaTime);
        transform.LookAt(target.position);
    }

    public void BackToDefault()
    {
        transform.position = Vector3.MoveTowards(transform.position, defaultPosition, moveSpeed * Time.deltaTime);
        transform.LookAt(defaultPosition);
    }
}
  • 속도moveSpeed는 바깥에서 조절 가능하게 뺄 수 있긴하다. TakeHit 컴포넌트는 이전에 다뤘고, 바뀐게 없기에 다루지 않는다

플레이 화면


profile
개발 박살내자

0개의 댓글