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);
    }
}

 

 

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

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

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

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

노트 최적화

오버랩은 레이어 마스크로 지정된 Collider를 찾는 메서드이다. IsTrigger를 사용할 경우, 노트가 너무 빠르면 감지가 안되는 경우가 있기 때문에 최적화를 위해  처음엔 RigidBody 대신 오버랩(Overlap)을 사용하려했다.

그런데 아예 노트와 노트 판정하는 위치의Transform 값을 받아와서 비교하면  Collider를 사용하지 않고 할 수 있었다.

 

노트를 리스트에 추가하기 위해 TimingManager 스크립트와 GetComponent로 연결하는데 연결이 되지 않고 null 값이 떴다. GetComponent는 자기 자신을 가져오는데 Note 스크립트와 TimingManager 스크립트가 같은 오브젝트에 있지 않았기 때문이었다.

Find 메서드를 사용하여 해결하였다.

public class Note : MonoBehaviour
{
    [SerializeField] private NoteMove noteMove;
    public KeyCode key;
    private Color color;
    TimingManager timingManager;
   

    private void Awake()
    {
        color = GetComponent<SpriteRenderer>().color;
        timingManager = FindObjectOfType<TimingManager>();

    }

    public void NoteCreate()
    {
        NoteMove note = Instantiate(noteMove, transform.position, Quaternion.identity);
        note.GetComponent<SpriteRenderer>().color = color;
        note.noteSpeed = PlaySetting.speed;
        
        timingManager.boxNoteList.Add(note.gameObject);
    }
}

 

트러블 슈팅

게임 플레이 도중 일시정지하거나 재시작 하는 기능을 추가하기로 했는데 노래 선택해서 플레이 하면서 일시정지 후, 게임을 나갔다가 다시 들어오면 게임 플레이 화면이 로딩되지 않는 현상이 있었다.

일시정지 기능을 Time.timeScale = 0으로 구현했는데 그 후에 다시 시간이 흐르도록 처리하지 않아서 생긴 문제였다.

 

VideoPlayer가 안보일 때

리듬 게임을 재생하면 노트 UI 뒷부분에서 비디오가 재생되도록 했는데 안보인다.

Video Player 설정을 보니 Render Mode가 Render Texture로 되어있었다.

Render Texture는 오브젝트의 텍스쳐에 붙여주어야하기 때문에 타겟 오브젝트가 존재해야한다.

 

2D게임이기 때문에 그냥 메인 카메라에서 가장 멀리 보이는 화면으로 설정했다.

 

잘 보인당ㅍvㅍ

+ Recent posts