마인크래프트 짭겜 개발노트 #4 "블록"

개발노트

2018. 10. 27. 13:42

이전 글에서는 너무 많은 버텍스가 문제였다면

이번에는 너무 많은 블록이 문제다.


블록이 담고있는 정보가 int 형 변수 하나(4bytes) 라고 가정해 보자.

그러면 청크 하나 당 4 x 2^16 = 2^18 = 256KB 의 메모리가 필요하고,

청크가 20개 로드되어 있으면 약 5MB 정도 필요하다.


그런데 여기에 변수가 몇 개 더 추가되고 청크가 한 번에 몇 백개씩 로드되기 시작하면 무시할 수 없는 크기가 될 것이다.


그리고 처리 속도에도 문제가 생기는데,

청크를 생성할 때 한 번에 많은 양의 블록 인스턴스를 생성 하거나, 청크를 저장하기 위해 직렬화할 때 상당히 느려진다.

생성해야 할 인스턴스 수가 만 단위가 되기 때문에 new 연산자의 무거움을 직접 체감해 볼 수 있다.


이 문제들을 마인크래프트에서는 어떻게 해결하고 있냐하면

1. 먼저 게임이 시작되면 필요한 모든 블록 인스턴스들을 1개씩 생성해 놓는다.

2. 청크는 각 좌표마다 블록 인스턴스를 생성하지 않고, 대신 미리 생성해 놓은 블록의 레퍼런스를 저장한다.



실제로 메모리에 올라와 있는 블록 인스턴스는 1개 뿐이고 청크는 이 인스턴스를 참조만 할 뿐이다.



각 좌표마다 서로 다른 블록 인스턴스가 있는 게 아니라 블록은 1개만 존재하므로 메모리를 획기적으로 절약할 수 있고

청크를 생성할 때 레퍼런스를 복사만 하므로 처리 속도도 획기적으로 빨라진다.


// Blocks Class

public static void Init()
{
    AIR                     = RegisterBlock(0, "air", new BlockAir());
    KEEPOUT                 = RegisterBlock(1, "keepout", new BlockKeepOut());
    WEED                    = RegisterBlock(2, "weed", new BlockWeed());
    TALLWEED                = RegisterBlock(3, "tallweed", new BlockTallWeed());
    APPLE                   = RegisterBlock(4, "apple", new BlockApple());
    ORANGE                  = RegisterBlock(5, "orange", new BlockOrange());
    DIRT                    = RegisterBlock(6, "dirt", new BlockDirt());
    GRASS                   = RegisterBlock(7, "grass", new BlockGrass());
 
    // ...
}

초기화 단계에서 이렇게 블록을 준비해 놓은 다음,


// Biome Class

public override void PaintTerrain(ChunkBundlePrimer primer, ChunkMap chunkMap, ChunkInfo chunkInfo, int seed, System.Random random, NoiseDictionary noises)
{
    Vector2i chunkCoord = primer.coord;
    FastNoise terrainNoise = noises["Terrain"];
    FastNoise veinNoise = noises["Vein"];
 
    for (int x = 0; x < Chunk.SIZE; ++x)
    {
        for (int z = 0; z < Chunk.SIZE; ++z)
        {
            int i = chunkCoord.x * Chunk.SIZE + x;
            int j = chunkCoord.y * Chunk.SIZE + z;
            int depth = 0;
 
            for (int y = Chunk.SIZE * ChunkBundle.SIZE - 1; y >= 0; --y)
            {
                Block b = primer.GetBlock(x, y, z);
 
                if (b == Blocks.AIR || b == Blocks.KEEPOUT)
                {
                    depth = -1;
                }
                else if (b == baseBlock)
                {
                    ++depth;
 
                    // 최외 표면
                    if (depth == 0)
                    {
                        primer.SetBlock(x, y, z, Blocks.GRASS);
                    }
                    // 표면부
                    else if (depth < 4)
                    {
                        primer.SetBlock(x, y, z, Blocks.DIRT);
                    }
                }
            }
        }
    }
}

블록을 배치할 때에는 준비 해 둔 블록 인스턴스를 참조하여 사용한다.

SetBlock(x, y, z, new BlockDirt()); 가 아닌, SetBlock(x, y, z, Blocks.DIRT); 라고 작성하는 식이다.


이러한 구조는 블록이 자신의 상태를 저장해 놓을 수 없다는 단점이 있다.

마인크래프트에서 상자 블록 두 개는 내용물이 서로 다르다. 하지만 위에서 설명한 방식대로라면 이것을 표현할 방법이 없다.

상자 블록은 하나만 존재하고, 인벤토리도 하나 뿐이기 때문이다.


그래서 마인크래프트는 자신의 상태를 저장해야 하는 일부 특수한 블록들을 위해 TileEntity 라는 클래스를 두고 있다.

TileEntity 를 사용하는 블록이 청크에 배치되면 새 TileEntity 인스턴스가 생성되고 블록들은 거기에 자신의 상태를 기록한다.