Unity

유니티의 생명주기

우대비 2025. 5. 21. 21:18
반응형

 

유니티 생명주기 완벽 가이드: 모노비헤이비어의 숨겨진 비밀

목차

  1. 들어가며
  2. 생명주기 개요
  3. 초기화 단계
  4. 물리 연산 단계
  5. 메인 업데이트 단계
  6. 렌더링 단계
  7. 비활성화/파괴 단계
  8. 마치며

들어가며

Unity 개발자라면 Start(), Update()와 같은 메서드들을 사용해 본 경험이 있을 것입니다. 하지만 이러한 메서드들이 정확히 언제, 어떤 순서로 호출되는지, 그리고 이를 어떻게 효과적으로 활용할 수 있는지 깊이 이해하는 개발자는 많지 않습니다. 본 글에서는 Unity의 생명주기를 세밀하게 분석하고, 실제 게임 개발 과정에서 이를 최적화하는 방법을 소개합니다.

생명주기 개요

Unity의 생명주기는 크게 5단계로 나눌 수 있습니다:

  1. 초기화 단계: 오브젝트 생성 및 초기 설정
  2. 물리 연산 단계: 물리 엔진 계산 수행
  3. 메인 업데이트 단계: 게임 로직 실행
  4. 렌더링 단계: 화면 출력 처리
  5. 비활성화/파괴 단계: 오브젝트 제거 및 정리

이제 각 단계별로 세부적인 메서드 호출 순서와 활용법을 알아보겠습니다.

초기화 단계

Awake() → OnEnable() → Start()

Awake()

  • 호출 시점: 스크립트 인스턴스가 로드될 때 단 한 번 호출
  • 특징: 다른 오브젝트의 참조를 설정하기 적합하며, OnEnable()보다 먼저 실행됨
  • 활용 팁: 싱글톤 패턴 구현, 의존성 관리, 컴포넌트 레퍼런스 캐싱에 적합
private void Awake()
{
    // 싱글톤 패턴 구현 예시
    if (Instance != null && Instance != this)
    {
        Destroy(gameObject);
        return;
    }
    Instance = this;
    
    // 컴포넌트 캐싱
    _rigidbody = GetComponent<Rigidbody>();
    _animator = GetComponent<Animator>();
}

OnEnable()

  • 호출 시점: 오브젝트가 활성화될 때마다 호출
  • 특징: Awake()Start() 사이에서 실행되며, 오브젝트를 비활성화했다가 다시 활성화할 때마다 호출됨
  • 활용 팁: 이벤트 리스너 등록, 초기 상태 리셋에 사용
private void OnEnable()
{
    // 이벤트 구독
    EventManager.OnGameStart += HandleGameStart;
    
    // 상태 초기화
    _isReady = true;
    _elapsedTime = 0f;
}

Start()

  • 호출 시점: 첫 번째 프레임 업데이트 전에 한 번 호출
  • 특징: Awake()OnEnable() 이후에 실행되며, 초기화가 필요한 모든 스크립트의 Awake()가 완료된 후 호출됨
  • 활용 팁: 다른 오브젝트의 초기화가 완료된 후 수행할 작업에 적합
private IEnumerator Start()
{
    // 다른 시스템 초기화 대기
    yield return new WaitForEndOfFrame();
    
    // 초기 게임 상태 설정
    _playerController.Initialize(_gameSettings);
    
    // UI 업데이트
    _uiManager.UpdatePlayerInfo();
}

물리 연산 단계

FixedUpdate() → 물리 연산 → OnTriggerXXX()/OnCollisionXXX()

FixedUpdate()

  • 호출 시점: 물리 엔진의 고정 타임스텝마다 호출 (기본 0.02초마다, Project Settings에서 조정 가능)
  • 특징: 프레임 레이트에 독립적으로 일정한 간격으로 호출되어 물리 계산에 적합
  • 활용 팁: Rigidbody 기반 이동, 힘 적용, 물리 시뮬레이션 제어에 사용
private void FixedUpdate()
{
    // 입력 값에 따른 물리 기반 이동
    Vector3 movement = new Vector3(_input.x, 0, _input.y) * _moveSpeed * Time.fixedDeltaTime;
    _rigidbody.MovePosition(_rigidbody.position + movement);
    
    // 힘 적용
    if (_isJumping)
    {
        _rigidbody.AddForce(Vector3.up * _jumpForce, ForceMode.Impulse);
        _isJumping = false;
    }
}

물리 충돌 이벤트

  • OnTriggerEnter/Stay/Exit: 트리거 콜라이더 간 접촉 시 호출
  • OnCollisionEnter/Stay/Exit: 물리적 충돌 발생 시 호출
  • 특징: 물리 연산 이후에 호출되며, 물리 처리 결과를 기반으로 게임 로직을 실행하기 적합
private void OnTriggerEnter(Collider other)
{
    // 태그 기반 처리 예시
    if (other.CompareTag("Item"))
    {
        // 아이템 획득 로직
        Item item = other.GetComponent<Item>();
        if (item != null)
        {
            _inventory.AddItem(item);
            item.OnCollected();
        }
    }
}

private void OnCollisionEnter(Collision collision)
{
    // 충돌 속도 계산
    float impactVelocity = collision.relativeVelocity.magnitude;
    
    // 충돌 강도에 따른 피해 처리
    if (impactVelocity > _damageThreshold)
    {
        float damage = impactVelocity * _damageMultiplier;
        _healthSystem.TakeDamage(damage);
        
        // 충돌 지점에 효과 생성
        foreach (ContactPoint contact in collision.contacts)
        {
            Instantiate(_impactEffectPrefab, contact.point, Quaternion.LookRotation(contact.normal));
        }
    }
}

메인 업데이트 단계

Update() → LateUpdate()

Update()

  • 호출 시점: 매 프레임마다 호출
  • 특징: 대부분의 게임 로직이 실행되는 곳으로, 프레임 레이트에 따라 호출 빈도가 달라짐
  • 활용 팁: 입력 처리, 타이머, 비물리 기반 이동, 게임 상태 관리에 사용
private void Update()
{
    // 입력 처리
    _input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
    _isJumping = Input.GetButtonDown("Jump");
    
    // 타이머 관리
    _cooldownTimer -= Time.deltaTime;
    if (_cooldownTimer <= 0f && _input.magnitude > 0.1f)
    {
        _cooldownTimer = _abilityCooldown;
        UseAbility();
    }
    
    // 비물리 기반 이동 (Transform 직접 조작)
    if (_movementType == MovementType.TransformBased)
    {
        transform.position += new Vector3(_input.x, 0, _input.y) * _moveSpeed * Time.deltaTime;
    }
    
    // 애니메이션 파라미터 업데이트
    _animator.SetFloat("Speed", _input.magnitude);
    _animator.SetBool("IsGrounded", _groundChecker.IsGrounded());
}

LateUpdate()

  • 호출 시점: 모든 Update() 호출이 완료된 후 매 프레임마다 호출
  • 특징: 다른 스크립트의 Update()에서 발생한 변경사항을 기반으로 작업하기 적합
  • 활용 팁: 카메라 추적, 최종 위치 조정, 애니메이션 후처리에 사용
private void LateUpdate()
{
    // 카메라 추적 예시
    if (_targetTransform != null)
    {
        // 타겟 추적 위치 계산
        Vector3 targetPosition = _targetTransform.position + _cameraOffset;
        
        // 카메라 스무딩 적용
        Vector3 smoothedPosition = Vector3.Lerp(transform.position, targetPosition, _smoothSpeed * Time.deltaTime);
        transform.position = smoothedPosition;
        
        // 카메라가 타겟을 바라보도록 설정
        transform.LookAt(_targetTransform.position + _lookOffset);
    }
    
    // UI 요소의 월드 위치 업데이트 (빌보드 효과)
    if (_worldSpaceUI != null)
    {
        _worldSpaceUI.rotation = Quaternion.LookRotation(_worldSpaceUI.position - Camera.main.transform.position);
    }
}

렌더링 단계

OnPreRender() → OnRenderObject() → OnPostRender() → OnRenderImage()

렌더링 콜백 메서드들

  • OnPreRender(): 카메라가 씬을 렌더링하기 직전에 호출
  • OnRenderObject(): 모든 일반 씬 렌더링이 완료된 후 호출
  • OnPostRender(): 카메라가 씬을 렌더링한 후 호출
  • OnRenderImage(): 씬 렌더링 후 이미지 효과 적용에 사용
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
    // 후처리 효과 예시 (블러 효과)
    if (_isBlurEnabled && _blurMaterial != null)
    {
        // 블러 강도 설정
        _blurMaterial.SetFloat("_BlurSize", _blurIntensity);
        
        // 임시 렌더 텍스처
        RenderTexture tempRT = RenderTexture.GetTemporary(
            source.width, source.height, 0, source.format);
        
        // 수평 블러
        Graphics.Blit(source, tempRT, _blurMaterial, 0);
        // 수직 블러
        Graphics.Blit(tempRT, destination, _blurMaterial, 1);
        
        // 임시 텍스처 해제
        RenderTexture.ReleaseTemporary(tempRT);
    }
    else
    {
        // 효과 없이 그대로 출력
        Graphics.Blit(source, destination);
    }
}

비활성화/파괴 단계

OnDisable() → OnDestroy()

OnDisable()

  • 호출 시점: 오브젝트가 비활성화될 때마다 호출
  • 특징: 오브젝트를 다시 활성화하면 OnEnable()이 호출됨
  • 활용 팁: 이벤트 리스너 해제, 임시 상태 저장에 사용
private void OnDisable()
{
    // 이벤트 구독 해제
    EventManager.OnGameStart -= HandleGameStart;
    
    // 실행 중인 코루틴 중지
    StopAllCoroutines();
    
    // 현재 상태 저장
    if (_shouldPersistState)
    {
        SaveSystem.SaveObjectState(gameObject.name, transform.position, _currentHealth);
    }
}

OnDestroy()

  • 호출 시점: 오브젝트가 파괴될 때 호출
  • 특징: 씬 전환이나 게임 종료 시에도 호출됨
  • 활용 팁: 영구적인 리소스 해제, 저장 작업, 풀링된 오브젝트 반환에 사용
private void OnDestroy()
{
    // 리소스 해제
    if (_asyncOperation != null)
    {
        _asyncOperation.Dispose();
    }
    
    // 데이터 저장
    PlayerPrefs.SetInt("LastScore", _currentScore);
    PlayerPrefs.Save();
    
    // 풀링 시스템에 오브젝트 반환
    if (_isPooled)
    {
        ObjectPoolManager.ReturnToPool(gameObject);
        // 이 경우 실제로 Destroy되지 않고 비활성화됨
    }
    
    // 싱글톤 인스턴스 정리
    if (Instance == this)
    {
        Instance = null;
    }
}

생명주기를 활용한 인터페이스 아키텍처

// 시스템 시작 시 초기화가 필요한 컴포넌트용 인터페이스
public interface IInitializable
{
    void Initialize();
}

// 매 프레임 업데이트가 필요한 컴포넌트용 인터페이스
public interface IUpdatable
{
    void Tick(float deltaTime);
}

// 게임 매니저
public class GameManager : MonoBehaviour
{
    private List<IInitializable> _initializables = new List<IInitializable>();
    private List<IUpdatable> _updatables = new List<IUpdatable>();
    
    private void Awake()
    {
        // 모든 IInitializable 컴포넌트 수집
        _initializables.AddRange(FindObjectsOfType<MonoBehaviour>().OfType<IInitializable>());
        
        // 모든 IUpdatable 컴포넌트 수집
        _updatables.AddRange(FindObjectsOfType<MonoBehaviour>().OfType<IUpdatable>());
    }
    
    private void Start()
    {
        // 모든 초기화 가능 컴포넌트 초기화
        foreach (var initializable in _initializables)
        {
            initializable.Initialize();
        }
    }
    
    private void Update()
    {
        float deltaTime = Time.deltaTime;
        
        // 모든 업데이트 가능 컴포넌트 업데이트
        foreach (var updatable in _updatables)
        {
            updatable.Tick(deltaTime);
        }
    }
}

마치며

Unity의 생명주기를 깊이 이해하는 것은 효율적이고 안정적인 게임 개발의 기초가 됩니다. 각 생명주기 메서드의 특성과 호출 시점을 정확히 파악하면, 의도한 대로 코드가 실행되도록 보장하고 성능을 최적화할 수 있습니다.

효과적인 생명주기 활용을 위한 핵심 요점을 다시 정리하면:

  1. 목적에 맞는 메서드 선택: 물리 연산은 FixedUpdate(), 일반 게임 로직은 Update(), 후처리는 LateUpdate()에 배치
  2. 초기화 로직 분리: 참조 설정은 Awake(), 다른 컴포넌트에 의존하는 초기화는 Start()에 배치
  3. 이벤트 관리: 이벤트 리스너 등록/해제는 OnEnable()/OnDisable()에서 처리
  4. 코루틴 활용: 분산 처리가 필요한 작업은 코루틴으로 구현
  5. 리소스 관리: 리소스 해제와 메모리 정리는 OnDisable()OnDestroy()에서 처리

이러한 원칙을 따르면서 설계하면, 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 유니티의 생명주기는 복잡하지만, 이를 마스터하면 게임 개발의 많은 문제를 우아하게 해결할 수 있습니다.

반응형
LIST

'Unity' 카테고리의 다른 글

유니티 애널리틱스  (0) 2025.05.23
유니티 생명주기 - 코루틴  (0) 2025.05.22
FishNet 사용법  (0) 2025.05.20
Unity 렌더링 파이프라인  (0) 2025.05.19
유니티 쿼터니언(Quaternion)  (1) 2025.05.16