유니티를 입문하실 때 생명주기를 이해하지 못하고 사용하시다 보면 많은 문제 상황에 부딪히게 됩니다. 넉백이 기기 환경마다 다르다 거나 카메라가 미세하게 떨리는 현상(jitter)이 발생하는 등. 이런 문제가 하나 둘 씩 쌓이다 보면 많은 시간을 할애하기에 생명주기를 이해하고 사용해야 개발기간이 단축됩니다. 그러므로 각 주기의 순서와 특징을 알아보는 시간을 갖겠습니다.
목차
[생명주기란 무엇인가?]
오브젝트가 생성되고 파괴되기까지 호출되는 함수의 순서를 말합니다. 각 단계마다 특정 함수가 호출되고, 이를 통해서 오브젝트의 초기화, 업데이트, 렌더링을 관리 할 수 있습니다.
[함수 호출 순서, 특징]
![[Unity] 유니티 생명주기(Lifecycle)와 FixedUpdate, Update, LateUpdate 5분만에 알아보기 2 유니티 생명주기(Lifecycle)](https://programmingdev.com/wp-content/uploads/2023/07/유니티_생명주기-2-optimized.jpg)
호출순서를 이미지화 한 것 입니다. 각각의 특징을 알아보겠습니다.
[Awake]
특징
오브젝트가 생성될 때 호출되는 함수이며, Start 함수보다 먼저 호출됩니다. 한번만 호출되며, 비활성화된 상태에서도 호출되는 함수입니다.
활용 예시
초기화가 필요한 변수나 컴포넌트를 설정하는데 사용됩니다. 예시로는 Rigidbody를 Awake에서 설정해주고, 중력 값이나 스피드 값은 Start 에서 설정해주는 방식으로 많이 사용됩니다.
using UnityEngine;
public class Player : MonoBehaviour
{
public float moveSpeed = 5f;
private Rigidbody2D rb;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
Debug.Log("플레이어가 생성되었습니다!");
}
private void Start()
{
rb.gravityScale = 2f;
Debug.Log("플레이어의 중력을 설정합니다!");
}
private void Update()
{
float horizontalInput = Input.GetAxis("Horizontal");
rb.velocity = new Vector2(horizontalInput * moveSpeed, rb.velocity.y);
}
}
[OnEnable]
특징
오브젝트가 활성화 될 때 호출되는 함수입니다. 활성화 비활성화를 자주할 때 초기화의 용도로 많이 사용합니다. Awake와 Start사이에 호출된다는 특징을 가집니다.
활용 예시
최적화를 위해서 미리 오브젝트를 로드 후 비활성화 한 뒤 활성화 하는 방식을 사용할 때가 있습니다.(오브젝트 풀링) 이런 방식을 사용할 때 초기화에 사용됩니다. 아래는 플레이어의 체력을 초기화 해주는 예시 입니다.
using UnityEngine;
public class HealthSystem : MonoBehaviour
{
public int maxHealth = 100;
private int currentHealth;
private void OnEnable()
{
currentHealth = maxHealth;
Debug.Log("플레이어의 체력을 초기화했습니다.");
}
public void TakeDamage(int damageAmount)
{
currentHealth -= damageAmount;
Debug.Log("플레이어가 " + damageAmount + "만큼의 데미지를 받았습니다.");
if (currentHealth <= 0)
{
OnPlayerDeath();
}
}
private void OnPlayerDeath()
{
Debug.Log("플레이어가 사망했습니다. 게임 오버 처리 등을 수행합니다.");
// 게임 오버 처리를 위한 코드 작성, 오브젝트를 삭제 시키는 게 아닌 비활성화(오브젝트 풀링)
}
}
[Start]
특징
오브젝트가 활성화 된 후 한번만 호출되는 함수입니다. 변수 값을 세팅하는 등의 초기화 작업에 많이 사용됩니다. Awake 이후 호출 되며 미리 설정된 값을 가져올 때 사용하기도 합니다.
활용 예시
캐릭터의 스피드 값이나 공격력 등을 설정할 때 많이 사용됩니다. 또는 Awake에서 받아온 데이터를 오브젝트에 세팅 해 줄 때 많이 사용합니다.
[FixedUpdate]
특징
물리 엔진의 업데이트 주기에 맞춰서 호출되는 함수이며, 물리 업데이트 단계에서 사용됩니다. 고정된 시간 간격으로 호출되며, 물리적인 움직임이나 충돌과 관련된 작업은 이 함수에서 처리해야 합니다.
활용 예시
Rigidbody를 이용한 이동 같이 물리적인 움직임을 구현하는데 사용 됩니다. 아래는 발판을 일정한 간격으로 이동시키는 예시입니다.
using UnityEngine;
public class Platform : MonoBehaviour
{
public float moveSpeed = 2f;
public float moveDistance = 2f;
private Vector3 initialPosition;
private Vector3 targetPosition;
private bool movingUp = true;
private void Start()
{
initialPosition = transform.position;
targetPosition = initialPosition + Vector3.up * moveDistance;
}
private void FixedUpdate()
{
MovePlatform();
}
private void MovePlatform()
{
// 물리 업데이트 주기에 맞게 발판을 위아래로 움직임
float step = moveSpeed * Time.fixedDeltaTime;
if (movingUp)
{
transform.position = Vector3.MoveTowards(transform.position, targetPosition, step);
if (transform.position == targetPosition)
{
movingUp = false;
}
}
else
{
transform.position = Vector3.MoveTowards(transform.position, initialPosition, step);
if (transform.position == initialPosition)
{
movingUp = true;
}
}
}
}
[OnTriggerEnter, OnTriggerStay, OnTriggerExit]
특징
충돌이 발생했을 때 호출되는 함수이며, Collider 컴포넌트의 트리거 옵션이 체크 돼야만 호출됩니다. 물리적인 영향을 주지 않으면서 콜라이더 간 상호작용이 가능합니다. Enter는 트리거에 들어왔을 때 호출되며, Stay는 트리거에 머무를 때 호출됩니다. Exit은 트리거에서 나오는 시점에 호출되는 함수입니다.
활용 예시
플레이어가 아이템을 주웠을 때 아이템을 획득하고 아이템 오브젝트를 비활성화 하는데 사용하거나, 캐릭터가 특정 구역에 진입하면 이벤트를 실행하는 등에 쓰입니다. 아래는 적 캐릭터가 특정 영역에 들어오면 함정을 발생 시키는 예시입니다.
using UnityEngine;
public class TrapArea : MonoBehaviour
{
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag("Enemy"))
{
// 적 캐릭터가 함정 구역에 진입하면 트리거를 감지하여 이벤트 실행
Enemy enemy = collision.GetComponent<Enemy>();
if (enemy != null)
{
enemy.ActivateTrapEffect();
}
}
}
}
[OnCollisionEnter, OnCollisionStay, OnCollisionExit]
특징
물리적인 충돌이 발생했을 때 호출되는 함수이며, 물리적인 상호작용을 하는데 사용됩니다. Rigidbody를 가진 오브젝트 간의 충돌에서만 작동한다는 특징이 있습니다.
활용 예시
물체들이 충돌할 때 특정 소리를 내거나 파티클 효과를 보여주는데 활용하거나, 총알이 플레이어를 가격 했을 때 데미지를 줄 때도 사용합니다.
using UnityEngine;
public class Bullet : MonoBehaviour
{
public int damageAmount = 10;
private void OnCollisionEnter(Collision2D collision)
{
// 충돌한 오브젝트가 플레이어인지 확인
if (collision.gameObject.CompareTag("Player"))
{
// 플레이어에게 데미지를 줌
PlayerHealth playerHealth = collision.gameObject.GetComponent<PlayerHealth>();
if (playerHealth != null)
{
playerHealth.TakeDamage(damageAmount);
}
// 총알 파괴
Destroy(gameObject);
}
}
}
위 예시에서 OnCollisionEnter를 OnTriggerEnter로 변환 후 사용하면 안되냐는 질문을 받을 때가 있습니다. OnTriggerEnter는 물리적인 충돌이 없기 때문에 총알이 일정 속도를 넘게 되면 감지를 못하게 될 경우가 있습니다. 이럴 경우 OnCollisionEnter를 사용하여 확실한 충돌을 발생시키는 것 입니다.
[Update]
특징
매 프레임마다 호출되는 함수이며, 주로 오브젝트의 상태 업데이트나, 로직을 처리하는데 사용됩니다. 이동, 애니메이션, 입력 같이 빠른 업데이트가 필요한 로직을 처리하는데 사용됩니다. 기기 상황에 따라서 프레임의 호출 주기가 달라질 수 있어서 Time.deltaTime를 사용하여 시간에 따라 값을 보정하기도 합니다.
활용 예시
특정 이벤트에 따라서 UI 업데이트를 하는데 이용되며, AI동작을 Update함수에서 구현하여 실시간으로 상태를 변경하기도 합니다. 아래는 플레이어 캐릭터를 키보드 입력에 따라서 이동하고 회전하는 코드의 예시 입니다.
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float moveSpeed = 5f;
public float rotationSpeed = 180f;
private void Update()
{
// 플레이어 이동 처리
float horizontalInput = Input.GetAxis("Horizontal");
float verticalInput = Input.GetAxis("Vertical");
Vector3 moveDirection = new Vector3(horizontalInput, verticalInput, 0f).normalized;
transform.Translate(moveDirection * moveSpeed * Time.deltaTime);
// 플레이어 회전 처리
if (moveDirection != Vector3.zero)
{
float targetAngle = Mathf.Atan2(moveDirection.y, moveDirection.x) * Mathf.Rad2Deg;
float angle = Mathf.MoveTowardsAngle(transform.eulerAngles.z, targetAngle, rotationSpeed * Time.deltaTime);
transform.eulerAngles = new Vector3(0f, 0f, angle);
}
}
}
위 코드와 같이 실시간 상태 변화를 처리하는데 유용합니다.
주의사항
매 프레임마다 호출 되므로 무거운 작업을 하게 되면 게임 성능의 치명적인 영향을 미칠 수 있습니다. 무거운 연산이 필요한 경우에는 FixedUpdate에서 처리 하거나, 이벤트 기반의 프로그래밍으로 대체하는 것이 바람 직 합니다.
[LateUpdate]
특징
매 프레임마다 호출되며 Update함수 이후에 호출됩니다. Update에서 처리된 변경 사항이 적용된 후 실행되기에 최종 결과를 반영하는데 사용 됩니다. 주로 카메라 움직임과 관련된 로직을 처리하는데 활용 됩니다. 카메라는 오브젝트의 이동에 따라 따라가야 하므로, 카메라의 위치 조정은 Update가 끝난 후에 실행 되어야 합니다. 이를 간과 했을 경우에는 카메라가 떨리는 현상이 발생할 수 있습니다.
활용 예시
아래는 카메라가 플레이어를 따라다니는 2D 플랫포머 게임의 예시 입니다.
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float moveSpeed = 5f;
public float jumpForce = 10f;
private Rigidbody2D rb;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
}
private void Update()
{
// 플레이어 이동 처리
float horizontalInput = Input.GetAxis("Horizontal");
rb.velocity = new Vector2(horizontalInput * moveSpeed, rb.velocity.y);
// 플레이어 점프 처리
if (Input.GetButtonDown("Jump"))
{
rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
}
}
}
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
public Transform target;
public Vector3 offset;
public float smoothSpeed = 0.125f;
private void LateUpdate()
{
// 카메라 위치 조정 (플레이어를 따라가도록)
Vector3 desiredPosition = target.position + offset;
Vector3 smoothedPosition = Vector3.Lerp(transform. Position, desiredPosition, smoothSpeed);
transform.position = smoothedPosition;
}
}
LateUpdate에서 target(캐릭터)쪽으로 Vector3.Lerp를 이용하여 카메라를 움직이는 것이 핵심 입니다.
[OnDisable]
특징
오브젝트가 비활성화 되거나 제거될 때 호출되는 함수이며, 자원을 해제하거나 메모리 정리 작업을 수행 할 때 유용하게 쓰입니다. 이러한 정리 작업은 OnDestroy함수에서 해도 되지만 오브젝트 풀링 기법을 사용 시 오브젝트를 재사용하기 때문에 파괴시키지 않고 활성화하는 방식을 사용합니다. 이러한 경우에 많이 사용합니다.
활용 예시
캐릭터 사망 시 이펙트 재생, 게임 오버 처리 등을 수행 할 수 있고, 특정 컴포넌트가 더 이상 사용되지 않을 때, 자원을 해제하거나 초기화하는 작업을 할 수 있습니다.
[OnDestroy]
특징
오브젝트가 파괴 되기 직전에 딱 한번만 호출되는 함수입니다. 풀링을 사용하지 않았을 경우에 자원해제나 초기화 작업을 해주기도 합니다.
활용 예시
플레이어가 상호작용 한 뒤 파괴되어야 할 경우에 OnDestroy 함수를 이용하여 상호작용에 따른 결과를 처리할 수 있습니다. 아래는 플레이어가 상자와 상호작용 하는 예시입니다.
PlayerController 스크립트
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private void Update()
{
if (Input.GetKeyDown(KeyCode.E))
{
// "E" 키를 눌렀을 때 상호작용 함수 호출
Interact();
}
}
private void Interact()
{
RaycastHit hit;
if (Physics.Raycast(transform.position, transform.forward, out hit, 2f))
{
// 상자와 충돌한 경우에만 실행
if (hit.collider.CompareTag("Box"))
{
// 상자와 상호작용하여 아이템 획득 함수 호출
hit.collider.gameObject.GetComponent<BoxController>().OpenBox();
}
}
}
}
BoxController 스크립트
using UnityEngine;
public class BoxController : MonoBehaviour
{
public GameObject itemPrefab;
public void OpenBox()
{
// 상자를 열어 아이템 생성
Instantiate(itemPrefab, transform.position + Vector3.up, Quaternion.identity);
// 상자 파괴
Destroy(gameObject);
}
private void OnDestroy()
{
// 상자가 파괴되기 전에 추가적인 동작 수행
Debug.Log("상자가 파괴되기 전에 추가적인 동작 수행");
}
}
[OnApplicationQuit]
특징
게임이 종료될 때 호출되는 함수이며, 빌드하여 실행하거나 에디터에서 종료할 때 모두 호출 됩니다. 주로 정리 작업이나 저장 작업 등을 처리하는데 사용됩니다.
활용 예시
게임의 데이터를 저장하여 데이터를 유지할 수 있도록 하거나 종료 시점에 로그를 남겨서 디버깅이나 분석에 활용할 수 있습니다. 예시로 던전 입장 시 플레이 하다가 종료 시키는 시점에 데이터 저장을 하여 진행 했던 부분부터 시작할 수 있도록 하는 경우가 있습니다.
주의사항
오래 걸리는 작업을 수행하면 게임 종료가 지연 되거나 예기치 않은 문제가 발생할 수 있습니다.
[마무리 하며]
위에서 설명했던 모든 함수들은 게임 개발 시 무조건 사용 되는 함수 이기에 중요하다고 말할 수 있습니다. 생명주기에 관해서 많은 글을 참고하시는 것도 좋지만 직접 사용해 봐야 각각의 특성을 이해할 수 있습니다. 추천 드리는 방법은 위에서 설명 드린 특성을 숙지하신 후에 간단한 게임을 제작해 보시면 습득하는데 많은 도움이 될 것입니다. 마지막으로 중요 시 해야 할 부분은 Update함수는 최적화에 많은 영향을 미치기 때문에 되도록 최소화 시켜서 제작하시길 바랍니다.
[참고한 사이트]
https://ctkim.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0