일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- TiL
- KRAFTON JUNGLE
- 크래프톤 정글
- 크래프톤정글
- 알고리즘
- 백준
- c#
- 4기
- 연결리스트
- 오블완
- 크래프톤 정글 4기
- 전쟁-전투
- User Stack
- pintos
- kraftonjungle
- 다익스트라
- 핀토스
- 유니티
- 티스토리챌린지
- project3
- 추상클래스와인터페이스
- 크래프톤정글4기
- 알고리즘수업-너비우선탐색2
- anonymous page
- 이벤트 함수 실행 순서
- C
- 파이썬
- 네트워크
- Unity
- BFS
- Today
- Total
말감로그
[유니티C# 스크립팅 마스터하기] - 4장 이벤트 주도적 프로그래밍 본문
이벤트 주도적 프로그래밍을 통해 성능 향상을 얻을 수 있다!
이벤트
: 행동들 간에 중요한 관계를 맺고, 이러한 연결이나 결합을 이벤트라고 부르는데, 각각의 개별적인 연결은 하나의 이벤트가 된다.
이벤트 주도적 프로그래밍
: 이벤트 주도적 프로그래밍은 게임 속 대부분의 사건들이 이벤트의 예라는 것을 알고서 일반적인 개념으로 이해하는 것으로부터 시작된다.
즉, 이벤트의 개념은 정해진 시간에 일어나는 것뿐만 아니라 특정 시간에 발생하는 특정 이벤트를 포함하는 것이다.
Update와 이벤트
: 게임 월드는 이벤트와 응답으로 조직된 온전한 시스템이라고 할 수 있다.
이런 식의 월드에서 Update 함수가 매 프레임 동작을 진행하는 방식에 의존하는 대신 이벤트를 사용하는 것이 성능을 향상시키는 데에 어떤 도움을 주는지 의문이 생길 수 있는데, 해답은 이벤트의 잦은 호출을 줄이는 것이다.
아래의 예제는
1. 체력이 떨어졌을 때는 구급 상자를 찾아 체력을 회복할 수 있도록 적의 체력을 추적해야 한다.
2. 탄약이 바닥났을 때는 적으로 하여금 탄약을 더 찾아 모으고 언제 시야에 들어온 플레이어에게 발사할지 합리적으로 판단할 수 있도록 탄약의 양을 추적해야 한다.
void Update()
{
// 적의 체력을 확인한다. 죽었는가
if (Health < 0)
{
// 죽었다면 죽는 동작 수행
Die();
return;
}
// 체력이 낮은 상태인지 확인
if (Health <= 20)
{
// 체력이 낮은 상태이면 구급 상자 찾는다.
RunAndFindHealthRestoure();
return;
}
// 탄약이 떨어졌는가
if (Ammo < 0)
{
// 탄약을 좀 더 찾아본다.
SearchMore();
return;
}
// 체력과 탄약이 충분하다. 플레이어가 보이면 발사
if (HaveLineOfSight)
{
FireAtPlayer();
}
}
-> 위 예제 코드는 많은 조건들로 가득 찬 무거운 Update 함수를 보여준다.
본질적으로 Update 함수는 이벤트 처리 및 응답을 하나로 합치도록 이끌기 때문에 불필요하게 비싼 처리 과정을 만들어낸다.
아래의 코드는 C# 프로퍼티를 포함하는 리팩토링된 적 클래스로서 훨씬 작아진 Update 함수를 볼 수 있다.
using UnityEngine;
public class EnemyObject : MonoBehaviour
{
private int _health = 100;
private int _ammo = 50;
// private 변수에 접근하기 위한 C# 접근자
public int Health
{
get { return _health; }
set
{
// 체력 값을 0-100 범위에 맞춘다.
_health = Mathf.Clamp(value, 0, 100);
// 죽었는지 검사
if (_health <= 0)
{
OnDead();
return;
}
// 체력을 검사하고 필요하면 이벤트를 발생시킨다.
if (_health <= 20)
{
OnHealthLow();
return;
}
}
}
public int Ammo
{
get { return _ammo; }
set
{
//탄약 값을 0-50 범위에 맞춘다.
_ammo = Mathf.Clamp(value,0,50);
// 탄약이 떨어졌는지 검사
if (_ammo <= 0)
{
// 탄약 소진 이벤트 호출
OnAmmoExpired();
return;
}
}
}
void Update()
{
}
// 이 이벤트는 체력이 낮을 때 호출된다.
void OnHealthLow()
{
// 여기에서 이벤트 응답 처리
}
// 이 이벤트는 적이 죽었을 때 호출된다.
void OnDead()
{
// 여기에서 이벤트 응답 처리
}
// 탄약 소진 이벤트
void OnAmmoExpired()
{
// 여기에서 이벤트 응답 처리
}
}
이벤트 주도적인 설계를 함으로써 성능 최적화 및 청결한 코드 유지가 가능해 진다.
이벤트 관리 ( EventListener / EventPoster / EventManager )
이벤트 주도적 프로그래밍을 이용하면 작업이 훨씬 용이해지는 면이 있다.
이벤트가 발생했을 때 모든 오브젝트에게 모든 종류의 이벤트에 대해 선택적으로 수신하도록 하여, 오브젝트에 이벤트가 발생했을 때 오브젝트가 쉽게 알 수 있게끔 하는 것이다.
바로, EventManager 클래스를 통해 오브젝트가 특정 이벤트를 수신할 수 있도록 하는 것이다.
이 시스템은 다음의 세가지 주요한 개념에 기반한다.
- 이벤트 리스너 (EventListener) : 자신이 발생시킨 이벤트를 포함한 어떤 이벤트가 발생하면 알기를 원하는 모든 오브젝트를 리스너라고 부른다.
실질적으로 대부분의 오브젝트는 하나 이상의 이벤트에 대한 리스너이다.
- 이벤트 포스터 (EventPoster) : 리스너와 반대로 오브젝트가 이벤트 발생을 알아차린 경우, 이 오브젝트는 다른 모든 리스너가 알 수 있게 이벤트에 대해 알려야 한다.
하지만, 설명한 의미의 진정한 포스터(발신자)가 되려면 오브젝트가 전역 레벨에서 이벤트를 발생시켜야 한다.
- 이벤트 매니저 (EventManager) : 여러 레벨에 걸쳐 계속 유지되며 전역적으로 접근이 가능한, 가장 중요한 싱글턴(EventManager) 오브젝트가 있다. 이 오브젝트는 리스너를 포스터에게 실질적으로 연결하는 역할을 한다.
이벤트 매니저는 포스터가 보낸 알림을 받고 적합한 모든 리스너에게 이벤트 형식으로 즉시 알림을 발생시킨다.
인터페이스를 통한 이벤트 관리
이벤트 처리 시스템의 첫째 구성 요소는 바로 특정 이벤트가 발생했을 때 알림을 받는 리스너이다.
리스너는 EventManager에 하나 혹은 그 이상의 이벤트에 대해 스스로를 리스너로 등록해야 동작하게 된다.
그런 다음, 실제로 이벤트가 발생하게 되면 함수 호출을 통해 리스너에게 바로 알림이 오게 된다.
C#에서 인터페이스는 껍데기뿐인 기본 추상 클래스와 같은 것이다.
인터페이스는 클래스처럼 메소드와 함수를 하나의 템플릿과 같은 단위의 집합으로 한데 모으는 역할을 한다.
하지만, 클래스와는 달리 인터페이스는 함수의 이름, 반환 형식, 파라미터와 같은 함수 원형을 선언하는 것만 허용된다.
인터페이스의 역할은 각각의 파생된 클래스의 특정 형식에 대해 알 필요 없이 다형성을 통해 다른 오브젝트들로 하여금 함수를 호출할 수 있게끔 하는 것이다.
인터페이스를 이용하여 리스너 오브젝트를 만들면 모든 오브젝트가 이벤트 리스너가 될 수 있는 능력을 가질 수가 있다.
pubic enum EVENT_TYPE
{
GAME_INIT,
GAME_END,
AMMO_CHANGE,
HELATH_CHANGE,
DEAD
};
//리스너 클래스에서 구현될 리스너 인터페이스
public interface IListener
{
// 이벤트가 발생할 때 리스너에서 호출할 함수
void OnEvent(EVENT_TYPE Event_Type, Component Sender, object Param = null);
}
IListener 인터페이스를 사용함으로써 클래스 상속을 사용하는 모든 오브젝트를 리스너로 만들 수 있는 능력을 가지게 되었다.
즉, 어떤 오브젝트든 스스로를 리스너로 선언하면 이벤트를 수신하는 것이 가능해진다.
using UnityEngine;
public class MyCustomListener : MonoBehaviour , IListener
{
void Start();
void Update();
// 이벤트 수신을 위해 OnEvent 함수를 구현한다.
public void OnEvent(EVENT_TYPE Event_Type, Component Sender, Object Param = null)
{
}
}
이벤트 매니저 만들기
: 이벤트가 실제로 발생했을 때 리스너들에게 이벤트를 호출하는 것은 이벤트 매니저의 의무이다.
EventManager 클래스는 지속되는 싱글턴 오브젝트로, 씬 안의 빈 게임 오브젝트에 붙이고 나면 정적 인스턴스 속성을 통해 모든 오브젝트에게 직접 접근할 수 있게 된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 리스너들에게 이벤트를 보내기 위한 이벤트 매니저 싱글턴
// IListener 구현과 함께 작동한다.
public class EventManager : MonoBehaviour
{
#region C# 프로퍼티
// 인스턴스에 접근하기 위한 Public 프로퍼티
public static EventManager Instance
{
get { return instance; }
set { }
}
#endregion
#region 변수들
// 이벤트 매니저 인스턴스에 대한 내부 참조(싱글턴 디자인 패턴)
private static EventManager instance = null;
// 리스너 오브젝트의 배열(모든 오브젝트가 이벤트 수신을 위해 등록되어 있다.)
private Dictionary<EVENT_TYPE, List<IListener>> Listeners = new Dictionary<EVENT_TYPE, List<IListener>>();
#endregion
#region 메소드
// 시작 시에 초기화를 위해 호출된다.
private void Awake()
{
// 인스턴스가 없는 경우 현재 인스턴스를 할당한다
if(instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject); // 씬에서 빠져나갈 때 파괴되는 것을 방지한다.
}
else
{
// 인스턴스가 이미 있다면 현재 인스턴스를 파괴한다. 싱글턴 오브젝트가 되어야 한다.
DestroyImmediate(this);
}
}
// 리스너 배열에 지정된 리스너 오브젝트를 추가하기 위한 함수
public void AddListener(EVENT_TYPE Event_Type, IListener Listener)
{
// 이 이벤트를 수신할 리스너의 리스트
List<IListener> ListenList = null;
// 이벤트 형식 키가 존재하는지 검사한다. 존재하면 이것을 리스트에 추가한다.
if (Listeners.TryGetValue(Event_Type, out ListenList))
{
ListenList.Add(Listener);
return;
}
// 아니면 새로운 리스트를 생성한다
ListenList = new List<IListener>();
ListenList.Add(Listener);
Listeners.Add(Event_Type, ListenList); // 내부의 리스너 리스트에 추가한다.
}
// 이벤트를 리스너에게 전달하기 위한 함수
public void PostNotification(EVENT_TYPE Event_Type, Component Sender , Object Param = null)
{
// 모든 리스너에게 이벤트에 대해 알린다.
// dl dlqpsxmfmf tntlsgksms fltmsjemfdml fltmxm
List<IListener> ListenList = null;
// 이벤트 항목이 없으면 ,알릴 리스너가 없으므로 끝낸다.
if (!Listeners.TryGetValue(Event_Type, out ListenList))
return;
// 항목이 존재한다. 이제 적합한 리스너에게 알려준다.
for(int i = 0; i < ListenList.Count; i++)
{
// 오브젝트가 null이 아니면 인터페이스를 통해 메시지를 보낸다.
if (!ListenList[i].Equals(null))
ListenList[i].OnEvent(Event_Type, Sender, Param);
}
}
// 이벤트 종류와 리스너 항목을 딕셔너리에서 제거한다.
public void RemoveEvent(EVENT_TYPE Event_Type)
{
// 딕셔너리의 항목을 제거한다.
Listeners.Remove(Event_Type);
}
// 딕셔너리에서 쓸모 없는 항목들을 제거 한다.
public void RemoveRedundancies()
{
// 새 딕셔너리 생성
Dictionary<EVENT_TYPE, List<IListener>> TmpListeners = new Dictionary<EVENT_TYPE, List<IListener>>();
// 모든 딕셔너리 항목을 순회한다.
foreach(KeyValuePair<EVENT_TYPE , List<IListener>> Item in Listeners)
{
// 리스트의 모든 리스너 오브젝트를 순회하며 null 오브젝트를 제거한다.
for(int i = Item.Value.Count -1; i >= 0; i--)
{
//null 이면 항목을 지운다.
if (Item.Value[i].Equals(null))
Item.Value.RemoveAt(i);
}
}
// 새로 최적화된 딕셔너리로 교체한다
Listeners = TmpListeners;
}
// 씬이 변경될 때 호출된다. 딕셔너리를 청소한다
private void OnLevelWasLoaded(int level)
{
RemoveRedundancies();
}
#endregion
}
이벤트 매니저 활용
씬 하나에 리스너와 포스터가 있는 실제 환경에서 동작할 EventManager 클래스를 어떻게 넣을 수 있는지 살펴보자.
먼저, 이벤트를 수신하기 위해 리스너는 EventManager 싱글턴 인스턴스에 등록되어야 한다.
보통 이 과정은 Start함수와 같이 최초 시점에 한번만 이루어진다.
void Start()
{
// 체력 변동 이벤트를 수신하기 위해 스스로를 리스너로 등록해야 한다.
EventManager.Instance.AddListener(EVENT_TYPE.HEALTH_CHANGE, this);
}
하나 이상의 리스너를 등록하고 나면 예제 코드와 같이 오브젝트가 발생한 이벤트를 EventManager에 알릴 수 있게 된다.
public int Health
{
get { return _health;}
set
{
_health = Mathf.Clamp(value, 0, 100);
//알림을 보낸다 - 체력 값이 변겨되었음
EventManager.Instance.PostNotification(EVENTTYPE.HEALTH_CHANGE, this, _health);
}
}
마지막으로 이벤트에 대한 알림을 보내고 나면 연관된 모든 리스너가 EventManager를 통해 업데이트 된다.
구체적으로 EventManager는 각 리스너의 OnEvent 함수를 호출해 다음 예제와 같이 필요에 따라 이벤트 데이터를 분석하고 응답할 기회를 리스너에게 제공한다.
//이벤트가 발생할 때 호출
public void OnEvent(EVENT_TYPE Event_Type, Component Sender, Object Param = null)
{
switch(Event_Type)
{
case EVENT_TYPE.HEALTH_CHANGE:
OnHealthChange(Sender, (int)Param);
break;
}
}
델리게이트를 이용한 대안
: 인터페이스는 이벤트 처리 시스템을 구현하는 효율적이고 깔끔한 방법이지만, C# 델리게이트라는 기능을 이용할 수도 있다.
기본적으로, 함수를 생성해 이 함수의 참조를 변수 안에 저장하는 것이 가능하다.
이 변수를 통해 참조 형식의 변수로서 함수를 취급하는 것이 가능해진다.
즉, 델리게이트를 이용하면 함수의 참조를 저장했다가 나중에 이 변수를 이용해 함수를 부를 수 있게 된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 가능한 게임 이벤트를 모두 열거한다
// 더 많은 이벤트가 리스트에 추가된다.
public enum EVENT_TYPE
{
GAME_INIT,
GAME_END,
AMMO_CHANGE,
HEALTH_CHANGE,
DEAD
};
// 리스너들에게 이벤트를 보내기 위한 이벤트 매니저 싱글턴
// IListener 구현과 함께 작동한다.
public class EventManager : MonoBehaviour
{
#region C# 프로퍼티
// 인스턴스에 접근하기 위한 Public 프로퍼티
public static EventManager Instance
{
get { return instance; }
set { }
}
#endregion
#region 변수들
// 이벤트 매니저 인스턴스에 대한 내부 참조(싱글턴 디자인 패턴)
private static EventManager instance = null;
// 이벤트를 위한 델리게이트 형식을 선언한다.
public delegate void OnEvent(EVENT_TYPE Event_Type, Component Sender, Object Param = null);
// 리스너 오브젝트의 배열(모든 오브젝트가 이벤트 수신을 위해 등록되어 있다.)
private Dictionary<EVENT_TYPE, List<OnEvent>> Listeners = new Dictionary<EVENT_TYPE, List<OnEvent>>();
#endregion
#region 메소드
// 시작 시에 초기화를 위해 호출된다.
private void Awake()
{
// 인스턴스가 없는 경우 현재 인스턴스를 할당한다
if(instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject); // 씬에서 빠져나갈 때 파괴되는 것을 방지한다.
}
else
{
// 인스턴스가 이미 있다면 현재 인스턴스를 파괴한다. 싱글턴 오브젝트가 되어야 한다.
DestroyImmediate(this);
}
}
// 리스너 배열에 지정된 리스너 오브젝트를 추가하기 위한 함
public void AddListener(EVENT_TYPE Event_Type, OnEvent Listener)
{
// 이 이벤트를 수신할 리스너의 리스트
List<OnEvent> ListenList = null;
// 이벤트 형식 키가 존재하는지 검사한다. 존재하면 이것을 리스트에 추가한다.
if (Listeners.TryGetValue(Event_Type, out ListenList))
{
ListenList.Add(Listener);
return;
}
// 아니면 새로운 리스트를 생성한다
ListenList = new List<OnEvent>();
ListenList.Add(Listener);
Listeners.Add(Event_Type, ListenList); // 내부의 리스너 리스트에 추가한다.
}
// 이벤트를 리스너에게 전달하기 위한 함수
public void PostNotification(EVENT_TYPE Event_Type, Component Sender , Object Param = null)
{
// 모든 리스너에게 이벤트에 대해 알린다.
// 이 이벤트를 수신하는 리스너들의 리스트
List<OnEvent> ListenList = null;
// 이벤트 항목이 없으면 ,알릴 리스너가 없으므로 끝낸다.
if (!Listeners.TryGetValue(Event_Type, out ListenList))
return;
// 항목이 존재한다. 이제 적합한 리스너에게 알려준다.
for(int i = 0; i < ListenList.Count; i++)
{
// 오브젝트가 null이 아니면 인터페이스를 통해 메시지를 보낸다.
if (!ListenList[i].Equals(null))
ListenList[i](Event_Type, Sender, Param);
}
}
// 이벤트 종류와 리스너 항목을 딕셔너리에서 제거한다.
public void RemoveEvent(EVENT_TYPE Event_Type)
{
// 딕셔너리의 항목을 제거한다.
Listeners.Remove(Event_Type);
}
// 딕셔너리에서 쓸모 없는 항목들을 제거 한다.
public void RemoveRedundancies()
{
// 새 딕셔너리 생성
Dictionary<EVENT_TYPE, List<OnEvent>> TmpListeners = new Dictionary<EVENT_TYPE, List<OnEvent>>();
// 모든 딕셔너리 항목을 순회한다.
foreach(KeyValuePair<EVENT_TYPE , List<OnEvent>> Item in Listeners)
{
// 리스트의 모든 리스너 오브젝트를 순회하며 null 오브젝트를 제거한다.
for(int i = Item.Value.Count -1; i >= 0; i--)
{
//null 이면 항목을 지운다.
if (Item.Value[i].Equals(null))
Item.Value.RemoveAt(i);
}
}
// 새로 최적화된 딕셔너리로 교체한다
Listeners = TmpListeners;
}
// 씬이 변경될 때 호출된다. 딕셔너리를 청소한다
private void OnLevelWasLoaded(int level)
{
RemoveRedundancies();
}
#endregion
}
MonoBehaviour 이벤트
- 마우스와 탭 이벤트
이 이벤트에는 OnMouseDown, OnMouseEnter, OnMouseExit가 포함된다.
이 이벤트들의 성공 여부는 마우스 이벤트가 검출될 충돌체 컴포넌트가 오브젝트의 크기와 유사한 크기로 설정되었는지 여부에 달려 있다.
오브젝트에 충돌체가 붙어있지 않다면 마우스 이벤트가 발생하지 않는다는 얘기이다.
하지만, 충돌체가 붙어있지만 마우스 이벤트가 발생하지 않을 때가 있는데, 활성화된 카메라의 현재 시점에서 클릭을 받길 원하는 오브젝트를 다른 오브젝트(충돌체를 가진)가 가리고 있는 경우이다.
-> 해결하려면 앞쪽에 위치한 오브젝트들에게 IgnoreRaycast 레이어를 할당해 레이캐스트 물리 연산을 무시하도록 하면 된다.
애플리케이션 포커스 상태에 따른 멈추기
- OnApplicationQuit : 게임이 종료되기 전, 씬과 씬의 내용물이 실직적으로 파괴되기 전 시점에 모든 오브젝트들에게 보내진다.
iOS 기기에서는 OnApplicationQuit이 호출되지 않는다.
iOS 에서는 보통 애플리케이션을 종료하거나 끄는 대신에 사용자가 다른 일을 하는 동안 중단시켰다가 다시 돌아왔을 때 계속 실행하는 식으로 동작하기 때문이다.
>> Edit > Project Settings > Player - Settings for iOS - Other Settings - Exit on Suspend 체크 박스 활성화 하면 동작 가능
- OnApplicationFocus : 데스크톱 컴퓨터에서 멀티 태스킹을 하던 중에 게임 창이 비활성화되었을 때처럼, 게임이 포커스를 잃었을 때 씬의 모든 오브젝트에게 보내지는 이벤트다.
이 이벤트는 멀티 플레이어 게임의 공유된 월드상에서 동작이나 이벤트가 계속 일어날 때, 플레이어가 적극적으로 참여하지 않는 상황이더라도 중요한 게임 내 이벤트로서 작용할 수 있다.
- OnApplicationPause : 절대적 중지의 경우에는 게임의 모든 활동과 이벤트가 완전히 중단되고, 이때 시간이 흐르지 않고 아무것도 진행되지 않는다. 상대적 중지의 경우에는 반대로 가장 일반적인 개념으로 게임이 스스로 중지 상태에 있다는 것을 알 수 있어서 게임 내 이벤트와 같은 이벤트는 멈추고 GUI 상호작용 및 유저 입력과 같은 이벤트는 멈추지 않을 수 있다.
OnApplicationPause는 전자의 개념이다.
PlayerSettings > Resolution - Run In Background 옵션이 활성화되지 않았을 때 데스크톱에서만 호출된다.
이 옵션을 비활성화하면 창의 포커스를 잃었을 때 데스크톱 게임이 자동으로 중지된다.
'Unity' 카테고리의 다른 글
[유니티 C# 스크립팅 마스터하기] - 3장 싱글턴과 정적 멤버, 게임 오브젝트와 월드 (0) | 2025.01.06 |
---|---|
[유니티 C# 스크립팅 마스터하기] - 2장 디버깅 (0) | 2025.01.03 |
[유니티 C# 스크립팅 마스터하기] - 1장 유니티 C# 복습 (0) | 2025.01.02 |
[Unity] 시네머신으로 카메라 이동 제한하기 (1) | 2024.12.25 |
[Unity] Device Simulator (디바이스 시뮬레이터) (0) | 2024.12.23 |