Programming 

스크립트로 게임 오브젝트 생성하고 파괴하기

 

작성 기준 버전 :: 2019.2

 

[본 포스트의 내용은 유튜브 영상으로도 시청하실 수 있습니다]

 

 

씬에 새로운 게임 오브젝트를 생성하는 제일 기본적인 방법은 하이어라키 뷰에 우클릭해서 생성하고자 하는 게임 오브젝트를 선택하는 것이었다. 하지만 이렇게 에디터의 하이어라키 뷰에서 생성한 게임 오브젝트는 다른 컴포넌트나 스크립트의 개입 없이는 파괴되지 않기 때문에, 정적인 오브젝트이다.

 

반대로 스크립트의 개입으로 게임 플레이 도중에 생성되고 파괴되는 것을 동적으로 오브젝트를 생성하거나 파괴한다고 한다.

 

그럼 이제부터 이렇게 스크립트를 이용해서 게임 플레이 도중에 게임 오브젝트를 생성하고 파괴하는 방법을 알아보도록 하자.

 

게임 오브젝트 생성하기

 

public class GameObjectCreator : MonoBehaviour

{

    void Start()

    {

        new GameObject();

    }

}

 

게임 오브젝트를 생성하기 위해서는 위의 예시 코드와 같이 new GameObject()라는 코드를 작성하면 된다.

 

이 코드는 C#에서는 GameObject라는 클래스의 객체를 메모리에 생성하는 코드이지만, 유니티 엔진에서는 GameObject 클래스의 객체를 메모리에 생성하면서 씬에 실제 게임 오브젝트 역시 생성하는 기능을 한다.

 

 

이대로 코드를 저장하고 에디터로 돌아가서 하이어라키 뷰에서 게임 오브젝트를 하나 생성하고, 방금 만든 GameObjectCreator 컴포넌트를 부착해준 뒤, 플레이 버튼을 보면 "New Game Object"라는 이름으로 게임이 시작되는 시점에는 씬에 없던 새로운 게임 오브젝트가 동적으로 생성된 것을 볼 수 있다.

 

하지만 이렇게 생성된 게임 오브젝트는 말 그대로 '빈' 게임 오브젝트이다. 게임에서 아무것도 없는 빈 게임 오브젝트가 할 수 있는 역할은 아마 없을 것이다. 빈 게임 오브젝트에 새로운 역할을 부여해주려면 새로운 컴포넌트를 부착해야 한다.

 

그러니 앞 단계에 이어서 게임 오브젝트에 스크립트로 컴포넌트를 붙이는 방법을 알아보자.

 

프로젝트 뷰에 우클릭해서 TestComponent라는 이름으로 C# 스크립트를 생성한다. 그리고 GameObjectCreator 스크립트 파일에서 아래와 같이:

 

public class GameObjectCreator MonoBehaviour

{

    void Start()

    {

        var newObj = new GameObject().AddComponent<TestComponent>();        newObj.name = "Test Component Game Object";

    }

}

 

.AddComponent 함수를 사용하면 비어있는 게임 오브젝트에 스크립트로 방금 만든 TestComponent 컴포넌트를 부착할 수 있다. 

 

그리고 .name 프로퍼티를 이용하면 게임 오브젝트의 이름을 바꿀 수 있다. 이렇게 생성한 게임 오브젝트의 이름을 적절하게 바꿔주면 나중에 에디터에서 작업할 때 찾고자 하는 게임 오브젝트를 빠르게 찾을 수 있게 된다.

 

 

코드를 저장하고 에디터로 돌아가서 실행해보면 게임이 플레이되면 아까처럼 새로운 게임 오브젝트가 생성되는데 새 게임 오브젝트의 이름이 "New Game Object"가 아닌 "Test Component Game Object"인 것을 볼 수 있다. 그리고 이 "Test Component Game Object"를 하이어라키 뷰에서 클릭해보면 이렇게 TestComponent가 부착되어 있는 것을 볼 수 있다.

 

게임 오브젝트 파괴하기

 

게임 오브젝트를 생성하기만 하고 생성된 게임 오브젝트를 적절하게 제거하지 않으면 게임이 사용하는 메모리가 점점 늘어나면서 게임이 점점 느려지고 마지막엔 결국 게임이 멈춰버릴 수도 있다.

 

그럼 이제 생성된 게임 오브젝트를 스크립트로 파괴하는 방법을 알아보자.

 

이번에는 TestComponent 스크립트 파일을 연다.

 

public class TestComponent : MonoBehaviour

{

    void Start()

    {

        Destroy(gameObject);

    }

 

    private void OnDestroy()    {        Debug.Log(name + "가 파괴됨!");

 

    }

}

 

Start 함수에서 Destory 함수를 호출하고 매개변수로 파괴하고자 하는 게임 오브젝트를 넣어주면 된다. 우리는 TestComponent가 부착된 게임 오브젝트가 파괴되기를 원하기 때문에 gameObject 프로퍼티를 넣어준다.

 

그리고 OnDestroy 함수를 만들고 게임 오브젝트가 파괴될 때, 파괴되는 게임 오브젝트의 이름과 함께 파괴되었다는 로그를 남겨주는 코드를 작성한다.

 

 

코드를 저장하고 에디터로 돌아가서 플레이해보면 게임이 시작되자마자 Test Component Game Object가 파괴되었다는 로그만 남기고 사라져버린다.

 

Destroy(gameObject, 5f);

 

게임 오브젝트가 생성되자마자 바로 파괴되는 것이 아니라 일정한 시간이 지난 뒤에 파괴되는 것을 원한다면, Destroy 함수의 두 번째 매개변수에 실수형 매개변수를 넣어주면 된다.

 

유니티 스크립트 작업의 기반이 되는 C# 프로그래밍에는 함수 오버로딩이라고 하는 기능을 제공하는데 이것은 똑같은 이름의 함수라도 입력하는 매개변수에 따라서 비슷하지만 다른 동작을 하도록 구현할 수 있도록 만들어준다.

 

gameObject만 매개변수로 넣었을 때의 Destory 함수는 입력받은 게임 오브젝트를 바로 파괴하지만, 두 번째 매개변수로 실수형 숫자를 같이 넣어주면, 넣어준 숫자만큼의 시간이 지난 후에 입력받은 gameObject를 파괴하는 식으로 조금 다르게 동작한다.

 

참고로 두 번째 매개변수의 시간은 초 단위로 동작한다. 그러니까 이 TestComponent가 부착된 게임 오브젝트는 생성되고나서 5초가 지나면 파괴된다.

 

코드를 저장하고 에디터로 돌아가서 플레이 버튼을 눌러보면 게임 오브젝트가 생성되고 약 5초가 지난 뒤 파괴되는 것을 확인할 수 있다.

 

이런 식으로 스크립트로 게임 오브젝트를 생성하고 원하는 시점에 파괴되게 만들 수 있다.

 

반응형

Programming 

오브젝트 풀링 기법

 

 

 

작성 기준 버전 :: 2019.2

 

프로그래밍에서 오브젝트를 생성하거나 파괴하는 작업은 꽤나 무거운 작업으로 분류된다. 오브젝트 생성은 메모리를 새로 할당하고 리소스를 로드하는 등의 초기화하는 과정으로, 오브젝트 파괴는 파괴 이후에 발생하는 가비지 컬렉팅으로 인한 프레임 드랍이 발생할 수 있다.

 

이러한 문제를 해결하기 위해서 사용되는 기법이 바로 오브젝트 풀링(Object pooling)이다.

 

오브젝트 풀링의 개념

 

오브젝트 풀링의 개념은 아주 간단하다. 

 

 

먼저 풀링을 할 오브젝트를 담을 오브젝트 풀을 구성한다.

 

 

그 다음 풀링할 오브젝트를 생성해서 오브젝트 풀에서 그 오브젝트들을 관리하게 만든다.

 

 

외부에서 해당 오브젝트가 필요하면 오브젝트 풀에서 꺼내간다.

 

 

오브젝트 풀에서 꺼낸 오브젝트의 사용이 끝나면 오브젝트를 풀에 돌려준다.

 

 

오브젝트 풀에서 오브젝트를 가져오려고 할 때, 모든 오브젝트가 이미 사용중이라면 새로운 오브젝트를 생성해서 꺼내준다.

 

이것이 바로 오브젝트 풀의 개념이다. 게임에 필요한 오브젝트를 미리 생성해서 필요할 때마다 꺼내쓰고 사용이 끝나면 오브젝트 풀에 돌려주는 것이다. 이렇게 함으로써, 오브젝트가 필요할 때마다 생성하고 다 쓰면 파괴하는 것이 아니라, 게임이 시작될 때, 필요한 만큼의 오브젝트만 생성하고, 모자라면 추가로 생성하고, 게임이 끝나면 파괴하는 방식으로 오브젝트의 생성/파괴 횟수를 줄일 수 있게 된다.

 

오브젝트 풀링 기법은 특히 메모리가 부족하고 CPU의 성능이 낮은 디바이스의 사양이 낮았던 예전에 주로 사용되었던 기법이지만, 현재에 와서도 게임의 최적화를 위해서 많이 사용되며, 모바일 게임이 많이 제작되면서 PC에 비해서 모자란 디바이스 성능으로 인해서 재조명 받는 기법이다.

 

보통 자주 생성되었다가 파괴되어야 하는 총알이나, 캐릭터가 뛸 때 발생하는 먼지 이펙트 같은 곳에 많이 사용되는 기법이다.

 

 

유니티 엔진에서의 구현

 

기본 세팅

 

기본적인 세팅은 다음과 같다.

 

 

씬 한 가운데 캐릭터의 역할을 할 초록색 박스를 배치했다.

 

public class Shooter : MonoBehaviour
{
    [SerializeField]
    private GameObject bulletPrefab;

    private Camera mainCam;

    void Start()
    {
        mainCam = Camera.main;
    }

    void Update()
    {
        if(Input.GetMouseButton(0))
        {
            RaycastHit hitResult;
            if(Physics.Raycast(mainCam.ScreenPointToRay(Input.mousePosition), out hitResult))
            {
                var bullet = Instantiate(bulletPrefab).GetComponent<Bullet>();
                var direction = new Vector3(hitResult.point.x, transform.position.y, hitResult.point.z) - transform.position;
                bullet.transform.position = direction.normalized;
                bullet.Shoot(direction.normalized);
            }
        }
    }
}

 

그리고 박스 게임 오브젝트에는 마우스 클릭 방향으로 총알 게임 오브젝트를 생성해서 발사하는 Shooter 컴포넌트가 부착되어 있다.

 

 

public class Bullet : MonoBehaviour
{
    private Vector3 direction;
    public void Shoot(Vector3 direction)
    {
        this.direction = direction;
        Destroy(gameObject, 5f);
    }

    void Update()
    {
        transform.Translate(direction);   
    }
}

 

그리고 지정된 방향으로 5초 간 날아가다 자동으로 소멸하는 총알 프리팹 역시 만들어두었다.

 

 

이것을 테스트 해보면 마우스를 클릭하면 엄청난 양의 총알 오브젝트가 새로 생성되고 시간이 지나면 모두 파괴되는 것을 볼 수 있다.

 

 

이 상황을 프로파일러를 통해서 확인해보면 총알을 생성하는 Instantiate와 파괴하는 Destroy가 어느 정도 성능을 잡아먹고 있는 것을 확인할 수 있다.

 

오브젝트 풀 구현

 

public class ObjectPool : MonoBehaviour
{
    public static ObjectPool Instance;

    [SerializeField]
    private GameObject poolingObjectPrefab;

    Queue<Bullet> poolingObjectQueue = new Queue<Bullet>();

    private void Awake()
    {
        Instance = this;

        Initialize(10);
    }

    private void Initialize(int initCount)
    {
        for(int i = 0; i < initCount; i++)
        {
            poolingObjectQueue.Enqueue(CreateNewObject());
        }
    }

    private Bullet CreateNewObject()
    {
        var newObj = Instantiate(poolingObjectPrefab).GetComponent<Bullet>();
        newObj.gameObject.SetActive(false);
        newObj.transform.SetParent(transform);
        return newObj;
    }

    public static Bullet GetObject()
    {
        if(Instance.poolingObjectQueue.Count > 0)
        {
            var obj = Instance.poolingObjectQueue.Dequeue();
            obj.transform.SetParent(null);
            obj.gameObject.SetActive(true);
            return obj;
        }
        else
        {
            var newObj = Instance.CreateNewObject();
            newObj.gameObject.SetActive(true);
            newObj.transform.SetParent(null);
            return newObj;
        }
    }

    public static void ReturnObject(Bullet obj)
    {
        obj.gameObject.SetActive(false);
        obj.transform.SetParent(Instance.transform);
        Instance.poolingObjectQueue.Enqueue(obj);
    }
}

 

오브젝트 풀링 기법을 구현하는 기초적인 코드의 전체는 위의 코드와 같다. 

 

public static ObjectPool Instance;

private void Awake()
{
    Instance = this;

    Initialize(10);
}

 

이 코드를 하나씩 살펴보자. 우선 오브젝트 풀의 경우 오브젝트를 생성하는 어떤 코드에서든 접근이 가능해야하는 경우가 많기 때문에 싱글톤 패턴으로 구현되는 경우가 많다. 

 

[SerializeField]
private GameObject poolingObjectPrefab;

 

그리고 생성해야할 오브젝트의 프리팹을 가지고 있다. 프리팹을 가지고 있어야 하는 이유는 처음 생성해둔 오브젝트를 모두 꺼내주고 나서, 꺼내준 오브젝트를 미처 돌려받기 전에, 오브젝트를 요청받았을 때, 새로운 오브젝트를 생성해서 꺼내주기 위한 것이다.

 

Queue<Bullet> poolingObjectQueue = new Queue<Bullet>();

 

큐(Queue)를 이용해서 생성된 총알 오브젝트를 순서대로 관리한다. 만약 오브젝트 풀에서 오브젝트를 꺼내주는 순서와 돌려받는 순서를 제대로 관리하지 못하면, 하나의 오브젝트를 여러 곳에 빌려주는 등의 문제가 발생할 수 있다.

 

private void Awake()
{
    Instance = this;

    Initialize(10);
}

 

게임 오브젝트가 생성되면 제일 먼저 실행되는 Awake() 함수에서는 싱글톤 패턴에서 사용되는 오브젝트 풀 인스턴스에 자신을 할당하는 일과 오브젝트 풀을 초기화하는 Initialize() 함수를 호출한다. 

 

private void Initialize(int initCount)
{
    for(int i = 0; i < initCount; i++)
    {
        poolingObjectQueue.Enqueue(CreateNewObject());
    }
}

 

Initialze() 함수에서는 매개변수로 받은 초기화 갯수 값에 따라서 CreateNewObject() 함수에서 만들어진 새로운 총알 오브젝트를 pooling Object Queue에 넣어준다. 이렇게 게임이 시작하기 전에 사용될 게임 오브젝트를 미리 적절한 갯수를 만들어 줌으로써 게임 플레이 도중에 발생할 과부하를 게임 준비 과정인 로딩으로 돌릴 수 있다. 모두 잘 알겠지만, 플레이어는 아주 조금 더 긴 로딩은 당장 용납하지만, 플레이 도중에 발생하는 렉은 용납하지 못한다.

 

private Bullet CreateNewObject()
{
    var newObj = Instantiate(poolingObjectPrefab).GetComponent<Bullet>();
    newObj.gameObject.SetActive(false);
    newObj.transform.SetParent(transform);
    return newObj;
}

 

CreateNewObject() 함수는 poolingObjectPrefab으로부터 새 게임 오브젝트를 만든 뒤 비활성화해서 반환하는 역할을 한다. 이렇게 곧바로 비활성화하는 이유는 아직 오브젝트 풀이 꺼내주지 않은 오브젝트는 비활성화된 것으로써 플레이어의 눈에 띄지 않아야하기 때문이다.

 

public static Bullet GetObject()
{
    if(Instance.poolingObjectQueue.Count > 0)
    {
        var obj = Instance.poolingObjectQueue.Dequeue();
        obj.transform.SetParent(null);
        obj.gameObject.SetActive(true);
        return obj;
    }
    else
    {
        var newObj = Instance.CreateNewObject();
        newObj.gameObject.SetActive(true);
        newObj.transform.SetParent(null);
        return newObj;
    }
}

 

GetObject() 함수는 오브젝트 풀이 가지고 있는 게임 오브젝트를 요청한 자에게 꺼내주는 역할을 한다. 단, 모든 오브젝트를 꺼내서 빌려줘서 poolingObjectQueue에 빌려줄 오브젝트가 없는 상태라면 CreateNewObject() 함수를 호출해서 새로운 오브젝트를 생성해서 빌려준다.

 

public static void ReturnObject(Bullet obj)
{
    obj.gameObject.SetActive(false);
    obj.transform.SetParent(Instance.transform);
    Instance.poolingObjectQueue.Enqueue(obj);
}

 

ReturnObject() 함수는 빌려준 오브젝트를 돌려받는 함수이다. 돌려받은 오브젝트를 비활성화한 뒤 정리하는 일을 처리한다.

 

무엇이든지 빌려줬으면 돌려받아야 한다. 이 원칙은 프로그래밍에서도 매우 중요한 원칙으로 사용한 오브젝트를 제대로 돌려받지 않으면 GetObject() 함수를 호출할 때, 계속해서 새로운 오브젝트를 생성하고 사라지지 않은 오브젝트들이 씬에 쌓이게 된다. 이것이 바로 메모리 누수이다.

 

 

코드 작성이 끝나면 씬에 새 게임 오브젝트를 배치하고 오브젝트 풀 컴포넌트를 부착하고 pooling Object Prefab 프로퍼티에 풀링해야할 게임 오브젝트의 프리팹을 할당해주면 된다.

 

 

그 다음에는 총알 오브젝트를 생성하는 부분과 파괴하는 부분을 수정해야 한다.

 

public class Shooter : MonoBehaviour
{
    void Update()
    {
        if(Input.GetMouseButton(0))
        {
            RaycastHit hitResult;
            if(Physics.Raycast(mainCam.ScreenPointToRay(Input.mousePosition), out hitResult))
            {
                var bullet = ObjectPool.GetObject(); // 수정
                var direction = new Vector3(hitResult.point.x, transform.position.y, hitResult.point.z) - transform.position;
                bullet.transform.position = direction.normalized;
                bullet.Shoot(direction.normalized);
            }
        }
    }
}

 

우선 생성하는 부분은 Shooter 클래스의 Update() 함수에 Instanitate()로 총알을 생성하던 부분을 ObjectPool.GetObject()로 대체하면 된다.

 

public class Bullet : MonoBehaviour
{
    private Vector3 direction;
    public void Shoot(Vector3 direction)
    {
        this.direction = direction;
        Invoke("DestroyBullet", 5f);
    }

    public void DestroyBullet()
    {
        ObjectPool.ReturnObject(this);
    }

    void Update()
    {
        transform.Translate(direction);   
    }
}

 

그 다음 파괴하는 부분은 Bullet 클래스에서 DestoryBullet() 함수에서 ObjectPool.ReturnObject()를 호출해서 오브젝트 풀에 돌려주도록 구현하고 Shoot() 함수에서 Destory() 함수를 호출하던 부분을 Invoke()로 5초 뒤에 DestoryBullet() 함수를 호출하도록 수정한다.

 

 

 

플레이를 해서 테스트 해보면 총알이 일정 갯수만큼 생성되고 나면 더 이상 생성되지 않으며, 시간이 지나면 소멸되어야 하는 총알 오브젝트들이 비활성화되서 오브젝트 풀로 반환되는 것을 확인할 수 있다.

 

 

프로파일러를 통해서 확인해보면 최대 생성량에 도달한 이후에는 Instatiate와 Destroy로 인한 작업이 발생하지 않는 것을 볼 수 있다.

반응형
  1. 료용 2020.05.04 01:14 신고

    베르님 안녕하세요 유투브 잘보고있습니다.

    이 글 정말 좋은글인데 제가 이해를 못하는부분이있어서 질문드립니다.

    Initialze() 이 함수 awake에서 10으로 주시지않았습니까? 그러면 queue에 10개의 bullet의 오브젝트가 들어있는거지않습니까??

    그런데 그러면 10개가 넘는다면 새로운 bullet를 생성해주시는데

    var newObj = Instance.CreateNewObject();

    newObj.gameObject.SetActive(true);

    newObj.transform.SetParent(null);

    return newObj;

    여기서 이렇게 새로 생성한다음에

    ReturnObject()

    으로 받는다면 queue에는 지금 영상에서 보듯이 수십개의 bullet가 저장되는 개념아닌가요?

    그렇다면 처음에 Initialze() 에 10으로 줄이유가없지않나요? 이부분이 너무헷갈려서 .. 유투브에서 물어보려다가 너무길게 될 질문이라 여기다가 올립니다 ㅠㅠ

    • wergia 2020.05.04 12:47 신고

      Awake 함수에서 Initialize로 10개를 미리 만들어두는 것은 게임에서 사용될 오브젝트의 갯수를 예측해서 미리 만들어두는 것입니다.

      예시에서는 미리 만들어둔 갯수를 모두 사용하고나면 이후에 생성되는 것을 보여드리기 위해서 10개만 만들었지만 실제로는 게임을 여러 번 실행해서 테스트를 진행해보고 평균적으로 게임에 몇 개의 오브젝트가 필요한지 산출해낸 뒤 그 갯수를 미리 생성해두게 만듭니다.

      그러면 게임이 시작될 때 그 오브젝트 갯수를 미리 생성해두는 과정이 처리됩니다.

      이렇게 미리 필요한 갯수를 적당히 생성해두면 대부분은 미리 생성해둔 오브젝트를 사용하고 오브젝트가 부족해지면 새로 생성해서 사용하게 됩니다.

    • 료용 2020.05.04 15:20 신고

      그렇다면 혹시 10개정도만 쓰겠다! 라는느낌으로 awake에 해놓은건가요? 어차피 10개이상 bullet가 생성된다면 queue 의 사이즈는 bullet의 반납되는 수만큼 늘어났다가 다시 queue에 있던 bullet을 빌려주는 이런개념인거죠? 10개를 훨씬넘어서는

    • wergia 2020.05.04 22:21 신고

      10개 정도 사용할 것 같다고 예측하고 10개만 생성해둔 겁니다. 하지만 예시에서는 예측을 넘어서서 더 많은 양을 사용하게 되었으니 추가로 생성한 거구요.

      그리고 추가로 생성한 Bullet은 사용이 모두 완료된 후에 다시 Queue로 반납되어서 보관됩니다.

    • 료용 2020.05.08 19:12 신고

      감사합니다 베르님 확이해갔어요

    • wergia 2020.05.08 20:21 신고

      다행입니다 ㅜㅜ
      설명하기가 어려워서 이해 잘하셨을까 했는데

    • 료용 2020.05.08 20:57 신고

      죄송합니다 ...

      베르님 딱하나만 더...

      그런데 bullet을 많이생성하게되면 queue가 이때까지 최대로 많이생성했던 bullet의 갯수만큼의 사이즈를 가지게되지않나요?
      그러면 그거대로 느려지게되는 이유가되지않는건가요?

    • wergia 2020.05.09 11:08 신고

      만약 풀링하는 오브젝트가 사용하는 리소스의 용량이 매우 크다면 메모리를 많이 소모할 수도 있습니다.

      하지만 대부분 풀링하는 오브젝트는 총알 화살 같은 물체이기 때문에 사용하는 리소스의 용량이 크지 않은 물체가 대부분입니다.

      그리고 Bullet 같은 오브젝트가 쌓여서 메모리가 완전히 소모되어 게임이 느려지는 순간은 굉장히 늦게 오는 편이고 그렇게 될 오브젝트가 쌓이기도 어려운 구조입니다.

      하지만 가비지 컬렉터로 인한 프레임 드랍은 언제든지 찾아올 수 있습니다.

      이러한 프레임 드랍을 없애는게 주요한 목표이기 때문에 오브젝트 풀링을 사용합니다.

      오브젝트를 미리 생성해서 게임이 끝날때까지 사용하다가 게임이 끝나고 난 뒤 로딩 타임에 소멸시켜서 가비지 컬렉팅으로 인한 프레임 드랍을 로딩시간으로 편입시키는 등의 방법으로 게임 중의 프레임 드랍을 최소화하는 겁니다.

    • 료용 2020.05.09 16:37 신고

      감사합니다 베르님 마지막설명이 딱 이해하기쉬워졌어요

    • wergia 2020.05.10 22:08 신고

      언제든 궁금하신 점 있으시면 질문해주세요!
      아는 부분이라면 최대한 답변해드릴게요!

  2. moody 2020.06.17 13:09

    너무 좋은 자료 잘보고갑니다 ! 이런글 올려주셔서 너무 감사해요

    • wergia 2020.06.22 23:44 신고

      다음에는 심화과정으로 한 번 만들어 보겠습니다 ㅎㅎ

  3. Groza 2020.12.03 20:58

    정말 감사합니다 ㅎㅎ

  4. 베르사랑 2020.12.16 17:57

    베르님 현재 디펜스 게임 제작중에 있습니다.
    베르님의 강의대로 오브젝풀링 구현 후
    여러개의 타워에서 업데이트로 빌려오는데
    가끔 어느 타워가 공격을 하지 않습니다.ㅜ
    이게 동시에 꺼내오면서 몇몇 타워들이 가져오질
    못하는건가요?ㅜ
    여러 타워 업데이트 문에서 호출하여 사용하고 싶은데
    방법이 없을까요?ㅜ

  5. Cargold 2021.08.11 18:16

    풀링은 선택이 아니라 필수!

DontDestroyOnLoad 

파괴하지 않을 게임 오브젝트 만들기

 

작성 기준 버전 :: 2019.1.4f1

 

[이 포스트의 내용을 유튜브 영상으로도 시청하실 수 있습니다]

 

유니티에서는 씬(Scene, 장면) 단위로 게임이 플레이될 공간이나 장소 등을 구현하며, 한 씬에서 다른 씬으로 넘어갈 때는, 기존 씬이 언로드되면서 기존 씬에 있던 게임 오브젝트(Game Object)는 모두 파괴된다. 아래의 예시를 보자.

 

 

위 이미지는 Destroy Object라는 이름의 게임 오브젝트 다섯 개를 배치한 Test Scene의 캡처 화면이다. 플레이가 시작되면 아무것도 없는 Other Scene으로 넘어가게 설계되어 있다.

 

 

설계대로 플레이 버튼을 누르면 Test Scene에서 Other Scene으로 이동하며 씬에 배치되어 있는 다섯 개의 Destroy Object가 사라지는 것을 볼 수 있다.

 

 

 

DontDestoryOnLoad 사용법

 

위의 예시에서 볼 수 있듯이 유니티에서는 새로운 씬을 불러오면 이전 씬에 남아있던 게임 오브젝트들은 모두 사라진다. 하지만 개발자의 의도나 설계에 따라서 몇몇 게임 오브젝트들은 다른 씬으로 넘어갈 때 파괴되지 않도록 할 필요성이 생길 수도 있다.

 

public class DontDestoryObject : MonoBehaviour
{
    private void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }
}

 

씬 로드시 파괴하지 않을 오브젝트로 만들려면 바로 위의 코드처럼 DontDestroyOnLoad()함수를 호출해서 매개변수로 자신의 게임 오브젝트를 전달하면 된다.

 

   

이렇게 파괴하지 않을 오브젝트로 만들어진 게임 오브젝트는 게임 플레이가 시작되고 함수가 호출되면 위의 이미지처럼 Test Scene에서 DontDestroyOnLoad 영역으로 옮겨진다.

 

 

만들어진 스크립트를 Dont Destroy Game Object라는 이름의 구체 게임 오브젝트에 붙인 다음 플레이 버튼을 눌러보면 Test Scene에서 Other Scene으로 넘어가면서 Destroy Object는 전부 사라지지만 Dont Destory Game Object는 남아있는 것을 확인할 수 있다.

 

 

Don't Destroy On Load 게임 오브젝트 파괴하기

 

Don't Destory On Load로 설정된 게임 오브젝트는 그럼 파괴할 수 없는 것인가? 라고 생각할 수도 있다. 하지만 제일 앞의 단어가 Can't가 아니라 Don't 임을 명심하자. 파괴할 수 없는 것이 아니라 파괴하지 않는 것이다.

 

Destroy(gameObject);

 

Don't Destory On Load로 설정된 게임 오브젝트는 Destroy() 함수를 이용하면 손쉽게 다시 파괴할 수 있다.

 

 

설계시 주의 사항

 

Don't Destory On Load 게임 오브젝트를 사용한 설계를 할 때는 주의할 점이 있다.

 

 

위의 이미지와 같이 Don't Destory On Load가 적용된 게임 오브젝트가 들어있는 씬과 다른 씬을 여러 번 왔다 갔다 하는 방식의 구조를 생각해보자. 

 

Test Scene에 진입하면 파괴하지 않는 게임 오브젝트가 Don't Destroy On Load 영역으로 이동되면서 씬을 이동해도 파괴되지 않게 될 것이다. 그 다음 Other Scene으로 이동했다가 다시 Test Scene으로 돌아오면 어떻게 될까? 파괴하지 않는 게임 오브젝트는 Don't Destroy On Load 영역으로 이동했으니 더 이상 Test Scene에 남아있지 않을까?

 

아니다. 씬을 새로 불러올 때는 해당 씬의 초기 상태로 불러오기 때문에 파괴하지 않는 게임 오브젝트는 새 것이 생성되어 있으며 이것은 그대로 다시 Don't Destroy On Load 영역으로 옮겨진다. 즉, 똑같은 파괴하지 않는 게임 오브젝트가 2개가 되어버리는 것이다.

 

 

그대로 두 씬을 왕복하면 같은 게임 오브젝트가 계속해서 누적되는 문제가 발생한다. 이 문제는 그대로 불필요한 메모리 사용을 증가시킬 것이고, 뿐만 아니라 예측할 수 없는 작동 문제 역시 일으킬 수 있다. 

 

 

해결 방법 1 : 중복 검사 후 파괴하기

 

public class DontDestoryObject : MonoBehaviour
{
    private void Awake()
    {
        var obj = FindObjectsOfType<DontDestoryObject>();
        if (obj.Length == 1)
        {
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

 

해당 게임 오브젝트를 파괴하지 않는 게임 오브젝트로 설정하는 이유가 게임 내에서 단 하나만 존재하는 오브젝트를 만들고자 하는 경우라면 가장 간단하고 손쉬운 해결 방법으로는 게임 오브젝트가 생성된 직후에 실행되는 Awake() 함수에서 현재 씬에 있는 같은 오브젝트가 몇 개인지 검사를 한 뒤 2개 이상이라면 생성된 오브젝트를 파괴하는 방법이다.

 

 

해결 방법 2 : 초기화 씬 구현하기

 

 

두 번째 방법으로는 파괴하지 않는 게임 오브젝트를 생성하는 전용 씬을 만들고 그 씬에서 파괴하지 않는 게임 오브젝트를 생성한 뒤 다른 씬만 오가는 방식이다. 이 방법 게임 내에서 단 하나만 존재하는 오브젝트를 만들고자 하는 경우에 유용한 방법이다.

 

 

해결 방법 3 : 라이프 사이클 관리하기

 

파괴하지 않는 게임 오브젝트는 위의 예시에서 볼 수 있듯이 씬을 이동한다고 해서 파괴되지 않는다. 그렇기 때문에 제대로 관리하지 않는다면 생성되는 족족 Don't Destroy On Load 영역에 게임 오브젝트가 쌓일 것이다. 

 

위의 두 가지 방법은 게임 내에서 해당 오브젝트를 단 하나만 만들고자 하는 경우에 유용한 방법이다. 만약 해당 오브젝트가 파괴되지 않아야 하지만 여러 개의 오브젝트를 만드는 것을 허용하는 구조라면 위의 방법들은 적절하지 않다.

 

또한 앞선 예시들은 모두 파괴하지 않을 게임 오브젝트를 정적으로 씬에 배치를 해서 씬이 불러와질 때마다 생성되게 만드는 구조를 채용했다. 하지만 이와 반대로 같은 종류의 파괴되지 않을 게임 오브젝트를 여러 개 생성하는 것을 허용하려고 한다면 정적인 씬 배치 방식보다는 프리팹으로 관리하며 원하는 시점에 생성하는 것이 좋다.

 

 

이러한 경우에 더욱 주의해야할 점은 파괴하지 않는 게임 오브젝트들이 동적으로 여러 개 생성되어 작동하고 있으며, 이후에도 계속해서 생성된다는 점이다. 때문에 파괴하지 않은 게임 오브젝트들에 대해서 제대로 추적/관리해야 하며 해당 오브젝트의 사용이 끝나면 파괴하는 식으로 라이프 사이클(Life Cycle) 관리가 필요하다.

반응형

Tutorial (4)

 

오브젝트 다루기 기초

 

작성 기준 버전 :: 2018.3.1f1

[본 튜토리얼의 내용은 유튜브 영상으로도 시청하실 수 있습니다]

 

이번 섹션에서는 유니티 에디터에서 오브젝트를 씬에 배치하고 이동하는 등의 오브젝트를 다루는 방법에 대해서 배워보자.

 

 

게임 오브젝트(Game Object)

 

 

게임 오브젝트는 씬에 배치되는 가장 기본 단위인 오브젝트로써 기본적으로 위치, 회전, 크기를 나타내는 트랜스폼 컴포넌트만을 가진 빈 오브젝트이다. 여기에 어떤 컴포넌트가 추가되느냐에 따라 천차만별로 달라질 수 있는 백지와 같은 오브젝트이다.

 

 

빈 게임 오브젝트에 Cube 메시를 가진 메시 필터 컴포넌트와, 메시를 그리기 위한 메시 렌더러 컴포넌트, 물리적인 실체를 가지기 위한 박스 콜라이더 컴포넌트를 추가함으로써 눈에 보이는 큐브 모양을 가진 게임 오브젝트가 되었다. 이처럼 게임 오브젝트에 어떤 컴포넌트를 붙이냐에 따라서, 그 컴포넌트는 플레이어가 조종하는 캐릭터가 될 수도 있고, 플레이어의 길을 막는 장애물이 되거나 플레이어와 싸우는 몬스터가 될 수도 있는 것이다.

 

 

오브젝트 생성하기

 

첫 번째로 알아볼 것은 오브젝트를 씬에 생성하는 방법이다.

 

 

하이어라키 탭 바로 아래에 있는 Create 드롭다운 메뉴를 누르면 생성할 수 있는 오브젝트들의 종류를 볼 수 있다. Create Empty 항목을 선택하면 앞에서 본 빈 게임 오브젝트를 생성한다.

 

 

이번에는 다음 과정들을 수행하기 위해서 Create>3D Object>Cube를 선택해서 눈에 보이는 상자 오브젝트를 추가하자.

 

 

Cube를 선택하면 씬 안에 새로운 큐브가 생겨난 것을 볼 수 있다. 이 외에도 몇 가지의 3D 오브젝트들을 생성해보자.

 

 

 

 

오브젝트 선택하기

 

이번에는 오브젝트를 선택해보자.

 

단일 오브젝트 선택하기

 

씬 뷰에서 오브젝트를 클릭하면 해당 오브젝트를 선택할 수 있고 선택된 오브젝튼느 테두리에 주황색 하이라이트가 표시된다.

 

 

오브젝트를 선택할 때 주의할 점이 있는데 씬 뷰에서 오브젝트를 선택하려고 할 때, 핸드툴 모드가 활성화된 상태에서는 오브젝트를 선택할 수 없다. W E R 등을 눌러서 다른 모드로 전환한 다음에 선택하도록 한다.

 

 

단일 오브젝트를 선택하는 또 다른 방법으로는 하이어라키 창에서 선택하고자 하는 오브젝트를 클릭하는 것이다.

 

 

하이어라키 창에서 오브젝트를 선택하면 다른 오브젝트를 잘못 선택할 확률이 줄어든다는 장점이 있다. 선택하고자 하는 오브젝트를 빨리 찾고 싶다면 오브젝트의 이름을 적절하게 정해서 보기 쉽게 정리하는게 좋다.

 

 

여러 오브젝트 선택하기

 

오브젝트 하나를 선택하는 방법 이외에도 여러 오브젝트를 한꺼번에 선택하는 방법도 있다.

 

전략 시뮬레이션 게임에서 여러 유닛을 한꺼번에 선택하듯이 선택하고자하는 오브젝트 근처에서 클릭하고 드래그하면 반투명한 사각형이 표시되는데 이 사각형 영역에 선택하고자 하는 오브젝트가 들어오게 만들면 여러 오브젝트가 동시에 선택된다. 이 방법은 손쉽게 여러 오브젝트를 선택할 수 있다는 장점이 있지만, 선택하고자하는 오브젝트 가까이에 선택하지 않으려고하는 다른 오브젝트가 함께 있다면 이 역시도 같이 선택될 수 있다는 단점이 있다.

 

 

여러 오브젝트를 선택하는 다른 방법으로는 역시 하이어라키 창에서 하는 방법이 있다. Shift 키를 사용하면 첫 번째 선택한 오브젝트와 두 번째 선택한 오브젝트 사이에 있는 모든 오브젝트가 선택되고, Ctrl 키를 사용하면 떨어져 있는 오브젝트를 하나씩 선택된 오브젝트에 추가시키며 선택할 수 있다.

 

 

 

 

 

 

 

오브젝트 이동시키기

 

배치한 오브젝트를 원하는 위치에 배치하기 위해서 오브젝트를 이동시키는 방법을 알아보자.

 

기즈모로 이동시키기

 

 

상단 버튼 중에 이동 툴 버튼 선택하거나 단축키 W키를 누르면 씬 뷰에서 오브젝트를 이동시킬 수 있게 된다.

 

이동 툴을 활성화시킨 채로 오브젝트를 선택하면 세 방향으로 뻗어나가는 화살표 모양의 기즈모가 오브젝트의 중앙에 생기는 것을 볼 수 있다.

 

 

세 화살표는 각 축 방향을 의미하며, 이 화살표를 클릭하고 드래그하면 그 축의 방향으로 오브젝트를 움직이게 된다. 빨간 화살표는 X축 방향, 초록 화살표는 Y축 방향, 파란 화살표는 Z축 방향이다.

 

 

각 축 화살표 사이를 보면 작은 면들이 보인다. 이것을 클릭하고 드래그하면 해당 색상의 축만 고정하고 나머지 축 방향으로 이동한다는 뜻이다.

 

 

즉 파란 면을 잡고 움직이면 오브젝트는 Z축은 고정한 채로 X축과 Y축에서만 움직이게 된다.

 

 

트랜스폼 컴포넌트로 이동시키기

 

오브젝트를 무브 툴과 기즈모가 아닌 인스펙터 창의 트랜스폼 컴포넌트를 이용해서 이동시킬 수도 있다. 기즈모를 통해 대강의 위치로 이동시키는 것이 아니라 정확히 원하는 좌표에 오브젝트를 가져다 놓고 싶다면 인스펙터 창의 트랜스폼 컴포넌트를 이용해서 오브젝트를 이동시키는게 더 좋다.

 

 

인스펙터 창에서 오브젝트를 이동시키는 다른 방법도 있는데 포지션의 각 축 이름에 마우스 커서를 가져다 대면 커서 앞에 양방향 화살표가 뜨는데 이 때 좌클릭 드래그를 하면 그 축의 값을 조절할 수도 있다.

 

 

 

오브젝트 회전시키기

 

오브젝트를 잘 배치하기 위해서는 위치뿐만 아니라 적절하게 회전시키는 것 역시 중요하다.

 

기즈모로 회전시키기

 

 

상단 버튼 중에 회전 툴 버튼을 클릭하거나 단축키 E를 누르면 씬 뷰에서 오브젝트를 회전시킬 수 있게 된다.

 

회전 툴을 활성화시킨 채로 오브젝트를 선택하면 구형의 회전 기즈모가 생긴다.

 

 

회전 기즈모에는 흰 원 안쪽으로 빨간 원, 초록 원, 파란 원이 보이는데 이것은 그 색상의 원 평면과 직교하는 축을 기준으로 회전한다는 의미이다.

 

 

기즈모의 축을 잡고 마우스를 움직이면 그 축을 기준으로만 회전한다. 아래의 이미지를 보면 순서대로 각각 X축, Y축, Z축을 잡고 오브젝트를 회전시키고 있다.

 

 

기즈모에서 축이 아닌 영역을 잡고 움직이면 오브젝트를 자유롭게 회전시킬 수 있다.

 

 

 

트랜스폼 컴포넌트로 회전시키기

 

오브젝트 이동과 마찬가지로 오브젝트 회전 역시 트랜스폼 컴포넌트를 통해서 할 수 있다. 인스펙터 창에서 트랜스폼 컴포넌트 값 중에 Rotation 값을 원하는 만큼 수정해주면 원하는 각도만큼 정확하게 오브젝트를 회전시킬 수 있다.

 

 

Rotation 값 중에 원하는 값의 이름에 마우스 커서를 대고 좌클릭 드래그하는 것으로도 오브젝트를 회전시킬 수 있다.

 

 

 

 

 

 

오브젝트 크기 조절하기

 

오브젝트의 적절한 크기 역시 자연스러운 오브젝트 배치에 있어서 중요한 요소 중에 하나이다. 의도되지 않을 잘못된 크기의 물체는 플레이어에게 큰 위화감을 주고 몰입을 방해하기 때문이다.

 

기즈모로 크기 조절하기

 

 

상단 버튼 중에 스케일 툴을 선택하거나 단축키 R키를 누르면 오브젝트의 크기를 조절할 수 있다.

 

스케일 툴을 활성화한 채로 오브젝트를 선택하면 이동 기즈모와 같은 형태지만 끝이 직육면체 모양인 기즈모가 생성된다.

 

 

빨간 색 막대는 오브젝트를 X축 방향으로 확대/축소하며, 초록 색 막대는 Y축 방향으로, 파란색 막대는 Z축 방향으로 오브젝트를 확대/축소한다.

 

 

가운데 있는 흰 색 정육면체를 잡고 마우스를 움직이면 모든 축 방향으로 오브젝트가 커졌다가 작아진다.

 

 

트랜스폼 컴포넌트로 크기 조절하기

 

인스펙터 창에서 트랜스폼 컴포넌트의 Scale 값을 통해서 오브젝트의 크기를 정확히 원하는 크기로 조절할 수 있다.

 

 

Scale 값 중에 원하는 값의 이름에 마우스 커서를 대고 좌클릭 드래그하는 것으로도 오브젝트의 크기를 조절할 수 있다.

 

 

 

오브젝트를 다른 오브젝트의 하위 오브젝트로 만들기

 

유니티에서는 단순히 하나의 오브젝트만을 사용하는 것이 아니라 오브젝트를 다른 오브젝트의 하위 오브젝트로 만들어서 사용하는 경우가 많다. 예를 들자면 캐릭터 오브젝트의 손에 무기 오브젝트를 붙여서 캐릭터가 무기를 들게 하거나, 차량 오브젝트를 만들 때, 차량의 몸체 오브젝트와 바퀴 오브젝트를 따로 만들어서 차량 몸체 오브젝트 하위에 바퀴 오브젝트를 붙이는 등의 방식을 사용한다.

 

이렇게 하면 어떤 장점이 있냐면 만약 캐릭터가 손에 들고 있는 무기를 다른 무기로 바꾸면 손에 무기 오브젝트를 다른 무기 오브젝트로 바꾸거나, 차량이 데미지를 입거나 폭발할 때, 차량의 바퀴가 차량에서 떨어져나가는 연출 등을 사용할 수 있게 된다. 만약 무기와 캐릭터가 통짜로 된 하나의 오브젝트라면 무기를 교체할 때마다, 캐릭터의 모델링마저 교체해야 될 것이고, 차량 몸체와 바퀴가 통짜로 된 하나의 오브젝트라면 차량에서 바퀴가 떨어져나가게 하기 위해서 바퀴가 떨어져 나가는 애니메이션을 만들어야 할 것이다.

 

이렇게 오브젝트를 다른 오브젝트의 하위 오브젝트로 만드는 것은 여러 곳에서 사용될 수 있는 좋은 기법으로 어떤 곳에 사용될 수 있는지 생각해보고 많이 활용해보는 것이 좋다.

 

한 오브젝트를 다른 오브젝트의 하위 오브젝트로 만드는 방법은 아주 간단하다. 하이어라키 뷰에서 다른 오브젝트의 하위 오브젝트로 만들고자 하는 오브젝트를 드래그해서 상위 오브젝트가 되고자 하는 오브젝트에 끌어다 놓으면 된다.

 

 

이렇게 해서 상위 오브젝트가 된 오브젝트를 부모 오브젝트라고 하며, 하위 오브젝트가 된 오브젝트를 자식 오브젝트라고 한다.

 

이렇게 다른 오브젝트의 하위 오브젝트가 된 자식 오브젝트는 부모 오브젝트의 이동, 회전, 스케일의 영향을 함께 받는다.

 

 

반응형

C++ 코드 생성자에서 콘텐츠 브라우저의 클래스와 리소스 불러오기

 

작성 기준 버전 :: 4.21.1

 

게임을 제작하는 과정에서 객체를 초기화할 때, 프로젝트에 포함된 다른 클래스나 오브젝트, 리소스를 가져와야하는 경우가 종종 생긴다.

 

 

그런 경우 블루프린트의 이벤트 그래프에서 작업하는 경우라면 위의 이미지와 같이 콘텐츠 브라우저에 있는 리소스나 블루프린트 클래스 등을 곧바로 선택할 수 있지만, C++ 코드에서는 직접 경로를 지정해서 코드를 작성해야 한다.

 

단, C++ 코드에서 직접 경로를 지정해서 리소스나 블루프린트 클래스를 가져올 때, 주의할 점은 리소스나 블루프린트 클래스의 경로나 파일명이 자주 바뀌는 상황을 피하는 게 좋다. 경로를 지정한 이후에 경로가 바뀌지 않을 것이 확실하다면 C++ 코드로 경로를 지정해서 가져오는게 낫겠지만 자주 바뀌는 상황이라면 바뀐 리소스를 불러오는 모든 코드를 일일이 찾아서 수정하고 컴파일하는 문제가 발생한다.

 

그렇기 때문에, 경로나 리소스의 파일명이 자주 바뀔 상황이라면 위의 이미지처럼 블루프린트를 이용해서 초기화를 진행하거나, 별도의 기능을 만들어서 일일이 경로를 지정하고 바꾸는 작업을 자동화시키는 것이 좋다.

 

우선 C++ 코드에서 콘텐츠 브라우저의 리소스나 블루프린트 클래스를 가져오기 위해서는 다음의 헤더를 전처리기로 포함시켜주어야 한다.

 

#include "UObject/ConstructorHelpers.h"

 

ConstructorHelpers는 생성자에 도움을 주는 클래스로 생성자에서 콘텐츠 브라우저의 리소스나 블루프린트 클래스를 불러오는 작업을 도와주는 기능들을 가지고 있다. ConstructorHelpers는 생성자에서 사용되는 기능이기 때문에 생성자 이외의 장소에서 ConstructorHelpers를 사용하려고 시도하면 컴파일 에러가 발생하게 된다.

 

 

C++ 코드에서 블루프린트 클래스 가져오기

 

콘텐츠 브라우저 패널에 Blueprints 폴더 안에 TestBlueprintClass라는 이름의 APawn 클래스를 상속받은 블루프린트 클래스가 있다고 가정할 때, 그것을 C++ 코드에 가져오기 위해서는 다음 예시와 같이 코드를 작성하면 된다.

 

static ConstructorHelpers::FClassFinder<APawn> BPClass(TEXT("/Game/Blueprints/TestBlueprintClass"));
if (BPClass.Succeeded() && BPClass.Class != NULL)
{
    // 가져온 BPClass.Class를 통한 작업
}

 

FString 경로를 통해서 불러오는 것이니 만큼, 오타나 변경된 경로나 파일명으로 인해서, 클래스가 제대로 불러와지지 않는 경우가 발생할 수 있기 때문에, Succeeded() 함수와 Class의 NULL 체크를 통해서 성공적으로 클래스가 불러와졌는지 체크하고 사용해야 한다.

 

클래스 탐색자(Class Finder)는 성공적으로 블루프린트 클래스를 가져온 경우, Class 멤버 변수 안에 TSubclassOf<T> 타입으로 해당 클래스를 가지고 있게 된다. 이것을 이용해서 필요한 작업을 진행하면 된다.

 

 

C++ 코드에서 리소스 가져오기

 

이번에 알아볼 것은 C++ 코드에서 콘텐츠 브라우저 패널의 리소스를 가져오는 과정이다. 리소스의 종류는 여러가지가 될 수 있는데 대표적인 것으로는 스태틱 메시나 텍스처를 예로 들 수 있다.

 

아래의 예시코드는 드롭된 아이템의 메시가 아이템의 종류에 따라서 달라진다는 가정하에 만들어졌다. 콘텐츠 브라우저의 Item/StaticMesh 폴더 안에 SM_Helmet 이라는 이름을 가진 헬멧 모양의 스태틱 메시가 있을 때, FObjectFinder를 통해서 가져올 수 있다.

 

DropItemStaticMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("DropItemMesh"));
RootComponent = DropItemStaticMeshComponent;

 

static ConstructorHelpers::FObjectFinder<UStaticMesh> HelmetStaticMesh(TEXT("/Game/Item/StaticMesh/SM_Helmet"));
if (HelmetStaticMesh.Succeeded() && HelmetStaticMesh.Object != nullptr)
{
    DropItemStaticMeshComponent->SetStaticMesh(HelmetStaticMesh.Object);
}

 

FObjectFinder를 통해서 가져온 오브젝트 역시 Succeeded() 함수와 Object 변수의 null 체크를 통해서 리소스가 제대로 불러와졌는지 체크를 한 뒤 사용해야 한다.

 

 

 

 

C++ 코드에서 C++ 클래스 가져오기

 

C++ 코드에서 블루프린트 클래스가 아닌 직접 작성한 C++ 클래스를 가져와서 사용하고 싶을 수도 있다. 예를 들어 게임 모드 클래스에서 기본 폰이나 기본 플레이어 컨트롤러를 설정하려고 할 때, C++로 작성한 폰 클래스나 플레이어 컨트롤러 클래스를 기반으로 블루프린트 클래스를 생성해서 넣어주는게 아니라 C++ 클래스를 곧바로 코드에서 넣어주고자 한다면 다음 예시 코드와 같이 작성하면 된다.

 

AYourProjectGameMode::AYourProjectGameMode()
{
    PlayerControllerClass = AYourCustomPlayerController::StaticClass();
}

 

StaticClass() 함수를 이용하면 런타임 중에 해당 클래스를 나타내는 UClass를 얻어낼 수 있다.

반응형

오브젝트 스폰(Object Spawn)


유니티에서 게임 오브젝트를 생성할 때는, Instantiate() 함수를 사용해서 생성한다. 이것을 스폰(Spawn)이라고 한다. 하지만 이러한 스폰은 당연하게도 단순히 Instantiate() 함수만 호출해서 소환한다고 해서 자동으로 네트워크에서 스폰되고 동기화되는 것은 아니다. 평범하게 스폰된 게임 오브젝트를 네트워크에서 스폰하고 동기화하기 위해서는 유니티에서 정한 일련의 제약과 과정을 거쳐야 한다.


먼저 오브젝트가 네트워크로 스폰되기 위해서는 네트워크 매니저의 spawnPrefabs라는 리스트에 스폰 가능한 프리팹으로 등록되어 있어야 하는데 여기에 등록될 프리팹은 반드시 루트 게임 오브젝트에 NetworkIdentity 컴포넌트를 가지고 있어야 하며, Network Behaviour 스크립트는 반드시 이 Network Identity 컴포넌트와 동일한 게임 오브젝트에 있어야 한다.



정상적으로 Network Identity 컴포넌트를 가진 프리팹의 경우 Inspector 뷰에서 확인해보면 프리팹에 Asset ID가 고유한 값으로 할당되어 있는 것을 확인할 수 있다. 네트워크 매니저는 이 Asset ID를 통해 지금 네트워크 스폰하려는 오브젝트가 유효하게 spawnPrefabs에 등록되어 있는 프리팹인지 확인하고 스폰하게 된다.





이렇게 제대로 만들어진 프리팹을 spawnPrefabs 리스트에 등록하는 것은 스크립트에서도 가능하고 유니티 에디터에서도 가능하다.




using UnityEngine;
using UnityEngine.Networking;

public class SpawnObjTestNetManager : NetworkManager
{
    void Awake ()
    {
        // Resources 폴더에 있는 Character라는 이름을 가진 프리팹 파일을 가져온다.
        GameObject prefab = Resources.Load<GameObject>("Character");
        // spawnPrefabs 리스트에 스폰할 오브젝트를 추가
        spawnPrefabs.Add(prefab);
    }
}


위의 예시와 같이 spawnPrefabs에 등록된 프리팹들은 네트워크를 통해 스폰 가능해진다. 네트워크를 통해 스폰하는 예시 코드는 다음과 같다.


using UnityEngine;
using UnityEngine.Networking;

public class SpawnObjTestNetManager : NetworkManager
{

    // 서버측에서 클라이언트가 접속했을 때의 콜백
    public override void OnServerConnect(NetworkConnection conn)
    {
        base.OnServerConnect(conn);
        // spawnPrefabs에 등록된 프리팹을 스폰한다.
        GameObject charObj = Instantiate(spawnPrefabs[0]);

        // 네트워크를 통해서 이 오브젝트가 생성되었음을 클라이언트에 알린다.
        NetworkServer.Spawn(charObj);
    }
}


위의 예시들을 따라 작성하고 빌드하여 서버에 클라이언트를 접속시키면 Character 프리팹이 서버와 클라이언트 양측에 모두 소환되는 것을 확인할 수 있다.





스폰된 오브젝트는 유니티 네트워크의 스포닝 시스템이 관리하게 되며, 스포닝 시스템에 속하게 된 오브젝트가 서버에서 변화가 있으면 그것이 클라이언트에도 전송되고, 서버에서 오브젝트가 소멸하면 클라이언트에서도 소멸하게 된다. 그리고 스폰된 오브젝트는 서버가 관리하는 네트워크 오브젝트 집합에도 추가되기 때문에, 이후에 다른 클라이언트가 게임에 참여하더라도 프로그래머가 별도의 처리를 만들 필요없이 자동으로 오브젝트가 소환되고 동기화 되어야할 값들이 동기화된다.



또한 스폰된 오브젝트는 각각 서버와 클라이언트에서 동일한 "netId"라고 하는 고유한 네트워크 인스턴스 ID를 가지게 되는데, 이 ID를 통해서 각각의 오브젝트에 메시지를 보내고 식별할 수 있다.




네트워크를 통해 오브젝트가 생성되는 자세한 과정은 유니티 네트워크 문서를 통해 확인할 수 있다.


https://docs.unity3d.com/kr/current/Manual/UNetSpawning.html

반응형

+ Recent posts