안녕하세요! 오늘은 유니티 게임 개발과 관련된 핵심 내용, 바로 유니티 엔진의 기본 개념과 모든 Unity 개발자가 반드시 알아야 할 라이프사이클(Lifecycle)에 대해 자세히 알아보겠습니다.
출처 정보: 이 글은 "[Unity 9기] 챌린지반 - 발표자 보기가 포함된 공유 화면 (2025년 5월 8일)" 강의 (강사: 진우원 님) 내용을 바탕으로 학습 및 정리한 내용입니다.
async/await
를 활용한 심층 비동기 프로그래밍유니티는 C# 기반의 컴포넌트 아키텍처로 구성된, 게임 및 인터랙티브 콘텐츠 제작용 실시간 3D 개발 플랫폼(엔진)입니다.
전통적인 객체 지향 프로그래밍이 주로 상속을 통해 기능을 확장하는 반면, 유니티는 게임 오브젝트(GameObject)에 필요한 기능 단위인 컴포넌트(Component)들을 조합(부착)하여 객체의 행동과 속성을 정의합니다.
Transform
, MeshFilter
, MeshRenderer
, BoxCollider
, 사용자 정의 스크립트 등)유니티의 개발 방식 자체가 컴포넌트 조합형이지만, 각 컴포넌트는 C# 클래스로 구현됩니다. 따라서 컴포넌트 내부를 설계하고 구현할 때는 객체 지향 원칙(다형성, 상속, 캡슐화 등)을 따라야 효과적입니다. 사용자 정의 스크립트는 대부분 MonoBehaviour
클래스를 상속받아 작성됩니다.
MonoBehaviour
클래스유니티에서 스크립트가 게임 오브젝트의 생명주기 동안 특정 시점에 자동으로 호출될 수 있도록 미리 정의된 이벤트 함수(메서드)들을 제공하는 기본 클래스입니다. 이러한 이벤트 함수들의 실행 순서를 유니티 라이프사이클이라고 하며, 이를 이해하는 것은 유니티 개발의 핵심입니다. 신입 개발자 면접 단골 질문이기도 합니다.
Awake
-> OnEnable
-> Reset
(에디터 전용) -> Start
FixedUpdate
(물리 관련 이벤트 함수들)OnMouseDown
, OnMouseUp
, OnMouseDrag
등Update
-> 코루틴(yield
) -> LateUpdate
OnDrawGizmos
(에디터 전용) -> OnGUI
OnApplicationPause
-> OnApplicationQuit
-> OnDisable
-> OnDestroy
Awake()
Start()
함수보다 먼저, 그리고 오브젝트가 비활성화 상태여도 호출됩니다.GetComponent<T>()
등OnEnable()
Awake()
직후 또는 오브젝트가 비활성화되었다가 다시 활성화될 때 호출됩니다.OnDisable
과 연계하여 활용할 수 있는 부분 스터디 (자세한 내용은 Ⅶ. Q&A 및 과제 안내 섹션 참고)Start()
Update()
메서드가 호출되기 직전, 스크립트가 활성화된 상태에서 단 한 번 호출됩니다. Awake()
보다는 늦게 호출됩니다.Awake()
가 끝난 후이므로, 다른 오브젝트를 참조하기에 안전합니다. (예: 카메라가 플레이어 타겟 설정)FixedUpdate()
Time > Fixed Timestep
설정에서 변경 가능). 프레임 속도와 무관하게 일정한 주기로 호출됩니다.Rigidbody
조작 (힘 가하기, 속도 변경 등). 프레임에 영향을 받지 않는 일관된 물리 처리가 필요할 때 사용합니다. (프레임 기반으로 물리 처리 시, PC 성능에 따라 결과가 달라질 수 있음)Update()
Time.deltaTime
사용 필수)Time.deltaTime
: 이전 프레임에서 현재 프레임까지 걸린 시간. 프레임 속도에 관계없이 일정한 속도로 움직이거나 연산하기 위해 사용합니다.LateUpdate()
Update()
함수가 호출된 후, 매 프레임마다 호출됩니다.Update
에서 결정된 값들을 기반으로 최종적인 조정을 할 때 사용합니다. (예: 캐릭터의 최종 위치에 따라 UI 업데이트)OnDisable()
OnEnable
에서 등록한 이벤트를 해제하여 메모리 누수 방지.OnEnable
과 연계하여 활용할 수 있는 부분 스터디 (자세한 내용은 Ⅶ. Q&A 및 과제 안내 섹션 참고)OnDestroy()
Destroy()
함수가 호출될 때.Update
에 넣으면 프레임에 따라 결과가 불안정해짐)NullReferenceException
등을 방지할 수 있습니다.강의에서 언급된 BaseController
와 오브젝트 풀링에서의 OnEnable
/OnDisable
활용 예시 코드입니다.
BaseController
및 PlayerController
예시다음은 기본적인 캐릭터의 움직임과 입력을 처리하는 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);
}
}
}
OnEnable
/ OnDisable
활용 예시오브젝트 풀링은 오브젝트를 반복적으로 생성하고 파괴하는 대신, 미리 생성해두고 재활용하는 최적화 기법입니다. 이때 OnEnable
과 OnDisable
이 매우 유용하게 사용됩니다.
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 라이프사이클)
OnEnable()
과 OnDisable()
을 실제 게임 로직에서 어떻게 활용할 수 있을지 다양한 예시 스터디 및 고민해보기.
🤔 OnEnable()
/ OnDisable()
활용 예시 더 깊이 파고들기
OnEnable()
과 OnDisable()
은 오브젝트의 활성화/비활성화 상태 변경 시점에 특정 로직을 실행하고자 할 때 매우 유용합니다. 특히 오브젝트 풀링과 같은 최적화 기법이나, UI 요소의 동적 관리, 특정 조건에 따른 기능 활성화/비활성화 등에 널리 사용됩니다.
예시 1: 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 활성화/비활성화 (최적화)
SetActive(true/false)
를 통해 제어됩니다.)OnEnable()
활용:OnDisable()
활용:// 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 (라이프사이클 관련 추가 학습 자료로 활용하시면 좋습니다.)
이상으로 유니티 엔진의 기본과 라이프사이클에 대한 정리를 마칩니다. 이 글이 유니티 개발을 시작하시거나 더 깊이 이해하고 싶은 분들께 도움이 되셨으면 좋겠습니다. 궁금한 점은 댓글로 남겨주세요! 😊