유니티 엔진의 이해와 라이프사이클

순한양·2025년 6월 3일
0

[Unity] 유니티 엔진의 이해와 라이프사이클 💯

안녕하세요! 오늘은 유니티 게임 개발과 관련된 핵심 내용, 바로 유니티 엔진의 기본 개념과 모든 Unity 개발자가 반드시 알아야 할 라이프사이클(Lifecycle)에 대해 자세히 알아보겠습니다.

출처 정보: 이 글은 "[Unity 9기] 챌린지반 - 발표자 보기가 포함된 공유 화면 (2025년 5월 8일)" 강의 (강사: 진우원 님) 내용을 바탕으로 학습 및 정리한 내용입니다.


Ⅰ. 강의 소개 및 향후 일정 🗓️

  • 지난 강의: OT (오리엔테이션)
  • 금일 강의: 유니티 엔진 및 Unity 라이프사이클
  • 향후 강의 일정:
    • 다음 주 화요일 (하승우 튜터님): C# 메모리 구조, 클래스와 구조체 차이
    • 다음 주 목요일 (하승우 튜터님): OOP (객체 지향 프로그래밍)
    • 그다음 주 (하승우 튜터님): 디자인 패턴 (오브젝트 풀링, MVP, MVVM, MVC 등)
    • 그다음 목요일 (본 강사): 씬 전환을 예시로 한 비동기 프로그래밍 (콜백 함수 포함)
    • 그다음 (하승우 튜터님): async/await를 활용한 심층 비동기 프로그래밍
    • 그다음 목요일 (본 강사): C# 확장 메서드
    • 6월 3일 (하승우 튜터님): 데이터 드리븐 개발 (JSON, 프리팹, CSV 활용)
    • 그 주 목요일 (본 강사): 프레임 디버거를 이용한 최적화 (드로우 콜, SetPass 콜, 에셋 번들 등)
    • 차주 (하승우 튜터님): 네트워크 프로그래밍 (클라이언트 관점)
    • 마지막 강의 (본 강사): 포트폴리오 및 면접 준비, 게임 업계 이야기
    • 미정 특강 (튜터님 미정)

Ⅱ. 유니티(Unity)란 무엇인가? 🎮

1. 정의

유니티는 C# 기반의 컴포넌트 아키텍처로 구성된, 게임 및 인터랙티브 콘텐츠 제작용 실시간 3D 개발 플랫폼(엔진)입니다.

2. 컴포넌트 기반 아키텍처 (Component-Based Architecture)

전통적인 객체 지향 프로그래밍이 주로 상속을 통해 기능을 확장하는 반면, 유니티는 게임 오브젝트(GameObject)에 필요한 기능 단위인 컴포넌트(Component)들을 조합(부착)하여 객체의 행동과 속성을 정의합니다.

  • 게임 오브젝트 (GameObject): 씬을 구성하는 모든 아이템의 기본 단위입니다. 그 자체로는 비어있는 컨테이너와 같습니다.
  • 컴포넌트 (Component): 게임 오브젝트에 부착되어 특정 기능을 수행합니다. (예: Transform, MeshFilter, MeshRenderer, BoxCollider, 사용자 정의 스크립트 등)
  • 장점:
    • 유연성: 필요한 기능을 쉽게 붙였다 뗐다 하며 다양한 객체를 만들 수 있습니다. (예: 같은 큐브라도 콜라이더 설정을 다르게 하여 벽 통과 여부 등을 조절)
    • 재사용성: 만들어둔 컴포넌트를 여러 게임 오브젝트에 재활용할 수 있습니다.
    • 직관성: 코드의 복잡도를 낮추고 유지보수가 용이합니다.

3. 객체 지향과의 관계

유니티의 개발 방식 자체가 컴포넌트 조합형이지만, 각 컴포넌트는 C# 클래스로 구현됩니다. 따라서 컴포넌트 내부를 설계하고 구현할 때는 객체 지향 원칙(다형성, 상속, 캡슐화 등)을 따라야 효과적입니다. 사용자 정의 스크립트는 대부분 MonoBehaviour 클래스를 상속받아 작성됩니다.


Ⅲ. 유니티 라이프사이클 (Unity Lifecycle) 🔄

1. MonoBehaviour 클래스

유니티에서 스크립트가 게임 오브젝트의 생명주기 동안 특정 시점에 자동으로 호출될 수 있도록 미리 정의된 이벤트 함수(메서드)들을 제공하는 기본 클래스입니다. 이러한 이벤트 함수들의 실행 순서를 유니티 라이프사이클이라고 하며, 이를 이해하는 것은 유니티 개발의 핵심입니다. 신입 개발자 면접 단골 질문이기도 합니다.

2. 라이프사이클 순서 (요약)

  • 초기화 (Initialization): 게임 시작 또는 오브젝트 활성화 시 처음 설정하는 단계
    • Awake -> OnEnable -> Reset (에디터 전용) -> Start
  • 물리 (Physics): 고정된 시간 간격으로 물리 연산 처리
    • FixedUpdate (물리 관련 이벤트 함수들)
  • 입력 이벤트 (Input Events): 마우스, 키보드 등 입력 처리
    • OnMouseDown, OnMouseUp, OnMouseDrag
  • 게임 로직 (Game Logic / Updates): 매 프레임마다 게임의 주요 로직, 애니메이션, 상태 업데이트 처리
    • Update -> 코루틴(yield) -> LateUpdate
  • 씬 렌더링 (Scene Rendering): 화면에 오브젝트들을 그리는 단계
    • 렌더링 관련 함수들 -> OnDrawGizmos (에디터 전용) -> OnGUI
  • 종료 및 비활성화 (Decommissioning / Deactivation): 오브젝트 파괴 또는 비활성화, 게임 종료 시
    • OnApplicationPause -> OnApplicationQuit -> OnDisable -> OnDestroy
  • 프레임: 이 모든 과정(물리 제외한 대부분)이 한 프레임 동안 일어납니다. (예: 60 FPS = 1초에 이 과정 60번 반복)

3. 주요 라이프사이클 메서드 상세 설명

Awake()

  • 호출 시점: 스크립트 인스턴스가 로드될 때 (오브젝트 생성 직후, 씬 로드 시). 게임 시작 전 단 한 번 호출. Start() 함수보다 먼저, 그리고 오브젝트가 비활성화 상태여도 호출됩니다.
  • 주요 용도:
    • 자기 자신의 컴포넌트 참조 및 초기화: GetComponent<T>()
    • 내부 변수 초기화: 다른 스크립트와의 참조 설정 전 필요한 값 설정
    • 싱글톤(Singleton) 패턴 구현 시 인스턴스 할당

OnEnable()

  • 호출 시점: 오브젝트(및 스크립트)가 활성화될 때마다 호출됩니다. Awake() 직후 또는 오브젝트가 비활성화되었다가 다시 활성화될 때 호출됩니다.
  • 주요 용도:
    • 이벤트 리스너 등록: 특정 이벤트에 함수를 연결합니다.
    • 리소스 구독 또는 상태 재설정: 오브젝트가 활성화될 때마다 필요한 설정 (예: 오브젝트 풀에서 재사용 시 초기 상태로 복원)
    • (과제) OnDisable과 연계하여 활용할 수 있는 부분 스터디 (자세한 내용은 Ⅶ. Q&A 및 과제 안내 섹션 참고)

Start()

  • 호출 시점: 첫 번째 Update() 메서드가 호출되기 직전, 스크립트가 활성화된 상태에서 단 한 번 호출됩니다. Awake()보다는 늦게 호출됩니다.
  • 주요 용도:
    • 다른 오브젝트의 컴포넌트 참조 및 초기화: 모든 오브젝트의 Awake()가 끝난 후이므로, 다른 오브젝트를 참조하기에 안전합니다. (예: 카메라가 플레이어 타겟 설정)
    • 다른 스크립트의 데이터에 의존하는 초기화 작업

FixedUpdate()

  • 호출 시점: 고정된 시간 간격으로 호출됩니다. (기본값 0.02초, Time > Fixed Timestep 설정에서 변경 가능). 프레임 속도와 무관하게 일정한 주기로 호출됩니다.
  • 주요 용도:
    • 물리(Physics) 연산: Rigidbody 조작 (힘 가하기, 속도 변경 등). 프레임에 영향을 받지 않는 일관된 물리 처리가 필요할 때 사용합니다. (프레임 기반으로 물리 처리 시, PC 성능에 따라 결과가 달라질 수 있음)

Update()

  • 호출 시점: 매 프레임마다 호출됩니다. 프레임 속도에 따라 호출 간격이 달라집니다.
  • 주요 용도:
    • 일반적인 게임 로직: 대부분의 비물리적 게임 로직, 상태 업데이트
    • 입력 처리: 키보드, 마우스 입력 감지 및 반응
    • 애니메이션 제어 (물리 기반 애니메이션 제외)
    • 타이머, 간단한 이동/회전 (Time.deltaTime 사용 필수)
  • Time.deltaTime: 이전 프레임에서 현재 프레임까지 걸린 시간. 프레임 속도에 관계없이 일정한 속도로 움직이거나 연산하기 위해 사용합니다.

LateUpdate()

  • 호출 시점: 모든 Update() 함수가 호출된 후, 매 프레임마다 호출됩니다.
  • 주요 용도:
    • 카메라 추적: 캐릭터의 모든 이동 및 애니메이션 업데이트가 끝난 후 카메라 위치를 조정하여 떨림 현상을 방지합니다.
    • 후처리(Post-processing) 로직: Update에서 결정된 값들을 기반으로 최종적인 조정을 할 때 사용합니다. (예: 캐릭터의 최종 위치에 따라 UI 업데이트)

OnDisable()

  • 호출 시점: 오브젝트(및 스크립트)가 비활성화될 때마다 또는 파괴되기 직전에 호출됩니다.
  • 주요 용도:
    • 이벤트 리스너 해제: OnEnable에서 등록한 이벤트를 해제하여 메모리 누수 방지.
    • 리소스 정리: 오브젝트가 비활성화될 때 점유하던 리소스 반환 (예: 오브젝트 풀로 돌아갈 때 상태 초기화)
    • (과제) OnEnable과 연계하여 활용할 수 있는 부분 스터디 (자세한 내용은 Ⅶ. Q&A 및 과제 안내 섹션 참고)

OnDestroy()

  • 호출 시점: 오브젝트가 파괴되기 직전, 마지막 프레임의 업데이트 이후 단 한 번 호출됩니다. 씬이 닫히거나 Destroy() 함수가 호출될 때.
  • 주요 용도:
    • 모든 리소스 해제 및 정리: 사용했던 메모리, 이벤트 구독, 파일 핸들 등 깨끗하게 정리.
    • 애플리케이션 종료 시 필요한 저장 작업 등.

Ⅳ. 라이프사이클 이해의 중요성 💡

  • 정확한 타이밍 제어: 각 기능을 적절한 라이프사이클 함수에 배치해야 게임이 의도대로 동작합니다. (예: 물리 연산을 Update에 넣으면 프레임에 따라 결과가 불안정해짐)
  • 성능 최적화: 불필요한 호출을 줄이고, 무거운 작업은 적절한 시점에 수행하도록 설계할 수 있습니다.
  • 버그 예방: 잘못된 순서로 코드가 실행되어 발생하는 논리적 오류나 NullReferenceException 등을 방지할 수 있습니다.
  • 협업 효율 증대: 팀원 모두가 라이프사이클을 이해하고 코드를 작성하면 일관성 있고 예측 가능한 코드를 만들 수 있습니다.
  • 툴에 종속되지 않는 개발: 유니티라는 툴을 효과적으로 '활용'하기 위해선 내부 동작 원리, 특히 라이프사이클에 대한 깊은 이해가 필수적입니다.

Ⅴ. 코드 예시 및 실제 적용 (강의 중 시연 요약) 👨‍💻

강의에서 언급된 BaseController와 오브젝트 풀링에서의 OnEnable/OnDisable 활용 예시 코드입니다.

1. BaseControllerPlayerController 예시

다음은 기본적인 캐릭터의 움직임과 입력을 처리하는 BaseController와 이를 상속받는 PlayerController의 간단한 예시입니다.

using UnityEngine;

// 기본 컨트롤러 클래스
public class BaseController : MonoBehaviour
{
    protected Rigidbody rb;
    protected Animator anim; // 애니메이터 컴포넌트
    public float moveSpeed = 5f;
    public float rotateSpeed = 180f; // 초당 회전 속도 (각도)

    // Awake는 스크립트 인스턴스가 로드될 때 호출됩니다.
    // 주로 자기 자신의 컴포넌트 참조 및 초기 설정에 사용됩니다.
    protected virtual void Awake()
    {
        rb = GetComponent<Rigidbody>();
        anim = GetComponent<Animator>(); 

        if (rb == null)
        {
            Debug.LogError("Rigidbody component not found on " + gameObject.name + ". Please add one.");
        }
        // 기타 초기 스탯 설정 등
        Debug.Log(gameObject.name + " Awake: Initialized Rigidbody and Animator (if present).");
    }

    // Start는 첫 번째 Update가 호출되기 직전에 한 번 호출됩니다.
    // 다른 오브젝트 참조나 Awake 이후의 초기화에 사용됩니다.
    protected virtual void Start()
    {
        Debug.Log(gameObject.name + " Start: Ready for game logic.");
    }

    // Update는 매 프레임마다 호출됩니다.
    // 주로 입력 처리, 비물리적 로직, 타이머 등에 사용됩니다.
    protected virtual void Update()
    {
        HandleInput(); // 자식 클래스에서 입력을 처리
        HandleRotation(); // 자식 클래스에서 회전을 처리
    }

    // FixedUpdate는 고정된 시간 간격으로 호출됩니다. (기본 0.02초)
    // 물리 연산은 여기에 작성해야 프레임 속도에 영향을 받지 않습니다.
    protected virtual void FixedUpdate()
    {
        HandleMovement(); // 자식 클래스에서 물리 기반 이동을 처리
    }

    // 입력을 처리하는 가상 메서드 (자식 클래스에서 오버라이드)
    protected virtual void HandleInput()
    {
        // 예: float moveInput = Input.GetAxis("Horizontal");
    }

    // 회전을 처리하는 가상 메서드 (자식 클래스에서 오버라이드)
    protected virtual void HandleRotation()
    {
        // 예: transform.Rotate(Vector3.up * rotateInput * rotateSpeed * Time.deltaTime);
    }

    // 이동을 처리하는 가상 메서드 (자식 클래스에서 오버라이드)
    protected virtual void HandleMovement()
    {
        // 예: rb.MovePosition(rb.position + transform.forward * moveInput * moveSpeed * Time.fixedDeltaTime);
    }
    
    // 오브젝트가 파괴될 때 호출됩니다.
    protected virtual void OnDestroy()
    {
        // 리소스 해제 로직 (예: 이벤트 구독 해제)
        Debug.Log(gameObject.name + " OnDestroy: Cleaning up any subscribed events or resources.");
    }
}

// BaseController를 상속받는 PlayerController 예시
public class PlayerController : BaseController
{
    private Camera mainCamera;
    private Vector3 moveDirection; // 플레이어의 이동 방향

    protected override void Awake()
    {
        base.Awake(); // 부모 클래스의 Awake()를 먼저 호출합니다.
        // PlayerController 고유의 추가적인 초기화 로직
        Debug.Log("PlayerController Awake: Player specific initializations.");
    }

    protected override void Start()
    {
        base.Start(); // 부모 클래스의 Start()를 먼저 호출합니다.
        mainCamera = Camera.main; // 메인 카메라 참조
        if (mainCamera == null)
        {
            Debug.LogError("Main Camera not found in the scene. Player rotation might be affected.");
        }
        Debug.Log("PlayerController Start: Main Camera referenced.");
    }

    protected override void HandleInput()
    {
        // 키보드 입력 받기
        float horizontalInput = Input.GetAxis("Horizontal"); // A, D 또는 좌우 화살표
        float verticalInput = Input.GetAxis("Vertical");   // W, S 또는 위아래 화살표

        // 이동 방향 벡터 계산 (정규화하여 대각선 이동 시 속도 유지)
        moveDirection = new Vector3(horizontalInput, 0, verticalInput).normalized;

        // 애니메이터가 있다면 이동 속도에 따라 애니메이션 파라미터 설정
        if (anim != null)
        {
            anim.SetFloat("MoveSpeed", moveDirection.magnitude); // "MoveSpeed" 파라미터로 이동 애니메이션 제어
        }
    }

    protected override void HandleRotation()
    {
        // 이동 방향이 있을 때만 회전 처리
        if (moveDirection != Vector3.zero)
        {
            // 목표 회전값 계산 (이동 방향을 바라보도록)
            Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
            // 부드러운 회전 (Slerp 사용)
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
        }
    }

    protected override void HandleMovement()
    {
        // Rigidbody가 있고, 이동 입력이 있을 때만 물리적 이동 처리
        if (rb != null && moveDirection.magnitude >= 0.1f) // 약간의 데드존을 두어 미세한 입력 무시
        {
            rb.MovePosition(rb.position + moveDirection * moveSpeed * Time.fixedDeltaTime);
        }
    }
}

2. 오브젝트 풀링(Object Pooling)에서의 OnEnable / OnDisable 활용 예시

오브젝트 풀링은 오브젝트를 반복적으로 생성하고 파괴하는 대신, 미리 생성해두고 재활용하는 최적화 기법입니다. 이때 OnEnableOnDisable이 매우 유용하게 사용됩니다.

using UnityEngine;
using System.Collections.Generic; // Queue 사용을 위해 필요

// 오브젝트 풀에서 관리될 오브젝트에 부착될 스크립트
public class PooledObject : MonoBehaviour
{
    public ObjectPool MyPool { get; set; } // 이 오브젝트를 관리하는 풀의 참조

    // 오브젝트가 풀에서 꺼내져 활성화될 때 호출됩니다.
    void OnEnable()
    {
        // 여기서 오브젝트 재사용을 위한 초기화를 수행합니다.
        // 예: 체력 초기화, 위치 초기화, 파티클 시스템 재시작, 적인 경우 AI 재시작 등
        Debug.Log(gameObject.name + " has been enabled and reset for use.");
        
        // 예시: 체력 컴포넌트가 있다면 체력을 최대로 설정
        /*
        Health healthComponent = GetComponent<Health>();
        if (healthComponent != null)
        {
            healthComponent.ResetHealth();
        }
        */
    }

    // 오브젝트가 풀로 돌아가 비활성화될 때 호출됩니다.
    void OnDisable()
    {
        // 여기서 오브젝트가 풀로 돌아가기 전 필요한 정리 작업을 수행합니다.
        // 예: 구독했던 이벤트 해제, 실행 중이던 코루틴 중지, 트레일 렌더러 클리어 등
        Debug.Log(gameObject.name + " has been disabled and is returning to the pool.");
    }

    // 외부에서 이 오브젝트를 풀로 되돌릴 때 호출하는 메서드
    public void ReturnToPool()
    {
        if (MyPool != null)
        {
            MyPool.ReturnObjectToPool(this.gameObject);
        }
        else
        {
            // 풀이 할당되지 않았다면 그냥 비활성화 (오류 상황일 수 있음)
            gameObject.SetActive(false);
            Debug.LogWarning(gameObject.name + " returned without a pool reference.");
        }
    }
}

// 간단한 오브젝트 풀 매니저 스크립트
public class ObjectPool : MonoBehaviour
{
    public GameObject objectPrefab; // 풀링할 오브젝트의 프리팹
    public int initialPoolSize = 10; // 초기 풀 크기

    // 사용 가능한 오브젝트들을 담아둘 큐
    private Queue<GameObject> availableObjects = new Queue<GameObject>();

    void Awake()
    {
        InitializePool(); // 게임 시작 시 풀 초기화
    }

    // 풀을 초기화하는 메서드
    void InitializePool()
    {
        for (int i = 0; i < initialPoolSize; i++)
        {
            CreateAndPoolObject();
        }
    }

    // 새 오브젝트를 생성하고 풀에 추가하는 도우미 메서드
    GameObject CreateAndPoolObject()
    {
        GameObject newObj = Instantiate(objectPrefab);
        newObj.SetActive(false); // 처음에는 비활성화 상태로 생성

        PooledObject pooledObjScript = newObj.GetComponent<PooledObject>();
        if (pooledObjScript != null)
        {
            pooledObjScript.MyPool = this; // PooledObject 스크립트에 풀 참조 설정
        }
        else
        {
            // PooledObject 스크립트가 없다면 추가하거나 경고를 표시할 수 있습니다.
            Debug.LogWarning("Prefab " + objectPrefab.name + " is missing PooledObject script for proper pooling.");
        }
        
        availableObjects.Enqueue(newObj); // 큐에 추가
        return newObj;
    }

    // 풀에서 오브젝트를 가져오는 메서드
    public GameObject GetObjectFromPool()
    {
        if (availableObjects.Count > 0)
        {
            GameObject objToServe = availableObjects.Dequeue(); // 큐에서 하나 꺼냄
            objToServe.SetActive(true); // 활성화 (이때 PooledObject의 OnEnable 호출됨)
            return objToServe;
        }
        else
        {
            // 풀이 비어있을 경우: 동적으로 확장하거나, null을 반환하거나, 경고를 표시할 수 있습니다.
            // 여기서는 간단히 하나 더 생성하고 반환합니다 (실제 프로젝트에서는 확장 정책을 고려해야 함).
            Debug.LogWarning("Object pool for " + objectPrefab.name + " is empty. Creating a new instance (consider increasing initial pool size).");
            GameObject newObj = CreateAndPoolObject(); // 새 오브젝트를 만들고 풀에도 넣어둡니다.
            availableObjects.Dequeue(); // 방금 넣은 것을 다시 꺼냅니다. (큐 관리를 위해)
            newObj.SetActive(true);
            return newObj; 
        }
    }

    // 사용한 오브젝트를 풀로 되돌리는 메서드
    public void ReturnObjectToPool(GameObject obj)
    {
        obj.SetActive(false); // 비활성화 (이때 PooledObject의 OnDisable 호출됨)
        availableObjects.Enqueue(obj); // 다시 큐에 추가
    }
}

Ⅵ. Unity 개발자가 알아야 할 주요 기술 분야 🛠️

  • UI/UX: UGUI, Canvas, RectTransform, EventSystem
  • Physics: Rigidbody, Collider, Raycast
  • Animation: Animator, Timeline, Blend Tree
  • Audio: AudioSource
  • Data Handling: JSON, ScriptableObject, 프리팹 (데이터 드리븐)
  • Optimization: Profiler, Frame Debugger, Addressables
  • Network: UnityWebRequest, REST API, WebSocket
  • Architecture & Patterns: MonoBehaviour 기반 설계, 디자인 패턴
  • Build & Deploy: 빌드 설정 (Android, iOS 등), CI/CD

이 모든 기술을 효과적으로 활용하기 위해서는 유니티 라이프사이클과 아키텍처에 대한 이해가 선행되어야 합니다.


Ⅶ. Q&A 및 과제 안내 🙋‍♂️

  • 질문: 컴포넌트 구조에서도 상속이 필요한 경우?

    • 답변: 유니티는 게임 오브젝트 수준에서 상속을 사용하기보다 컴포넌트를 조합하는 방식입니다. 하지만 각 컴포넌트 자체는 C# 클래스이므로, 컴포넌트 내부 로직을 구현할 때 클래스 간의 상속이나 인터페이스 활용은 객체 지향 원칙에 따라 적극적으로 사용됩니다.
  • 과제 (제출 불필요, 슬랙 스레드에 자유롭게 답변):

    1. 금일 강의 내용 복습 (Unity 라이프사이클)

    2. OnEnable()OnDisable()을 실제 게임 로직에서 어떻게 활용할 수 있을지 다양한 예시 스터디 및 고민해보기.

      🤔 OnEnable() / OnDisable() 활용 예시 더 깊이 파고들기

      OnEnable()OnDisable()은 오브젝트의 활성화/비활성화 상태 변경 시점에 특정 로직을 실행하고자 할 때 매우 유용합니다. 특히 오브젝트 풀링과 같은 최적화 기법이나, UI 요소의 동적 관리, 특정 조건에 따른 기능 활성화/비활성화 등에 널리 사용됩니다.

      예시 1: UI 팝업창 관리

      • 시나리오: 게임 내에서 특정 조건(예: 버튼 클릭, 이벤트 발생)에 따라 팝업 UI가 나타나고 사라집니다. 팝업이 활성화될 때는 내부 버튼들의 이벤트 리스너를 등록하고, 비활성화될 때는 해제하여 메모리 누수 및 오류를 방지해야 합니다.
      • OnEnable() 활용:
        • 팝업창이 SetActive(true)로 활성화될 때 호출됩니다.
        • 팝업 내 버튼들의 onClick 이벤트에 필요한 함수들을 연결(구독)합니다. (예: confirmButton.onClick.AddListener(OnConfirm);)
        • 팝업 등장 애니메이션을 재생할 수 있습니다.
      • OnDisable() 활용:
        • 팝업창이 SetActive(false)로 비활성화될 때 호출됩니다.
        • OnEnable()에서 등록했던 버튼 이벤트 리스너들을 모두 해제합니다. (예: confirmButton.onClick.RemoveListener(OnConfirm);) 이 과정은 매우 중요합니다!
        • 팝업 퇴장 애니메이션을 재생할 수 있습니다.
      // UIPopup.cs
      using UnityEngine;
      using UnityEngine.UI; // UI 요소를 사용하기 위해 필요
      
      public class UIPopup : MonoBehaviour
      {
          public Button confirmButton;
          public Button cancelButton;
      
          void OnEnable()
          {
              Debug.Log(gameObject.name + " Popup Enabled: Subscribing to button events.");
              // 버튼 이벤트 구독
              if (confirmButton != null) confirmButton.onClick.AddListener(OnConfirmClicked);
              if (cancelButton != null) cancelButton.onClick.AddListener(OnCancelClicked);
              
              // 팝업 등장 애니메이션 시작 (필요하다면)
              // PlayAppearAnimation(); 
          }
      
          void OnDisable()
          {
              Debug.Log(gameObject.name + " Popup Disabled: Unsubscribing from button events.");
              // 버튼 이벤트 구독 해제 (메모리 누수 방지 및 오류 예방)
              if (confirmButton != null) confirmButton.onClick.RemoveListener(OnConfirmClicked);
              if (cancelButton != null) cancelButton.onClick.RemoveListener(OnCancelClicked);
      
              // 팝업 퇴장 애니메이션 시작 (필요하다면)
              // PlayDisappearAnimation();
          }
      
          void OnConfirmClicked()
          {
              Debug.Log("Confirm button clicked!");
              // 확인 버튼 로직 처리
              gameObject.SetActive(false); // 팝업 닫기
          }
      
          void OnCancelClicked()
          {
              Debug.Log("Cancel button clicked!");
              // 취소 버튼 로직 처리
              gameObject.SetActive(false); // 팝업 닫기
          }
      }

      예시 2: 플레이어 버프/디버프 효과 관리

      • 시나리오: 플레이어가 특정 아이템을 획득하면 일정 시간 동안 이동 속도 증가 버프를 받습니다. 버프 효과를 관리하는 스크립트가 플레이어 오브젝트에 추가되거나, 별도 컴포넌트로 활성화됩니다.
      • OnEnable() 활용:
        • 버프 효과 스크립트가 활성화될 때 호출됩니다.
        • 플레이어의 이동 속도를 증가시킵니다.
        • 버프 지속 시간을 관리하는 코루틴을 시작합니다.
        • 버프 시각 효과(파티클 등)를 활성화합니다.
      • OnDisable() 활용:
        • 버프 지속 시간이 끝나거나, 다른 이유로 효과가 비활성화될 때 호출됩니다.
        • 플레이어의 이동 속도를 원래대로 복구합니다.
        • 실행 중이던 버프 지속 시간 코루틴을 중지합니다.
        • 버프 시각 효과를 비활성화합니다.
      // SpeedBuff.cs
      using UnityEngine;
      using System.Collections; // 코루틴 사용을 위해 필요
      
      public class SpeedBuff : MonoBehaviour
      {
          public float duration = 5f; // 버프 지속 시간
          public float speedMultiplier = 1.5f; // 속도 증가 배율
          private PlayerMovement playerMovement; // 플레이어 이동 스크립트 참조
          private float originalSpeed;
      
          void Awake()
          {
              // 플레이어 이동 스크립트 미리 찾아두기 (실제로는 더 안전한 방법 사용 권장)
              playerMovement = GetComponentInParent<PlayerMovement>(); 
                              // 또는 GetComponent<PlayerMovement>() 만약 같은 오브젝트에 있다면
          }
      
          void OnEnable()
          {
              if (playerMovement == null)
              {
                  Debug.LogError("PlayerMovement script not found for SpeedBuff.");
                  enabled = false; // PlayerMovement 없으면 이 스크립트 비활성화
                  return;
              }
      
              Debug.Log("Speed Buff Enabled: Increasing player speed.");
              originalSpeed = playerMovement.moveSpeed; // 원래 속도 저장
              playerMovement.moveSpeed *= speedMultiplier; // 속도 증가
      
              // 버프 시각 효과 활성화 (필요하다면)
              // ShowBuffEffect(true);
      
              StartCoroutine(BuffTimer()); // 버프 지속 시간 타이머 시작
          }
      
          void OnDisable()
          {
              if (playerMovement != null && playerMovement.moveSpeed != originalSpeed) // 이미 원래 속도면 중복 실행 방지
              {
                  Debug.Log("Speed Buff Disabled: Reverting player speed.");
                  playerMovement.moveSpeed = originalSpeed; // 속도 복구
              }
              
              // 버프 시각 효과 비활성화 (필요하다면)
              // ShowBuffEffect(false);
      
              StopAllCoroutines(); // 이 스크립트에서 실행 중인 모든 코루틴 중지
          }
      
          IEnumerator BuffTimer()
          {
              yield return new WaitForSeconds(duration);
              Debug.Log("Speed Buff duration ended.");
              gameObject.SetActive(false); // 또는 enabled = false; 를 통해 OnDisable 호출 유도
          }
      }
      
      // 간단한 PlayerMovement 스크립트 (SpeedBuff와 연동 예시)
      public class PlayerMovement : MonoBehaviour
      {
          public float moveSpeed = 5f;
          // Update 등에서 moveSpeed를 사용하여 이동 로직 구현...
          void Update()
          {
              // 예시 이동 로직
              float horizontal = Input.GetAxis("Horizontal");
              float vertical = Input.GetAxis("Vertical");
              Vector3 movement = new Vector3(horizontal, 0, vertical);
              transform.Translate(movement * moveSpeed * Time.deltaTime, Space.World);
          }
      }

      예시 3: 적 AI 활성화/비활성화 (최적화)

      • 시나리오: 넓은 맵에서 플레이어와 멀리 떨어진 적들은 연산을 줄이기 위해 AI 로직을 비활성화합니다. 플레이어가 가까이 다가가면 AI를 다시 활성화하여 반응하도록 합니다. (이는 주로 특정 트리거 영역 진입/이탈 시 SetActive(true/false)를 통해 제어됩니다.)
      • OnEnable() 활용:
        • 적 오브젝트 또는 AI 스크립트가 활성화될 때 호출됩니다.
        • AI 행동 패턴(순찰, 추적 등)을 시작합니다.
        • 적의 시야 감지, 공격 로직 등을 활성화합니다.
        • 애니메이션 상태를 'Idle' 또는 'Patrol' 등으로 초기화합니다.
      • OnDisable() 활용:
        • 적이 비활성화될 때 호출됩니다.
        • 실행 중인 모든 AI 관련 코루틴(예: 특정 경로 이동)을 중지합니다.
        • 타겟 정보를 초기화합니다.
        • 애니메이션을 정지하거나 기본 상태로 돌립니다. (리소스 절약)
      // EnemyAI.cs
      using UnityEngine;
      using System.Collections;
      
      public class EnemyAI : MonoBehaviour
      {
          public enum AIState { Idle, Patrolling, Chasing, Attacking }
          public AIState currentState;
          public float detectionRadius = 10f;
          public Transform playerTransform; // 플레이어 참조 (실제로는 더 동적으로 찾음)
      
          private Coroutine aiLogicCoroutine;
      
          void OnEnable()
          {
              Debug.Log(gameObject.name + " AI Enabled: Starting AI routines.");
              // 플레이어 탐색 (실제 게임에서는 GameManager나 다른 방식을 통해 플레이어 참조를 얻어옴)
              if (playerTransform == null)
              {
                  GameObject player = GameObject.FindGameObjectWithTag("Player");
                  if (player != null) playerTransform = player.transform;
              }
      
              currentState = AIState.Idle; // 초기 상태 설정
              if (aiLogicCoroutine != null) StopCoroutine(aiLogicCoroutine); // 이전 코루틴 중지
              aiLogicCoroutine = StartCoroutine(AIStateMachine()); // AI 상태 머신 시작
              
              // 애니메이션 초기화 등
              // GetComponent<Animator>().Play("Idle"); 
          }
      
          void OnDisable()
          {
              Debug.Log(gameObject.name + " AI Disabled: Stopping AI routines.");
              if (aiLogicCoroutine != null)
              {
                  StopCoroutine(aiLogicCoroutine); // AI 로직 코루틴 중지
                  aiLogicCoroutine = null;
              }
              // 기타 정리 작업 (예: 공격 중이었다면 공격 중단)
          }
      
          IEnumerator AIStateMachine()
          {
              while (true) // enabled 상태 동안 계속 실행
              {
                  switch (currentState)
                  {
                      case AIState.Idle:
                          // Idle 로직 (예: 주변 둘러보기, 잠시 후 Patrolling으로 전환)
                          // Debug.Log(gameObject.name + " is Idle.");
                          yield return new WaitForSeconds(2f); // 예시: 2초 대기
                          if (CanSeePlayer()) currentState = AIState.Chasing;
                          else currentState = AIState.Patrolling;
                          break;
                      case AIState.Patrolling:
                          // Patrolling 로직 (예: 정해진 경로 순찰)
                          // Debug.Log(gameObject.name + " is Patrolling.");
                          yield return new WaitForSeconds(3f); // 예시: 3초 순찰
                           if (CanSeePlayer()) currentState = AIState.Chasing;
                          break;
                      case AIState.Chasing:
                          // Chasing 로직 (예: 플레이어 추적)
                          // Debug.Log(gameObject.name + " is Chasing Player.");
                          if (playerTransform != null)
                          {
                              // 간단한 추적 로직 (실제로는 NavMeshAgent 등 사용)
                              // transform.position = Vector3.MoveTowards(transform.position, playerTransform.position, Time.deltaTime * 3f);
                              if (!CanSeePlayer()) currentState = AIState.Patrolling; // 플레이어 놓치면 다시 순찰
                              // if (Vector3.Distance(transform.position, playerTransform.position) < 2f) currentState = AIState.Attacking; // 가까우면 공격
                          } else {
                              currentState = AIState.Idle; // 플레이어 없으면 Idle
                          }
                          yield return null;
                          break;
                      case AIState.Attacking:
                          // Attacking 로직
                          // Debug.Log(gameObject.name + " is Attacking Player.");
                          yield return new WaitForSeconds(1f); // 예시: 1초 공격
                          // if (playerTransform == null || Vector3.Distance(transform.position, playerTransform.position) > 2.5f) 
                          //    currentState = AIState.Chasing; // 멀어지면 다시 추적
                          break;
                  }
                  yield return null; // 다음 프레임까지 대기
              }
          }
      
          bool CanSeePlayer()
          {
              if (playerTransform == null) return false;
              return Vector3.Distance(transform.position, playerTransform.position) < detectionRadius;
          }
      }

참고 자료

  • 본 포스팅은 "[Unity 9기] 챌린지반 - 발표자 보기가 포함된 공유 화면 (2025년 5월 8일)" 강의 (강사: 진우원 님) 내용을 기반으로 작성되었습니다.
  • 유니티 공식 문서: Unity Manual - Order of execution for event functions (라이프사이클 관련 추가 학습 자료로 활용하시면 좋습니다.)

이상으로 유니티 엔진의 기본과 라이프사이클에 대한 정리를 마칩니다. 이 글이 유니티 개발을 시작하시거나 더 깊이 이해하고 싶은 분들께 도움이 되셨으면 좋겠습니다. 궁금한 점은 댓글로 남겨주세요! 😊

profile
개발 입문자

0개의 댓글