반응형
유니티 생명주기 완벽 가이드: 모노비헤이비어의 숨겨진 비밀
목차
들어가며
Unity 개발자라면 Start()
, Update()
와 같은 메서드들을 사용해 본 경험이 있을 것입니다. 하지만 이러한 메서드들이 정확히 언제, 어떤 순서로 호출되는지, 그리고 이를 어떻게 효과적으로 활용할 수 있는지 깊이 이해하는 개발자는 많지 않습니다. 본 글에서는 Unity의 생명주기를 세밀하게 분석하고, 실제 게임 개발 과정에서 이를 최적화하는 방법을 소개합니다.
생명주기 개요
Unity의 생명주기는 크게 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의 생명주기를 깊이 이해하는 것은 효율적이고 안정적인 게임 개발의 기초가 됩니다. 각 생명주기 메서드의 특성과 호출 시점을 정확히 파악하면, 의도한 대로 코드가 실행되도록 보장하고 성능을 최적화할 수 있습니다.
효과적인 생명주기 활용을 위한 핵심 요점을 다시 정리하면:
- 목적에 맞는 메서드 선택: 물리 연산은
FixedUpdate()
, 일반 게임 로직은Update()
, 후처리는LateUpdate()
에 배치 - 초기화 로직 분리: 참조 설정은
Awake()
, 다른 컴포넌트에 의존하는 초기화는Start()
에 배치 - 이벤트 관리: 이벤트 리스너 등록/해제는
OnEnable()
/OnDisable()
에서 처리 - 코루틴 활용: 분산 처리가 필요한 작업은 코루틴으로 구현
- 리소스 관리: 리소스 해제와 메모리 정리는
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 |