[Unity] 공간 분할 기법
![[Unity] 공간 분할 기법](/_next/image?url=https%3A%2F%2Fdata.develog.develrocket.com%2Fupload%2Fdevelog%2Fuser_1777093328305%2F1780148521742-s14hdb%2F_____2026-05-30_224000.png&w=3840&q=75)
본문 로딩 중...
댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!
지금 진행중인 미니 프로젝트에서 나무, 돌, 동물과 같은 자원을 맵에 배치하는데 어느 순간부터 프레임 드랍이 심해졌다.
일단 동물에게 NavMeshAgent를 할당했고 동물들에게 각각 Idle, Roam 상태를 번갈아 변경할 수 있도록 구현했다.
맵은 큐브 형태고 200x200 큐브로 만든 후 거기에 각각 자원들일 배치했고,
그 위에 또 동물들을 약 50마리 정도 풀어놨다.

게임은 대충 이런 느낌의 컨셉(오른쪽 위에 잘 보면 토끼 있음)
아무튼 자원이나 동물 요소들을 모두 Instantiate해버리니 프레임 드랍이 심하게 발생했다.
아직 자원이랑 동물을 별로 추가하지도 않았는데 프레임 드랍이 발생한거면
나중에 다른 자원이나 동물들을 추가할 때는 아예 게임이 멈춰버리지 않을까 싶었다.
공간 분할(Spatial Partitioning)은 게임에서 맵 공간을 여러 구역으로 나눠서 필요한 것만 검사하는 최적화 기법
지금 만드는 게임도 플레이어 주변만 오브젝트를 활성화하면 되기 때문에 공간 분할 기법을 적용 시켜봤다.
using System.Collections.Generic;
using UnityEngine;
public class ResourceChunkManager : MonoBehaviour
{
public const int CHUNK_SIZE = 10;
private const int ACTIVE_RADIUS = 2;
public static ResourceChunkManager Instance { get; private set; }
public class ResourceSpawnInfo
{
public GameObject Prefab;
public Vector3 Position;
}
private class ChunkData
{
public List<ResourceSpawnInfo> PendingSpawns = new();
public List<GameObject> SpawnedObjects = new();
}
private Dictionary<Vector2Int, ChunkData> _chunks = new();
private HashSet<Vector2Int> _activeChunks = new();
private Vector2Int _lastPlayerChunk = new(int.MinValue, int.MinValue);
private Transform _resourceParent;
private Transform _playerTransform;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
// 맵 생성 시 ResourceGenerator가 호출
public void Initialize(Transform resourceParent)
{
_chunks.Clear();
_activeChunks.Clear();
_lastPlayerChunk = new Vector2Int(int.MinValue, int.MinValue);
_resourceParent = resourceParent;
}
// 스폰 정보만 등록 (Instantiate 안 함)
public void RegisterSpawnInfo(Vector3 worldPosition, GameObject prefab)
{
var chunkCoord = WorldToChunk(worldPosition);
var chunk = GetOrCreateChunk(chunkCoord);
chunk.PendingSpawns.Add(
new ResourceSpawnInfo { Prefab = prefab, Position = worldPosition }
);
}
// 자원 파괴 시 ResourceObject가 호출
public void UnregisterResource(GameObject resourceObject)
{
var chunkCoord = WorldToChunk(resourceObject.transform.position);
if (!_chunks.TryGetValue(chunkCoord, out var chunk))
return;
chunk.SpawnedObjects.Remove(resourceObject);
}
// 맵 생성 완료 후 TileMapGenerator가 호출
public void StartTracking(Transform playerTransform)
{
_playerTransform = playerTransform;
_lastPlayerChunk = new Vector2Int(int.MinValue, int.MinValue);
UpdateChunks();
}
private void Update()
{
if (_playerTransform == null)
return;
if (WorldToChunk(_playerTransform.position) == _lastPlayerChunk)
return;
UpdateChunks();
}
private void UpdateChunks()
{
var playerChunk = WorldToChunk(_playerTransform.position);
_lastPlayerChunk = playerChunk;
var newActiveChunks = new HashSet<Vector2Int>();
for (int dy = -ACTIVE_RADIUS; dy <= ACTIVE_RADIUS; dy++)
for (int dx = -ACTIVE_RADIUS; dx <= ACTIVE_RADIUS; dx++)
newActiveChunks.Add(new Vector2Int(playerChunk.x + dx, playerChunk.y + dy));
foreach (var coord in _activeChunks)
if (!newActiveChunks.Contains(coord))
DeactivateChunk(coord);
foreach (var coord in newActiveChunks)
if (!_activeChunks.Contains(coord))
ActivateChunk(coord);
_activeChunks = newActiveChunks;
}
private void ActivateChunk(Vector2Int chunkCoord)
{
if (!_chunks.TryGetValue(chunkCoord, out var chunk))
return;
if (chunk.PendingSpawns.Count > 0)
{
foreach (var info in chunk.PendingSpawns)
{
var obj = Instantiate(
info.Prefab,
info.Position,
Quaternion.identity,
_resourceParent
);
chunk.SpawnedObjects.Add(obj);
}
chunk.PendingSpawns.Clear();
}
foreach (var obj in chunk.SpawnedObjects)
if (obj != null)
obj.SetActive(true);
}
private void DeactivateChunk(Vector2Int chunkCoord)
{
if (!_chunks.TryGetValue(chunkCoord, out var chunk))
return;
foreach (var obj in chunk.SpawnedObjects)
if (obj != null)
obj.SetActive(false);
}
private Vector2Int WorldToChunk(Vector3 worldPos)
{
int chunkX = Mathf.FloorToInt(worldPos.x / CHUNK_SIZE);
int chunkZ = Mathf.FloorToInt(worldPos.z / CHUNK_SIZE);
return new Vector2Int(chunkX, chunkZ);
}
private ChunkData GetOrCreateChunk(Vector2Int coord)
{
if (!_chunks.TryGetValue(coord, out var chunk))
{
chunk = new ChunkData();
_chunks[coord] = chunk;
}
return chunk;
}
// 맵 재생성 시 호출
public void Clear()
{
_chunks.Clear();
_activeChunks.Clear();
_lastPlayerChunk = new Vector2Int(int.MinValue, int.MinValue);
_playerTransform = null;
}
}
using System.Collections.Generic;
using UnityEngine;
public class AnimalChunkManager : MonoBehaviour
{
public const int CHUNK_SIZE = 10;
private const int ACTIVE_RADIUS = 2;
public static AnimalChunkManager Instance { get; private set; }
public class AnimalSpawnInfo
{
public GameObject Prefab;
public Vector3 Position;
}
private class ChunkData
{
public List<AnimalSpawnInfo> PendingSpawns = new();
public List<Animal> SpawnedAnimals = new();
}
private Dictionary<Vector2Int, ChunkData> _chunks = new();
private HashSet<Vector2Int> _activeChunks = new();
private Vector2Int _lastPlayerChunk = new(int.MinValue, int.MinValue);
private Transform _animalParent;
private Transform _playerTransform;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
// 맵 생성 시 AnimalGenerator가 호출
public void Initialize(Transform animalParent)
{
_chunks.Clear();
_activeChunks.Clear();
_lastPlayerChunk = new Vector2Int(int.MinValue, int.MinValue);
_animalParent = animalParent;
}
// 스폰 정보만 등록 (Instantiate 안 함)
public void RegisterSpawnInfo(Vector3 worldPosition, GameObject prefab)
{
var chunkCoord = WorldToChunk(worldPosition);
var chunk = GetOrCreateChunk(chunkCoord);
chunk.PendingSpawns.Add(new AnimalSpawnInfo { Prefab = prefab, Position = worldPosition });
}
// 동물 파괴 시 Animal.Die()가 호출
public void UnregisterAnimal(Animal animal)
{
var chunkCoord = WorldToChunk(animal.transform.position);
if (!_chunks.TryGetValue(chunkCoord, out var chunk))
return;
chunk.SpawnedAnimals.Remove(animal);
}
// 동물이 청크를 이동했을 때 Animal이 호출
public void UpdateAnimalChunk(Animal animal, Vector2Int oldChunk, Vector2Int newChunk)
{
if (_chunks.TryGetValue(oldChunk, out var old))
old.SpawnedAnimals.Remove(animal);
var next = GetOrCreateChunk(newChunk);
next.SpawnedAnimals.Add(animal);
// 새 청크가 활성 범위 밖이면 바로 비활성화
if (!_activeChunks.Contains(newChunk))
{
animal.OnDeactivate();
animal.gameObject.SetActive(false);
}
}
// 맵 생성 완료 후 TileMapGenerator가 호출
public void StartTracking(Transform playerTransform)
{
_playerTransform = playerTransform;
_lastPlayerChunk = new Vector2Int(int.MinValue, int.MinValue);
UpdateChunks();
}
private void Update()
{
if (_playerTransform == null)
return;
if (WorldToChunk(_playerTransform.position) == _lastPlayerChunk)
return;
UpdateChunks();
}
private void UpdateChunks()
{
var playerChunk = WorldToChunk(_playerTransform.position);
_lastPlayerChunk = playerChunk;
var newActiveChunks = new HashSet<Vector2Int>();
for (int dy = -ACTIVE_RADIUS; dy <= ACTIVE_RADIUS; dy++)
for (int dx = -ACTIVE_RADIUS; dx <= ACTIVE_RADIUS; dx++)
newActiveChunks.Add(new Vector2Int(playerChunk.x + dx, playerChunk.y + dy));
foreach (var coord in _activeChunks)
if (!newActiveChunks.Contains(coord))
DeactivateChunk(coord);
foreach (var coord in newActiveChunks)
if (!_activeChunks.Contains(coord))
ActivateChunk(coord);
_activeChunks = newActiveChunks;
}
private void ActivateChunk(Vector2Int chunkCoord)
{
if (!_chunks.TryGetValue(chunkCoord, out var chunk))
return;
if (chunk.PendingSpawns.Count > 0)
{
foreach (var info in chunk.PendingSpawns)
{
var obj = Instantiate(
info.Prefab,
info.Position,
Quaternion.identity,
_animalParent
);
var animal = obj.GetComponent<Animal>();
if (animal != null)
chunk.SpawnedAnimals.Add(animal);
}
chunk.PendingSpawns.Clear();
}
foreach (var animal in chunk.SpawnedAnimals)
{
if (animal != null)
{
bool needsActivate = !animal.gameObject.activeSelf;
animal.gameObject.SetActive(true);
if (needsActivate)
animal.OnActivate();
}
}
}
private void DeactivateChunk(Vector2Int chunkCoord)
{
if (!_chunks.TryGetValue(chunkCoord, out var chunk))
return;
foreach (var animal in chunk.SpawnedAnimals)
{
if (animal != null)
{
animal.OnDeactivate();
animal.gameObject.SetActive(false);
}
}
}
public Vector2Int WorldToChunk(Vector3 worldPos)
{
int chunkX = Mathf.FloorToInt(worldPos.x / CHUNK_SIZE);
int chunkZ = Mathf.FloorToInt(worldPos.z / CHUNK_SIZE);
return new Vector2Int(chunkX, chunkZ);
}
private ChunkData GetOrCreateChunk(Vector2Int coord)
{
if (!_chunks.TryGetValue(coord, out var chunk))
{
chunk = new ChunkData();
_chunks[coord] = chunk;
}
return chunk;
}
// 맵 재생성 시 호출
public void Clear()
{
_chunks.Clear();
_activeChunks.Clear();
_lastPlayerChunk = new Vector2Int(int.MinValue, int.MinValue);
_playerTransform = null;
}
}
동물 청크 판단은 다른 코드에서 작성


플레이어가 이동하면 자원과 동물들이 활성화 비활성화 된다.
(토끼 수를 줄여놔서 잘 안보이지만 동물들도 다 적용된 상태입니다)