✅ 오늘의 학습 목표
1. C# 중급 (2)
1. Sealed
sealed 키워드는 C#에서 상속을 막을 때 사용하는 키워드다.
이 기능은 주로 더 이상 바뀌면 안 되는 핵심 로직을 보호하고 싶을 때 사용한다.
using UnityEngine;
public abstract class ParentClass : MonoBehaviour
{
//public virtual void Method() // 가상 함수 / 특정 함수를 봉인하고 싶을 때 virtual 사용
//{
// Debug.Log("Parent Method");
//}
public abstract void Method(); // 추상 함수 -> 클래스 앞에 abstract 넣어야 함
}
public class StudySealed : ParentClass
{
public sealed override void Method() // 오버라이드한 함수
{
// base.Method(); // 부모 클래스의 함수 기능을 가져오는 것 / 추상 함수에서 사용할 수 없음
Debug.Log("Override Method");
}
}
//public class ChildClass : StudySealed
//{
// public override void Method() // 상위 클래스에서 sealed로 묶여 있기 때문에 사용 불가
// {
// }
//}
🚨 주의할 점
함수에 sealed를 붙이려면 그 함수가 먼저 오버라이드 가능한 함수여야 한다.
즉, 부모 클래스에서 virtual 또는 abstract로 정의된 함수를 자식 클래스에서 override 한 다음,
그 자식 클래스에서 sealed를 붙여서 더 이상 이 함수는 오버라이드 못 하게 막는 구조이다.
2. Delegate
1. 기본 동작
delegate는 C#에서 함수를 변수처럼 다룰 수 있게 해주는 문법이다.
함수를 매개변수로 넘기거나, 여러 함수를 체인처럼 연결하거나, 이벤트 처리에 활용할 수 있어서 특히 Unity에서 정말 자주 쓰인다. (유니티의 Button)
using UnityEngine;
public class StudyDelegate : MonoBehaviour
{
public delegate void Mydelegate(int n);
public Mydelegate myDelegate;
void Start()
{
// 할당 - 옛날 방식
// myDelegate = new Mydelegate(MethodA);
// 할당 - 표준 방식
myDelegate = MethodA;
myDelegate += MethodB; // 체인 방식
myDelegate += MethodC; // 체인 방식
myDelegate -= MethodB; // 체인 방식
// 호출
// myDelegate();
myDelegate?.Invoke(10); // null 체크 연산자
}
private void MethodA(int a)
{
Debug.Log("Method A " + a);
}
private void MethodB(int b)
{
Debug.Log("Method B " + b);
}
private void MethodC(int c)
{
Debug.Log("Method C " + c);
}
}
delegate도 enum처럼 '형식을 먼저 정의해 놓고 사용하는' 구조라는 점에서는 비슷하지만,
enum은 상수를 묶는 데 쓰이고, delegate는 함수를 변수처럼 다루기 위한 구조이다.
델리게이트는 여러 개의 함수를 체인처럼 연결해서 한 번에 순차적으로 호출할 수 있는 특징이 있다.
위 코드에서는 MethodA → MethodB → MethodC 순서로 연결한 뒤 MethodB를 제거했다.
그래서 최종적으로 연결된 함수는 MethodA → MethodC 순서로 실행된다.
이렇게 델리게이트는 함수를 연결해서 사용하기 때문에 null 체크 연산자를 같이 사용하는 것이 좋다.
연결된 함수가 없다면 null 체크 연산자에서 걸러져 함수를 실행하지 않기 때문이다.
이렇게 한 번의 호출로 여러 함수를 실행하거나 어떤 함수가 실행될지를 결정할 때 사용하는 것이 바로 델리게이트 이다.
[외부 클래스 호출]
using UnityEngine;
using UnityEngine.UI;
public class StudyDelegate : MonoBehaviour
{
public delegate void Mydelegate();
public Mydelegate onKeyDown;
public KeyCode keyCode = KeyCode.Space;
public float timer;
private bool isTimer = true;
void Start()
{
AddMethod(Respond);
AddMethod(StopTimer);
AddMethod(StopBomb);
}
void Update()
{
if (isTimer)
timer += Time.deltaTime;
if (Input.GetKeyDown(keyCode))
onKeyDown?.Invoke();
}
public void AddMethod(Mydelegate myDelegate)
{
onKeyDown += myDelegate;
}
private void Respond()
{
Debug.Log("키가 눌렸습니다.");
}
private void StopTimer()
{
isTimer = false;
Debug.Log("타이머 정지");
}
private void StopBomb()
{
Debug.Log("폭탄 기능 정지");
}
}
using UnityEngine;
public class ExternalClass : MonoBehaviour
{
public StudyDelegate studyDelegate;
void Awake()
{
studyDelegate.AddMethod(StopEvent1);
studyDelegate.AddMethod(StopEvent2);
}
private void StopEvent1()
{
Debug.Log("Stop Event 1");
}
private void StopEvent2()
{
Debug.Log("Stop Event 2");
}
}
델리게이트의 진짜 장점은 외부 클래스에서도 함수 등록이 가능하다는 것이다.
이제 키보드가 눌리면,
StudyDelegate 안에 있던 함수 + ExternalClass에서 등록한 함수까지 총 5개의 함수가 순서대로 실행된다.
(델리게이트에 함수가 등록된 순서대로 실행되기 때문에 Unity 생명주기상 ExternalClass의 Awake()에서 먼저 등록한 함수들이 앞쪽에 실행된다.)
[OnEnalbe() / Disable()]
using UnityEngine;
public class StudyDelegate : MonoBehaviour
{
public delegate void TimerStart();
public TimerStart onTimerStart;
public delegate void TimerEnd();
public TimerEnd onTimerEnd;
private float timer = 5f;
private bool isTimer = true;
void OnEnable()
{
onTimerStart += StartEvent;
onTimerEnd += EndEvent;
}
void Start()
{
onTimerStart?.Invoke();
}
void OnDisable()
{
onTimerStart -= StartEvent;
onTimerEnd -= EndEvent;
}
void Update()
{
if (!isTimer)
return;
timer -= Time.deltaTime;
if (timer <= 0f)
{
isTimer = false;
onTimerEnd?.Invoke();
}
}
private void StartEvent()
{
Debug.Log("타이머 시작");
}
private void EndEvent()
{
Debug.Log("타이머 종료");
}
}
델리게이트는 함수 자체를 변수처럼 저장해서 나중에 호출할 수 있는 기능이다.
이 예제에선 onTimerStart랑 onTimerEnd 두 개를 만들어서 각각 타이머 시작과 종료 시점에 쓰도록 했다.
타이머가 작동되면 Start()에서 onTimerStart.Invoke()로 시작 알림을 보내고,
시간이 다 되면 onTimerEnd.Invoke()를 호출해서 종료 이벤트를 날린다.
중요한 건 이 두 델리게이트를 OnEnable()에서 연결하고 OnDisable()에서 제거했다는 점인데,
이건 Unity에서 델리게이트 쓸 때 거의 정석처럼 쓰는 방식이라고 한다.
컴포넌트가 껐다 켜질 때 함수가 중복으로 연결되는 걸 막기 위해 이렇게 연결/해제를 정확히 해주는 거라고 보면 된다.
+=만 쓰고 -=를 안 하면 나중에 함수가 중복 호출되는 문제도 생기고, 메모리 누수 같은 문제로 이어질 수 있기 때문이다.
2. Event
Event는 델리게이트의 기능을 확장하여 외부 클래스에서 이벤트를 안전하게 관리할 수 있게 해 준다.
event가 붙은 델리게이트는 해당 델리게이트를 선언한 클래스 내부에서만 .Invoke()를 통해 직접 호출할 수 있어 불필요하게 외부에서 강제로 실행되는 것을 막아준다.
using UnityEngine;
public class StudyEvent : MonoBehaviour
{
public delegate void InputKeyHandler();
public event InputKeyHandler onInputKey;
void Start()
{
onInputKey += InputKeyEvent;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
onInputKey?.Invoke();
}
private void InputKeyEvent()
{
Debug.Log("Key Event");
}
}
using UnityEngine;
public class ExternalClass : MonoBehaviour
{
public StudyEvent studyEvent;
void Awake()
{
studyEvent = FindFirstObjectByType<StudyEvent>();
}
void Start()
{
studyEvent.onInputKey += Event1;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
// studyEvent.onInputKey?.Invoke(); // 사용 불가!
}
}
private void Event1()
{
Debug.Log("Event 1");
}
}
3. Unity Event
UnityEvent는 유니티 에디터에서도 연결할 수 있게 만들어진 직렬화 가능한 클래스이다.
일반 델리게이트와 달리, 유니티 에디터의 인스펙터 창에서 직접 함수를 연결할 수 있는 장점이 있다.
using UnityEngine;
using UnityEngine.Events;
public class StudyUnityEvent : MonoBehaviour
{
[SerializeField] private UnityEvent onUnityEvent;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
onUnityEvent?.Invoke();
}
void OnEnable()
{
onUnityEvent.AddListener(MethodA);
}
void OnDisable()
{
onUnityEvent.RemoveListener(MethodA);
}
public void AddMethod(UnityAction action)
{
onUnityEvent.AddListener(action);
}
private void MethodA()
{
Debug.Log("Method A");
}
}
using UnityEngine;
public class ExternalClass : MonoBehaviour
{
public StudyUnityEvent studyUnityEvent;
void Awake()
{
studyUnityEvent = FindFirstObjectByType<StudyUnityEvent>();
}
void Start()
{
studyUnityEvent.AddMethod(MethodB);
}
void Update()
{
// studyUnityEvent.onUnityEvent?.Invoke(); // 사용 불가
}
private void MethodB()
{
Debug.Log("Method B");
}
}
UnityEvent는 델리게이트가 아니다
UnityEvent는 델리게이트처럼 생겼지만 event 키워드를 사용할 수 없다.
직렬화 가능한 클래스이기 때문에 유니티 인스펙터에서 직접 함수 연결이 가능하고,
그래서 [SerializeField] private UnityEvent onUnityEvent;처럼 선언하는 방식이 일반적이다.
외부 클래스에서 직접 접근은 불가능
일반적인 델리게이트였다면, 외부 클래스에서 .Invoke()를 통해 직접 호출하거나 += 연산자로 함수를 바로 추가할 수 있다.
하지만 UnityEvent는 SerializeField로 선언된 상태이기 때문에 외부 클래스에서는 직접 접근이 불가능하다.
즉, 외부 클래스에서는 onUnityEvent.Invoke()처럼 호출하거나 AddListener()를 직접 사용할 수 없다.
이 문제를 해결하기 위해 내부에서 AddMethod() 같은 중계 함수를 만들어서 외부 클래스는 이 함수를 통해 UnityEvent에 리스너를 등록하도록 만든다.
UnityAction
UnityEvent와 함께 자주 등장하는 타입이 바로 UnityAction이다.
델리게이트의 일종인데 매개변수나 반환값이 없는 메서드를 대신 저장하는 타입이라고 보면 된다.
UnityEvent는 내부적으로 UnityAction만 받도록 설계돼 있어서
onUnityEvent.AddListener(...)에 연결할 함수를 무조건 UnityAction 타입으로 넘겨야 한다.
4. EventHandler
EventHandler는 매개변수가 2개짜리 델리게이트 타입이다
- object sender → 이벤트를 발생시킨 객체
- EventArgs e → 이벤트에 대한 정보
누가 이벤트를 발생시켰고 무슨 일이 있었는지 알려주는 이벤트용 도우미라고 생각하면 편하다.
UnityEvent와 큰 차이점은 없지만 sender, EventArgs 타입을 이벤트 정보로 전달하는 데 쓰이는 구조이다.
using System;
using UnityEngine;
public class StudyEventHandler : MonoBehaviour
{
private event EventHandler handler;
public event EventHandler Handler
{
add
{
Debug.Log($"{value.Method} 추가");
handler += value;
}
remove
{
Debug.Log($"{value.Method} 삭제");
handler -= value;
}
}
void OnEnable()
{
Handler += MethodA;
}
void OnDisable()
{
Handler -= MethodA;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
handler?.Invoke(this, EventArgs.Empty);
}
}
private void MethodA(object o, EventArgs e)
{
Debug.Log("MethodA");
}
}
[EventArgs]
이벤트를 보낼 때, 단순한 신호가 아니라 여러 정보를 함께 전달하고 싶을 때가 있다.
예를 들어 캐릭터 생성 이벤트를 발생시키면서 이름, 레벨, 체력 같은 데이터를 함께 보내고 싶다면,
EventArgs를 상속한 클래스를 만들어 그 안에 필요한 데이터를 담아준다.
using System;
using UnityEngine;
public class StudyEventHandler : MonoBehaviour
{
public class CharacterData : EventArgs
{
public string name;
public int level;
public float hp;
public float mp;
public float damage;
// 생성자
public CharacterData(string name, int level, float hp, float mp, float damage)
{
this.name = name;
this.level = level;
this.hp = hp;
this.mp = mp;
this.damage = damage;
}
}
private event EventHandler handler;
void Start()
{
handler += CreateCharacter;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
CharacterData data = new CharacterData("A", 1, 2, 3, 4);
handler?.Invoke(this, data);
}
}
private void CreateCharacter(object o, EventArgs e)
{
// GameObject character = Instantiate(characterPrefab);
var data = (CharacterData)e;
Debug.Log($"{data.name} / {data.level} 데이터를 가진 캐릭터 생성");
}
}
EventArgs를 상속하는 이유는 기본 EventHandler 델리게이트가 object와 EventArgs를 매개변수로 받도록 정해져 있기 때문이다.
따라서 이 형식에 맞춰주기 위해 EventArgs를 상속해야 한다.
[제너릭]
EventHandler<T>는 이벤트에서 전달할 데이터 타입을 미리 명확하게 지정할 수 있도록 만든 델리게이트 형식이다.
using System;
using UnityEngine;
public class StudyEventHandler2 : MonoBehaviour
{
public class DataClass : EventArgs
{
public string dataName;
public DataClass(string dataName)
{
this.dataName = dataName;
}
}
private event EventHandler<DataClass> handler;
void Start()
{
handler += MethodB;
DataClass dataClass = new DataClass("Hello Unity");
handler?.Invoke(this, dataClass);
}
void MethodB(object o, DataClass e)
{
Debug.Log(e.dataName);
}
}
T에는 EventArgs를 상속한 타입이 들어가며, 이 방식의 장점은 형변환 없이 직접 사용이 가능하다는 것이다.
3. Lambda
[익명 함수]
익명 함수는 이름 없이 바로 사용하는 함수다.
using UnityEngine;
public class StudyLambda : MonoBehaviour
{
public delegate void MyDelegate(string s); // 익명 함수
public MyDelegate myDelegate;
void Start()
{
myDelegate += delegate(string s)
{
OnLog(s);
};
myDelegate?.Invoke("Hello Unity");
}
private void OnLog(string s)
{
Debug.Log(s);
}
}
delegate(파라미터)로 시작하고, 중괄호 안에 실행할 내용을 넣는다.
별도로 메서드 이름을 만들 필요 없이 지금 이 자리에서 바로 실행할 코드를 작성할 수 있다.
[Lambda]
람다식은 익명 함수를 더 간결하게 표현하는 방법이다.
using UnityEngine;
public class StudyLambda : MonoBehaviour
{
public delegate void MyDelegate(string s);
public MyDelegate myDelegate;
void Start()
{
myDelegate += (n) => OnLog(n);
myDelegate?.Invoke("Lambda");
}
private void OnLog(string s)
{
Debug.Log(s);
}
}
(매개변수) => 실행내용 형식으로 작성한다.
위 예제처럼 한 줄일 경우 중괄호 없이 바로 호출할 수 있다.
using UnityEngine;
using UnityEngine.UI;
public class StudyLambda : MonoBehaviour
{
public delegate void MyDelegate(string s);
public MyDelegate myDelegate;
public Button button;
void Start()
{
// 버튼에 1개의 기능을 등록하는 방법
button.onClick.AddListener(ButtonEvent);
// button.onClick.AddListener(OnLog("Hello")); // 사용 불가
// 익명함수로 여러 기능을 등록하는 방법
button.onClick.AddListener(delegate
{
ButtonEvent();
OnLog("Lambda");
});
// 람다식으로 1개의 기능을 등록하는 방법
button.onClick.AddListener(() => OnLog("Hello"));
// 람다식으로 여러 기능을 등록하는 방법
button.onClick.AddListener(() =>
{
ButtonEvent();
OnLog("Lambda");
});
}
private void ButtonEvent()
{
Debug.Log("Button Event");
}
private void OnLog(string msg)
{
Debug.Log(msg);
}
}
AddListener는 매개변수가 없는 함수를 파라미터로 갖고 와야 하기 때문에 OnLog("Hello")같이 매개변수가 있는 함수를 넣으면 컴파일 에러가 발생한다.
따라서 이럴 경우 람다함수나 익명함수를 사용해야 한다.
[List 활용]
using System.Collections.Generic;
using UnityEngine;
public class StudyLambda2 : MonoBehaviour
{
public List<int> intList = new List<int>();
void Start()
{
for (int i = 0; i < 10; i++)
intList.Add(i);
intList.RemoveAll(n => n % 2 == 0);
}
}
4. Func
Func는 결괏값을 반환하는 메서드를 저장하는 델리게이트 타입이다.
Action은 반환값없는 메서드를 저장한다면, Func는 반환값이 있는 메서드를 저장한다.
반환형은 항상 마지막에 오며, 매개변수는 0개 이상 받을 수 있다.
using System;
using UnityEngine;
public class StudyFunc : MonoBehaviour
{
public Func<int, int, int> myFunc;
void Start()
{
myFunc += AddMethod;
myFunc += MinusMethod; // Func 특성상 마지막 값만 반환함 -> 델리게이트 체인 사용 지양
int result = myFunc(10, 20);
Debug.Log(result);
}
private int AddMethod(int num1, int num2)
{
return num1 + num2;
}
private int MinusMethod(int num1, int num2)
{
return num1 - num2;
}
}
Func도 다른 델리게이트처럼 += 연산으로 여러 메서드를 체이닝 할 수는 있지만
호출 시에는 마지막에 등록된 메서드의 반환값만 유효하기 때문에 값의 누적 처리나 합산 등에는 적합하지 않다.
[List 활용]
여러 개의 함수를 저장하고 각각의 결과를 받아보고 싶다면 Func를 List에 담아 순차적으로 호출하면 된다.
using System;
using System.Collections.Generic;
using UnityEngine;
public class StudyFunc2 : MonoBehaviour
{
public List<Func<int, int, int>> funcList = new List<Func<int, int, int>>();
void Start()
{
funcList.Add((a, b) => a + b);
funcList.Add((a, b) => a - b);
funcList.Add((a, b) => a * b);
foreach (var func in funcList)
{
int result = func(10, 20);
Debug.Log(result);
}
}
}
- Func<int, int, int> → int 두 개를 받아서 int 하나를 반환하는 함수 구조
- funcList 라는 리스트 안에 덧셈, 뺄셈, 곱셈을 수행하는 람다식 3개를 넣었다.
- Start() 함수에서 funcList 안의 모든 함수들을 순회하면서 func(10, 20) 실행
리스트나 컬렉션 대신 멤버 변수를 여러 개 선언해서 각각의 의미를 가진 Func을 따로 정의할 수도 있다.
using System;
using UnityEngine;
public class StudyFunc3 : MonoBehaviour
{
public int hp = 100;
public Func<int> GetHp;
public Func<float, float> GetRemainHp;
public Func<string> GetAction;
void Start()
{
// 현재 체력
GetHp = () => hp;
// 데미지 받은 이후의 체력
GetRemainHp = (dmg) => hp - dmg;
GetAction = () =>
{
if (GetHp() > 50)
return "공격";
else if (GetHp() > 20)
return "도망";
else
return "죽음";
};
}
}
위 코드와 같이 하나의 상태(HP)를 기준으로 데이터를 여러 방식으로 해석하고 응답하는 구조를 만들 수 있다는 뜻이다.
- 현재 상태를 조회하는 함수
- 변화를 계산하는 함수
- 상태에 따른 행동을 결정하는 함수
5. Predicate
T 타입의 값을 받아서 true 또는 false를 반환하는 함수형 델리게이트로, 어떤 조건을 검사해서 맞는지 아닌지를 판단해 주는 함수이다.
Predicate는 List<T>.Find()나 Exists() 같은 컬렉션 메서드에서 자주 사용되며 조건에 맞는 요소를 찾거나 필터링할 때 유용하다.
using System;
using UnityEngine;
public class StudyPredicate : MonoBehaviour
{
public Predicate<int> myPredicate;
public int level = 10;
void Start()
{
myPredicate = n => n <= 10;
string msg = myPredicate(level) ? "초보자 사냥터 입장 가능" : "초보자 사냥터 입장 불가능";
Debug.Log(msg);
}
}
이렇게 조건문을 Predicate를 통해 처리하여 ture, false 여부에 따라 값이 달라지는 것을 구현할 수 있다.
6. Action
Action은 반환값이 없는 (void) 델리게이트 타입으로 인자는 0개 이상 가질 수 있다.
직접 delegate void MyDelegate() 같은 델리게이트를 선언하지 않고도 Action을 사용하면 짧고 간결하게 델리게이트를 정의하고 사용할 수 있다.
using System;
using UnityEngine;
public class StudyAction : MonoBehaviour
{
// public delegate void MyDelegate();
// public MyDelegate myDelegate;
public event Action action;
// public delegate void MyDelegate2(string s);
// public MyDelegate myDelegate2;
public Action<string> action2;
public Action<string, int, float, bool> action3;
void Start()
{
action += () => Debug.Log("Action");
// action += () =>
// {
// Debug.Log("Action 1");
// Debug.Log("Action 2");
// };
action?.Invoke();
action2 += msg => Debug.Log(msg);
action2?.Invoke("Hello Unity");
}
}
기존 델리게이트는 delegate 키워드로 반환값과 매개변수를 정의하고 그 타입으로 변수를 선언해야 했지만,
Action은 직접 delegate 타입을 선언하지 않아도 바로 델리게이트 변수를 만들고 사용할 수 있다.
[외부 클래스 호출]
Action을 활용하면 외부 클래스 호출도 훨씬 간단하고 직관적으로 클래스 간의 기능을 연결할 수 있다.
using System;
using UnityEngine;
public class ButtonManager : MonoBehaviour
{
public static Action emergencyStopButton;
void Start()
{
emergencyStopButton += StopMessage;
}
private void StopMessage()
{
Debug.Log("긴급 정지 실행");
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
emergencyStopButton?.Invoke();
}
}
}
using UnityEngine;
public class ExternalClass : MonoBehaviour
{
void Start()
{
ButtonManager.emergencyStopButton += TimerView;
}
private void TimerView()
{
}
}
ButtonManager에서 긴급 정지 버튼 이벤트를 정의하고 외부 클래스에서 해당 이벤트에 추가 기능을 연결하였다.
이런 구조를 사용하면 여러 컴포넌트 간의 의존도를 낮추고, 필요한 기능만 동적으로 추가할 수 있어서 유지보수가 편하다는 장점이 있다.
단, 외부 클래스에서 직접 구독한 이벤트는 해제(-=)를 안 해주면 메모리 누수나 참조 문제를 일으킬 수 있으므로 주의해야 한다.

오늘 하루종일 코드 공부만해서 이거 다 담아낸 게 맞는지 잘 모르겠다
복습 필수...ㅠ
'Unity > 멋쟁이사자처럼 부트캠프' 카테고리의 다른 글
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(56일차) - 게임 디자인 패턴 (1) (9) | 2025.08.06 |
|---|---|
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(55일차) - C# 중급 (3), LINQ (3) | 2025.08.05 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(53일차) - 3D 편집도구 실습 및 C# 중급 (1) (5) | 2025.08.01 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(52일차) - FPS 게임 마무리 (9) (7) | 2025.07.31 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(51일차) - FPS 게임 (8) (7) | 2025.07.30 |