NavMeshBuilder
라는 정적 클래스를 이용해서 런타임 중에 NavMesh를 생성/업데이트할 수 있다. 비동기 방식도 지원한다.
UnityEngine.AI
, UnityEditor.AI
두 네임스페이스에 같은 이름의 클래스가 들어있다!!!! 런타임에서 사용할 것이기 때문에 꼭 UnityEngine.AI
네임스페이스 것을 사용하자.
런타임에서 비동기로 NavMesh를 업데이트하려면 NavMeshBuilder.UpdateNavMeshDataAsync()
메서드를 사용하면 된다. 매개변수 정보는 다음과 같다.
NavMeshData
: 말 그대로 NavMeshData. position, rotation, name, sourceBounds 등 정보가 들어있다.NavMeshBuildSettings
: NavMesh를 생성/갱신할 때 사용할 설정 정보 구조체List<NavMeshBuildSource>
: NavMesh를 생성하기 위한 Source들의 리스트. (ex. mesh, terrain 등등)Bounds
: NavMesh 생성 범위를 지정하는 박스 정보어떤 매개변수가 필요한지 알았으니, 이제 매개변수를 하나하나 준비해보자.
private NavMeshData _data;
public NavMeshDataInstance NavMeshDataInstance => _instance;
private void Start()
{
_data = new NavMeshData();
_instance = NavMesh.AddNavMeshData(_data);
}
NavMeshData
는 그냥 기본 생성자로 생성해서 NavMesh
클래스에 넣어줬다.
NavMesh
클래스는 유니티에서 baked NavMesh에 접근하기 위한 싱글톤 클래스라고 한다.
NavMeshBuildSettings buildSettings = NavMesh.GetSettingsByID(0);
기본값으로 생성해줬다.
List<NavMeshBuildSource>
public void UpdateChunkSources(List<Chunk> activeChunks)
{
if (activeChunks.Count == 0)
return;
if (_sources == null)
{
_sources = new();
return;
}
_sources.Clear();
foreach (var chunk in activeChunks)
{
NavMeshBuildSource source = new()
{
shape = NavMeshBuildSourceShape.Mesh,
sourceObject = chunk.Mesh,
transform = chunk.TransformMatrix,
area = 0,
};
_sources.Add(source);
}
}
World
클래스에서 이미 현재 활성화된 청크의 리스트를 갖고 있으므로 청크가 갱신될 때마다 이 메서드를 이용해서 넘겨주고 있다.
밑의 반복문 안에서 chunk의 Mesh 정보로 NavMeshBuildSource
를 생성 중이다.
특이한 점은, tranform은 Matrix4x4
자료형이다. chunk 오브젝트의 transform.localToWorldMatrix 속성을 넘겨줬다.
private Bounds CalculateViewBounds()
{
float sizeX = _world.VoxelData.ChunkSizeX * _world.WorldData.ViewChunkRange * 2f;
float sizeY = _world.VoxelData.ChunkSizeY + 30f;
float sizeZ = _world.VoxelData.ChunkSizeZ * _world.WorldData.ViewChunkRange * 2f;
Bounds bounds = new()
{
center = _player.position,
size = Vector3.one,
extents = new Vector3(sizeX, sizeY, sizeZ),
};
return bounds;
}
현재 활성화된 Chunk를 이용해 NavMesh를 생성 중이고, Chunk의 활성화 조건은 Player의 시야 범위 내에 있을 때 이므로, Bounds도 플레이어의 시야범위 정도로 잡아줬다.
public AsyncOperation UpdateNavMesh(Action<AsyncOperation> callback = null)
{
if (_data == null || _sources == null)
return null;
var buildSettings = NavMesh.GetSettingsByID(0);
var bounds = CalculateViewBounds();
var op = NavMeshBuilder.UpdateNavMeshDataAsync(_data, buildSettings, _sources, bounds);
op.completed += callback;
return op;
}
코루틴 내에서 사용하는 방법도 만들어두기 위해 AsyncOperation
을 return하도록 했다.
또, 생성 완료 시 호출될 콜백도 매개변수로 지정해줄 수 있도록 해봤다.
AsyncOperation
을 return하도록 하면 다음과 같은 방법으로도 사용 가능하다.
private IEnumerator Start()
{
_player = Managers.Game.Player.transform;
_world = Managers.Game.World;
_data = new NavMeshData();
_instance = NavMesh.AddNavMeshData(_data);
while (IsActive)
{
yield return UpdateNavMesh();
}
}
IsActive라는 bool 타입 프로퍼티를 true로 설정하면 끊임없이 비동기 생성을 반복하는 구조로 사용 가능하다.
우리 게임은 아직 실시간으로 NavMesh의 갱신이 있다기 보다는 Player의 Chunk 이동 시 갱신이 필요한 구조라서 아직 이 방법의 사용은 보류중이다.
private IEnumerator InitializeWorldNavMeshBuilderCoroutine()
{
_navMeshBuilder = new GameObject(nameof(WorldNavMeshBuilder)).AddComponent<WorldNavMeshBuilder>();
_navMeshBuilder.IsActive = false; // 주기적으로 업데이트 X. 일단은 청크 정보가 바뀔 때마다 업데이트합니다.
UpdateChunksInViewRange();
yield return null;
yield return _navMeshBuilder.UpdateNavMesh();
}
위에서 작업한 WorldNavMeshBuilder
라는 클래스를 World
클래스에서 초기화 할 수 있도록 했다.
// 1. 리소스 로드
ResourceLoad((key, count, total) =>
{
if (count == total)
{
Managers.Game.GenerateWorldAsync((progress, argument) =>
{
// 맵 생성 진행 중 콜백
Debug.Log(progress + ": " + argument);
},
() =>
{
// 맵 생성 완료 시 콜백
// 3. 객체 생성, 초기화
SpawnPlayer();
UIInitialize();
Managers.Game.World.InitializeWorldNavMeshBuilder(); // ★
var mon = Managers.Resource.GetCache<GameObject>("Skeleton.prefab");
Instantiate(mon);
SpawnObject();
});
}
});
player의 위치정보, 활성화된 chunk의 정보가 필요해서 맵 생성, 플레이어 스폰이 완료된 후에 초기화해줘야하는 의존성이 있다.
/// <summary> 시야범위 내의 청크 생성 </summary>
public void UpdateChunksInViewRange()
{
_prevActiveChunks = _currentActiveChunks;
_currentActiveChunks = new();
for (int x = -WorldData.ViewChunkRange; x <= WorldData.ViewChunkRange; x++)
{
for (int z = -WorldData.ViewChunkRange; z <= WorldData.ViewChunkRange; z++)
{
if (_chunkMap.TryGetValue(_currentPlayerCoord + new ChunkCoord(x, z), out var currentChunk))
{
_currentActiveChunks.Add(currentChunk);
currentChunk.SetActive(true);
_prevActiveChunks.Remove(currentChunk);
// NavMesh Source 전달
_navMeshBuilder.UpdateChunkSources(_currentActiveChunks);
}
}
}
foreach (var chunk in _prevActiveChunks)
chunk.IsActive = false;
// NavMesh 갱신
_navMeshBuilder.UpdateNavMesh();
}
청크가 갱신되면 NavMeshBuilder에게 갱신을 요청한다.
프레임도 120 ~ 180 프레임이 나와서 아직은 괜찮은 것 같다.