Prefab 하나로 Data에 따라 Sprite나 Animation 등을 바꿔주며 다른 Object가 되도록 설정할 수 있도록 구현하기 위해 Data와 Resource를 구분하였다. 프로젝트에서 내가 맡은 부분은 Resource로 리소스를 불러오고 외부에서 리소스를 사용하라 수 있도록 했다.

 

구현 방법

소규모 프로젝트에서는 Resources.Load 방식으로 구현하는 것이 더 간단하기 때문에 모든 리소스를 Resources 폴더에 넣고 Resources.Load 또는 Resources.LoadAll 함수를 통해 리소스를 불러오는 방식을 택하였다. 

 

구현 기능

게임 시작 시 게임에 Resource 경로에서 필요한 리소스(Sprite, Prefab, JsonData, Animation)를 불러온다.

Initialize( ): 리소스를 불러올 경로를 지정하여 Dictionary에 추가한다. 현재 Sprites의 경로가 지정되어 있지 않기 때문에 후에 수정해야 한다.

Load~( ): Dictionary의 key값으로 리소스가 있는지 확인 후, 리턴한다.

Instantiate( ): 오브젝트가 풀 안에 있는지 확인 후, parent 하위에 prefab을 생성하거나 내보낸다.

Destroy( ): 필요 없는 오브젝트를 파괴한다.

public class ResourceManager : MonoBehaviour
{
    private Dictionary<string, Sprite> _sprites = new();
    private Dictionary<string, GameObject> _prefabs = new();
    private Dictionary<string, TextAsset> _jsonData = new();
    private Dictionary<string, RuntimeAnimatorController> _animControllers = new();

    public void Initialize()
    {
        Sprite[] sprites = Resources.LoadAll<Sprite>("Sprites/"); // TODO: 경로 지정.
        foreach (Sprite sprite in sprites)
        {
            _sprites.Add(sprite.name, sprite);
        }

        GameObject[] objs = Resources.LoadAll<GameObject>("Prefabs");
        foreach (GameObject obj in objs)
        {
            _prefabs.Add(obj.name, obj);
        }

        TextAsset[] texts = Resources.LoadAll<TextAsset>("JsonData");
        foreach (TextAsset t in texts)
        {
            _jsonData.Add(t.name, t);
        }

        RuntimeAnimatorController[] controllers = Resources.LoadAll<RuntimeAnimatorController>("Animations");
        foreach (RuntimeAnimatorController controller in controllers)
        {
            _animControllers.Add(controller.name, controller);
        }
    }

    // 리소스가 있는지 확인.
    public GameObject LoadPrefab(string key)
    {
        if (!_prefabs.TryGetValue(key, out GameObject prefab))
        {
            Debug.LogError($"[ResourceManager] LoadPrefab({key}): Failed to load prefab.");
            return null;
        }
        return prefab;
    }
    public Sprite LoadSprite(string key)
    {
        if (!_sprites.TryGetValue(key, out Sprite sprite))
        {
            Debug.LogError($"[ResourceManager] LoadSprite({key}): Failed to load sprite.");
            return null;
        }
        return sprite;
    }
    public TextAsset LoadJsonData(string key)
    {
        if (!_jsonData.TryGetValue(key, out TextAsset data))
        {
            Debug.LogError($"[ResourceManager] LoadJsonData({key}): Failed to load jsonData.");
            return null;
        }
        return data;
    }
    public RuntimeAnimatorController LoadAnimController(string key)
    {
        if (!_animControllers.TryGetValue(key, out RuntimeAnimatorController controller))
        {
            Debug.LogError($"[ResourceManager] LoadJsonData({key}): Failed to load animController.");
            return null;
        }
        return controller;
    }

    // 오브젝트가 풀 안에 있는지 없는지 확인 후 생성.
    public GameObject Instantiate(string key, Transform parent = null, bool pooling = false)
    {
        GameObject prefab = LoadPrefab(key);
        if (prefab == null)
        {
            Debug.LogError($"[ResourceManager] Instantiate({key}): Failed to load prefab.");
            return null;
        }

        if (pooling) return Main.Pool.Pop(prefab);

        GameObject obj = GameObject.Instantiate(prefab, parent);
        obj.name = prefab.name;
        return obj;
    }

    // 필요없는 오브젝트 파괴.
    public void Destroy(GameObject obj)
    {
        if (obj == null) return;

        if (Main.Pool.Push(obj)) return;

        Object.Destroy(obj);
    }
}

 

 

  • 아이템 오브젝트와 아이템 데이터 따로 관리하기

  • Ditionary로 관리

 

언어 호환을 한다면 아래의 유니티 기본 표로 관리하기 때문에 Key값만 있어도 된다.

 

  • 아이템 능력치

능력치별로 나누면 없어도 되는 데이터까지 가지게 되므로 Modifiers로 한 번에 묶고 Split 함수로 잘라쓴다.

csv 파일로 저장할 때, 쉼표(,)로 항목이 나뉘어 저장되므로 다른 문자로 나눈다.

'부트캠프 > Study' 카테고리의 다른 글

var, 제네릭, object  (0) 2024.02.04
Asset  (0) 2024.01.22
<Unity> 유니티 내장 오브젝트 풀(Object Pool)  (1) 2024.01.11
enable과 SetActive의 차이점  (0) 2023.12.28
<프로젝트> Unity 게임 개발 심화 개인과제  (1) 2023.12.27

Pool과 Prefab을 연동하여 구현하는 것을 오브젝트 풀이라고 한다.

오브젝트 풀 구현 방법에는 여러 가지가 있는데 오늘은 그중 유니티에 내장되어있는 오브젝트 풀에서 공부했다.

 

일단 일반적인 방법으로 Prefab 하나에 오브젝트 풀 스크립트를 붙여주는 것인데 이 방법의 단점은 Prefab을 하나 만들 때마다 오브젝트 풀 클래스를 만들어줘야하는 것이다. 그래서 유니티에서 지원하는 풀링을 이용해볼 것이다.

 

먼저 유니티에서 제공하는 Pool을 만들려면 OnCreate, OnGet, OnRelease, OnDestroy 콜백 함수 네 가지가 필요하다. 이 함수들을 IObjectPool 인터페이스를 상속하는 ObjectPool 클래스에 넣어준다. 제네릭으로 GameObject라는 자료형으로 제한한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;

public class Pool {
    private GameObject _prefab; // 어떤 프리팹과 엮일 것인가.
    private IObjectPool<GameObject> _pool; // 프리팹들을 담을 풀.
    private Transform _root; // 오브젝트가 생성될 위치.
	
    // 읽기 속성 프로퍼티 - 오브젝트가 비어있다면 생성해서 반환.
    private Transform Root {
        get {
            if (_root == null) {
                GameObject obj = new() { name = $"[Pool_Root] {_prefab.name}" };
                _root = obj.transform;
            }
            return _root;
        }
    }

	// 생성자 - MonoBehaviour를 상속받고 있지 않기 때문에 생성자로부터 초기화.
    public Pool(GameObject prefab) {
        this._prefab = prefab;
        this._pool = new ObjectPool<GameObject>(OnCreate, OnGet, OnRelease, OnDestroy);
    }

    public GameObject Pop() { // 스택, 선입후출/후입선출 형식의 자료 꺼내기.
        return _pool.Get(); // OnGet 호출.
    }

    public void Push(GameObject obj) { // 자료 들여보내기.
        _pool.Release(obj); // OnRelease 호출.
    }

	// 유니티에서 제공하는 Pool을 사용하기 위한 콜백 함수.
    #region Callbacks

    private GameObject OnCreate() {
        GameObject obj = GameObject.Instantiate(_prefab); // 오브젝트 생성.
        obj.transform.SetParent(Root); // 생성될 위치 등록.
        obj.name = _prefab.name; // 프리팹 이름 등록.
		// List.Add(obj);
        return obj;
    }
    private void OnGet(GameObject obj) { // Pool이 이미 있는 경우.
        obj.SetActive(true);
    }

    private void OnRelease(GameObject obj) { // Pool을 사용하지 않을 때.
        obj.SetActive(false);
    }

    private void OnDestroy(GameObject obj) { // Pool이 너무 많은 경우 정리.
        GameObject.Destroy(obj);
    }

    #endregion

}

 

이제 Manager를 통해 Pool을 관리해보자.

public class PoolManager {
	
    // 딕셔너리 생성.
    private Dictionary<string, Pool> _pools = new();

    public GameObject Pop(GameObject prefab) {
        // #1. 풀이 없으면 새로 만든다.
        if (_pools.ContainsKey(prefab.name) == false) {
            CreatePool(prefab);
        }

        // #2. 해당 풀에서 하나 가져온다.
        return _pools[prefab.name].Pop();
    }

    public bool Push(GameObject obj) {
        // #1. 풀이 있는지 확인한다.
        if (_pools.ContainsKey(obj.name) == false) return false;

        // #2. 풀에 게임 오브젝트를 넣는다.
        _pools[obj.name].Push(obj);

        return true;
    }

    private void CreatePool(GameObject prefab) { // 유지보수를 위해 Pool 생성을 함수로 만든다.
        Pool pool = new(prefab);
        _pools.Add(prefab.name, pool);
    }

	// 씬에 변화가 있을 때 비워주기 위한 함수.
    public void Clear() {
        _pools.Clear();
    }
}

 

테스트로 프리팹을 넣어 오브젝트 풀을 사용해보자.

public class BaseScene : MonoBehaviour {

    private bool _initialized;

    void Start() {
        Initialize();
    }

    protected virtual bool Initialize() {
        if (_initialized) return false;
        // 각종 초기화 함수.
        // DataManager 초기화
        // GameManager 초기화

        _initialized = true;
        return true;
    }

}

 

public class GameScene : BaseScene {

    public GameObject prefab1;
    public GameObject prefab2;

    protected override bool Initialize() {
        if (!base.Initialize()) return false;

        GameObject obj1 = Main.Pool.Pop(prefab1);
        GameObject obj2 = Main.Pool.Pop(prefab1);
        GameObject obj3 = Main.Pool.Pop(prefab1);
        GameObject obj4 = Main.Pool.Pop(prefab1);
        GameObject obj5 = Main.Pool.Pop(prefab1);

        Main.Pool.Push(obj2);
        Main.Pool.Push(obj4);

        return true;
    }

}

 

 

유니티 ObjectPool은 캡슐화가 잘 되어있다. 내부 로직을 몰라도 CallBack 함수 네 가지만 사용해도 ObjectPool을 이용할 수 있기 때문에 협업에 유용하다.

Manager로 관리하는 경우에도 마찬가지이다. Pool 클래스를 몰라도 생성자와 Pop, Push 함수만 알아도 손쉽게 사용할 수 있다.

추가로 그렇기 때문에 협업에서 중요한 점은 내부 로직을 몰라도 함수명만 봐도 어떤 함수인지 알 수 있도록 쉽게 이용할 수 있게 명확히 표기하는 것이 좋다.

'부트캠프 > Study' 카테고리의 다른 글

Asset  (0) 2024.01.22
<Unity> Item 관리  (0) 2024.01.12
enable과 SetActive의 차이점  (0) 2023.12.28
<프로젝트> Unity 게임 개발 심화 개인과제  (1) 2023.12.27
<Unity> 프로젝트 빌드 하는 방법(Building)  (1) 2023.12.26

팀프로젝트가 마무리 되었다. 발표를 마치고 아쉬웠던 점을 팀원분들과 이야기했다.

일단 어디서 잘못 건든건지 리듬게임에 가장 치명적인 싱크가 안맞는 문제가 있었다는게 아쉬웠다. 노트 생성이 게임이 시작하자마자 되도록 바뀌어 있어서 노트와 노트 타격하는 곳의 거리 계산이 적용되고 있지 않는데 아마 노트 클론을 리스트로 옮기면서 생긴 버그 같다.

그 외에도 씬 전환 시, 아이템 장착이나 설정 옵션이 뜨지 않는 등의 문제가 있었다.

실력이 부족하여 구현하지 못한 아쉬움도 많았지만  평생 꼭 도전해보고 싶었던 리듬게임이라는 장르를 도전하여 제작한 과정이 진심으로 즐거웠다. 나중에 최종 프로젝트가 끝나고 개인 과제를 좀더 다듬어서 다시 도전해보고 싶다.

+ Recent posts