World 클래스와 Chunk 클래스의 맵 데이터 딕셔너리 구조 변경
Chunk 생성에 Job System 적용
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
청크 생성 시간을 더 단축하기 위해서 청크에 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),
};
[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);
}
}
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으로 처리할 수 있는 부분이 대부분 간단 연산이거나 리스트 추가 정도 뿐이여서 그런 것 같다.