생명주기 심화: 코루틴의 실행 흐름
코루틴(Coroutine)은 Unity의 생명주기와 밀접하게 연관된 강력한 비동기 프로그래밍 도구입니다. 코루틴을 사용하면 실행을 여러 프레임에 걸쳐 분산시킬 수 있으며, 이를 통해 무거운 작업을 수행하면서도 게임의 프레임 레이트를 유지할 수 있습니다.
코루틴과 생명주기의 관계
코루틴은 MonoBehaviour의 생명주기와 밀접하게 연결되어 있습니다:
- 코루틴은
StartCoroutine()
을 호출한 MonoBehaviour가 활성화된 상태에서만 실행됩니다. - MonoBehaviour가 비활성화되면(
OnDisable()
), 실행 중인 모든 코루틴이 자동으로 일시 중지됩니다. - MonoBehaviour가 다시 활성화되면(
OnEnable()
), 일시 중지된 코루틴이 자동으로 재개되지 않습니다. - GameObject가 파괴되면(
OnDestroy()
), 실행 중인 모든 코루틴이 자동으로 종료됩니다. StopAllCoroutines()
를 호출하면 해당 MonoBehaviour의 모든 코루틴이 중지됩니다.StopCoroutine()
을 호출하면 특정 코루틴만 중지할 수 있습니다.
코루틴 실행 시점과 yield 명령어의 의미
코루틴의 작동 원리: 코루틴을 시작하면, 첫 번째 yield 지점까지 즉시 실행된 후 제어권을 반환합니다. 다음 실행은 yield 명령어에 따라 결정된 시점에 계속됩니다.
yield 명령어 | 실행 시점 | 사용 사례 |
---|---|---|
yield return null |
다음 프레임의 Update() 직전 |
프레임 단위로 작업을 분산, 애니메이션 구현 |
yield return new WaitForFixedUpdate() |
다음 FixedUpdate() 이후 |
물리 계산 결과를 기다릴 때, 물리 기반 작업 수행 |
yield return new WaitForEndOfFrame() |
현재 프레임의 모든 카메라와 렌더링 작업이 완료된 후 | 렌더링 후 처리, 스크린샷 캡처, GUI 작업 |
yield return new WaitForSeconds(time) |
지정된 시간(초)이 경과한 후 | 지연된 액션, 쿨다운, 타이머 구현 |
yield return new WaitUntil(() => condition) |
지정된 조건이 참이 될 때까지 | 특정 조건 대기(예: 애니메이션 완료, 로딩 완료) |
yield return new WaitWhile(() => condition) |
지정된 조건이 참인 동안 대기 | 특정 상태가 지속되는 동안 대기(예: 일시 정지 상태) |
yield return StartCoroutine(AnotherCoroutine()) |
다른 코루틴이 완료될 때까지 | 순차적 코루틴 실행, 종속 작업 처리 |
yield return asyncOperation |
AsyncOperation(씬 로딩, 비동기 자원 로드 등)이 완료될 때까지 | 자원 로딩, 씬 전환 처리 |
고급 코루틴 패턴: 실전 예제
순차적 작업 처리 패턴
private IEnumerator GameStartSequence()
{
// 1. 페이드 인 효과
yield return StartCoroutine(FadeInEffect(1.0f));
// 2. 인트로 애니메이션 재생
_animator.SetTrigger("PlayIntro");
// 애니메이션이 완료될 때까지 대기
// AnimationClip.length를 사용하는 것보다 더 정확함
yield return new WaitUntil(() => !_animator.GetCurrentAnimatorStateInfo(0).IsName("Intro") ||
_animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f);
// 3. 초기 리소스 로드
AsyncOperation resourceLoad = Resources.LoadAsync<GameObject>("GameAssets");
yield return resourceLoad;
if (resourceLoad.isDone)
{
GameObject assets = resourceLoad.asset as GameObject;
Instantiate(assets);
}
// 4. 게임 시작 알림
GameManager.Instance.OnGameReady();
// 5. 플레이어 컨트롤 활성화 (0.5초 지연)
yield return new WaitForSeconds(0.5f);
_playerController.enabled = true;
Debug.Log("게임 시퀀스 완료!");
}
주기적 작업 실행 패턴
private IEnumerator AIUpdateRoutine()
{
// 재사용 가능한 WaitForSeconds 객체 캐싱
WaitForSeconds updateInterval = new WaitForSeconds(0.2f);
while (true) // 무한 루프
{
// 높은 계산 비용의 AI 처리
foreach (var enemyAI in _activeEnemies)
{
if (enemyAI != null)
{
enemyAI.UpdatePathfinding();
enemyAI.EvaluateTargets();
// 몇 개의 AI만 처리한 후 다음 프레임까지 대기 (성능 최적화)
if (_activeEnemies.IndexOf(enemyAI) % 5 == 0 && _activeEnemies.IndexOf(enemyAI) > 0)
{
yield return null;
}
}
}
// 모든 AI 업데이트 후 지정된 간격만큼 대기
yield return updateInterval;
}
}
타임아웃 패턴
private IEnumerator LoadOperationWithTimeout(string resourcePath, float timeoutSeconds)
{
float startTime = Time.time;
bool isTimedOut = false;
bool isCompleted = false;
// 비동기 로드 시작
AsyncOperation asyncLoad = Resources.LoadAsync<GameObject>(resourcePath);
// 완료 콜백 등록
asyncLoad.completed += (op) => { isCompleted = true; };
// 타임아웃 또는 완료될 때까지 대기
while (!isCompleted && !isTimedOut)
{
// 진행 상황 업데이트
float progress = asyncLoad.progress;
_loadingBar.fillAmount = progress;
// 타임아웃 확인
if (Time.time - startTime > timeoutSeconds)
{
isTimedOut = true;
Debug.LogWarning($"로드 작업 타임아웃: {resourcePath}");
}
yield return null;
}
// 결과 처리
if (isCompleted && !isTimedOut)
{
Debug.Log($"리소스 로드 완료: {resourcePath}");
GameObject loadedResource = asyncLoad.asset as GameObject;
if (loadedResource != null)
{
Instantiate(loadedResource);
}
}
else
{
// 타임아웃 처리
_errorPanel.SetActive(true);
_errorText.text = "리소스 로드 실패. 네트워크 연결을 확인하세요.";
}
}
성능 최적화 팁: WaitForSeconds
와 같은 yield 객체를 반복해서 생성하면 가비지 컬렉션에 부담을 줄 수 있습니다. 자주 사용하는 시간 간격은 캐싱하여 재사용하는 것이 좋습니다.
생명주기와 성능 최적화
생명주기 메서드 선택과 성능 영향
Unity의 생명주기 메서드는 각각 호출 빈도와 실행 맥락이 다르므로, 적절한 메서드를 선택하는 것이 성능에 큰 영향을 미칩니다.
메서드 | 호출 빈도 | 성능 영향 | 최적화 권장사항 |
---|---|---|---|
Update() |
매 프레임 | 높음 (프레임 레이트에 직접적 영향) | - 무거운 계산 피하기 - 빈 Update() 메서드 제거 - 조건부 실행으로 불필요한 처리 방지 |
FixedUpdate() |
물리 타임스텝마다 (기본 0.02초) | 중간 (물리 계산에 영향) | - 물리 관련 코드만 넣기 - Fixed Timestep 값 최적화 - 불필요한 물리 계산 최소화 |
LateUpdate() |
모든 Update() 후 매 프레임 | 중간 (프레임 마지막에 영향) | - 카메라 관련 코드나 최종 위치 조정에만 사용 - 무거운 계산 피하기 |
코루틴 | yield 지시자에 따라 다름 | 낮음 (분산 실행 가능) | - 무거운 작업 분산에 사용 - yield 객체 캐싱 - 필요 없는 코루틴은 명시적 중지 |
OnRenderImage() |
카메라 렌더링 후 매 프레임 | 매우 높음 (GPU 성능에 영향) | - 간단한 셰이더 사용 - 조건부 처리로 불필요한 경우 바로 Blit - 저해상도 렌더 텍스처 사용 고려 |
Update() 최적화 전략
커스텀 업데이트 매니저 구현
public class UpdateManager : MonoBehaviour
{
public enum UpdateFrequency
{
EveryFrame,
EverySecondFrame,
EveryFifthFrame,
OncePerSecond
}
// 업데이트 빈도별 액션 목록
private List<Action> _everyFrameActions = new List<Action>();
private List<Action> _everySecondFrameActions = new List<Action>();
private List<Action> _everyFifthFrameActions = new List<Action>();
private List<Action> _oncePerSecondActions = new List<Action>();
// 시간 및 프레임 카운터
private int _frameCount = 0;
private float _secondCounter = 0;
private static UpdateManager _instance;
public static UpdateManager Instance
{
get
{
if (_instance == null)
{
GameObject go = new GameObject("UpdateManager");
_instance = go.AddComponent<UpdateManager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
public void RegisterAction(Action action, UpdateFrequency frequency)
{
switch (frequency)
{
case UpdateFrequency.EveryFrame:
_everyFrameActions.Add(action);
break;
case UpdateFrequency.EverySecondFrame:
_everySecondFrameActions.Add(action);
break;
case UpdateFrequency.EveryFifthFrame:
_everyFifthFrameActions.Add(action);
break;
case UpdateFrequency.OncePerSecond:
_oncePerSecondActions.Add(action);
break;
}
}
public void UnregisterAction(Action action)
{
_everyFrameActions.Remove(action);
_everySecondFrameActions.Remove(action);
_everyFifthFrameActions.Remove(action);
_oncePerSecondActions.Remove(action);
}
private void Update()
{
_frameCount++;
_secondCounter += Time.deltaTime;
// 매 프레임 실행
for (int i = 0; i < _everyFrameActions.Count; i++)
{
_everyFrameActions[i].Invoke();
}
// 2프레임마다 실행
if (_frameCount % 2 == 0)
{
for (int i = 0; i < _everySecondFrameActions.Count; i++)
{
_everySecondFrameActions[i].Invoke();
}
}
// 5프레임마다 실행
if (_frameCount % 5 == 0)
{
for (int i = 0; i < _everyFifthFrameActions.Count; i++)
{
_everyFifthFrameActions[i].Invoke();
}
}
// 1초마다 실행
if (_secondCounter >= 1.0f)
{
_secondCounter = 0;
for (int i = 0; i < _oncePerSecondActions.Count; i++)
{
_oncePerSecondActions[i].Invoke();
}
}
}
}
사용 예시:
private void Start()
{
// 매 프레임 업데이트가 필요한 플레이어 이동
UpdateManager.Instance.RegisterAction(UpdatePlayerMovement, UpdateManager.UpdateFrequency.EveryFrame);
// 2프레임마다 업데이트해도 충분한 UI
UpdateManager.Instance.RegisterAction(UpdateUI, UpdateManager.UpdateFrequency.EverySecondFrame);
// 5프레임마다 업데이트하는 AI 로직
UpdateManager.Instance.RegisterAction(UpdateAI, UpdateManager.UpdateFrequency.EveryFifthFrame);
// 1초마다 업데이트하는 게임 시간
UpdateManager.Instance.RegisterAction(UpdateGameTime, UpdateManager.UpdateFrequency.OncePerSecond);
}
private void OnDestroy()
{
// 등록된 액션 제거
UpdateManager.Instance.UnregisterAction(UpdatePlayerMovement);
UpdateManager.Instance.UnregisterAction(UpdateUI);
UpdateManager.Instance.UnregisterAction(UpdateAI);
UpdateManager.Instance.UnregisterAction(UpdateGameTime);
}
오브젝트 풀링과 생명주기 최적화
오브젝트 풀링은 Instantiate()
와 Destroy()
호출의 오버헤드를 줄여 성능을 향상시키는 중요한 기법입니다. 이는 생명주기 메서드와 밀접하게 관련됩니다.
고급 오브젝트 풀 구현 예제
public class AdvancedObjectPool : MonoBehaviour
{
[System.Serializable]
public class Pool
{
public string poolName;
public GameObject prefab;
public int initialSize;
[Tooltip("풀이 부족할 때 자동으로 확장할지 여부")]
public bool autoExpand = true;
[Tooltip("자동 확장 시 한 번에 추가할 오브젝트 수")]
public int expandSize = 5;
[Tooltip("최대 유지할 오브젝트 수 (0 = 무제한)")]
public int maxSize = 0;
[HideInInspector]
public Queue<PooledObject> queue = new Queue<PooledObject>();
}
// 풀링된 오브젝트를 위한 컴포넌트
public class PooledObject : MonoBehaviour
{
public Pool parentPool;
private IPooledObject[] _pooledBehaviours;
private void Awake()
{
// IPooledObject 인터페이스를 구현한 모든 컴포넌트 찾기
_pooledBehaviours = GetComponentsInChildren<IPooledObject>(true);
}
public void OnGetFromPool()
{
// 모든 IPooledObject 구현체에 풀에서 꺼내진 이벤트 알림
foreach (var behaviour in _pooledBehaviours)
{
behaviour.OnObjectSpawn();
}
}
public void OnReturnToPool()
{
// 모든 IPooledObject 구현체에 풀로 돌아가는 이벤트 알림
foreach (var behaviour in _pooledBehaviours)
{
behaviour.OnObjectDespawn();
}
}
}
// 풀링된 오브젝트의 행동을 정의하는 인터페이스
public interface IPooledObject
{
void OnObjectSpawn();
void OnObjectDespawn();
}
[SerializeField] private List<Pool> _pools = new List<Pool>();
private Dictionary<string, Pool> _poolDictionary = new Dictionary<string, Pool>();
private static AdvancedObjectPool _instance;
public static AdvancedObjectPool Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<AdvancedObjectPool>();
if (_instance == null)
{
GameObject obj = new GameObject("AdvancedObjectPool");
_instance = obj.AddComponent<AdvancedObjectPool>();
DontDestroyOnLoad(obj);
}
}
return _instance;
}
}
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
// 풀 초기화
InitializePools();
}
private void InitializePools()
{
foreach (Pool pool in _pools)
{
_poolDictionary[pool.poolName] = pool;
// 초기 오브젝트 생성
for (int i = 0; i < pool.initialSize; i++)
{
CreateNewPooledObject(pool);
}
}
}
private PooledObject CreateNewPooledObject(Pool pool)
{
GameObject obj = Instantiate(pool.prefab, transform);
obj.SetActive(false);
PooledObject pooledObj = obj.GetComponent<PooledObject>();
if (pooledObj == null)
{
pooledObj = obj.AddComponent<PooledObject>();
}
pooledObj.parentPool = pool;
pool.queue.Enqueue(pooledObj);
return pooledObj;
}
public GameObject SpawnFromPool(string poolName, Vector3 position, Quaternion rotation)
{
if (!_poolDictionary.ContainsKey(poolName))
{
Debug.LogWarning($"풀 '{poolName}'이 존재하지 않습니다!");
return null;
}
Pool pool = _poolDictionary[poolName];
// 풀이 비었을 때 자동 확장
if (pool.queue.Count == 0)
{
if (pool.autoExpand)
{
// 최대 크기 확인
if (pool.maxSize > 0 && pool.queue.Count + pool.expandSize > pool.maxSize)
{
Debug.LogWarning($"풀 '{poolName}'이 최대 크기에 도달했습니다!");
return null;
}
// 확장
for (int i = 0; i < pool.expandSize; i++)
{
CreateNewPooledObject(pool);
}
Debug.Log($"풀 '{poolName}'을 {pool.expandSize}만큼 확장했습니다.");
}
else
{
Debug.LogWarning($"풀 '{poolName}'이 비었으나 자동 확장이 꺼져 있습니다!");
return null;
}
}
// 풀에서 오브젝트 가져오기
PooledObject obj = pool.queue.Dequeue();
// 오브젝트가 파괴되었는지 확인 (씬 전환 등으로 인해)
if (obj == null)
{
// 새 오브젝트 생성
obj = CreateNewPooledObject(pool);
pool.queue.Dequeue(); // 새로 생성된 오브젝트 큐에서 제거
}
// 위치와 회전 설정
obj.transform.position = position;
obj.transform.rotation = rotation;
// 오브젝트 활성화
obj.gameObject.SetActive(true);
// OnObjectSpawn 호출
obj.OnGetFromPool();
return obj.gameObject;
}
public void ReturnToPool(GameObject obj)
{
PooledObject pooledObj = obj.GetComponent<PooledObject>();
if (pooledObj == null || pooledObj.parentPool == null)
{
Debug.LogWarning("이 오브젝트는 풀에서 가져온 것이 아닙니다!");
return;
}
// OnObjectDespawn 호출
pooledObj.OnReturnToPool();
// 오브젝트 비활성화
obj.SetActive(false);
// 풀에 반환
pooledObj.parentPool.queue.Enqueue(pooledObj);
}
}
구현 예시 - 발사체 컴포넌트:
public class Projectile : MonoBehaviour, AdvancedObjectPool.IPooledObject
{
[SerializeField] private float _speed = 10f;
[SerializeField] private float _lifetime = 3f;
private Rigidbody _rigidbody;
private TrailRenderer _trailRenderer;
private float _timer;
private void Awake()
{
_rigidbody = GetComponent<Rigidbody>();
_trailRenderer = GetComponent<TrailRenderer>();
}
// 풀에서 꺼내질 때 호출
public void OnObjectSpawn()
{
_timer = 0;
// 발사 방향으로 속도 설정
_rigidbody.velocity = transform.forward * _speed;
// 트레일 리셋
if (_trailRenderer != null)
{
_trailRenderer.Clear();
}
}
// 풀로 돌아갈 때 호출
public void OnObjectDespawn()
{
// 속도 리셋
_rigidbody.velocity = Vector3.zero;
_rigidbody.angularVelocity = Vector3.zero;
}
private void Update()
{
_timer += Time.deltaTime;
// 수명이 다하면 풀로 반환
if (_timer >= _lifetime)
{
AdvancedObjectPool.Instance.ReturnToPool(gameObject);
}
}
private void OnCollisionEnter(Collision collision)
{
// 충돌 효과 생성
Vector3 hitPoint = collision.contacts[0].point;
AdvancedObjectPool.Instance.SpawnFromPool("HitEffect", hitPoint, Quaternion.identity);
// 풀로 반환
AdvancedObjectPool.Instance.ReturnToPool(gameObject);
}
}
실전 활용 패턴
생명주기와 의존성 주입
의존성 주입(Dependency Injection)은 컴포넌트 간의 결합도를 낮추고 테스트 가능성을 높이는 중요한 디자인 패턴입니다. Unity의 생명주기와 결합하면 더 유연하고 유지보수하기 쉬운 아키텍처를 만들 수 있습니다.
// 서비스 로케이터 패턴과 생명주기를 결합한 의존성 관리자
public class ServiceLocator : MonoBehaviour
{
private static Dictionary<Type, object> _services = new Dictionary<Type, object>();
// Awake에서 초기화
private void Awake()
{
DontDestroyOnLoad(gameObject);
// 기본 서비스 등록
RegisterService<IInputService>(new InputService());
RegisterService<IAudioService>(new AudioService());
RegisterService<IDataService>(new PlayerPrefsDataService());
// 생명주기 이벤트를 사용해 서비스 초기화
StartCoroutine(InitializeServicesSequentially());
}
// 서비스 순차 초기화 (의존성 순서 고려)
private IEnumerator InitializeServicesSequentially()
{
// 데이터 서비스 먼저 초기화
IDataService dataService = GetService<IDataService>();
yield return StartCoroutine(dataService.Initialize());
// 데이터 로드 후 설정 서비스 초기화
var settings = dataService.LoadGameSettings();
RegisterService<ISettingsService>(new SettingsService(settings));
// 나머지 서비스 초기화
foreach (var service in _services.Values.OfType<IInitializableService>())
{
yield return StartCoroutine(service.Initialize());
}
Debug.Log("모든 서비스 초기화 완료");
}
// OnApplicationQuit 생명주기 메서드에서 서비스 정리
private void OnApplicationQuit()
{
foreach (var service in _services.Values.OfType<IDisposableService>())
{
service.Dispose();
}
_services.Clear();
}
// 서비스 등록
public static void RegisterService<T>(T service)
{
_services[typeof(T)] = service;
}
// 서비스 조회
public static T GetService<T>()
{
if (_services.TryGetValue(typeof(T), out object service))
{
return (T)service;
}
Debug.LogError($"서비스를 찾을 수 없음: {typeof(T).Name}");
return default;
}
}
// 서비스 인터페이스
public interface IInitializableService
{
IEnumerator Initialize();
}
public interface IDisposableService
{
void Dispose();
}
// 입력 서비스 예시
public interface IInputService : IInitializableService, IDisposableService
{
Vector2 MovementInput { get; }
bool JumpPressed { get; }
event Action OnFireAction;
}
// 입력 서비스 구현
public class InputService : IInputService
{
public Vector2 MovementInput { get; private set; }
public bool JumpPressed { get; private set; }
public event Action OnFireAction;
public IEnumerator Initialize()
{
Debug.Log("입력 서비스 초기화 중...");
// 초기화 로직
yield return null;
}
public void Update()
{
// 입력 처리
MovementInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
JumpPressed = Input.GetButtonDown("Jump");
if (Input.GetButtonDown("Fire1"))
{
OnFireAction?.Invoke();
}
}
public void Dispose()
{
// 정리 로직
}
}
사용 예시 - 플레이어 컨트롤러:
public class PlayerController : MonoBehaviour
{
[SerializeField] private float _moveSpeed = 5f;
[SerializeField] private float _jumpForce = 5f;
private IInputService _inputService;
private Rigidbody _rigidbody;
private bool _isJumping = false;
// 생명주기 메서드 Start에서 의존성 해결
private void Start()
{
_rigidbody = GetComponent<Rigidbody>();
```html
// 서비스 로케이터에서 입력 서비스 가져오기
_inputService = ServiceLocator.GetService<IInputService>();
// 이벤트 구독
_inputService.OnFireAction += FireWeapon;
}
// OnDestroy 생명주기 메서드에서 이벤트 구독 해제
private void OnDestroy()
{
if (_inputService != null)
{
_inputService.OnFireAction -= FireWeapon;
}
}
private void Update()
{
// 점프 입력 감지
if (_inputService.JumpPressed && !_isJumping)
{
_isJumping = true;
}
}
private void FixedUpdate()
{
// 이동 처리
Vector3 movement = new Vector3(_inputService.MovementInput.x, 0, _inputService.MovementInput.y);
_rigidbody.MovePosition(_rigidbody.position + movement * _moveSpeed * Time.fixedDeltaTime);
// 점프 처리
if (_isJumping)
{
_rigidbody.AddForce(Vector3.up * _jumpForce, ForceMode.Impulse);
_isJumping = false;
}
}
private void FireWeapon()
{
// 무기 발사 로직
Debug.Log("무기 발사!");
// 풀링된 발사체 생성
AdvancedObjectPool.Instance.SpawnFromPool("Projectile", transform.position + transform.forward, transform.rotation);
}
}
복합 생명주기 관리 시스템
큰 게임에서는 다양한 시스템이 서로 다른 생명주기 단계에서 초기화되고 업데이트되어야 합니다. 이를 효과적으로 관리하기 위한 복합 생명주기 관리 시스템을 구현할 수 있습니다.
// 게임 시스템의 생명주기 단계
public enum SystemLifecycleStage
{
PreInitialize,
Initialize,
PostInitialize,
EarlyUpdate,
Update,
LateUpdate,
PreRender,
PostRender,
PreShutdown,
Shutdown
}
// 게임 시스템 인터페이스
public interface IGameSystem
{
string SystemName { get; }
int InitializationPriority { get; }
int UpdatePriority { get; }
bool IsInitialized { get; }
void OnRegister();
void Initialize();
void Shutdown();
}
// 업데이트 가능한 시스템 인터페이스
public interface IUpdatableSystem : IGameSystem
{
void OnEarlyUpdate();
void OnUpdate();
void OnLateUpdate();
}
// 렌더링 관련 시스템 인터페이스
public interface IRenderSystem : IGameSystem
{
void OnPreRender();
void OnPostRender();
}
// 생명주기 관리자 클래스
public class SystemLifecycleManager : MonoBehaviour
{
private List<IGameSystem> _registeredSystems = new List<IGameSystem>();
private List<IUpdatableSystem> _updatableSystems = new List<IUpdatableSystem>();
private List<IRenderSystem> _renderSystems = new List<IRenderSystem>();
private bool _isInitialized = false;
private bool _isShuttingDown = false;
private static SystemLifecycleManager _instance;
public static SystemLifecycleManager Instance
{
get
{
if (_instance == null)
{
GameObject go = new GameObject("SystemLifecycleManager");
_instance = go.AddComponent<SystemLifecycleManager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
private void Start()
{
StartCoroutine(InitializationSequence());
}
private IEnumerator InitializationSequence()
{
Debug.Log("시스템 초기화 시작...");
// 등록된 시스템을 초기화 우선순위에 따라 정렬
_registeredSystems.Sort((a, b) => a.InitializationPriority.CompareTo(b.InitializationPriority));
// 시스템 등록 이벤트 호출
foreach (var system in _registeredSystems)
{
system.OnRegister();
yield return null; // 프레임 단위로 분산
}
// PreInitialize 단계 이벤트 발생
OnLifecycleStage(SystemLifecycleStage.PreInitialize);
yield return null;
// 각 시스템 초기화
foreach (var system in _registeredSystems)
{
Debug.Log($"시스템 초기화 중: {system.SystemName}");
system.Initialize();
yield return null; // 프레임 단위로 분산
}
// Initialize 단계 이벤트 발생
OnLifecycleStage(SystemLifecycleStage.Initialize);
yield return null;
// 업데이트 가능 시스템 필터링 및 정렬
_updatableSystems = _registeredSystems.OfType<IUpdatableSystem>().ToList();
_updatableSystems.Sort((a, b) => a.UpdatePriority.CompareTo(b.UpdatePriority));
// 렌더링 시스템 필터링
_renderSystems = _registeredSystems.OfType<IRenderSystem>().ToList();
// PostInitialize 단계 이벤트 발생
OnLifecycleStage(SystemLifecycleStage.PostInitialize);
_isInitialized = true;
Debug.Log("모든 시스템 초기화 완료");
}
private void Update()
{
if (!_isInitialized || _isShuttingDown)
return;
// EarlyUpdate 호출
OnLifecycleStage(SystemLifecycleStage.EarlyUpdate);
foreach (var system in _updatableSystems)
{
system.OnEarlyUpdate();
}
// Update 호출
OnLifecycleStage(SystemLifecycleStage.Update);
foreach (var system in _updatableSystems)
{
system.OnUpdate();
}
}
private void LateUpdate()
{
if (!_isInitialized || _isShuttingDown)
return;
// LateUpdate 호출
OnLifecycleStage(SystemLifecycleStage.LateUpdate);
foreach (var system in _updatableSystems)
{
system.OnLateUpdate();
}
}
private void OnPreRender()
{
if (!_isInitialized || _isShuttingDown)
return;
// PreRender 호출
OnLifecycleStage(SystemLifecycleStage.PreRender);
foreach (var system in _renderSystems)
{
system.OnPreRender();
}
}
private void OnPostRender()
{
if (!_isInitialized || _isShuttingDown)
return;
// PostRender 호출
OnLifecycleStage(SystemLifecycleStage.PostRender);
foreach (var system in _renderSystems)
{
system.OnPostRender();
}
}
private void OnApplicationQuit()
{
StartCoroutine(ShutdownSequence());
}
private IEnumerator ShutdownSequence()
{
if (_isShuttingDown)
yield break;
_isShuttingDown = true;
// PreShutdown 단계 이벤트 발생
OnLifecycleStage(SystemLifecycleStage.PreShutdown);
yield return null;
// 시스템 종료 (역순)
for (int i = _registeredSystems.Count - 1; i >= 0; i--)
{
var system = _registeredSystems[i];
Debug.Log($"시스템 종료 중: {system.SystemName}");
system.Shutdown();
yield return null;
}
// Shutdown 단계 이벤트 발생
OnLifecycleStage(SystemLifecycleStage.Shutdown);
_registeredSystems.Clear();
_updatableSystems.Clear();
_renderSystems.Clear();
Debug.Log("모든 시스템 종료 완료");
}
// 시스템 등록
public void RegisterSystem(IGameSystem system)
{
if (!_registeredSystems.Contains(system))
{
_registeredSystems.Add(system);
Debug.Log($"시스템 등록: {system.SystemName}");
// 이미 초기화된 상태라면 즉시 초기화
if (_isInitialized && !system.IsInitialized)
{
system.OnRegister();
system.Initialize();
// 업데이트 가능 시스템이면 목록에 추가
if (system is IUpdatableSystem updatableSystem)
{
_updatableSystems.Add(updatableSystem);
_updatableSystems.Sort((a, b) => a.UpdatePriority.CompareTo(b.UpdatePriority));
}
// 렌더링 시스템이면 목록에 추가
if (system is IRenderSystem renderSystem)
{
_renderSystems.Add(renderSystem);
}
}
}
}
// 시스템 제거
public void UnregisterSystem(IGameSystem system)
{
if (_registeredSystems.Contains(system))
{
// 종료 호출
system.Shutdown();
// 목록에서 제거
_registeredSystems.Remove(system);
if (system is IUpdatableSystem updatableSystem)
{
_updatableSystems.Remove(updatableSystem);
}
if (system is IRenderSystem renderSystem)
{
_renderSystems.Remove(renderSystem);
}
Debug.Log($"시스템 제거: {system.SystemName}");
}
}
// 생명주기 이벤트 발생
private void OnLifecycleStage(SystemLifecycleStage stage)
{
// 여기에 다른 시스템이나 메시지 시스템에 알림을 보낼 수 있음
Debug.Log($"생명주기 단계 진행: {stage}");
}
}
구현 예시 - 플레이어 시스템:
public class PlayerSystem : MonoBehaviour, IUpdatableSystem
{
public string SystemName => "PlayerSystem";
public int InitializationPriority => 100; // 낮은 값이 먼저 초기화
public int UpdatePriority => 10; // 낮은 값이 먼저 업데이트
public bool IsInitialized { get; private set; }
private PlayerController _playerController;
private void Awake()
{
// 라이프사이클 매니저에 등록
SystemLifecycleManager.Instance.RegisterSystem(this);
}
public void OnRegister()
{
Debug.Log("플레이어 시스템 등록됨");
}
public void Initialize()
{
Debug.Log("플레이어 시스템 초기화 중...");
// 플레이어 생성 또는 찾기
_playerController = FindObjectOfType<PlayerController>();
if (_playerController == null)
{
GameObject playerObj = Resources.Load<GameObject>("Player");
if (playerObj != null)
{
_playerController = Instantiate(playerObj).GetComponent<PlayerController>();
}
}
IsInitialized = true;
}
public void OnEarlyUpdate()
{
// 플레이어 입력 처리 등 빠른 업데이트가 필요한 작업
}
public void OnUpdate()
{
// 기본 업데이트 작업
if (_playerController != null)
{
// 플레이어 관련 업데이트 로직
}
}
public void OnLateUpdate()
{
// 카메라 추적, 애니메이션 후처리 등
}
public void Shutdown()
{
Debug.Log("플레이어 시스템 종료 중...");
IsInitialized = false;
}
private void OnDestroy()
{
// 라이프사이클 매니저에서 제거
if (SystemLifecycleManager.Instance != null)
{
SystemLifecycleManager.Instance.UnregisterSystem(this);
}
}
}
마치며
Unity의 생명주기를 깊이 이해하는 것은 효율적이고 안정적인 게임 개발의 기초가 됩니다. 각 생명주기 메서드의 특성과 호출 시점을 정확히 파악하면, 의도한 대로 코드가 실행되도록 보장하고 성능을 최적화할 수 있습니다.
본 글에서 심층적으로 살펴본 내용을 요약하면:
- 생명주기 구조 이해: Unity 오브젝트의 생명주기는 초기화, 물리 연산, 메인 업데이트, 렌더링, 비활성화/파괴 단계로 나뉩니다.
- 코루틴과 생명주기 통합: 코루틴은 생명주기 메서드와 밀접하게 연동되어 분산 처리의 핵심 도구로 활용됩니다.
- 성능 최적화 전략: 올바른 생명주기 메서드 선택과 업데이트 빈도 조절을 통해 성능을 크게 향상시킬 수 있습니다.
- 오브젝트 풀링과 생명주기 활용: 오브젝트의 생성/파괴 비용을 줄이기 위한 풀링 시스템은 생명주기 이벤트를 효과적으로 활용합니다.
- 디자인 패턴과 생명주기 연동: 상태 패턴, 의존성 주입, 시스템 아키텍처는 생명주기 이벤트와 결합하여 확장성 있는 코드 구조를 만듭니다.
- 복합 시스템 관리: 대규모 게임에서는 여러 시스템의 생명주기를 효과적으로 관리하는 아키텍처가 필요합니다.
효과적인 생명주기 활용을 위한 핵심 요점을 다시 정리하면:
- 목적에 맞는 메서드 선택: 물리 연산은
FixedUpdate()
, 일반 게임 로직은Update()
, 후처리는LateUpdate()
에 배치 - 초기화 로직 분리: 참조 설정은
Awake()
, 다른 컴포넌트에 의존하는 초기화는Start()
에 배치 - 이벤트 관리: 이벤트 리스너 등록/해제는
OnEnable()
/OnDisable()
에서 처리 - 코루틴 활용: 분산 처리가 필요한 작업은 코루틴으로 구현하고, yield 객체는 캐싱하여 사용
- 리소스 관리: 리소스 해제와 메모리 정리는
OnDisable()
과OnDestroy()
에서 처리 - 업데이트 최적화: 모든 로직을 매 프레임 업데이트하지 말고, 필요한 빈도에 맞게 분산 실행
이러한 원칙을 따르면서 설계하면, 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 유니티의 생명주기는 복잡하지만, 이를 마스터하면 게임 개발의 많은 문제를 우아하게 해결할 수 있습니다.
추가 학습 자료: Unity의 생명주기에 대해 더 자세히 알아보려면 Unity 공식 문서의 실행 순서 페이지를 참고하세요. 또한 Unity Profiler를 활용하여 생명주기 메서드의 실행 시간을 측정하고 최적화하는 방법을 익히는 것도 중요합니다.
```
'Unity' 카테고리의 다른 글
유니티 애널리틱스 (0) | 2025.05.23 |
---|---|
유니티의 생명주기 (0) | 2025.05.21 |
FishNet 사용법 (0) | 2025.05.20 |
Unity 렌더링 파이프라인 (0) | 2025.05.19 |
유니티 쿼터니언(Quaternion) (1) | 2025.05.16 |