• 카테고리

    질문 & 답변
  • 세부 분야

    게임 프로그래밍

  • 해결 여부

    미해결

unitychan 프리팹에 Poolable 스크립크를 추가하면 nullreference 에러가 발생합니다.

23.04.25 08:08 작성 조회수 355

0

pool manager #3강을 듣고 있습니다.

유니티짱 프리팹에 Poolable을 삽입하고 실행하면

NullReferenceException 에러가 발생합니다.

코드는 아래와 같습니다.

답변해주시면 감사하겠습니다. ㅜㅜ

//Poolable.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Poolable : MonoBehaviour
{
    //오브젝트가 이 컴포넌트를 들고 있으면 메모리 풀링 적용

    public bool isUsing;
    //풀링이 됐는지 확인
}

 

//PoolManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolManager
{
    #region Pool
    class Pool
    {
        public GameObject Original { get; private set; }
        #region 설명
        /*이 속성은 get 접근자를 가지고 있어 Original GameObject을 가져올 수 있습니다. 또한 private set 접근자를 가지고 있어 Original GameObject을 Pool 클래스 내부에서만 설정할 수 있습니다.

private set 접근자가 있는 속성을 사용하면 값을 읽을 수는 있지만 클래스 외부에서 그 값을 변경할 수 없습니다. 이렇게 하면 Original GameObject은 Pool 클래스의 lnit() 메서드로만 설정할 수 있으므로 풀을 특정 GameObject로 초기화할 수 있습니다.*/
        #endregion
        public Transform Root { get; set; }
        #region 설명
        /*
        //이 코드는 Pool 클래스의 Root 속성을 나타냅니다. 이 속성은 해당 풀에 속한 오브젝트들을 담을 게임 오브젝트의 Transform 컴포넌트입니다. get 접근자와 set 접근자를 모두 가지고 있으므로, 해당 속성은 읽기 및 쓰기가 가능합니다. Root 속성이 null인 경우, 새로운 게임 오브젝트의 Transform 컴포넌트를 만들어 Root 속성으로 할당합니다.
        */
        #endregion
        Stack<Poolable> _poolStack = new Stack<Poolable>();
        #region 설명
        /* Pool 클래스 내부에 Stack<Poolable> 타입의 _poolStack 멤버 변수를 선언합니다. 이 스택은 풀링을 위해 사용되며, Poolable 객체의 인스턴스들을 담아 놓습니다*/
        #endregion
        public void lnit(GameObject original, int count = 5)
        #region 설명
        /*인자로 original, count를 받아서 Original, Root를 초기화합니다. Original은 Pool 클래스 내에서 사용되는 GameObject 변수이며, Root는 Pool 내에서 생성된 객체들의 부모 객체로 사용됩니다.

그리고 for문을 사용하여 count만큼 Create 함수를 호출하여 Poolable 객체들을 생성하고 Push 메서드를 호출하여 Stack에 추가합니다. Push 메서드는 Pool 클래스 내부에 구현되어 있으며, Poolable 객체를 Stack에 추가하고 비활성화합니다.
         */
        #endregion
        {
            Original = original;
            Root = new GameObject().transform;
            Root.name = $"{original.name}_Root";

            for (int i = 0; i < count; i++)
            {
                Push(Create());
            }
        }

        Poolable Create()
        #region 설명
        /*Create() 메소드는 새로운 객체를 생성하고, 이를 Poolable 컴포넌트를 추가한 뒤 반환합니다.

첫 번째로 Instantiate 메소드를 사용하여 Original 게임 오브젝트의 복사본을 만들어 go 변수에 할당합니다. 이후 go 변수의 이름을 Original 게임 오브젝트의 이름으로 변경합니다.

마지막으로 go.GetOrAddComponent<Poolable>() 메소드를 호출하여 Poolable 컴포넌트가 이미 추가되어 있으면 해당 컴포넌트를 반환하고, 추가되어 있지 않으면 새로운 Poolable 컴포넌트를 추가한 뒤 반환합니다. 이렇게 반환된 Poolable 컴포넌트가 Push 메소드에서 사용됩니다.
*/
        #endregion
        {
            GameObject go = Object.Instantiate<GameObject>(Original);
            // 복사본은 go에 저장
            go.name = Original.name;
            return go.GetOrAddComponent<Poolable>();
        }

        public void Push(Poolable poolable)
        #region 설명
        /*Push 함수는 Poolable 객체를 받아서 _poolStack 스택에 저장합니다. 이 함수는 객체 풀링의 핵심입니다.

먼저 poolable이 null인 경우, 즉 재활용할 객체가 없는 경우 함수를 바로 종료합니다.

그렇지 않으면, poolable의 transform의 parent를 Root로 설정하여 원본 객체의 하위 개체로 추가합니다. 그리고 gameObject를 비활성화하고, Poolable.isUsing 변수를 false로 설정하여 해당 객체가 사용 중이 아님을 나타냅니다. 마지막으로, poolable을 _poolStack 스택에 푸시합니다. 이렇게 되면 풀에 객체
가 추가되고, 다음에 필요할 때 재활용할 수 있습니다.*/
        #endregion
        {
            if (poolable == null)
                return;

            poolable.transform.parent = Root;
            #region 설명
            /*
transform은 Unity 게임 오브젝트의 구성 요소 중 하나로, 게임 오브젝트의 위치, 회전 및 크기를 관리합니다. transform.parent 속성은 해당 게임 오브젝트의 부모 객체의 transform을 나타냅니다.

따라서 poolable.transform.parent = Root; 코드는 poolable 게임 오브젝트의 transform의 부모를 Root로 설정하여, Root 아래에 생성된 모든 게임 오브젝트가 Root의 자식으로서 계층 구조적으로 관리되도록 합니다.*/
            #endregion
            poolable.gameObject.SetActive(false);
            #region 설명
            /* 인자로 전달된 poolable 객체의 게임 오브젝트를 비활성화합니다. 이것은 해당 객체가 풀에 반환되어 사용 가능한 상태가 되었음을 의미합니다.*/
            #endregion
            //업데이트문을 받지않고 수면상태
            poolable.isUsing = false;

            _poolStack.Push(poolable);
        }

        public Poolable Pop(Transform parent)
        #region 설명
        /*Pop 함수는 오브젝트 풀에서 사용 가능한 Poolable 객체를 꺼내서 반환하는 함수입니다. parent 매개변수는 반환된 Poolable 객체를 부모로 하는 Transform을 설정할 때 사용됩니다.

함수는 다음과 같은 동작을 수행합니다:

_poolStack 스택에서 사용 가능한 Poolable 객체를 가져옵니다.
_poolStack 스택에 사용 가능한 Poolable 객체가 없으면 Create() 함수를 호출하여 새로운 Poolable 객체를 만듭니다.
가져온 혹은 새로 만든 Poolable 객체를 활성화합니다.
parent 매개변수로 받은 Transform을 부모로 하는 Poolable 객체의 Transform을 설정합니다.
isUsing 변수를 true로 설정하여 해당 객체가 사용 중임을 나타냅니다.
Poolable 객체를 반환합니다.*/
        #endregion
        {
            Poolable poolable;

            if (_poolStack.Count > 0)
                poolable = _poolStack.Pop();
            else
                poolable = Create();

            poolable.gameObject.SetActive(true);
            poolable.transform.parent = parent;
            poolable.isUsing = true;

            return poolable;
        }

    }
    #endregion

    Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();
    #region 설명
    /*Dictionary는 key-value pair를 저장할 수 있는 C#의 데이터 구조 중 하나입니다
     이 코드는 Dictionary<string, Pool> 타입의 _pool 변수를 선언합니다. Dictionary는 key-value 쌍의 컬렉션으로, 각각의 key에 대응하는 value를 저장합니다. 여기서 key는 string 타입이고 value는 Pool 타입입니다. 따라서 _pool 변수는 string 타입의 key와 Pool 타입의 value를 가지는 Dictionary입니다. 이 변수는 나중에 게임 오브젝트를 풀링할 때 사용됩니다.*/
    #endregion

    //다른곳에 기생 리소스 매니저 보조

    Transform _root;
    #region 설명
    /*_root 라는 이름의 Transform 타입 변수를 선언하고 있습니다. Transform은 Unity Engine에서 게임 오브젝트의 위치, 회전, 크기 등의 정보를 담고 있는 컴포넌트입니다. _root 변수는 풀링된 객체들이 모여있는 루트 오브젝트를 가리키는 역할을 합니다. 이 변수는 lnit() 함수에서 생성됩니다.*/
    #endregion
    //트렌스폼은 게임 오브젝트와 연결

    public void lnit()
    #region 설명
    /*lnit 함수는 Pool 클래스가 생성될 때 호출되며, Pool 클래스 내부에서 사용되는 변수인 _root를 초기화합니다.

_root는 Pool 클래스에서 생성된 모든 객체들이 생성될 때 사용되는 부모 객체입니다.

먼저 if문을 사용하여 _root가 null인지 확인합니다. null이라면, _root가 존재하지 않으므로 새로운 GameObject를 생성하고 _root 변수에 할당합니다.

GameObject가 생성되면 해당 GameObject의 이름을 "@Pool_Root"으로 설정하고, Object.DontDestroyOnLoad 함수를 사용하여 게임 오브젝트를 씬 전환시 파괴되지 않도록 합니다.

이렇게 함으로써, Pool 클래스는 게임이 실행되는 동안 계속해서 _root 객체를 사용할 수 있으며, 필요한 경우 Poolable 객체들을 _root 객체의 하위에 위치시켜 관리할 수 있습니다.*/
    #endregion
    {
        if (_root == null)
        {
            _root = new GameObject { name = "@Pool_Root" }.transform;
            Object.DontDestroyOnLoad(_root);
        }
        //게임이 실행된이후로 영구적으로 존재
        //풀링이 필요하면 pool_root 산하로 이동해서 보관
    }

    public void CreatePool(GameObject original, int count = 5)
    #region 설명
    /*이 함수는 GameObject을 이용해서 새로운 객체 풀을 생성하는 역할을 합니다. 함수 인자로 original과 count를 받습니다. original은 객체 풀링을 위해 복제할 원본 게임 오브젝트를 의미합니다. count는 생성할 초기 오브젝트 개수를 나타냅니다.

함수 내부에서는 Pool 객체를 생성하고 lnit() 메소드를 이용해 생성한 original과 count를 초기화합니다. 그리고 이 Pool 객체의 Root 프로퍼티를 _root의 자식으로 설정합니다. 마지막으로, _pool 딕셔너리에 original의 이름과 pool을 추가합니다.

따라서, CreatePool() 메소드를 호출하여 객체 풀을 생성하면 해당 원본 게임 오브젝트를 original로 하는 Pool 객체가 생성되고, 이 Pool 객체는 _root 아래에 생성된 후, _pool 딕셔너리에 추가됩니다. 이렇게 생성된 객체 풀은 나중에 필요할 때 GetPool() 메소드를 이용하여 언제든지 접근할 수 있습니다.*/
    #endregion
    {
        Pool pool = new Pool();
        pool.lnit(original, count);
        pool.Root.parent = _root;
        //루트를 pool_root에 연결

        _pool.Add(original.name, pool);

    }
    public void Push(Poolable poolable)
    #region 설명
    /*위 코드는 입력받은 Poolable 객체를 다시 풀에 반환하는 메서드입니다. 입력받은 Poolable 객체의 GameObject 이름을 키로 사용하여, 해당 GameObject의 프리팹이 풀에 존재하는지 검사합니다.

ContainsKey 메서드는 Dictionary 클래스에 있는 메서드로, 지정된 키가 Dictionary 컬렉션에 포함되어 있는지 여부를 확인합니다. 여기서는 _pool 딕셔너리에 입력받은 Poolable 객체의 GameObject 이름이 존재하지 않으면, 해당 객체의 게임 오브젝트를 파괴하고 메서드를 종료합니다.

만약 프리팹이 존재하면 해당 객체를 다시 Push 메서드를 이용하여 해당 프리팹의 Pool 객체에 반환합니다.*/
    #endregion
    //풀에다가 푸시하는 함수
    //다 사용한다음에 반환하는 작업
    {
        string name = poolable.gameObject.name;
        if (_pool.ContainsKey(name) == false)
        #region 설명
        /*ContainsKey는 Dictionary<TKey, TValue>에 키가 포함되어 있는지 여부를 반환하는 메서드입니다. 키가 포함되어 있으면 true를, 그렇지 않으면 false를 반환합니다.

예를 들어, Dictionary<string, int> 인스턴스 dict가 있다고 가정해보겠습니다. 이 경우 dict.ContainsKey("apple")은 dict에 "apple"이라는 키가 포함되어 있는지 확인하고, 있으면 true를, 없으면 false를 반환합니다.*/
        #endregion
        {
            GameObject.Destroy(poolable.gameObject);
            return;
        }

        _pool[name].Push(poolable);
    }
    public Poolable Pop(GameObject original, Transform parent = null)
    #region 설명
    /*해당 함수는 풀에서 객체를 꺼내는 역할을 수행하는 함수입니다. 먼저, _pool이 해당 original 객체의 키를 가지고 있는지 확인합니다. 만약 해당 original 객체의 키가 없으면, CreatePool 함수를 통해 새로운 풀을 생성합니다.

그러나 해당 함수에서는 객체를 반환하지 않고, null을 반환하고 있습니다. 객체를 반환하기 위해서는 Pool 클래스의 Pop 함수를 호출해야 합니다. 또한, Transform parent 인자를 활용하여, 해당 객체를 생성한 후, 지정된 부모 객체의 자식으로 추가할 수 있습니다.*/
    #endregion
    // 꺼내는 함수
    // parent는 없을 수 도 있다고 체크함.
    {
        if (_pool.ContainsKey(original.name) == false)
            //풀에 오리지날 네임이 있는지 확인
            CreatePool(original);

        return null;
    }

    public GameObject GetOriginal(string name)
    #region 설명
    /*이 코드는 이름이 name인 GameObject가 포함된 풀의 Original GameObject를 가져오는 함수입니다.

name: 가져오려는 GameObject의 이름
_pool Dictionary에 name을 key로 가지는 Pool이 있는지 검사합니다. 없다면 null을 반환합니다. 있다면 해당 Pool의 Original GameObject를 반환합니다. 이를 통해 필요한 GameObject를 재사용할 수 있습니다.*/
    #endregion
    {
        if (_pool.ContainsKey(name) == false)
            return null;
        return _pool[name].Original;
    }

    //@@신이동할때 캐시를 날린다.

    public void Clear()
    {
        foreach (Transform child in _root)
            #region 설명
            /* _root의 자식들에 대해 반복문을 실행하는 코드입니다. _root는 Transform 타입의 변수이며, 생성한 Pool 객체들의 Root Transform을 담고 있는 부모 객체입니다. 즉, _root의 자식들은 Pool 객체의 Root Transform입니다. 따라서 이 코드는 생성한 모든 Pool 객체들의 Root Transform에 대해 반복문을 실행합니다.*/
            #endregion
            //루트 산하에 접근
            GameObject.Destroy(child.gameObject);
        _pool.Clear();
        //풀 초기화
    }
}

 

 

답변 1

답변을 작성해보세요.

0

NULL 크래시는 꼭꼭꼭꼭꼭꼭꼭 스스로 해결하는 연습을 평상시에 하셔야 합니다.
NULL 크래시가 난 부분에 breakpoint를 걸어보고,
유니티에 연결해서 call stack을 살펴보면서 무엇이 문제인지 해결해보시기 바랍니다.

Eunu Sa님의 프로필

Eunu Sa

질문자

2023.04.25

ㅜㅜ 정말 죄송합니다....

디버깅을 하면서 어떤부분을 확인해야되는지 모르겠습니다....

콜스택을 살펴보는데 unitychan 프리펩 경로도 확인했고 스펠링도 확인했습니다. ㅜㅜ

어느부분을 체크해야되는지 알려주시면 감사하겠습니다.!!

imageimage

전체 프로젝트 압축해서 rookiss@naver.com 로 보내주시기 바랍니다.