Object라는 건 프로그래밍 외에도 다양한 곳에서 다양한 의미로 사용된다.
어째 현실에서도 Object 데이터 타입이랑 똑같다...
이번 글에서는 유니티에서 Objec와 관련된 내용들을 정리해 봤다.
Scene
게임의 한 단계 / 장면을 구성하는 모든 리소스와 상태를 담는 .unity 파일이다.
독립적인 세계(레벨) 단위로 메뉴, 플레이, 엔딩 등 씬을 분리해 개발, 테스트, 로딩을 최적화할 수 있다.
씬 간의 전환은 SceneManager.LoadScene() 함수를 통해 이루어진다.
using UnityEngine.SceneManagement;
// 싱글 로드
SceneManager.LoadScene("Game");
// 애드티브 로드(멀티 씬)
SceneManager.LoadScene("UI", LoadSceneMode.Additive);
// 언로드
SceneManager.UnloadSceneAsync("UI");
로딩 화면 별도씬 + 애드티브 로드로 컨텐츠 스트리밍 할 것을 권장한다고 한다.
공통 시스템 (Audio, Save, network)은 Persistent 씬으로 독립하고 DontDestroyOnLoad로 유지하는 식으로 관리할 수 있다.
GameObject
빈 껍데기와 계층구조.
Transform(위치, 회전, 스케일)만 담는 컨테이너로 실제 동작은 Component가 담당한다.
부모-자식 구조를 통해 로컬 트랜스폼으로 묶어 관리한다.

Component
실제 기능을 보유한 모듈
Transform, Renerer, Colider, MonoBehaviour 등 소스코드로 이루어져있기에 수정도 가능하다.
한 클래스가 하나의 역할에 집중하도록 모듈화하는 것을 원칙으로 한다.
[SerializeField] 를 통해 private 변수도 에디터 인스펙터에서 의존성을 주입할 수 있다.
Prefab
씬의 GameObject 구성을 에셋으로 저장한 템플릿, 재사용과 일관성 유지를 위해 사용하는 프리셋으로 한번 만들어두면 복사 / 재사용이 가능하다.
원본을 수정하면 연결된 인스턴스들도 같이 업데이트 된다.
런타임 중에 Instaitiate(prefab)로 동적 스폰이 가능하다.
Instantiate / Destroy 비용 구조
GameObject는 런타임 중에 Instantiate(), Destroy()를 통해 동적으로 생성하거나 파괴할 수 있다.
Instantiate : 메모리 할당 + 컴포넌트 초기화 + Awake / OnEnable 호출
Destory : 즉시 파괴가 아닌 스케줄링 -> GC 압력 증가, 프레임 경합 지점에서 스파이크 발생 가능.
그렇기 때문에 빈번하게 다량의 오브젝트를 생성-삭제 할 경우, 오브젝트 풀링 사용을 권장한다.
Object Pooling
게임에서 반복적으로 사용되는 오브젝트를 미리 생성해 두고, 필요할 때 활성화/비활성화 하며 재사용하는 기법이다.
생성, 파괴시 프레임 드랍과 GC 부담을 줄일 수 있어 많이 사용되는 최적화 기법이다.
간단하게 총알을 자동으로 계속 발사하는 오브젝트 풀링을 구현해보자.
using UnityEngine;
public class Bullet : MonoBehaviour
{
[SerializeField] private float speed = 20f;
void Update()
{
// Z축(정면)으로 계속 이동
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
void OnCollisionEnter(Collision collision)
{
Destroy(gameObject);
}
}
이렇게 구성하면 단순히 총알을 Destroy 하게 된다.
하지만
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletSpawner : MonoBehaviour
{
[Header("Bullet Settings")]
[SerializeField] private GameObject bulletPrefab;
[SerializeField] private Transform firePoint;
[SerializeField] private float bulletSpeed = 20f;
[SerializeField] private float fireInterval = 0.2f;
[SerializeField] private int prewarmCount = 30;
private readonly Queue<GameObject> _pool = new();
private float _timer;
void Start()
{
// 초기 풀 생성
for (int i = 0; i < prewarmCount; i++)
{
var bullet = Instantiate(bulletPrefab);
bullet.SetActive(false);
_pool.Enqueue(bullet);
}
}
void Update()
{
_timer += Time.deltaTime;
if (_timer >= fireInterval)
{
_timer = 0f;
Fire();
}
}
private void Fire()
{
if (!bulletPrefab || !firePoint) return;
// 풀에서 가져오기
GameObject bullet = _pool.Count > 0 ? _pool.Dequeue() : Instantiate(bulletPrefab);
bullet.transform.SetPositionAndRotation(firePoint.position, firePoint.rotation);
bullet.SetActive(true);
if (bullet.TryGetComponent<Rigidbody>(out var rb))
{
rb.linearVelocity = firePoint.forward * bulletSpeed;
}
//일정 시간 후 반환
StartCoroutine(ReturnToPool(bullet, 3f));
}
private IEnumerator ReturnToPool(GameObject bullet, float delay)
{
yield return new WaitForSeconds(delay);
bullet.SetActive(false);
_pool.Enqueue(bullet);
}
}
BulletSpawner를 만들고, Queue를 이용해 pool을 만들어 준다.
그리고 기존의 총알은 Destroy가 아닌 SetActive를 이용해서
void OnCollisionEnter(Collision collision)
{
gameObject.SetActive(false);
}
비활성화로 바꿔준다.
실제로 적용해보면


Bullet의 Clone들이 활성, 비활성화 되면서 재활용되는 것을 볼 수 있다.
주의점
Instaintiate()로 생성한 오브젝트는 메모리 힙에 새로 할당되므로, 생성 직후 참조를 유지하지 않으면 GC가 회수할 수 없다.
Destroy() 호출 시 즉시 메모리가 해제되는 것이 아니라, 프레임이 끝난 후에 GC가 수거한다. 따라서 생성 / 파괴가 빈번한 경우 성능 저하가 발생할 수 있다.
필요하다면 DestroyImmediate()를 사용할 수 있으나, 런타임 중 남용하면 예상치 못한 문제를 발생시킬 수 있어 에디터 환경에서만 사용이 권장된다.
'Unity' 카테고리의 다른 글
| Unity - EventSystem (이벤트 시스템) (0) | 2025.10.20 |
|---|---|
| Unity - Coroutine, yield, Invoke, IEnumerator (0) | 2025.10.17 |
| Unity - Rigidbody, Collider - 물리, 충돌 처리 (0) | 2025.10.16 |
| Unity - Transform, 위치 이동 (0) | 2025.10.15 |
| Unity - 생명주기 (Life Cycle) (1) | 2025.10.13 |