odyssey

#9 odyssey 개발일지 : 동적 지형생성과 오브젝트 풀링

san10 2023. 5. 6. 23:21

(드디어) 중간고사가 끝나서 다시 스터디와 개발을 시작하려고 한다!

 

지형을 어떻게 만들지 생각해 봤는데...

미리 여러 패턴의 지형을 생성해놓고,

런타임중에 적당히 이어붙이려고 한다!

 

우선 테스트를 위해 적당한 지형 메쉬 3개를 만들어서 프리팹화 했다

Plane1, Plane2, Steep1

 

위의 세 타입의 지형중 랜덤하게 하나를 뽑아서 이어붙인다.

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

public class TerrainManager : MonoBehaviour
{
    public ObstacleGenerator obstacleGenerator;

    public List<GameObject> terrainPrefabs = new List<GameObject>();
    public List<TerrainData> terrainDatas = new List<TerrainData>();

    private TerrainType terrainType;
    private Vector3 previousTerrainEndPoint=Vector3.zero; // endPoint알아내기용
    
    public void TerrainGenerator()
    {
        TerrainType selectedTerrain = (TerrainType)UnityEngine.Random.Range(0, System.Enum.GetValues(typeof(TerrainType)).Length);

        switch (selectedTerrain)
        {
            case TerrainType.Plane1:
                {
                    Vector3 terrainPoint = previousTerrainEndPoint - terrainDatas[0].terrainStartLocalPoint;
                    GameObject Plane1 = Instantiate(terrainPrefabs[0], terrainPoint, Quaternion.identity);
                    previousTerrainEndPoint = Plane1.transform.TransformPoint(terrainDatas[0].terrainEndLocalPoint);
                }
                break;

            case TerrainType.Plane2:
                {
                    Vector3 terrainPoint = previousTerrainEndPoint - terrainDatas[1].terrainStartLocalPoint;
                    GameObject Plane2 = Instantiate(terrainPrefabs[1], terrainPoint, Quaternion.identity);
                    previousTerrainEndPoint = Plane2.transform.TransformPoint(terrainDatas[1].terrainEndLocalPoint);
                }
                break;

            case TerrainType.Steep1:
                {
                    Vector3 terrainPoint = previousTerrainEndPoint - terrainDatas[2].terrainStartLocalPoint;
                    GameObject Steep1 = Instantiate(terrainPrefabs[2], terrainPoint, Quaternion.identity);
                    previousTerrainEndPoint = Steep1.transform.TransformPoint(terrainDatas[2].terrainEndLocalPoint);
                }
                break;
        }
       

    }

TerrainData는 지형의 정보를 담고있는 SciprtableObject인데,

현재는 지형의 첫번째 포인트의 로컬 위치와 마지막 포인트의 로컬위치를 가지고 있다.

 

다음에 올 지형의 월드위치는

이전에 생성한 지형의 마지막 포인트의 월드위치 - 현재 생성할 지형의 첫번째 포인트의 로컬위치이기 때문에

마지막 포인트의 위치와 첫번째 포인트의 위치가 필요했기 때문이다..

 

 

어쨋든 TerrainGenerator를 실행시켜보면

이렇게 지형이 생성된다! ^___^

 

런타임

그리고 지형을 5초마다 생성하고 15초마다 삭제하게 만들었다!

그런데 일정시간마다 지형을 생성하니..

캐릭터가 지형 생성속도보다 너무 빠르거나 느려서

아래로 추락하는 경우가 많이 생긴다..

 

일단은 이렇게 해놓고 지형 생성 기준을 시간이 아닌 다른 요소로 해야할 것 같다.

 

오브젝트 풀링

지금은 코드를 보면 알겠지만

지형을 생성할 때마다 Instantiate를 이용하여 지형을 생성한다.

지형을 없앨때는 Destroy를 사용하는데 

이 두 함수는 상당한 비용이 든다.

 

그래서 오브젝트 풀링이란

자주 삭제되고 생성되는 객체라면 미리 메모리(오브젝트 풀)을 할당해서

필요할때 풀에서 꺼내쓰고, 다시 필요없어질때 풀에 넣어서 재사용 하는것이다.

 

그런데 막상 구현하려고 보니..

책과 인터넷의 대부분의 예제는 한 오브젝트에 대한 오브젝트 풀링을 다룬다.

그러나 지금 지형의 종류는 하나가 아니라 3개이고,

나중에는 더 추가될 것이다.

 

그래서 풀링해야할 대상이 여러개면 어떻게 해야하는지 고민을 많이 했는데..

딕셔너리를 사용해서 구현하기로 했다!

 

TerrainManager

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

public class TerrainManager : MonoBehaviour
{
    public List<Terrain> terrainPrefabs = new List<Terrain>();
    public List<TerrainData> terrainDatas = new List<TerrainData>();

    private Dictionary<TerrainType, ObjectPool<Terrain>> terrainPoolDictionary;

    void Start()
    {
        
        terrainPoolDictionary = new Dictionary<TerrainType, ObjectPool<Terrain>>();
        int i = 0;
        foreach (Terrain terrain in terrainPrefabs)
        {
            TerrainType terrainType = (TerrainType)i;
            ObjectPool<Terrain> objectPool = new ObjectPool<Terrain>(
                () => CreateTerrain(terrain, terrainType),
                OnGet,
                OnRelease,
                OnDestroy,
                maxSize: 10
                );
                                                            
            terrainPoolDictionary.Add(terrainType, objectPool);
            i++;
            
        }
        TerrainGenerator();
    }
}

우선 TerrainType을 키로 가지고 ObjectPool<T>을 값으로 가지는 terrainPoolDictionary를 만들었다

여기서 TerrainType은 지형을 구분하기 위핸 enum 이다.

 

Start부분에서 terrainPoolDictionary를 초기화 했다.

private Terrain CreateTerrain(Terrain terrain,TerrainType type)
{
    Terrain t = Instantiate(terrain);
    t.SetPool(terrainPoolDictionary[type]);
    return t;
}

private void OnGet(Terrain terrain)
{
    terrain.gameObject.SetActive(true);
}

private void OnRelease(Terrain terrain)
{
    terrain.time = 0;
    terrain.gameObject.SetActive(false);
}

private void OnDestroy(Terrain terrain)
{
    Destroy(terrain);
}

그리고 콜백을 구현했다..

 

CreateTerrain(): 각 지형을 초기화한다.

그리고 t.SetPool(terrainPoolDictionary[type]); 을 통해 인스턴스가 어느풀에 들어가야 하는지 지정한다.

 

OnGet(): 풀에서 인스턴스를 꺼낼때 호출된다.

 

OnRelease() : OnGet과는 반대로 다시 인스턴스를 풀에 넣을 때 호출한다.

 

OnDestroy() : 풀에 공간이 더이상 없을때 호출된다.

풀에 공간이 없으면 Destroy()로 파괴하게 만들었다.

public void TerrainGenerator()
{
    TerrainType selectedTerrain = (TerrainType)UnityEngine.Random.Range(0, System.Enum.GetValues(typeof(TerrainType)).Length);


    switch (selectedTerrain)
    {
        case TerrainType.Plane1:
            {
                Vector3 terrainPoint = previousTerrainEndPoint - terrainDatas[0].terrainStartLocalPoint;
                Terrain Plane1 = terrainPoolDictionary[selectedTerrain].Get();
                Plane1.transform.position = terrainPoint;
                previousTerrainEndPoint = Plane1.transform.TransformPoint(terrainDatas[0].terrainEndLocalPoint);
            }
            break;

        case TerrainType.Plane2:
            {
                Vector3 terrainPoint = previousTerrainEndPoint - terrainDatas[1].terrainStartLocalPoint;
                Terrain Plane2 = terrainPoolDictionary[selectedTerrain].Get();
                Plane2.transform.position = terrainPoint;
                previousTerrainEndPoint = Plane2.transform.TransformPoint(terrainDatas[1].terrainEndLocalPoint);
            }
            break;

        case TerrainType.Steep1:
            {
                Vector3 terrainPoint = previousTerrainEndPoint - terrainDatas[2].terrainStartLocalPoint;
                Terrain Steep1 = terrainPoolDictionary[selectedTerrain].Get();
                Steep1.transform.position = terrainPoint;
                previousTerrainEndPoint = Steep1.transform.TransformPoint(terrainDatas[2].terrainEndLocalPoint);
            }
            break;
    }

}

TerrainGenerator 부분도 수정했다.

더이상 Instantiate로 생성하지 않고, Get을 통해 오브젝트 풀에서 가져온다..

 

Terrain

using UnityEngine;
using UnityEngine.Pool;

public class Terrain : MonoBehaviour
{
    private IObjectPool<Terrain> terrainPool;
    public float time;

    public void SetPool(IObjectPool<Terrain> pool)
    {
        terrainPool = pool;
    }

    void FixedUpdate()
    {
        time += Time.deltaTime;
        if (time >= 15f)
        {
            terrainPool.Release(this);
            time = 0;
        }
    }
}

Terrain이라는 클래스도 새로 만들었다!

terrainPool은 자신이 들어가야 하는 풀이고,

15초마다 다시 풀로 돌아가게 만들었다.

 

어쨌든 이대로 실행해보면..

사실 실행해보기 전까지는 오브젝트 풀링이 잘 작동하는지 어떻게 알지?라고 생각했는데

하이라이키창에 오브젝트가 비활성화/활성화 되는게 보인다..^_____^

 

오브젝트 풀링을 이렇게 구현했고..

그런데 TerrainManager가 오브젝트 풀링을 할 필요는 없을 것 같아서

컴포넌트 분리를 하려고 하는데..

생각보다 너무 글이 길어져서 다음글에 써야겠다! ^___^