1-1
기본개념

옵저버 패턴, 한 문장으로?
"구독과 알림" 시스템이라고 생각하면 가장 쉽습니다.
1. 쉬운 비유: 유튜브 채널 구독 📢
옵저버 패턴을 이해하기 가장 좋은 비유는 '유튜브'입니다.
- 유튜버 (Subject, 주체): 새로운 영상을 올리는 사람. 중요한 이벤트를 만드는 장본인입니다.
- 구독자 (Observer, 관찰자): 유튜버의 새 영상에 관심 있는 사람들.
- "구독" 버튼 (Subscribe): 구독자가 "나 이 채널에 관심 있으니 새 소식 알려줘!"라고 등록하는 행위입니다.
- "알림" (Notify): 유튜버가 새 영상을 올리면, 유튜브 시스템이 모든 구독자에게 "새 영상 올라왔어요!"라고 알려주는 행위입니다.
핵심은 이것입니다:
- 유튜버는 구독자가 누구인지, 몇 명인지 일일이 알 필요가 없습니다. 그냥 "영상 올렸으니 구독자들에게 알려줘!"라고 소리치기만 하면 됩니다.
- 구독자들은 유튜버가 지금 뭘 하는지 계속 감시할 필요가 없습니다. 그냥 가만히 있다가 알림이 오면 반응하면 됩니다.
이렇게 **서로에 대한 정보를 최소화하고 느슨하게 연결(느슨한 결합, Loose Coupling)**하는 것이 옵저버 패턴의 가장 큰 장점입니다.
2. 왜 유니티에서 이게 중요한가요?
게임을 만들다 보면 이런 상황이 아주 많습니다.
나쁜 예 (스파게티 코드):
플레이어의 HP가 0이 되었다고 가정해봅시다.
이때 Player 스크립트가 직접 다른 객체들에게 알려줘야 합니다.
Generated csharp
// Player.cs (나쁜 예)
public class Player : MonoBehaviour
{
public UIManager uiManager;
public GameManager gameManager;
public SoundManager soundManager;
public AchievementSystem achievementSystem;
public void Die()
{
// ...사망 처리...
// 모든 객체에게 직접 알려줘야 함
uiManager.ShowGameOverUI();
gameManager.EndGame();
soundManager.PlayDeathSound();
achievementSystem.UnlockFirstDeathAchievement();
// 만약 죽었을 때 해야 할 일이 100개라면? 100줄의 코드가 추가됨...
}
}
문제점:
- Player가 너무 많은 다른 클래스(UI, 게임매니저 등)를 알고 있어야 합니다.
- 죽었을 때 해야 할 일이 추가될 때마다 Player 코드를 수정해야 합니다. (예: 몬스터가 환호하는 기능 추가)
- 코드가 복잡해지고 유지보수가 어려워집니다. 이걸 스파게티 코드라고 부릅니다.
좋은 예 (옵저버 패턴 적용):
Player는 그냥 "나 죽었다!" 라고 소리치기만 합니다. 그러면 관심 있는 객체들이 알아서 듣고 각자 할 일을 합니다.
Generated csharp
// Player.cs (좋은 예)
public class Player : MonoBehaviour
{
// "나 죽었어!" 라는 방송 채널 (이벤트)
public static event Action OnPlayerDied;
public void Die()
{
// ...사망 처리...
// 구독자들에게 "나 죽었다"고 방송만 하면 끝!
// 누가 듣고 있는지는 알 필요 없음.
OnPlayerDied?.Invoke();
}
}
이제 UIManager, GameManager 등은 Player의 방송을 "구독"만 해두면 됩니다.
Generated csharp
// UIManager.cs (구독자 1)
void OnEnable() {
Player.OnPlayerDied += ShowGameOverUI; // "나 Player 사망 소식 구독할래"
}
void OnDisable() {
Player.OnPlayerDied -= ShowGameOverUI; // "이제 구독 취소할래" (중요!)
}
void ShowGameOverUI() { /* 게임 오버 UI 표시 */ }
Generated csharp
// SoundManager.cs (구독자 2)
void OnEnable() {
Player.OnPlayerDied += PlayDeathSound; // "나도 구독할래"
}
void OnDisable() {
Player.OnPlayerDied -= PlayDeathSound;
}
void PlayDeathSound() { /* 사망 사운드 재생 */ }
장점:
- Player는 누가 자기 소식을 듣는지 전혀 몰라도 됩니다. 그냥 자기 할 일만 합니다.
- 새로운 기능(예: 몬스터 환호)을 추가할 때 Player 코드는 건드릴 필요 없이, Monster 스크립트에서 구독만 추가하면 됩니다.
- 코드가 매우 깔끔하고 유연해집니다.
3. 유니티에서 옵저버 패턴 구현하는 3가지 방법
가장 많이 쓰는 순서대로 알려드릴게요.
방법 1: C# 이벤트 (delegate와 event) - 가장 표준적이고 강력함
방금 위 예제에서 보여드린 방법입니다. 프로그래머들 사이의 소통에 가장 이상적입니다.
- delegate: "이런 형식의 메소드만 구독할 수 있습니다" 라고 정해주는 메소드의 설계도입니다. void MethodName() 같은 형식을 지정합니다. Action이나 Func를 사용하면 미리 정의된 델리게이트를 편하게 쓸 수 있습니다.
- event: delegate를 안전하게 포장한 방송 채널입니다. 구독(+=)과 구독 취소(-=)만 가능하게 제한해서 외부에서 실수로 방송을 강제로 틀거나 구독자 목록을 지워버리는 것을 막아줍니다.
예제 코드 (플레이어 체력 변화 알림)
Generated csharp
// ----------------- 유튜버 (Subject) -----------------
// Player.cs
using System;
using UnityEngine;
public class Player : MonoBehaviour
{
// Action<int> : int형 매개변수 하나를 받는 메소드를 구독할 수 있는 채널
public static event Action<int> OnHealthChanged;
private int _health = 100;
public int Health
{
get => _health;
set
{
_health = value;
// 체력이 변했으니 구독자들에게 현재 체력을 알려주자!
OnHealthChanged?.Invoke(_health);
Debug.Log("플레이어 체력 변경! 현재 체력: " + _health);
}
}
// 테스트용 코드
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Health -= 10; // 스페이스바 누르면 체력 10 감소
}
}
}
Generated csharp
// ----------------- 구독자 (Observer) -----------------
// HealthUI.cs
using UnityEngine;
using UnityEngine.UI;
public class HealthUI : MonoBehaviour
{
public Text healthText;
// 구독 시작
void OnEnable()
{
Player.OnHealthChanged += UpdateHealthText;
}
// 구독 취소 (메모리 누수 방지를 위해 필수!)
void OnDisable()
{
Player.OnHealthChanged -= UpdateHealthText;
}
// 알림을 받았을 때 실행할 메소드
void UpdateHealthText(int currentHealth)
{
healthText.text = "HP: " + currentHealth;
}
}
방법 2: 유니티 이벤트 (UnityEvent) - 기획자/디자이너와 협업에 최적!
UnityEvent를 사용하면 코드를 수정하지 않고 유니티 인스펙터 창에서 드래그 앤 드롭으로 기능을 연결할 수 있습니다.
- 장점: 비-프로그래머도 이벤트 발생 시 어떤 일이 일어날지 쉽게 설정할 수 있습니다.
- 단점: C# 이벤트보다 약간의 성능 오버헤드가 있을 수 있습니다.
예제 코드
Generated csharp
// ----------------- 유튜버 (Subject) -----------------
// CoinCollector.cs
using UnityEngine;
using UnityEngine.Events; // UnityEvent를 사용하기 위해 필요
public class CoinCollector : MonoBehaviour
{
// 인스펙터에 노출될 이벤트 채널
public UnityEvent OnCoinCollected;
public UnityEvent<int> OnScoreUpdated; // 파라미터가 있는 유니티 이벤트
private int score = 0;
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Coin"))
{
Destroy(other.gameObject);
// 1. 코인을 먹었다고 알림 (파라미터 없음)
OnCoinCollected?.Invoke();
// 2. 점수가 올랐다고 알림 (파라미터 있음)
score += 10;
OnScoreUpdated?.Invoke(score);
}
}
}
사용법:
- CoinCollector 스크립트를 가진 오브젝트를 선택합니다.
- 인스펙터 창에 On Coin Collected와 On Score Updated 슬롯이 보입니다.
- + 버튼을 눌러 슬롯을 추가합니다.
- 알림을 받을 오브젝트(예: UIManager, SoundManager)를 드래그해서 슬롯에 넣습니다.
- 드롭다운 메뉴에서 실행하고 싶은 public 함수를 선택합니다.
방법 3: 인터페이스를 이용한 직접 구현 - 원리 이해에 좋음
가장 정석적이고 교과서적인 방법이지만, C# 이벤트나 유니티 이벤트가 있어서 잘 사용하지는 않습니다. 패턴의 원리를 공부하기에 좋습니다.
- IObserver (관찰자) 인터페이스를 만듭니다.
- Subject (주체) 클래스는 IObserver 목록을 가집니다.
- Subject는 변경이 생기면 목록에 있는 모든 IObserver의 알림 메소드를 호출합니다.
✅ 최종 요약 및 추천
| 방법 | 장점 | 단점 | 추천 상황 |
| C# 이벤트 | 성능 좋음, 타입 안전성 높음, 코드 기반으로 명확함 | 인스펙터에서 설정 불가 | 프로그래머 간의 로직 연결, 핵심 시스템 구현 시 강력 추천! (가장 보편적) |
| 유니티 이벤트 | 인스펙터에서 드래그 앤 드롭으로 설정 가능, 직관적 | C# 이벤트보다 약간 느림 | 기획자/디자이너가 자주 수정해야 하는 부분 (UI, 사운드, 이펙트 연결 등) |
| 인터페이스 | 패턴의 원리를 명확히 이해 가능, 유연한 구조 설계 | 코드가 길어지고 직접 관리할 게 많음 | 학습 목적 또는 매우 복잡하고 특수한 시스템 설계 시 |
결론적으로, "이제 스파게티 코드는 그만! 구독과 알림으로 깔끔하게 만드세요!" 이것이 바로 옵저버 패턴의 핵심입니다.
1-2
Player.OnPlayerDied += PlayDeathSound; 이게 머지
Player.OnPlayerDied += PlayDeathSound; 이게 뭔가요?
한마디로 **"방송 채널에 구독 신청을 하는 코드"**입니다.
조금 더 자세히, 마치 레고 블록처럼 분해해서 설명해 드릴게요.
Player.OnPlayerDied += PlayDeathSound;
- Player.OnPlayerDied: 방송 채널입니다.
- Player라는 방송국에서 운영하는 OnPlayerDied (플레이어 사망)이라는 주제의 채널이라고 생각하세요. 이 채널은 "플레이어가 죽었을 때"만 방송을 송출합니다.
- +=: "구독하기" 버튼입니다.
- 이 연산자는 오른쪽에 있는 '기능'을 왼쪽에 있는 '방송 채널'의 구독자 목록에 추가합니다.
- 즉, "이제부터 OnPlayerDied 채널에서 방송하면 나한테도 알려줘!" 라고 등록하는 행위입니다.
- PlayDeathSound: **알림을 받으면 할 일(메소드/함수)**입니다.
- 방송국에서 "플레이어 죽었다!"는 알림이 오면, SoundManager는 PlayDeathSound라는 행동을 하기로 약속한 것입니다.
전체 흐름을 정리하면 이렇습니다:
- 구독 신청: SoundManager가 시작될 때(OnEnable), Player.OnPlayerDied += PlayDeathSound; 코드를 통해 '플레이어 사망' 채널을 구독합니다.
- 대기: SoundManager는 평소에는 아무것도 안 하고 자기 할 일을 하다가 가만히 있습니다.
- 이벤트 발생: 플레이어가 죽어서 Player 스크립트에서 OnPlayerDied.Invoke() 라는 방송 송출 명령을 내립니다.
- 알림 및 실행: '플레이어 사망' 채널은 모든 구독자(SoundManager, UIManager 등)에게 "방송 시작!" 신호를 보냅니다. 신호를 받은 SoundManager는 약속했던 PlayDeathSound 메소드를 실행합니다.
중요! 구독 취소(-=)
+= 로 구독을 했다면, 해당 객체가 사라질 때(OnDisable 등) 반드시 Player.OnPlayerDied -= PlayDeathSound; 로 구독 취소를 해야 합니다. 안 그러면 존재하지 않는 유령 구독자가 메모리에 남아 문제를 일으킬 수 있습니다 (메모리 누수).
1-3
또한 자바에서,만약 hp<=0이면 게임 종료하게 하는 게 있으면,
난, 매 이벤트마다 isGameOver인지 확인함
또한 매 이벤트마다, hp<=0인지 확인함
true이면 isGameOver==true
이건 변수가 한개라서 간단햇던건가
말씀하신 방식은 옵저버 패턴과 정반대에 있는 "폴링(Polling, 계속 확인하기)" 방식입니다.
질문: "이건 변수가 한개라서 간단했던 건가요?"
답변: 네, 맞습니다. 감시할 대상이 hp 하나뿐이고, 그에 따른 결과도 isGameOver 하나로 단순할 때는 폴링 방식이 직관적이고 간단해 보입니다.
하지만 게임의 규모가 조금만 커져도 이 방식은 큰 문제를 일으킵니다.
두 방식을 아이에 비유해 볼게요.
나의 방식: 폴링 (계속 확인하기)
"아빠, 지금 도착했어? 지금은? 지금은?"
- 어떻게 동작하나요?
- 게임의 매 프레임(Update문)마다 또는 매 이벤트마다 궁금한 쪽에서 상태를 계속 확인합니다.
- UIManager가 Update에서 계속 묻습니다: if (player.hp <= 0) { ... }
- SoundManager가 Update에서 또 묻습니다: if (player.hp <= 0) { ... }
- 장점:
- 구현이 매우 단순하고 직관적입니다. (if문 하나면 되니까)
- 치명적인 단점:
- 비효율적: 플레이어의 HP에 변화가 없어도 매 프레임 불필요한 확인 작업을 계속합니다. CPU 자원을 낭비하는 셈이죠.
- 스파게티 코드: 플레이어의 죽음에 반응해야 할 객체가 10개라면,
- (예를 들어, 플레이어가 아니라 펫의 체력<=0일떄, 펫 부활, 피흘림, 플레이어 눈물 흘림등 여러가지이면)
- 10개의 다른 장소에서 똑같은 if (player.hp <= 0) 코드가 흩어져 있게 됩니다. 로직을 수정하려면 10군데를 다 찾아다녀야 합니다.
옵저버 패턴 (알림 받기)
"아빠, 도착하면 깨워주세요."
- 어떻게 동작하나요?
- 관심 있는 객체들이 Player에게 "혹시 죽으면 저한테 알려주세요"라고 딱 한 번만 등록해 둡니다.
- Player는 자신의 HP가 0 이하로 떨어지는 그 순간, 자신을 구독한 모든 객체에게 "나 죽었어!" 라고 단 한 번 알려줍니다.
- 장점:
- 효율적: 이벤트가 발생했을 때만 코드가 실행됩니다. 불필요한 확인 작업이 전혀 없습니다.
- 깔끔한 코드 (느슨한 결합): Player는 누가 자기 소식을 듣는지 알 필요가 없고, 구독자들은 그냥 소식이 오기만 기다리면 됩니다. 서로 독립적으로 작동하여 코드를 추가하거나 빼기가 매우 쉽습니다.
최종 비교
| 폴링 (계속 확인하기) | 옵저버 패턴 (알림 받기) | |
| 비유 | "지금 도착했어?"라고 계속 묻기 | "도착하면 알려줘"라고 등록하기 |
| 주도권 | 내가 상태를 계속 훔쳐본다 | 상대방이 변경되면 알려준다 |
| 효율성 | 비효율적 (계속 물어봐야 함) | 효율적 (필요할 때만 말함) |
| 확장성 | 나쁨 (궁금한 사람이 늘면 코드가 지저분해짐) | 좋음 (구독자만 추가하면 끝) |
| 언제 쓸까? | 아주 간단한 1:1 상호작용, 다른 방법이 없을 때 | 대부분의 게임 로직 (UI 업데이트, 사운드, 게임 상태 변경 등) |
결론적으로, 옵저버 패턴은 "필요할 때만 알려줘!" 시스템을 만들어, 불필요한 확인 작업을 없애고 코드를 훨씬 효율적이고 유연하게 만들어주는 멋진 방법입니다. 처음에는 조금 복잡해 보여도, 한번 익숙해지면 스파게티 코드를 피할 수 있는 가장 강력한 무기 중 하나가 될 겁니다.
1-4
event: 구독(+=), 구독취소(-=)만 가능하게 만드는 '안전장치'. 외부에서 멋대로 방송하거나 구독자 목록을 지우는 걸 막아줍니다.>이게 왜 이렇게 가능한거지 ?
enent라는 단어때문에?
결론부터 말씀드리면, 네, event 라는 키워드 자체가 C# 언어에서 약속한 특별한 규칙(문법)이기 때문입니다.
이걸 이해하려면 먼저 delegate(델리게이트)를 알아야 합니다.
1-4(1)delegate (대리자): 메소드를 담는 변수
C#에서 delegate는 '메소드의 설계도'이자, 그 설계도에 맞는 '메소드를 담을 수 있는 특별한 변수'입니다.
Generated csharp
// "리턴값 없고, 매개변수도 없는 모든 메소드는 'MyDelegate' 타입이야!" 라고 설계도를 만듦
public delegate void MyDelegate();
이제 MyDelegate 타입의 변수를 만들면, 저 설계도에 맞는 어떤 메소드든 담을 수 있습니다.
Generated csharp
public class Boss
{
// MyDelegate 타입의 변수 'onAttack' 선언.
// 여기에 이제 '리턴값 없고, 매개변수 없는' 메소드들을 담을 수 있음.
public MyDelegate onAttack;
public void Attack()
{
Debug.Log("보스가 공격!");
// onAttack에 담겨있는 모든 메소드들을 실행!
onAttack?.Invoke();
}
}
다른 스크립트에서 이렇게 사용할 수 있습니다.
Generated csharp
public class PlayerAction
{
public Boss a_boss;
void Start()
{
// 보스의 onAttack 변수에 'Dodge' 메소드를 할당(=)함.
a_boss.onAttack = Dodge;
// 여기에 다른 메소드를 또 추가할 수도 있음.
a_boss.onAttack += Shout; // += 연산자로 구독자 추가
// 🚨 이런 위험한 짓도 가능해짐!
// 1. 다른 곳에서 멋대로 방송을 틀어버림
a_boss.onAttack.Invoke();
// 2. 다른 구독자들이 등록한 것을 싹 다 지워버림
a_boss.onAttack = null;
}
void Dodge() { Debug.Log("플레이어가 회피!"); }
void Shout() { Debug.Log("플레이어가 소리침!"); }
}
문제점: onAttack이 그냥 public 변수이기 때문에, 클래스 외부에서 =(할당), Invoke()(실행) 등 모든 것을 마음대로 할 수 있습니다. 이건 매우 위험합니다. 마치 방송국 송출 버튼을 아무나 누를 수 있게 열어둔 것과 같습니다.
1-4(2)
event (이벤트): delegate의 안전 포장지
event 키워드를 delegate 변수 앞에 붙이면, C# 컴파일러는 이 변수에 강력한 제약을 겁니다.
Generated csharp
public class Boss
{
// 'event' 키워드 하나만 추가!
public event MyDelegate onAttack;
public void Attack()
{
Debug.Log("보스가 공격!");
onAttack?.Invoke(); // 'Boss' 클래스 내부에서는 여전히 Invoke() 가능
}
}
이제 PlayerAction 스크립트에서 onAttack에 접근하면 어떻게 될까요?
Generated csharp
public class PlayerAction
{
public Boss a_boss;
void Start()
{
// ✅ 이건 허용됨 (구독 신청)
a_boss.onAttack += Dodge;
a_boss.onAttack += Shout;
// ✅ 이것도 허용됨 (구독 취소)
a_boss.onAttack -= Dodge;
// 🚨 이제부터 아래 코드는 컴파일 에러 발생!
// a_boss.onAttack.Invoke(); // 에러: 클래스 외부에서 멋대로 방송 불가!
// a_boss.onAttack = null; // 에러: 클래스 외부에서 멋대로 구독자 목록 초기화 불가!
}
void Dodge() { Debug.Log("플레이어가 회피!"); }
void Shout() { Debug.Log("플레이어가 소리침!"); }
}
정리: event는 C# 문법 그 자체입니다. delegate 변수를 외부에는 "구독(+=)" 과 "구독 취소(-=)" 기능만 노출시키고, "방송 송출(Invoke)" 은 오직 해당 클래스 내부에서만 가능하도록 제한하는 역할을 합니다. 이 규칙 덕분에 코드가 훨씬 안전해지는 것입니다.
1-5 public class Player : MonoBehaviour는 무슨 뜻인가요?
이 구문은 C#의 '상속(Inheritance)' 문법입니다.
public class Player: Player라는 이름의 새로운 클래스(설계도)를 만든다는 뜻입니다.
: (콜론): "오른쪽에 있는 클래스의 모든 기능과 특징을 물려받겠다"는 상속의 의미입니다.
MonoBehaviour: 유니티 엔진이 제공하는 아주 특별한 기본 클래스입니다. 유니티에서 게임 오브젝트(캐릭터, 카메라, 바닥 등 씬에 보이는 모든 것)에 붙일 수 있는 스크립트는 반드시 이 MonoBehaviour를 상속받아야 합니다.
왜 MonoBehaviour를 상속받아야 할까요?
MonoBehaviour를 상속받는 순간, 내 Player 클래스는 유니티 엔진과 소통할 수 있는 수많은 기본 기능들을 공짜로 얻게 됩니다. 대표적인 기능들은 다음과 같습니다.
- 생명주기(Lifecycle) 함수:
- Awake(): 스크립트가 처음 생성될 때 딱 한 번 호출
- Start(): 첫 프레임 업데이트 직전에 딱 한 번 호출
- Update(): 매 프레임마다 호출 (가장 많이 사용)
- OnEnable() / OnDisable(): 오브젝트가 활성화/비활성화 될 때 호출
- OnDestroy(): 오브젝트가 파괴될 때 호출
이런 함수들은 우리가 직접 호출하지 않아도 유니티 엔진이 "때가 되면" 알아서 불러줍니다. MonoBehaviour를 상속받지 않으면 유니티는 Update를 불러줘야 할지조차 모릅니다.
- 유니티 엔진 기능 접근:
- gameObject: 이 스크립트가 붙어있는 게임 오브젝트 자체
- transform: 위치, 회전, 크기 정보 (gameObject.transform)
- GetComponent<T>(): 게임 오브젝트에 붙어있는 다른 컴포넌트(예: Rigidbody, AudioSource)를 가져오는 기능
- Instantiate(): 새로운 게임 오브젝트를 복제해서 만드는 기능
- 코루틴(StartCoroutine) 등 수많은 유니티 핵심 기능들
결론: public class Player : MonoBehaviour는 "Player라는 이름의 클래스를 만드는데, 이건 유니티 게임 오브젝트에 붙여서 사용할 수 있고, Update나 Start 같은 유니티의 핵심 기능들을 모두 물려받아 사용할 수 있는 스크립트다" 라는 의미입니다.
1-6
// OnPlayerDied 이벤트(방송 채널)에 등록된 구독자가 있는지 확인하고(null이 아닌지),
// 있다면(?.), 등록된 모든 메소드를 실행(Invoke)하라는 명령.
// 이것이 바로 "방송 송출"의 순간.
자바에서 조건 삼항 연산자랑 비슷하네 대신 false일때 실행하는게 없는건가
OnPlayerDied?.Invoke();
문법적인 모양새가 C#의 **Null 조건부 연산자(Null-conditional Operator)**와 Java의 **삼항 연산자(Ternary Operator)**가 비슷해 보일 수 있습니다.
하지만 두 연산자는 목적과 기능이 완전히 다릅니다.
하나씩 비교해서 설명해 드릴게요.
1. Java의 삼항 연산자 (? :)
삼항 연산자는 "조건에 따라 두 값 중 하나를 선택하는" 연산자입니다. if-else 문을 한 줄로 줄여 쓰는 효과가 있죠.
문법: (조건) ? (참일 때의 값) : (거짓일 때의 값);
예시:
Generated java
int score = 85;
String result = (score > 60) ? "합격" : "불합격";
// score > 60 조건이 참(true)이므로, result 변수에는 "합격"이 할당됩니다.
// 위의 코드는 아래의 if-else문과 완전히 똑같습니다.
String result;
if (score > 60) {
result = "합격";
} else {
result = "불합격";
}
핵심:
- 세 부분으로 구성됩니다: 조건, 참일 때의 값, 거짓일 때의 값.
- 항상 값을 반환합니다. (결과물을 내놓습니다)
- 거짓일 때의 동작(:)이 반드시 있어야 합니다.
2. C#의 Null 조건부 연산자 (?.)
Null 조건부 연산자는 "객체가 null이 아닐 때만 멤버에 접근하고, null이면 아무것도 안 하고 넘어가라" 는 의미의 안전장치입니다.
문법: (객체)?.(멤버 접근 또는 메소드 호출)
예시: OnPlayerDied?.Invoke();
이 코드를 풀어서 쓰면 아래와 같습니다.
Generated csharp
// 만약 OnPlayerDied가 null이 아니라면,
if (OnPlayerDied != null)
{
// OnPlayerDied에 등록된 메소드들을 호출(Invoke)해라.
OnPlayerDied.Invoke();
}
// else 부분은 아예 없습니다. null이면 그냥 이 줄을 무시하고 지나갑니다.
왜 이게 필요할까요?
OnPlayerDied는 event 즉, 구독자 목록입니다. 만약
1-7
근데 리턴값이 없다는건 어케 알수있지?
또한 매개변수와 리턴값이 있는 메소드의 형태라면 어케 하지?
delegate할때
action은 리턴값없다는말,
Action을 보고 알기
C#에서는 자주 사용하는 델리게이트 형식을 미리 만들어서 제공합니다. 그중 가장 대표적인 것이 Action과 Func입니다.
- Action: 리턴값이 없는(void) 메소드를 위한 델리게이트입니다.
- Action: 매개변수 없음, 리턴값 없음. void MethodName() 형태.
- Action<int>: int 타입 매개변수 1개, 리턴값 없음. void MethodName(int num) 형태.
- Action<string, bool>: string과 bool 타입 매개변수 2개, 리턴값 없음. void MethodName(string s, bool b) 형태.
따라서 코드에서 Action을 보면 "아, 이건 리턴값 없는 단순 실행용 이벤트구나!" 라고 바로 파악할 수 있습니다.// Action을 썼다는 것 자체가 "리턴값은 void입니다" 라는 약속입니다. public static event Action OnPlayerDied; // Action<int>를 썼다는 것은 "매개변수로 int 하나를 받고, 리턴값은 void입니다" 라는 뜻. public static event Action<int> OnHealthChanged;Use code with caution.C#
delegate 키워드 썻을때는
자바에서, 인터페이스에 구현없는, 메소드 있을때, 그 형태보고 판단할수있는것과 비슷
가령,
public bool cal(int num1,int num2);
//좀 이상하긴하지만, 뭐 특정수 이상이면 true 리턴이라고 가정하자
1-8
매개변수와 리턴값이 있는 메소드는 어떻게 처리하나요?
이것도 두 가지 경우로 나누어 설명해 드릴게요.
Case 1: 매개변수는 있고, 리턴값은 없는 경우 (가장 흔함)
이벤트가 발생했을 때, "어떤 정보"를 함께 전달하고 싶을 때 사용합니다. 예를 들어, 플레이어의 체력이 바뀌었을 때 "현재 체력이 몇인지"를 구독자들에게 알려주는 상황입니다.
이때는 Action<T> 를 사용하면 아주 편리합니다.
1. 방송 채널 만들기 (Player.cs)
Generated csharp
// Player.cs
using System;
using UnityEngine;
public class Player : MonoBehaviour
{
// int 타입의 데이터를 함께 전달하는 방송 채널을 만듭니다.
public static event Action<int> OnHealthChanged;
private int _health = 100;
public int Health
{
get { return _health; }
set
{
_health = value;
Debug.Log("플레이어 체력 변경! 현재 체력: " + _health);
// 방송을 송출할 때, 현재 체력(_health)을 함께 실어서 보냅니다.
OnHealthChanged?.Invoke(_health);
}
}
// 테스트용
void Update()
{
if (Input.GetKeyDown(KeyCode.H))
{
Health -= 10;
}
}
}
2. 구독하고 데이터 받기 (HealthUI.cs)
구독하는 쪽에서는 int 매개변수를 받을 수 있는 메소드를 준비해야 합니다.
Generated csharp
// HealthUI.cs
using UnityEngine;
using UnityEngine.UI; // Text를 사용하기 위해 필요
public class HealthUI : MonoBehaviour
{
public Text healthText;
void OnEnable()
{
// Player의 채널을 구독합니다.
Player.OnHealthChanged += UpdateUI;
}
void OnDisable()
{
Player.OnHealthChanged -= UpdateUI;
}
// Player가 Invoke할 때 보낸 int 데이터는 이 'currentHealth' 매개변수로 쏙 들어옵니다.
private void UpdateUI(int currentHealth)
{
Debug.Log("HealthUI: 체력 변경 알림 수신! 전달받은 값: " + currentHealth);
healthText.text = "HP: " + currentHealth;
}
}
Case 2: 매개변수와 리턴값이 모두 있는 경우 (조금 특수함)
이 경우는 약간 다릅니다. 일반적인 "알림" 목적의 옵저버 패턴과는 조금 다른 용도로 쓰입니다. 주로 "구독자들에게 질문을 던지고, 그 답을 취합하고 싶을 때" 사용합니다.
이때는 Func<T, TResult> 를 사용합니다. Func의 마지막 타입 파라미터는 항상 리턴 타입입니다.
- Func<bool>: 매개변수 없음, bool 리턴. bool MethodName()
- Func<int, string>: int 매개변수 1개, string 리턴. string MethodName(int num)
- Func<GameObject, Vector3, bool>: GameObject, Vector3 매개변수 2개, bool 리턴. bool MethodName(GameObject g, Vector3 v)
예시: "이 아이템을 사용할 수 있는 캐릭터가 있나요?" 라고 물어보고, 단 한 명이라도 "네, 제가 쓸 수 있어요!" 라고 true를 반환하면 아이템을 사용하는 로직.
주의점: 델리게이트는 여러 메소드를 등록(+=)할 수 있습니다. 만약 리턴값이 있는 델리게이트에 여러 구독자가 등록되면, Invoke()를 호출했을 때 가장 마지막에 등록된 구독자의 리턴값만 받게 됩니다. 모든 구독자의 리턴값을 받으려면 특별한 처리가 필요합니다.
1. 모든 구독자의 리턴값 받기 (GetInvocationList)
Generated csharp
// "이 스킬의 데미지에 보너스를 줄 수 있는 버프가 있나요?" 라고 묻고, 모든 보너스 수치를 합산하는 예제
public class Skill
{
// Func<int, int>: 기본 데미지(int)를 받아서, 추가된 최종 데미지(int)를 리턴
public event Func<int, int> OnCalculateBonusDamage;
public void UseSkill()
{
int baseDamage = 50;
int finalDamage = baseDamage;
if (OnCalculateBonusDamage != null)
{
// GetInvocationList()를 사용해 모든 구독자(메소드)의 목록을 가져온다.
foreach (Func<int, int> subscriber in OnCalculateBonusDamage.GetInvocationList())
{
// 각 구독자에게 질문을 던지고(메소드를 호출하고), 그 리턴값을 누적한다.
finalDamage += subscriber.Invoke(baseDamage);
}
}
Debug.Log("최종 데미지: " + finalDamage);
}
}
2. 버프 구독자들
Generated csharp
public class FireBuff
{
public void AddFireBonus(int baseDamage)
{
int bonus = (int)(baseDamage * 0.2f); // 기본 데미지의 20% 보너스
Debug.Log("화염 버프: + " + bonus);
return bonus;
}
}
public class StrengthBuff
{
public void AddStrengthBonus(int baseDamage)
{
int bonus = 10; // 고정 10 보너스
Debug.Log("힘 버프: + " + bonus);
return bonus;
}
}
이처럼 리턴값이 있는 델리게이트는 단순 "알림"을 넘어, 시스템의 특정 데이터를 여러 모듈에서 가공하고 취합하는 강력한 도구로 활용될 수 있습니다.
✅ 최종 요약
| 구분 | 목적 | 주로 사용하는 델리게이트 | 특징 |
| 리턴값 없음 | 단순 알림 / 명령 ("플레이어 죽었다!", "UI 갱신해!") | Action, Action<T> | 가장 일반적인 옵저버 패턴. 구현이 간단하고 직관적. |
| 리턴값 있음 | 데이터 요청 / 가공 / 취합 ("데미지 보너스 얼마야?", "이거 사용 가능해?") | Func<T>, Func<T, TResult> | 구독자에게 질문을 던지고 답을 받음. 모든 답을 받으려면 GetInvocationList 필요. |
1-9
아니,
get,set메소드 구현할수있네
c#에서도
프로퍼티가 있음에도 불구하고
네, 맞습니다! C#에서는 데이터를 다루는 세 가지 주요 방법이 있으며, 각각의 역할과 사용 목적이 명확히 다릅니다. 말씀하신 것처럼 프로퍼티가 있음에도 불구하고 전통적인 get, set 메소드를 여전히 사용하고, 또 사용해야 할 때가 있습니다.
이 세 가지 방법을 비교하며 왜 그런지 명확하게 설명해 드릴게요.
1. Public 필드 (Public Field) - 가장 위험하고 권장되지 않음
이것은 변수를 그냥 public으로 선언해서 외부에서 직접 접근하게 하는 방식입니다.
Generated csharp
public class Player
{
// 누구나 이 변수에 직접 접근해서 마음대로 값을 바꿀 수 있음
public int health = 100;
}
// 다른 클래스에서
Player player = new Player();
player.health = 99999; // 아무런 제약 없이 값을 마음대로 조작 가능
player.health = -100; // 체력이 음수가 되는 말도 안 되는 상황 발생 가능
- 장점: 코드가 가장 짧고 간단하다.
- 치명적인 단점: 캡슐화(Encapsulation)가 완전히 깨집니다. 데이터의 무결성을 보장할 방법이 전혀 없습니다. 클래스의 내부 상태가 외부의 코드에 의해 언제든지 오염될 수 있어 매우 위험합니다. 절대 사용하지 않는 것이 좋습니다.
2. 프로퍼티 (Property) - 가장 C#답고 세련된 방법
프로퍼티는 필드(변수)처럼 사용하지만, 내부적으로는 메소드처럼 작동하는 특별한 멤버입니다. 데이터를 안전하게 보호(캡슐화)하면서도 사용하기 편리하게 만드는 것이 주 목적입니다.
Generated csharp
public class Player
{
private int _health = 100; // 데이터는 private으로 숨겨서 보호한다.
// Health라는 '대문자' 이름의 프로퍼티를 외부에 공개한다.
public int Health
{
// 값을 '읽어갈 때' 호출되는 부분 (Getter)
get
{
return _health;
}
// 값을 '설정할 때' 호출되는 부분 (Setter)
set
{
// 'value'는 새로 들어온 값을 가리키는 C#의 키워드
if (value < 0)
{
_health = 0; // 체력이 음수가 되지 않도록 방어하는 로직 추가
}
else
{
_health = value;
}
Debug.Log("체력이 변경되었습니다. 현재 체력: " + _health);
}
}
}
// 다른 클래스에서
Player player = new Player();
int currentHealth = player.Health; // get 블록이 호출됨
player.Health = -100; // set 블록이 호출됨 -> 방어 로직에 의해 _health는 0이 됨
- 장점:
- 사용이 편리함: 변수처럼 player.Health 로 쉽게 접근할 수 있습니다.
- 캡슐화 보장: get, set 블록 안에 유효성 검사, 로그 출력, 다른 이벤트 호출 등 부가적인 로직을 넣을 수 있습니다.
- 읽기 전용/쓰기 전용 설정 가능: set을 private으로 만들거나 아예 없애서 읽기만 가능한 프로퍼티를 만들 수 있습니다.
3. Get/Set 메소드 (Getter/Setter Methods) - 프로퍼티와 목적이 다름!
프로퍼티가 있음에도 불구하고, 다음과 같은 특정 상황에서는 전통적인 Get, Set 메소드를 사용하는 것이 더 적합합니다.
Generated csharp
public class Player
{
private int _health = 100;
// 현재 체력 값을 '단순히 가져오는' 목적의 메소드
public int GetHealth()
{
return _health;
}
// 체력을 '변경하는 행위' 자체에 더 복잡한 로직이 포함될 때 사용하는 메소드
public void TakeDamage(int damage)
{
if (damage < 0) damage = 0; // 데미지는 음수일 수 없음
_health -= damage;
if (_health < 0)
{
_health = 0;
}
PlayHitAnimation(); // 데미지를 입는 '행위'에는 애니메이션 재생이 포함됨
UpdateHealthUI(); // UI 업데이트도 이 '행위'의 일부임
if (_health == 0)
{
Die(); // 죽는 처리까지 포함
}
}
}
왜 프로퍼티 대신 메소드를 쓸까요?
- '값'이 아닌 '행위(Action)'를 표현할 때:
- 프로퍼티는 "속성(Attribute)의 값을 얻거나 설정한다" 는 의미가 강합니다. player.Health = 50; 은 "플레이어의 체력이라는 속성을 50으로 바꾼다"는 느낌입니다.
- 메소드는 "어떤 동작을 수행한다" 는 의미가 강합니다. player.TakeDamage(50); 은 "플레이어가 50의 데미지를 입는다"는 '행위'를 표현합니다. 이 행위에는 체력 감소뿐만 아니라, 피격 애니메이션, 사운드 재생, UI 업데이트 등 여러 가지 부가 작업이 포함될 수 있습니다.
- 매개변수(Parameter)가 필요할 때:
- 프로퍼티의 set 접근자는 value라는 단 하나의 매개변수만 암묵적으로 가집니다.
- 만약 데미지를 주는 주체(GameObject attacker), 데미지 타입(DamageType type) 등 추가적인 정보가 필요한 '행위'라면 프로퍼티로는 표현할 수 없고, 반드시 메소드를 사용해야 합니다.
// 이런 복잡한 행위는 프로퍼티로 불가능 public void TakeDamage(int damage, GameObject attacker, DamageType type) { // ... }Use code with caution.C# - 계산이 오래 걸리거나 리소스를 많이 소모할 때:
- 프로퍼티는 변수처럼 빠르게 접근할 수 있을 것이라는 기대를 갖게 합니다.
- 만약 Get 하는 과정이 데이터베이스를 조회하거나, 복잡한 물리 계산을 하는 등 시간이 오래 걸리는 작업이라면, GetSomething() 형태의 메소드로 만들어 "이것은 단순한 값 조회가 아니라 시간이 걸리는 작업이다"라는 것을 명시적으로 알려주는 것이 좋습니다.
✅ 최종 비교 및 결론
| Public 필드 | 프로퍼티 (Property) | Get/Set 메소드 | |
| 목적 | (사용 금지) | 속성 값의 안전한 접근 및 제어 (캡슐화) | **행위(Action)**의 수행, 복잡한 로직 처리 |
| 사용 형태 | player.health | player.Health | player.GetHealth(), player.TakeDamage() |
| 매개변수 | 없음 | set에 암묵적인 value 1개 | 여러 개 가능 |
| 캡슐화 | X (깨짐) | O (핵심 목적) | O (핵심 목적) |
| 언제 쓸까? | 절대 쓰지 마세요. | 변수의 값을 읽거나 설정할 때, 간단한 유효성 검사가 필요할 때. 대부분의 변수 접근에 사용. | 값을 변경하는 과정에 **복잡한 로직, 추가 매개변수, 다른 부가 작업(애니메이션, 사운드)**이 필요할 때. |
결론적으로, C#에서는 프로퍼티와 Get/Set 메소드는 경쟁 관계가 아니라, 서로 다른 역할을 수행하는 상호 보완적인 관계입니다.
- "속성" 을 다룰 때는 프로퍼티.
- "행위" 를 다룰 때는 메소드.
1-10
뭐야 action자체가 메소드가 아니란 말인가???
그냥 그앞에 붙이는 키워드인가
아니 근데 delegate랑 event같이씀?
아 델리게이트의 한 종류가 event??
event안쓰고 델리게이트 구현하려면 event대신에 delegate라는 키워드 써야하는거고 ?
1. Action은 메소드가 아니라 '설계도'입니다.
"뭐야 action자체가 메소드가 아니란 말인가??? 그냥 그앞에 붙이는 키워드인가"
네, 맞습니다! Action은 메소드 자체가 아니라, **"이런 형태의 메소드만 여기에 담을 수 있다"**고 약속하는 '델리게이트 타입(설계도)' 입니다.
- int는 "정수만 담을 수 있는 변수 타입"입니다. (int a = 10;)
- string은 "문자열만 담을 수 있는 변수 타입"입니다. (string s = "hello";)
- Action은 **"리턴값 없고 매개변수 없는 메소드만 담을 수 있는 변수 타입"**입니다. (Action myAction = SomeMethod;)
Action은 C#이 미리 만들어 놓은 편리한 델리게이트의 한 종류일 뿐입니다. int가 편리한 숫자 타입인 것처럼요.
잘못된 생각: Action = 메소드 (X)
올바른 생각: Action = 특정 형태의 메소드를 담을 수 있는 변수의 타입(설계도) (O)
2. delegate와 event는 '함께' 씁니다.
"아니 근데 delegate랑 event같이씀?"
네! 둘은 항상 짝꿍처럼 같이 다닙니다. event는 혼자 존재할 수 없고, 반드시 어떤 delegate 타입을 기반으로 만들어져야 합니다.
- delegate (설계도): "어떤 형태의 구독자를 받을 것인가?"를 정의합니다.
- event (안전장치): 그 설계도로 만들어진 변수를 안전하게 포장해서, 외부에서는 구독(+=)과 구독 취소(-=)만 가능하게 만듭니다.
Generated csharp
- Action<int>: delegate입니다. "int 하나를 받는 메소드"라는 설계도죠.
- event: OnHealthChanged라는 변수를 안전하게 만드는 키워드입니다.
둘의 관계를 비유하자면 이렇습니다.
- delegate: 콘센트의 모양 (110V용, 220V용 등)
- event: 그 콘센트에 씌우는 "안전 커버"
안전 커버(event)는 콘센트(delegate) 없이는 의미가 없습니다.
3. event는 delegate의 '수식어'입니다.
"아 델리게이트의 한 종류가 event??"
이 표현이 약간 오해를 살 수 있습니다. 더 정확한 표현은 이렇습니다.
"event는 delegate 변수를 꾸며주는 '수식어(modifier)' 또는 '키워드'이다."
- public이 변수를 "공개"로 만들어주는 수식어인 것처럼,
- static이 변수를 "정적"으로 만들어주는 수식어인 것처럼,
- event는 delegate 타입의 변수를 "이벤트의 규칙을 따르도록" 만들어주는 특별한 수식어입니다.
delegate의 한 종류가 event가 아닙니다.
event는 delegate를 더 안전하게 사용하기 위해 덧붙이는 기능입니다.
4. event 없이 delegate만 쓸 수도 있습니다. (하지만 위험!)
"event안쓰고 델리게이트 구현하려면 event대신에 delegate라는 키워드 써야하는거고 ?"
이 부분이 가장 헷갈리는 포인트인데, 두 가지 경우가 있습니다.
Case 1: Action, Func 같은 미리 만들어진 델리게이트 사용 (추천!)
이때는 delegate 키워드를 직접 쓸 필요가 없습니다. Action 자체가 이미 델리게이트 타입이니까요.
Generated csharp
// [event 안 쓴 위험한 버전]
// "Action 타입의 public 변수 onPlayerDied를 선언한다."
public Action OnPlayerDied;
// [event 쓴 안전한 버전]
// "Action 타입의 public 변수 OnPlayerDied를 선언하되, event 규칙을 적용한다."
public event Action OnPlayerDied;
Case 2: 나만의 델리게이트 타입을 직접 정의해서 사용
Action이나 Func에 없는 아주 특별한 형태의 델리게이트를 만들고 싶을 때, 이때 delegate 키워드를 사용해서 '설계도'를 직접 만듭니다.
Generated csharp
// 1. 설계도 직접 만들기 (이때 delegate 키워드 사용)
public delegate bool CanUseItemDelegate(Item item, PlayerStat stats);
public class PlayerInventory
{
// 2. 위에서 만든 설계도로 변수 선언하기 (event를 써서 안전하게)
public event CanUseItemDelegate OnCheckItemAvailability;
public void CheckItem(Item targetItem, PlayerStat playerStat)
{
bool canUse = true;
// 구독자들에게 "이 아이템 쓸 수 있어?"라고 물어봄
if(OnCheckItemAvailability != null)
{
// 리턴값을 받아서 처리...
canUse = OnCheckItemAvailability.Invoke(targetItem, playerStat);
}
}
}
✅ 최종 정리: 머릿속에 이렇게 넣으세요!
- delegate는 "메소드 설계도"다.
- Action, Func는 C#이 미리 만들어준 편리한 delegate 설계도다.
- 내가 직접 public delegate void MyDel(); 처럼 설계도를 만들 수도 있다.
- 이 설계도를 사용해 "메소드를 담을 변수"를 선언한다.
- public Action myAction;
- public MyDel myDelegateVariable;
- 이 변수를 안전하게 만들고 싶으면 앞에 event 키워드를 붙인다.
- public event Action myAction;
- public event MyDel myDelegateVariable;
- 이렇게 하면 외부에서는 += 와 -= 만 가능하다.
"event를 안 쓴다" 는 것은 3번 과정에서 event 키워드만 쏙 빼는 것을 의미합니다.
"delegate를 쓴다" 는 것은 1번 과정에서 설계도를 직접 정의하거나, Action/Func 같은 델리게이트 타입을 사용하는 것 모두를 의미합니다.
1-11
아 결국, 리턴값있는, 이벤트?처리하려면 새로운 클래슬ㄹ 만들어야한다는거구나>아님
근데 OnEnable()의 역할은 뭐지? 이름은 항상 이걸로 해야하나 ? 정해져있나
왜update메소드인데 invoke햇던게 들어오는거지
1. 리턴값 있는 이벤트 처리를 위해 새 클래스가 필요한가?
아 결국, 리턴값있는, 이벤트?처리하려면 새로운 클래슬ㄹ 만들어야한다는거구나
아니요, 꼭 그렇지는 않습니다! 제가 보여드린 Skill, FireBuff, StrengthBuff 예시는 이해를 돕기 위해 클래스를 분리한 것일 뿐입니다.
리턴값이 있는 이벤트를 처리하는 메소드는 같은 클래스 안에 있어도 전혀 문제없습니다.
예를 들어, Player 클래스 하나에서 모든 것을 처리할 수도 있습니다.
Generated csharpusing System;
using UnityEngine;
public class Player : MonoBehaviour
{
// Func<int, int>: 기본 공격력(int)을 받아 최종 공격력(int)을 리턴하는 설계도
public event Func<int, int> OnCalculateAttackPower;
public int baseAttack = 10; // 기본 공격력
void Start()
{
// 1. "힘 버프" 메소드를 이벤트에 구독시킨다. (같은 클래스 안에 있음)
OnCalculateAttackPower += ApplyStrengthBuff;
// 2. "장비 보너스" 메소드를 이벤트에 구독시킨다. (같은 클래스 안에 있음)
OnCalculateAttackPower += ApplyEquipmentBonus;
}
public void Attack()
{
int finalAttackPower = baseAttack;
// 구독자가 있다면
if (OnCalculateAttackPower != null)
{
// 모든 구독자(메소드)들에게 "기본 공격력 줄게, 보너스 얼마야?" 라고 묻고
// 그 리턴값들을 모두 더한다.
foreach (Func<int, int> subscriber in OnCalculateAttackPower.GetInvocationList())
{
finalAttackPower += subscriber.Invoke(baseAttack);
}
}
Debug.Log("최종 공격력으로 공격: " + finalAttackPower);
}
// --- 구독자로 등록될 메소드들 (모두 Player 클래스 안에 있음) ---
// 힘 버프는 고정 +5 보너스를 리턴
private int ApplyStrengthBuff(int basePower)
{
return 5;
}
// 장비 보너스는 기본 공격력의 10%를 보너스로 리턴
private int ApplyEquipmentBonus(int basePower)
{
return (int)(basePower * 0.1f);
}
}
핵심은 클래스를 분리하느냐가 아니라, Func 델리게이트와 GetInvocationList()를 사용해 리턴값을 어떻게 취합하느냐 입니다. 클래스를 분리하는 것은 코드를 더 체계적으로 관리하고 싶을 때 선택하는 구조적인 문제입니다.
2. OnEnable()의 역할과 이름 규칙
근데 OnEnable()의 역할은 뭐지? 이름은 항상 이걸로 해야하나 ? 정해져있나
네, 완벽하게 정해져 있습니다! OnEnable은 유니티 엔진과 개발자 사이의 특별한 약속입니다.
OnEnable은 유니티 생명주기(Lifecycle) 함수 중 하나입니다.
MonoBehaviour를 상속받은 스크립트에서 아래와 같이 정확한 이름과 형태로 함수를 작성하면, 유니티 엔진이 특정 시점에 알아서 이 함수를 호출해 줍니다.
- 역할: 이 스크립트가 붙어있는 게임 오브젝트가 활성화(Active)될 때마다 단 한 번씩 호출됩니다.
- 게임 씬이 처음 로드될 때 (오브젝트가 켜져 있다면)
- 인스펙터 창에서 오브젝트의 체크박스를 껐다가 다시 켤 때
- gameObject.SetActive(true) 코드로 오브젝트를 켤 때
- 이름 규칙: 반드시 OnEnable 이어야 합니다. onEnable, Onenable 등 철자나 대소문자가 다르면 유니티 엔진이 인식하지 못하고 평범한 일반 함수로 취급합니다.
- 주요 사용처:
- 이벤트 구독(+=): 리소스를 할당하고 연결하는 작업을 하기에 가장 이상적인 장소입니다. 오브젝트가 활성화될 때 필요한 시스템에 자신을 등록합니다.
- 초기화: Start()와 비슷하지만, 오브젝트가 비활성화되었다가 다시 켜질 때마다 반복적으로 초기화해야 하는 로직에 사용됩니다.
- 짝꿍 함수, OnDisable(): OnEnable의 정반대입니다. 오브젝트가 비활성화될 때 호출됩니다. 여기서 OnEnable에서 했던 작업들을 정리합니다. (예: 이벤트 구독 취소(-=) ) 이 과정을 제대로 하지 않으면 메모리 누수나 버그의 원인이 됩니다.
3. UpdateUI(int currentHealth)가 값을 받는 원리
이부분이 이해안가 왜update메소드인데 invoke햇던게 들어오는거지
아주 좋은 질문입니다. 여기서 혼동이 오는 이유는 메소드 이름에 'Update'가 들어가서, 유니티의 생명주기 함수인 Update()와 헷갈리셨기 때문입니다.
결론부터 말씀드리면, UpdateUI는 우리가 직접 만든 일반 메소드일 뿐, 유니티의 Update()와는 전혀 상관없습니다.
이름을 RefreshHealthDisplay나 OnHealthChanged_UpdateUI처럼 바꿔도 똑같이 동작합니다.
이제 마법이 어떻게 일어나는지 단계별로 다시 보겠습니다.
1단계: 구독 신청 (약속 맺기)
HealthUI.cs의 OnEnable()에서 이 코드가 실행됩니다.
Generated csharp// "Player야, 너희 방송국 OnHealthChanged 채널에서 방송하면,
// 우리 HealthUI의 UpdateUI 메소드를 실행시켜줘.
// 방송할 때 보내주는 int 데이터는 UpdateUI의 매개변수로 넘겨주고!"
Player.OnHealthChanged += UpdateUI;
이 순간, Player의 OnHealthChanged 이벤트는 "아, HealthUI의 UpdateUI라는 메소드가 내 구독자가 되었구나. 이 메소드는 int 값을 하나 받을 수 있네." 라고 자신의 구독자 명단에 기록해 둡니다.
2단계: 방송 송출 (약속 이행)
Player.cs에서 체력이 바뀌고 이 코드가 실행됩니다.
Generated csharp3단계: 자동 호출과 데이터 전달 (마법의 순간)
- OnHealthChanged 이벤트는 자신의 구독자 명단을 봅니다. 명단에는 HealthUI의 UpdateUI 메소드가 기록되어 있습니다.
- 이벤트는 Invoke(80) 할 때 들고 온 int 값, 80을 가지고 기록된 UpdateUI 메소드를 자동으로 호출해 줍니다.
- 마치 아래와 같은 코드가 내부적으로 실행되는 것과 같습니다.Generated csharp
// (이벤트 시스템 내부에서 일어나는 일) // "명단에 있는 HealthUI의 UpdateUI를 호출하자. // 아까 받아온 80을 매개변수로 넣어서!" UpdateUI(80);Use code with caution.C# - 결과적으로 HealthUI.cs의 UpdateUI 메소드가 실행되고, currentHealth 라는 매개변수에는 Player가 보낸 80이라는 값이 쏙 들어오게 됩니다.
핵심 정리:
- UpdateUI는 유니티의 Update()가 아닙니다. 그냥 우리가 만든 일반 메소드입니다.
- += 연산자로 구독을 신청할 때, 메소드의 이름(정확히는 메모리 주소)이 이벤트의 구독자 목록에 등록됩니다.
- Invoke(값)가 호출되면, 이벤트는 등록된 모든 메소드를 실행하면서 괄호 안의 값을 해당 메소드의 매개변수로 전달해 줍니다.
이것이 바로 델리게이트와 이벤트가 서로 다른 스크립트 간에 데이터를 전달하고 소통하는 핵심 원리입니다.
1-12
너무 이론적인것부터 하려니깐 재미가 없다
그냥 만들어가면서, 그 과정에서 모르는것 찾아보고..
뭐 그렇게 할까?
13가지 디자인 패턴이나, c#에 대한 문법 다 공부하기도 전에 지쳐서 그만둬버릴것같다
근데 무슨 게임 만들어야할지 모르겟는데
그냥 유튜브에 있는 예제 만들어도 되기는 하지만,
재미있는 장르나,목표가 아니면 하다가 지칠것같고
내가 원하는 게임 설꼐하려니깐,
코딩이 아닌, 설계하는데에 너무 많은 시간이 소요될것같은데
아직 c#코드해석도 잘 안되고/..
'개발 > 유니티' 카테고리의 다른 글
| [유니티] 자바에서 만든 프로젝트 유니티로 옮기기(2) (14) | 2025.07.24 |
|---|---|
| [유니티] 자바에서 만든 프로젝트 유니티로 옮기기(1) (13) | 2025.07.22 |
| [유니티] LINQ (8) | 2025.07.18 |
| [유니티] 델리게이트와 이벤트 (12) | 2025.07.18 |
| [유니티]c#문법5 :스트림 이해 및 예제 (15) | 2025.07.15 |