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 내 블록 한 칸을 말하는 겁니다.
그리고 왜 그러는지 모르겠는데 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한 이유는 청크의 중심 위치를 오브젝트에 맞추기 위해서 나눴습니다.
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
'유니티 > Script' 카테고리의 다른 글
Unity Edit Mode에서 오브젝트 부서지는 효과 만들기 (0) | 2025.02.07 |
---|---|
Unity Play Mode에서 오브젝트 부서지는 효과 만들기 (0) | 2025.02.01 |
폴더를 클릭하면 Inspector 바뀌는 거 방지 (0) | 2025.01.01 |