내일배움캠프 Unity 80일차 TIL - 팀 9와 4분의 3 - 개발일지

Wooooo·2024년 2월 20일
0

내일배움캠프Unity

목록 보기
82/94

[오늘의 키워드]

World 클래스와 Chunk 클래스의 맵 데이터 딕셔너리 구조 변경
Chunk 생성에 Job System 적용


[World 클래스와 Chunk 클래스의 맵 데이터 딕셔너리 구조 변경]

기존 구조

World 클래스에서 모든 블럭의 데이터를 딕셔너리로 관리
하나의 딕셔너리에 대략 70만개의 블럭 데이터가 있게 됨
-> 아무리 딕셔너리라도 검색 시 약간의 병목이 발생했다.

하지만 청크에서 블럭을 생성할 때, 메시와 맞닿아있는 블럭이 있는지 검사하는데 블럭은 정육면체니까 6번 검사를 함.
-> 70만개의 블럭이 6번 검사하니까 420만번 검색을 해야하는데, 약간의 병목이라도 420만번 발생하는 중

변경 구조

World 클래스에서는 ChunkMap 딕셔너리만 관리하도록 변경
Chunk 클래스에 자신의 범위 내에 속하는 맵 데이터를 관리하는 LocalMap 딕셔너리 추가
-> 특정 위치에 있는 블럭 데이터 검색 시, World -> ChunkMap -> Chunk -> LocalMap 순으로 검색하는 기능 추가

ChunkMap의 크기는 대략 2000개, 청크마다 존재하는 LocalMap의 크기는 대략 수 백개.
-> 이전보다 검색속도가 빨라짐

변경 코드

public class World : MonoBehaviour
{
    private Dictionary<ChunkCoord, Chunk> _chunkMap = new(comparer: new ChunkCoord());
    public Dictionary<ChunkCoord, Chunk> ChunkMap => _chunkMap;

    public WorldMapData GetBlock(Vector3Int pos)
    {
        ChunkCoord cc = ConvertChunkCoord(pos);

        if (_chunkMap.TryGetValue(cc, out var chunk))
        {
            if (chunk.LocalMap.TryGetValue(pos, out var block))
                return block;
            else
                return null;
        }
        else
            return null;
    }

    public bool CheckVoxel(Vector3 pos)
    {
        Vector3Int intPos = Vector3Int.FloorToInt(pos);

        var block = GetBlock(intPos);

        if (block == null)
            return false;
        else
            return block.type.IsSolid;
    }
}

청크 생성 시간 비교

기존 17초 83

변경 11초 64


[Chunk 생성에 Job System 적용]

청크 생성 시간을 더 단축하기 위해서 청크에 Job System을 적용

문제

청크 생성 시 참조해야하는 값이 많은데, 잡 시스템에서는 참조형의 사용이 불가능.

VoxelData 클래스의 정보값 같은 경우는 복사해서 넘겨주면 되지만, 특히 문제가 되는 부분은 메시 생성 시 맞닿아 있는 부분에 다른 블럭이 존재하는지 검사하는 기능이 World 클래스와의 참조가 불가피함.

해결

해당 메시 (face)를 그릴지 말지를 미리 계산해 배열에 담아두고, 그 배열을 Job에게 전달하기로 함
마찬가지로 메시의 텍스처 아틀라스 ID도 미리 계산하여 배열에 담아 Job에게 전달하기로 함

    List<bool> checks = new();
    List<int> textures = new();
    List<Vector3> positions = new();

    foreach (var e in _localMap.Values)
    {
        if (e.type is NormalBlockType normal)
        {
            for (int i = 0; i < 6; i++)
            {
                checks.Add(World.CheckVoxel(e.position + _data.faceChecks[i]));
                textures.Add(normal.GetTextureID(i));
            }
            positions.Add(e.position);
        }
        else if (e.type is SlideBlockType slide)
        {
            slide.AddVoxelDataToChunk(this, e.position, e.forward);
        }
    }

    var job = new ChunkJob()
    {
        checks = new(checks.ToArray(), Allocator.TempJob),
        textures = new(textures.ToArray(), Allocator.TempJob),
        positions = new(positions.ToArray(), Allocator.TempJob),
        faceIdx = 0,
        vertextIdx = 0,
        textureAtlasWidth = _data.TextureAtlasWidth,
        textureAtlasHeight = _data.TextureAtlasHeight,
        normalizeTextureAtlasWidth = _data.NormalizeTextureAtlasWidth,
        normalizeTextureAtlasHeight = _data.NormalizeTextureAtlasHeight,
        uvXBeginOffset = _data.uvXBeginOffset,
        uvXEndOffset = _data.uvXEndOffset,
        uvYBeginOffset = _data.uvYBeginOffset,
        uvYEndOffset = _data.uvYEndOffset,
        vertices = new(0, Allocator.TempJob),
        triangles = new(0, Allocator.TempJob),
        uv = new(0, Allocator.TempJob),
    };

ChunkJob.cs

[BurstCompile]
public struct ChunkJob : IJob
{
    // 입력
    public NativeArray<bool> checks;
    public NativeArray<int> textures;
    public NativeArray<Vector3> positions;
    public int faceIdx;
    public int vertextIdx;

    // 텍스처 정보
    public int textureAtlasWidth;
    public int textureAtlasHeight;
    public float normalizeTextureAtlasWidth;
    public float normalizeTextureAtlasHeight;
    public float uvXBeginOffset;
    public float uvXEndOffset;
    public float uvYBeginOffset;
    public float uvYEndOffset;

    // 출력
    public NativeList<Vector3> vertices;
    public NativeList<int> triangles;
    public NativeList<Vector2> uv;

    public void Execute()
    {
        for (int blockCount = 0; blockCount < positions.Length; blockCount++)
        {
            for (int i = 0; i < 6; i++)
            {
                if (checks[faceIdx + i])
                    continue;

                for (int j = 0; j < 4; j++)
                    vertices.Add(positions[blockCount] + VoxelLookUpTable.voxelVerts[VoxelLookUpTable.voxelTris[i][j]]);

                int textureX = textures[faceIdx + i] % textureAtlasWidth;
                int textureY = textureAtlasHeight - (textures[faceIdx + i] / textureAtlasWidth) - 1;

                if (!(textureX < 0 || textureY < 0 || textureX >= textureAtlasWidth || textureY >= textureAtlasHeight))
                {
                    float uvX = textureX * normalizeTextureAtlasWidth;
                    float uvY = textureY * normalizeTextureAtlasHeight;

                    uv.Add(new Vector2(uvX + uvXBeginOffset, uvY + uvYBeginOffset));
                    uv.Add(new Vector2(uvX + uvXBeginOffset, uvY + normalizeTextureAtlasHeight + uvYEndOffset));
                    uv.Add(new Vector2(uvX + normalizeTextureAtlasWidth + uvXEndOffset, uvY + uvYBeginOffset));
                    uv.Add(new Vector2(uvX + normalizeTextureAtlasWidth + uvXEndOffset, uvY + normalizeTextureAtlasHeight + uvYEndOffset));
                }

                triangles.Add(vertextIdx);
                triangles.Add(vertextIdx + 1);
                triangles.Add(vertextIdx + 2);
                triangles.Add(vertextIdx + 2);
                triangles.Add(vertextIdx + 1);
                triangles.Add(vertextIdx + 3);
                vertextIdx += 4;
            }
            faceIdx += 6;
        }
    }

    public (NativeList<Vector3> vertices, NativeList<int> triangles, NativeList<Vector2> uv) GetResult()
    {
        checks.Dispose();
        textures.Dispose();
        positions.Dispose();

        return (vertices, triangles, uv);
    }
}

Chunk.cs

private IEnumerator GenerateChunkJobCoroutine()
{
    //foreach(var e in _localMap.Values)
    //e.type.AddVoxelDataToChunk(this, e.position, e.forward);

    List<bool> checks = new();
    List<int> textures = new();
    List<Vector3> positions = new();

    foreach (var e in _localMap.Values)
    {
        if (e.type is NormalBlockType normal)
        {
            for (int i = 0; i < 6; i++)
            {
                checks.Add(World.CheckVoxel(e.position + _data.faceChecks[i]));
                textures.Add(normal.GetTextureID(i));
            }
            positions.Add(e.position);
        }
        else if (e.type is SlideBlockType slide)
        {
            slide.AddVoxelDataToChunk(this, e.position, e.forward);
        }
    }

    var job = new ChunkJob()
    {
        checks = new(checks.ToArray(), Allocator.TempJob),
        textures = new(textures.ToArray(), Allocator.TempJob),
        positions = new(positions.ToArray(), Allocator.TempJob),
        faceIdx = 0,
        vertextIdx = 0,
        textureAtlasWidth = _data.TextureAtlasWidth,
        textureAtlasHeight = _data.TextureAtlasHeight,
        normalizeTextureAtlasWidth = _data.NormalizeTextureAtlasWidth,
        normalizeTextureAtlasHeight = _data.NormalizeTextureAtlasHeight,
        uvXBeginOffset = _data.uvXBeginOffset,
        uvXEndOffset = _data.uvXEndOffset,
        uvYBeginOffset = _data.uvYBeginOffset,
        uvYEndOffset = _data.uvYEndOffset,
        vertices = new(0, Allocator.TempJob),
        triangles = new(0, Allocator.TempJob),
        uv = new(0, Allocator.TempJob),
    };

    var handle = job.Schedule();

    if (!handle.IsCompleted)
        yield return null;

    handle.Complete();

    var (vertices, triangles, uv) = job.GetResult();
    _vertices = new(vertices.AsArray());
    _triangles = new(triangles.AsArray());
    _uvs = new(uv.AsArray());

    vertices.Dispose();
    triangles.Dispose();
    uv.Dispose();

    CreateMesh();
}

적용 전 후 비교

아쉽게도, 에디터 상에서나 안드로이드 빌드 후 모바일 상에서나 생성 시간의 차이는 거의 없었다.
아무래도 청크 생성에 큰 비중을 차지하는 부분은 생성된 메시데이터로 메시를 생성하는 부분, 콜라이더를 생성하는 부분이고, Job으로 처리할 수 있는 부분이 대부분 간단 연산이거나 리스트 추가 정도 뿐이여서 그런 것 같다.

profile
game developer

0개의 댓글