XR 플밍 - 8. UnityEngine3D 입문 - 게임 오브젝트, 프리펩, 오브젝트 풀 패턴(4/15)

이형원·2025년 4월 15일
0

XR플밍

목록 보기
44/215

지금까지 유니티의 인터페이스와 기초적인 내용을 알아 보았으며, 게임 구성을 위한 요소들을 알아보고자 한다.

1. 게임 오브젝트(Game Object)

-지형, 건물, 하늘까지 보이는 모든 것, 보이지 않는 것까지 포함하여 모두가 게임 오브젝트이다.

게임 오브젝트는 게임의 씬을 구성하는, 존재하는 모든 오브젝트를 일컫는 말이다.
육안에 보이는 물체 또는 육안으로 보이지 않는 기능까지 포함하여 모든 구성품을 말한다.

다만 게임 오브젝트 자체만의 배치로는 독자적인 기능을 할 수 없으며 실질적인 기능은 컴포넌트가 수행한다. 게임 오브젝트에 컴포넌트를 만들어 붙이는 것으로 게임은 작동하며, 게임 오브젝트는 컴포넌트를 가지기 위한 컨테이너라 할 수 있다.

2. 컴포넌트(Component)

특정한 기능을 수행할 수 있도록 구성한 작은 기능적 단위로, 게임오브젝트의 작동과 관련한 부품이라 할 수 있다. 게임 오브젝트의 특성에 맞춰 추가, 삭제하는 방식의 조립형 부품이라 할 수 있다.

컴포넌트는 게임 오브젝트를 구성하는 요소라고 할 수 있다.

단적으로 위의 그림처럼 Transform(위치,방향,크기 등 정보), Mesh(외관), 스크립트, 컬러까지 모든 요소가 컴포넌트라 할 수 있다.

게임 프로그래머로서 해야 할 일은, 이런 컴포넌트를 관리하고, 스크립트를 만들어 필요한 기능을 구현하여 게임을 작동하게 하는 것이라 할 수 있다.

2.1 gameObject 프로퍼티

gameObject 프로퍼티의 정의를 살펴 보면 위와 같이 적혀 있다.

해당 컴포넌트가 붙어 있는 게임 오브젝트를 가리키고 있으며, 컴포넌트는 항상 게임 오브젝트에 붙어 있다.

게임 오브젝트에 대한 참조를 할 수 있는 프로퍼티이며, 자기 자신에 대한 참조를 하고 싶을 경우 그냥 gameObject.(참조할 내용)을 적어주면 된다는 것이다.

그러면 gameObject로 가져올 수 있는 정보가 어떠한 것이 있는지 알아보자.

gameObject.name - 게임 오브젝트의 이름
gameObject.activeSelf - 게임 오브젝트의 활성화 여부
gameObject.activeInHierachy - 하이어라키 상에서의 활성화 여부
gameObject.tag - 게임 오브젝트의 태그
gameObject.layer - 게임 오브젝트의 레이어

여기서 몇 가지만 알아보자.

  • 하이어라키 상에서의 활성화 여부?
    오브젝트 자체의 활성화가 true라 할 지라도, 부모 오브젝트가 비활성화된 상태의 경우 오브젝트가 비활성화되어 있을 수도 있다. 따라서 하이어라키 상에서의 활성화 여부를 확인할 필요가 있다.

  • 게임 오브젝트의 이름을 가져오는 것보다 태그를 쓰는 걸 권장한다.

태그의 사용 이유 : 이름 가지고 게임 오브젝트를 찾는 경우에는 문제가 발생할 수 있다.

-이름에 오타가 나면?
-오브젝트가 너무 많아서 찾는데 시간이 걸릴 수도 있다?
-복사한 오브젝트의 경우 이름이 달라질 수도 있는데?

이와 같은 이유로 태그로 찾는 경우가 훨씬 더 빠르면서 안전하게 찾을 수 있다.

  • 레이어의 역할

아래와 같은 상황을 생각해 보자

만약 몬스터가 있는데, 몬스터가 의도하지 않은 상황으로 동전을 물리력으로 밀어낸다.

이와 같은 상황을 방지하기 위해 몬스터와 동전을 다른 레이어로 분리하는 방법을 사용할 수 있다.
또한 다른 물체보다 더 위에서 보이게 하는 효과도 있다.

public GameObject target;
public Transform camTransform;
public Camera camComponent;

private void Start()
{
	// target = GameObject.Find("Main Camera")	// 이름으로 찾기 : 비추천
	target = GameObject. FindGameObjectWithTag("Main Camera");	// 태그로 찾는 방법을 추천

	camTransform = target.transform;		// 타겟의 트랜스폼을 가져올 수 있음
	camComponent = target.GetComponent<Camera>();	// 타겟의 컴포넌트 정보 자체를 가져올 수 있음

	target.AddComponent<Rigidbody>();	// 타겟에게 컴포넌트를 붙일 수도 있음

	Destroy(gameObject);		// 게임오브젝트를 파괴
	Destroy(camComponent);	// 다른 게임 오브젝트를 파괴할 수도 있음
}

3. 프리팹 (Prefab)

미리 만들어 놓은 게임오브젝트, 템플릿, 설계도라 할 수 있다.
프리팹 시스템을 이용하여 게임오브젝트를 생성, 설정 및 저장할 수 있음
자주 사용하는 게임오브젝트 구성을 복사하여 런타임 시점에 생성하려는 경우 사용

3.1 프리팹의 생성

프리팹 생성은 크게 두 가지 방법이 있다.

  1. 프로젝트 창에서 생성하기

    프로젝트 창에서 프리팹을 생성할 수 있다. 프리팹을 생성한 후 그 내용을 편집할 수 있는 곳으로 접근할 수 있다.

  2. 하이어라키 창에서 만들어 놓은 오브젝트를 프로젝트 창으로 내린다.

    하이어라키 창에 이미 생성되어 있는 게임 오브젝트를 드래그하여 프로젝트 창에 넣으면 그대로 프리팹이 생성된다.

3.2 프리팹의 특징

  • 게임 오브젝트를 프리팹화 시킨 프리팹 에셋은 게임 오브젝트의 모든 컴포넌트, 속성값, 자식 게임오브젝트를 가지고 있다.
  • 프리팹 에셋을 게임씬에 추가할 경우 프리팹 인스턴스로 생성된다.
  • 프리팹 에셋의 변경 사항은 프리팹 인스턴스에게 자동으로 반영된다.
    예시로 프리팹의 오브젝트 색깔을 변경하였을 경우, 해당 프리팹으로 생성한 모든 인스턴스가 변경한 색깔로 변경된다.
  • 프리팹 인스턴스에 변경 사항이 있을 경우 해당 변경 사항은 프리팹 인스턴스에 영향을 받지 않는다.
    예시로, 프리팹으로 씬 뷰에 만든 오브젝트 하나의 색깔을 변경하였을 경우, 해당 인스턴스의 변경사항은 모든 프리팹에 영향을 주지 않는다.
    예를 들어 색깔만 변경한 인스턴스가 있을 때, 해당 인스턴스는 프리팹의 색 변경은 반영되지 않아도 다른 변경 사항(크기 등)은 반영된다.

-> 프리팹 에셋을 클래스, 프리팹 인스턴스를 클래스의 인스턴스로 볼 수 있음

3.3 생성과 삭제

  • 생성 : Instantiate
    생성 함수는 오버로딩 되어 있어 다음과 같이 사용할 수 있다.

    -instantiate(prefeb) - 생성
    -instantiate(prefeb, Vector vector, Rotation rotation) - 생성할 때 위치와 각도 설정 가능

  • 파괴
    파괴 함수 또한 오버로딩 되어 있어 다음과 같이 사용할 수 있다.

    -Destroy(target) - 파괴
    -Destroy(target, time) - time 만큼 시간이 지난 후 파괴

4. 메모리 단편화와 오브젝트 풀 패턴

4.1 메모리 단편화

게임은 생성과 삭제가 많이 발생하는 프로그램이다. 이걸 그대로 전부 지우고 생성하기를 반복하다 보면 메모리 단편화가 발생할 수 있다.

  • 내부 단편화

    메모리를 할당할 때 프로세스가 필요한 양보다 더 큰 메모리가 할당되어 메모리 공간이 낭비되는 상황

  • 외부 단편화

메모리의 할당 및 해제 작업의 반복으로 여유공간이 단편적으로 존재하여, 총 메모리 공간은 충분하지만 실제로 할당이 불가한 상황

메모리 단편화를 해결하기 위해 가비지 컬렉터가 데이터를 지우고 공간을 확보하는 과정에서 gc스파이크(멈칫하는 현상)가 발생한다.

이와 같은 문제를 방지하기 위해 오브젝트 풀 패턴을 구현해보도록 한다.

4.2 오브젝트 풀 패턴

프로그램 내에서 빈번하게 재활용하는 인스턴스들을 풀에 보관한 뒤 인스턴스의 생성&삭제 대신 대여&반납을 사용하는 기법을 말한다.
최적화를 위한 기법으로 사용하기 좋다.

구현
1. 인스턴스들을 보관할 풀을 생성
2. 프로그램의 시작시 풀에 인스턴스들을 생성하여 보관
3. 인스턴스 생성이 필요할 때 풀에서 대여하여 사용
4. 인스턴스 삭제가 필요할 때 풀에 반납하여 보관

4.3 장단점

장점
1. 빈번하게 사용하는 인스턴스 생성에 소요되는 오버헤드를 줄인다
2. 빈번하게 사용하는 인스턴스 삭제에 부담되는 가비지 콜렉터의 동작을 줄인다

주의점
1. 미리 생성해놓은 인스턴스들이 사용하지 않는 경우에도 메모리를 차지하고 있게 된다.
2. 오브젝트 풀로 인해 힙영역의 여유공간이 줄어들어 오히려 프로그램에 부담이 되는 경우가 있다.

이와 같은 점에서 오브젝트 풀 패턴을 구현하기 좋은 상황은, 아래와 같은 예시가 있다.

  1. FPS 게임에서의 탄환을 오브젝트 풀 패턴으로 구현
  2. 뱀서류 게임에서 몬스터가 반복 생성되고 삭제되는 상황에서 사용

4.4 코드 예시

// 오브젝트 풀을 저장할 Empty 공간에 컴포넌트 사용
public class ObjectPool : MonoBehaviour		
{
    [SerializeField] List<PooledObject> pool = new List<PooledObject> ();
    [SerializeField] PooledObject prefeb;
    [SerializeField] int size;

    private void Awake()
    {
        for(int i = 0; i < size ; i++)
        {
            PooledObject instance = Instantiate(prefeb);
            instance.gameObject.SetActive(false);
            pool.Add(instance);
        }
    }

    public PooledObject GetPool(Vector3 position, Quaternion rotation)
    {
        if(pool.Count == 0)
        {
            return Instantiate(prefeb, position, rotation);
        }

        PooledObject instance = pool[pool.Count - 1];
        pool.RemoveAt(pool.Count - 1);

        instance.returnPool = this;
        instance.transform.position = position;
        instance.transform.rotation = rotation;
        instance.gameObject.SetActive(true);

        return instance;
    }

    public void ReturnPool(PooledObject instance)
    {
        instance.gameObject.SetActive(false);
        pool.Add(instance);
    }
}


// 오브젝트 풀에 저장할 프리팹에 사용
public class PooledObject : MonoBehaviour
{
    public ObjectPool returnPool;
    public GameObject burstEffectPrefeb;
    [SerializeField] float returnTime;
    private float timer;

    private void OnEnable()
    {
        timer = returnTime;
    }

    private void Update()
    {
        timer -= Time.deltaTime;
        if(timer <= 0)
        {
            Instantiate(burstEffectPrefeb, transform.position, transform.rotation);
            ReturnPool();
        }
    }

    public void ReturnPool()
    {
        if (returnPool == null)
        {
            Destroy(gameObject);
        }
        else
        {
            returnPool.ReturnPool(this);
        }
    }
}


// 탄환 발사 기능
public class Shooter : MonoBehaviour
{
    [SerializeField] GameObject bulletPrefeb;
    [SerializeField] Transform muzzlePoint;
    [SerializeField] ObjectPool bulletPool;

    [Range(10, 30)]
    [SerializeField] float bulletSpeed;
    public void Fire()
    {
        //GameObject instance = Instantiate(bulletPrefeb, muzzlePoint.position, muzzlePoint.rotation);
        PooledObject instance = bulletPool.GetPool(muzzlePoint.position, muzzlePoint.rotation);
        Rigidbody bulletRigidbody = instance.GetComponent<Rigidbody>();
        bulletRigidbody.velocity = muzzlePoint.forward * bulletSpeed;

        //Destroy(instance, 3);
    }
}


public class TankController : MonoBehaviour
{
    [SerializeField] Shooter shooter;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            shooter.Fire();
        }
    }
}
profile
게임 만들러 코딩 공부중

0개의 댓글