유니티/Script

Unity Isometric Chunk System

어나더원 2025. 6. 7. 13:56

 

Isometric Tilemap 에서 사용할 수 있는 청크 시스템입니다.

카메라의 거리에 따라 청크가 로드되고 언로드 되는 기능이 있습니다.

이 글은 Cell Layout을 Isometric Z as Y로 진행했습니다.


WorldManager

public class WorldManager : MonoBehaviour
{
    [SerializeField]
    private Grid grid;
    
    [SerializeField]
    private Tilemap tilemap;

    [SerializeField]
    private TileBase baseTile;
    
    /// <summary>
    /// 개별 타일 생성
    /// </summary>
    /// <param name="pos"></param>
    public void GenerateTile(Vector3Int pos)
    {
        tilemap.SetTile(pos, baseTile);
    }

    /// <summary>
    /// 청크 내 블록들의 월드 좌표를 가지고 와서 타일 생성
    /// </summary>
    /// <param name="blockMap"></param>
    public void GenerateTiles(List<Vector3Int> blockMap)
    {
        var tileArray = new TileBase[blockMap.Count];
        for (int i = 0; i < tileArray.Length; i++)
        {
            // BlockMap 내 블록들 하나 하나에 Tile을 적용
            tileArray[i] = baseTile;
        }
        tilemap.SetTiles(blockMap.ToArray(), tileArray);
    }

    public void GenerateTiles(List<Vector3Int> blockMap, TileBase tile)
    {
        var tileArray = new TileBase[blockMap.Count];
        for (int i = 0; i < tileArray.Length; i++)
        {
            tileArray[i] = tile;
        }
        tilemap.SetTiles(blockMap.ToArray(), tileArray);
    }

    /// <summary>
    /// 월드 좌표를 타일 맵 내 Cell 좌표로 변경 
    /// </summary>
    /// <param name="worldPos"></param>
    /// <returns></returns>
    public Vector3Int GetCellPos(Vector3 worldPos)
    {
        var pos = tilemap.WorldToCell(worldPos);
        // 왜 인지 모르겠는데 Isometric Z as Y는 World 0,0이
        // 5, 5 으로 나와서 정상화
        var x = pos.x - 5;
        var y = pos.y - 5;

        return new Vector3Int(x, y, pos.z);
    }
}

 

위 스크립트에서 중요한 함수는 GetCellPos 함수입니다.

해당 함수는 기존 Grid 월드 좌표를 Isometric Grid 기준 월드 좌표로 바꿔줍니다.

참고로 Cell은 Grid 내 블록 한 칸을 말하는 겁니다.

Cell 좌표

 

그리고 왜 그러는지 모르겠는데 Isometric Z as Y로 Grid를 설정해 주면 Ceil 좌표에서 +5로 나와서 x, y에 -5 해줬습니다.

제가 잘못 설정해서 그럴 수도 있으니 해당 함수 로그 출력해서 좌표가 맞는지 확인하셔야 합니다.


Chunk

public class Chunk : IEquatable<Chunk>
{
    /// <summary>
    /// 상대 좌표
    /// 청크를 구분하는 ID
    /// </summary>
    private Vector2Int _localPos;
    public Vector2Int LocalPos => _localPos;

    /// <summary>
    /// 해당 청크가 가지고 있는 블록들의 월드 좌표들
    /// </summary>
    public List<Vector3Int> ChunkWorldMap { get; set; } = new List<Vector3Int>();

    public Chunk(Vector2Int localPos)
    {
        _localPos = localPos;
    }

    public bool Equals(Chunk other)
    {
        return other is not null && _localPos.Equals(other._localPos);
    }
}

ChunkUtils

public static class ChunkUtils
{
    public const int ChunkXSize = 8, ChunkYSize = 8, ChunkZSize = 4;
    public const int HalfChunkXSize = ChunkXSize >> 1, HalfChunkYSize = ChunkYSize >> 1, HalfChunkZSize = ChunkZSize >> 1;
    
    /// <summary>
    /// 청크 상대 좌표로 변경
    /// 청크의 중심 좌표를 ChunkXSize / 2, ChunkYSize / 2 만큼 빼줘서 상대 좌표 계산
    /// </summary>
    /// <param name="worldPos">청크 월드 좌표</param>
    /// <returns></returns>
    public static Vector2Int ToChunkPosition(Vector3 worldPos)
    {
        var x = (int) worldPos.x - HalfChunkXSize;
        var y = (int) worldPos.y - HalfChunkYSize;
        return new Vector2Int(x / ChunkXSize, y / ChunkYSize);
    }

    /// <summary>
    /// 청크 월드 좌표로 변경
    /// </summary>
    /// <param name="chunkPos">청크 상대 좌표</param>
    /// <returns></returns>
    public static Vector2Int ToWorldPosition(Vector2Int chunkPos)
    {
        return new Vector2Int(
            chunkPos.x * ChunkXSize,
            chunkPos.y * ChunkYSize);
    }
}

 

현재 오브젝트의 위치를 청크의 Local 좌표로 변경해줍니다.

청크 크기에 나누기 2한 이유는 청크의 중심 위치를 오브젝트에 맞추기 위해서 나눴습니다.

위 스크립트 기준 8x8 당 1청크


Chunk Loader

private Camera _cam;

private readonly List<Chunk> _visibleChunks = new List<Chunk>();
private const int VisibleDistance = 2;

public Vector3 CameraWorldPosition => _cam.transform.position;
public Vector2Int CameraChunkPosition { get; set; } = Vector2Int.zero;

private void Update()
{
    UpdateCameraChunkPos();
    CreateVisibleChunks();
    RemoveInvisibleChunks();
}

/// <summary>
/// 카메라 월드 위치를 청크 로컬 위치로 업데이트
/// </summary>
private void UpdateCameraChunkPos()
{
    var camPos = WorldManager.Instance.GetCellPos(CameraWorldPosition);
    CameraChunkPosition = ChunkUtils.ToChunkPosition(camPos);
}

/// <summary>
/// 카메라의 현재 위치에 따라 카메라 주변 Distance의 청크 거리만큼 청크들 생성
/// </summary>
private void CreateVisibleChunks()
{
    for (int i = -VisibleDistance; i <= VisibleDistance; i++)
    {
        for (int j = -VisibleDistance; j <= VisibleDistance; j++)
        {
            var pos = CameraChunkPosition + new Vector2Int(i, j);
            var chunk = new Chunk(pos);
            
            if (!_visibleChunks.Contains(chunk))
            {
                _visibleChunks.Add(chunk);
                LoadChunk(chunk);
            }
        }
    }
}

/// <summary>
/// 거리에 따라 안 보이는 청크는 언로드
/// </summary>
private void RemoveInvisibleChunks()
{
    for (int i = _visibleChunks.Count - 1; i >= 0; i--)
    {
        if (Vector2Int.Distance(CameraChunkPosition, _visibleChunks[i].LocalPos) >= VisibleDistance + 1)
        {
            UnloadChunk(_visibleChunks[i]);
            _visibleChunks.RemoveAt(i);
        }
    }
}

public void LoadChunk(Chunk chunk)
{
    var chunkWorldMap = CreateChunkWorldMap(chunk.LocalPos);
    chunk.ChunkWorldMap = chunkWorldMap;
    WorldManager.Instance.GenerateTiles(chunkWorldMap);
}

public void UnloadChunk(Chunk chunk)
{	
	// 2번째 인수를 null로 설정하면 1번째 인수 범위의 타일들이 사라짐
    WorldManager.Instance.GenerateTiles(chunk.ChunkWorldMap, null);
}

/// <summary>
/// 청크 월드 좌표 생성
/// </summary>
/// <param name="chunkPos">청크 상대 좌표</param>
/// <returns>상대 좌표에 따라 청크 안에 존재하는 블록들의 월드 좌표 값들</returns>
private List<Vector3Int> CreateChunkWorldMap(Vector2Int chunkPos)
{
    var chunkWorldMap = new List<Vector3Int>();
    
    // 청크를 중앙에 위치시키기 위해 8x8 기준 -4 ~ 4까지 증가 후 x, y로 지정
    var x = ChunkUtils.HalfChunkXSize - ChunkUtils.ChunkXSize;
    
    for (int i = 0; x < ChunkUtils.HalfChunkXSize; x++, i++)
    {
        var y = ChunkUtils.HalfChunkYSize - ChunkUtils.ChunkYSize;
        
        for (int j = 0; y < ChunkUtils.HalfChunkYSize; y++, j++)
        {
            chunkWorldMap.Add(
                new Vector3Int(
                    chunkPos.x * ChunkUtils.ChunkXSize + x, 
                    chunkPos.y * ChunkUtils.ChunkYSize + y, 
                    0));
        }
    }
    
    return chunkWorldMap;
}

참고한 사이트

Romelian/Chunk-System: The scripts for a 2D, or 3D Chunk System for unity

 

GitHub - Romelian/Chunk-System: The scripts for a 2D, or 3D Chunk System for unity

The scripts for a 2D, or 3D Chunk System for unity - Romelian/Chunk-System

github.com