Unity

Prefab

gcreators 2024. 9. 24. 19:19

유니티에는 Prefab 라고 부르는게 있다

언리얼에서는 Blueprint 라고 부른다고 한다.

 

Prefab은 게임 오브젝트의 재사용성을 위해 생겨난 개념이다

 

우리가 하는 게임에는 참 많은 것들이 존재하는데

 

그중에는 돌, 나무 등 무수히 많은 오브젝트도 있다

 

만약에 내가 만든 게임에 1만 그루의 나무가 있는데

 

크리스마스를 맞이해서 이벤트로 이 나무에 크리스마스 장식을 하라는 지시가 내려왔다.

 

1만여개의 오브젝트의 이미지를 변경하라....? 그날로 퇴근은 없는 것 이다.

 

무식하게 이 1만 그루의 나무를 모두 오브젝트를 다 때려 박아서 만든다면..

 

만들때도 힘들고 변경할때는 더 힘들게 되는 것이다

 

그럼 어떻게 해야하느냐?

 

이 1만 그루의 오브젝트가 만약 하나의 오브젝트를 재사용해서 만든 것이고

 

그 원본인 오브젝트만 변경했을 때 복제한 1만 여개의 나무 오브젝트들이 모두 똑같이 변경된다면

 

작업이 아주 쉬워지지 않겠는가?

 

그런 생각에서 나온 것이 바로 Prefab이다.

Hierarchy에 만든 게임 오브젝트를 Project에 드레그를 하면

Prefab을 만들 수 있으며 다시 Project창의 게임 오브젝트를 Hierarchy창으로 드래그하면

게임 오브젝트의 복제가 가능하다.

 

그리고 Project창에 만든 Prefab을 더블클릭하면

이런식으로 해당 오브젝트를 변경 할 수 있다.

 

그리고 복제한 오브젝트를 변경하는 작업을 하고 모든 오브젝트에 적용시키고 싶다면

prefab의 인스펙터창에 생겨난 Prefab - Overrides의 Apply All을 누르면 복제한 모든 오브젝트가 바뀌게 된다.

 

prefab 하나로 많은 오브젝트를 통제할 수 있으므로 가급적 모든 게임오브젝트는 prefab으로 만들어두는 것이 좋다

※ 단 Prefab을 이용할때는 최상위 부모에 위치하는 게임 오브젝트는 빈 오브젝트를 만들어서 관리 하는 것이 좋다

public class BallSpawner : MonoBehaviour
{
    //Prefab을 가져오는 방법
    //1. 인스펙터로
    [SerializeField]
    private GameObject ballPrefab = null;
    
    //2. 파일을 불러오는 법
    private GameObject loadBallPrefab = null;
    
    private void Start()
	{
    //Resources : 리소스 관리 클래스
    //Resources로 파일을 부를때는 확장자가 들어가지 않음
    //loadBallPrefab = (GameObject)Resources.Load("Prefabs\\P_Ball"); //명시적 형변환 - 자료형이 안맞아도 무조건 바꿈
    loadBallPrefab = Resources.Load("Prefabs\\P_Ball") as GameObject; //타입을 먼저 검사해서 바뀔 수 없다면 null, 바뀔 수 있다면 GameObject 
    loadBallPrefab = Resources.Load<GameObject>("Prefabs\\P_Ball");//게임 오브젝트를 로드하는 템플릿이 만들어져서 동작함
                                                                   //템플릿이라 복사본이 만들어짐 -> 낭비가 심해보일 수 있음 하지만
                                                                   //LoadAll이라는 옵션 - 폴더의 모든 오브젝트를 불러옴 -> 템플릿으로만 동작가능

	}
    
 }

Prefab을 코드상으로 가져오는 법에는 두가지가 있는데

하나는 인스펙터에 등록 할 수 있도록 SerializeField를 쓰는 것이고

다른 하나는 

Resources 라는 클래스를 활용해 파일을 호출하는 방법이다.

Resources 클래스를 통해 파일을 호출 할 때는 

Resources라는 폴더가 경로의 기준이 되기에 Prefabs 라는 폴더를 이곳에 만든 후 Prefab을 관리하는 것을 추천한다

 

Resources 클래스 내부에 Load 메서드를 활용하면

위처럼 두가지 방법으로 나눠서 사용이 가능한데

아래의 템플릿을 이용하는 방법은 복사본이 만들어져서 낭비가 있는 것으로 보이지만

 

LoadAll이라는 메서드를 통해 Prefab의 모든 파일(오브젝트)을 불러야 할 경우에는 해당 옵션만을 이용할 수 있으니 이를 참고하자

 

그리고 Prefab은 게임 오브젝트를 파일로 저장하는 것이다보니 다른 씬을 만들어도 활용이 가능하다

생산성을 높일 수 있는 수단이므로 가급적 게임 오브젝트는 Prefab으로 만들도록 하자

 

Object Pooling

Prefab과 직접적인 관련은 없지만 Prefab 자체가 효율적인 게임 오브젝트 재사용과 관리를 위해 사용하는 것인 만큼

그와 관련된 개념이 이 Object Pooling이다

 

Object Pooling은 미리 만들어둔 오브젝트에 대해 동적할당을 해제 하는 것이 아닌

다시 재활용을 하는 개념이다.

 

탄막슈팅 게임 혹은 FPS게임에는 흔히들 총알이라는 오브젝트를 설정하고 이를 핵심요소로 활용하여 만든 게임들이다.

발사된 총알들 하나하나가 모두 게임 오브젝트이니 Prefab을 활용한다면 쉽게 발사되는 총알들을 구현 할 수 있다

그런데 플레이 하는 유저들이 열심히 총알을 쏘면

이 총알이 오브젝트에 맞아 상호작용을 일으키던 빗나가서 맵 밖으로 이탈하던 게임의 메모리 관리를 위해 이미 사용한 오브젝트는 동적할당을 해제 하는 것이 맞을 것이다.

 

그런데 이 동적할당을 계속 새로하는 것은 당연하게도 메모리에 새로운 주소를 계속 할당 받고 삭제를 반복하다보니

이는 지속되게 되면 메모리 단편화 현상이 나올 가능성으로 이어질 수가 있다.

 

그러니 어차피 메모리에 할당되는 이 총알.. 그냥 해제하지 않고 잠깐 화면상에서만 숨겼다가 다시 총알이 발사 되면 발사 위치로 바꿔서 발사되는 형식으로 만든다면 메모리 활용이 좀 더 효율적으로 될 것이다.

이렇게 재활용을 하면 메모리에 부담이 없어지게 되니 Object Pooling은 꼭 필요한 개념이라고 할 수 있다.

 

using UnityEngine;
using System.Collections.Generic;

//Object Pooling
public class ObjectPool : MonoBehaviour
{
    [SerializeField]
    private GameObject bulletPrefab = null; //총알 Prefab 인스펙터창에 담을 수 있도록

    [SerializeField]
    private List<Bullet> bulletList = null; //총알들의 List를 만들어서 효율적인 관리가 되도록

    private void Start()
    {
        bulletList = new List<Bullet>(); //리스트 초기화
    }

    private void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            Vector3 mousePosToWorld = GetMousePositionToWorld(); //마우스의 좌표가 월드좌표가 되는 메서드

            Vector3 dir = GetDirectionCameraToCursor(); // 카메라를 기준으로 월드좌표화 된 마우스 좌표 정규화 진행

            Bullet bullet = null; //총알 초기화
            
            if(GetValidBullet(out bullet)) //총알이 비활성화인지 검사 진행
            {
                bullet.Init(mousePosToWorld, dir); //마우스 클릭시 카메라가 보는 방향으로 총알이 나가도록 함
            }
            else 
            {
                bullet = SpawnBullet(mousePosToWorld); //마우스가 클릭된 곳에 총알 오브젝트 동적할당
                bullet.Init(mousePosToWorld, dir); //마우스가 클릭된 곳에서 부터 카메라가 보는 방향으로 날아가는 총알
                bulletList.Add(bullet); //새로 동적할당된 총알 오브젝트를 리스트에 추가
            }
            
        }
        Debug.Log(GetMousePositionToWorld()); //마우스 좌표확인
    }
    private Vector3 GetMousePositionToWorld() //마우스 좌표를 월드좌표로 변경하는 메소드
    {
        Vector3 mousePos = Input.mousePosition; //마우스 위치를 담는 벡터형 변수

        mousePos.z = Camera.main.nearClipPlane;//마우스 위치의 z축을 카메라의 최소화면에 맞게 변화

        Vector3 screenToWorld = Camera.main.ScreenToWorldPoint(mousePos);//마우스 좌표를 월드좌표로 변경 

        Debug.Log(screenToWorld);

        return screenToWorld;
    }
    private Vector3 GetDirectionCameraToCursor()
    {
        return (GetMousePositionToWorld() - Camera.main.transform.position).normalized;//월드좌표로 변경된 마우스좌표를 정규화
    }

    private Bullet SpawnBullet(Vector3 _pos) //총알 오브젝트 동적할당 메서드
    {
        GameObject bulletGo = Instantiate(bulletPrefab, _pos, Quaternion.identity, transform);

        return bulletGo.GetComponent<Bullet>();
    }

    private bool GetValidBullet(out Bullet _bullet) //총알 오브젝트 할당전 비활성화 오브젝트가 있는지 확인하는 메서드
    {
        foreach(Bullet bullet in bulletList)
        {
            if(bullet.IsDead)
            {
                _bullet = bullet;
                return true;
            }
        }
        _bullet = null;
        return false;
    }


}

 

using UnityEngine;

public class Bullet : MonoBehaviour
{
    private float moveSpeed = 10f;

    private Vector3 oriPos = Vector3.zero;
    private float destroyDist = 10f;
    private float duration = 2f;

    private Vector3 dir = Vector3.forward;
    public bool IsDead //게임오브젝트의 활성화 상태 확인
    {
        get { return !gameObject.activeSelf; }
    }

    private void Update()
    {
        transform.Translate(dir * moveSpeed * Time.deltaTime);//총알 이동
        if(Vector3.Distance(oriPos, transform.position) > destroyDist)
        {
            SelfDestroy();//조건 달성시 총알 오브젝트 비활성화
        }
        Debug.DrawLine(transform.position, transform.position + (dir * 5f), Color.yellow);
    }

    public void Init(Vector3 _pos, Vector3 _dir) //총알 오브젝트 활성화 메서드
    {
        transform.position = _pos;
        oriPos = transform.position;
        dir = _dir;

        gameObject.SetActive(true);

        Invoke("SelfDestroy", duration);//일정 시간 후 오브젝트 비활성화
    }

    private void SelfDestroy() //오브젝트 비활성화 메서드
    {
        //Destroy(gameObject); //비활성화가 아닌 할당해제는 Destroy로 가능
        gameObject.SetActive(false);
    }
}

이를 각각의 게임오브젝트에 컴포넌트 추가를 하고 테스트를 해보자

마우스로 게임창을 누르면 총알이 나갈 것이고 조건에 따라 비활성화가 되는 것을 확인 할 수 있다

그리고 총알이 모두 활성화 되었을때는 추가적인 동적 할당이 이루어지지만

비활성화된 게임오브젝트가 있으면 활성화 하여 추가적인 동적할당 없이 재활용 하는 것을 확인 할 수 있다.

'Unity' 카테고리의 다른 글

Lerp(선형 보간)  (0) 2024.10.01
Coroutine(코루틴)  (0) 2024.09.30
Collision(충돌)  (0) 2024.09.23
Transform(2) - position  (0) 2024.09.20
Transform(1) - 좌표  (0) 2024.09.20