[Unity] Script

Lingtea_luv·2025년 4월 15일
0

Unity

목록 보기
2/30
post-thumbnail

Object


컴포넌트와 오브젝트를 구분하는 것은 중요하다. 둘의 차이를 확실히 알고 각각의 기능을 최대한으로 사용하여 훌륭한 게임 오브젝트를 구현해보자.

GameObject

개발자에게 오브젝트는 조금 더 넓은 범위에 속하는 개념이다. 게임 오브젝트는 게임 내에 존재하는 모든 것으로 화면에 보이는 것들과 그 외의 것들 모두 게임 오브젝트라고 볼 수 있다.

이처럼 오브젝트는 특정 물체에만 적용되는 것이 아니고, 형체가 존재하지 않아 보이지 않더라도 하나의 기능으로써 존재하기도 한다.

Component

모든 물체는 기능과 형태를 갖는다. 즉 물체는 자신에게 주어진 기능과 성질을 간직하고 있는 매개체라고 말할 수 있다. 유니티는 기능과 형태를 오브젝트에 주입하는 용도로써 컴포넌트를 사용하며, 우리는 컴포넌트를 제작하고 수정하여 오브젝트의 기능과 형태를 설정할 수 있는 것이다.

Transform

모든 게임 오브젝트가 공통으로 가지고 있는 컴포넌트로 물체가 가지는 가장 기본적인 특징인 위치, 회전각, 크기에 대한 3축(x,y,z)의 정보를 가지고 있다.

  1. Position : 위치에 대한 정보
  2. Rotation : 회전각
  3. Scale : 크기(배율)

Prefab


로봇을 생산하는 공장의 경우 설계도를 활용하여 대량 생산하는 것이 가능할 것이다. 이와 마찬가지로 유니티는 프리팹이라는 설계도를 가지고 있으며, 이를 사용하여 오브젝트를 대량으로 생산하는 것이 가능하다.

하이어라키에서 오브젝트 간 계층 구조를 활용하여 객체를 생성한 뒤, 이를 project 창으로 드래그하여 가져오면 Prefab으로 등록이 되며, 기존 객체를 삭제한 뒤 Prefab을 드래그하여 Scene이라 하이어라키에 추가하면 Prefab의 복제본이 계속해서 생성된다. (Prototype 패턴 구현)

Prefab의 가장 큰 장점은 Prefab 변경시 해당 Prefab으로 만든 복사본의 정보가 모두 변경되는 것이다. 하나씩 변경할 필요없이 한번에 수정이 가능하여 편하다는 장점을 가지며, 해당 Prefab을 기본 골자로 하여 추가로 정보를 바꾸고 싶은 경우 해당 객체의 인스펙터로 가서 수정하면 된다.

오브젝트 계층 구조

로봇을 생산하기 위해서는 머리, 몸통, 팔, 다리로 세분화하여 제작할 필요가 있다. 유니티에서는 오브젝트에 하위 객체의 오브젝트를 추가하는 방식의 계층을 이루는 구조 형태로 구현한다.

계층 구조의 장점

하위 객체로 추가된 오브젝트는 자신의 위치 정보인 local Space와 전체 공간에서의 위치정보 World Space를 동시에 가지기에 그룹화 된 Prefab의 동작 제어가 간편하다.

계층 구조의 단점

다만 계층의 깊이가 깊어질수록 아래와 같은 여러 문제가 나타나기에 유지보수를 위한 가시성을 확보하면서 성능적으로 효율이 높은 구조를 생각할 필요가 있다.

  1. 복잡도 증가 : 계층의 깊이가 깊을수록 하위, 상위 객체 간 접근성이 떨어지며 상위 객체에 접근하기 위해 하위 객체에서 적어야할 코드량이 많아진다. 이는 필연적으로 코드의 가독성을 떨어뜨리게 될 것이고 상,하위 객체간 의존성이 증가하는 결과도 낳게 될 것이다.

  2. 성능 저하 : 마찬가지로 상위 객체 또한 하위 객체의 데이터를 얻기 위해 순회하는 시간이 길어지기에 성능저하로 이어지게 된다.

  3. 메모리 사용량 증가 : 하위 객체는 상위 객체의 정보를 함께 가지기 때문에, 계층 깊이가 깊어질수록 메모리 사용량이 증가한다.

컴포넌트 참조

자기 자신(gameObject)

gameObject.name					// 이름
gameObject.activeSelf			// 활성화 여부(자신)
gameObject.activeInHierarchy	// 활성화 여부(하이어라키 상)
gameObject.tag					// 태그
gameObject.layer				// 레이어

transform.position				// 자신의 Transform 컴포넌트

게임 오브젝트 찾기

target = GameObject.Find("Main Camera"); 

→ 이름으로 찾기 : 모든 게임 오브젝트를 순회하며 탐색 = 시간이 오래걸림
또한 오브젝트가 비활성화된 경우 찾지 못하기에 주의해야한다.

target = GameObject.FindGameObjectWithTag("MainCamera");

→ 태그로 찾기 : 태그가 붙어있는 것들만 순회하며 탐색 = 시간이 적게걸림
태그 미지정 시 예외가 발생할 수 있기에 태그 지정에 주의를 해야한다.

컴포넌트 참조(타 오브젝트)

// transform 컴포넌트
camTransform = target.transform;		

// 기타 컴포넌트 탐색 및 참조
Camera cam = target.GetComponent<Camera>();
if (target.TryGetComponent(out Camera cam)) { ... }	

컴포넌트 추가 및 제거

target.AddComponent<Rigidbody>();		// 런타임 중 Rigidbody 추가
target.GetOrAddComponent<Rigidbody>();	// 런타임 중 추가 및 참조

Destroy(cam);							// 컴포넌트 제거
Destroy(gameObject);					// 오브젝트(자신) 제거

Object Pool Pattern


프로그램 내에서 빈번하게 재활용되는 인스턴스들을 풀에 보관한 뒤 생성과 파괴 대신 대여와 반납을 사용하는 디자인 패턴

게임 최적화 기법 중 하나인 오브젝트 풀 패턴은 C#과 유니티에서 기본이 되는 디자인패턴이다. GC와 밀접한 연관이 있으며, 단순히 패턴의 구현방법, 사용방법을 넘어 장단점까지 제대로 알아보자.

Instantiate & Destroy

프로그래밍에서 생성과 파괴는 메모리를 할당하고 삭제하는 것과 같다. 특히 힙메모리에 데이터가 올라가고 나서 내려가는(제거하는) 역할은 GC(가비지컬렉터)가 담당하는데, GC가 힙 메모리 데이터를 제거하며 힙 공간을 정리하는 작업은 큰 연산을 필요로 하기에 해당 과정과 문제점을 알아둘 필요가 있다.

메모리 단편화

생성과 파괴가 반복적으로 일어날 경우 메모리 단편화 증상이 발생한다. 메모리 단편화란 메모리 공간이 충분히 존재하지만 할당이 불가능한 상태를 의미하며, 유니티에서는 외부 단편화로 인한 메모리 단편화 현상을 주의해야한다.

외부 단편화는 반복적인 메모리 할당 및 해제로 인하여 사용하지 않는 메모리 공간이 중간중간에 생겨 메모리 공간 자체는 충분하지만 실제로는 할당이 안되는 상태이다.

총알이나 이펙트 등 단순히 생성과 파괴가 반복적으로 발생하는 게임에서는 굉장히 빈번하게 발생하기 때문에 이를 방지하기 위해 오브젝트 풀 패턴이 등장하게 되었다.

구현

사용 용도로 다양한 구현방법이 있으며, 가장 기본적으로 배열을 활용한다.
1. 인스턴스들을 보관할 풀을 생성
2. 프로그램의 시작시 풀에 인스턴스들을 생성하여 보관
3. 인스턴스 생성이 필요할 때 풀에서 대여하여 사용
4. 인스턴스 삭제가 필요할 때 풀에 반납하여 보관

public class BulletPool : MonoBehaviour		
{
    [SerializeField] private Queue<PooledBullet> bulletQue;
    [SerializeField] private PooledBullet bullet;
    [SerializeField] private int size;

    void Awake()
    {
        bulletQue = new Queue<PooledBullet>();
        for (int i = 0; i < size; i++)
        {
            PooledBullet instance = Instantiate(bullet);
            instance.gameObject.SetActive(false);
            bulletQue.Enqueue(instance);
        }
    }

    public PooledBullet GetPool(Vector3 position, Quaternion rotation)
    {
        if (bulletQue.Count == 0)
        {
            return Instantiate(bullet, position, rotation);
            // ExpandPool() → pool의 크기를 확장하는 것도 좋은 방법이다.
        }

        PooledBullet instance = bulletQue.Dequeue();
        instance.returnPool = this;
        instance.transform.position = position;
        instance.transform.rotation = rotation;
        instance.gameObject.SetActive(true);
        return instance;
    }

    public void ReturnPool(PooledBullet bullet)
    {
        bullet.gameObject.SetActive(false);
        bulletQue.Enqueue(bullet);
    }
}
public class PooledBullet : MonoBehaviour
{
    public BulletPool returnPool;
    [SerializeField] private float runTime;
    private float _timer;

    private void OnEnable()
    {
        _timer = runTime;
    }

    private void Update()
    {
        _timer -= Time.deltaTime;
        if (_timer <= 0)
        {
            ReturnPools();
        }
    }

    private void ReturnPools()
    {
        if (returnPool == null)
        {
            Destroy(this);
        }
        else
        {
            returnPool.ReturnPool(this);
        }
    }
}

장단점

장점

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

단점

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

0개의 댓글