160 lines
8.9 KiB
C#
160 lines
8.9 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// WLIslandNavMesh.cs — WL 쪽 변경(해변 걷기 콜라이더 · 상인 대역 캐릭터) 뒤 FI 섬 내비메시를 다시 굽는다 (2026-10-15)
|
|
//
|
|
// ■ 왜 — FI `IslandManager` 는 자기 변경(확장·소품)에서만 `NavMeshSurface.UpdateNavMesh` 를 부른다.
|
|
// WL 이 런타임에 깐 콜라이더·NavMeshModifierVolume 은 다음 FI 갱신까지 내비메시에 안 들어간다.
|
|
// ■ 해변 걷기(PD 「섬의 가장자리까지 조금 더 많이 이동」) — FI 에이전트 설정(경사 30° · 반지름 0.5)으로는 해변 경사(실측 28~46°)에서 끊긴다.
|
|
// 그래서 **같은 소스·같은 에이전트 종류 ID** 로 경사·반지름만 바꾼 설정(`WLIslandLookSettings.beachWalkNav*`)으로 통째로 다시 굽고 바꿔 끼운다
|
|
// (실측 16 m 섬 4~8 ms · 끼우는 동안 에이전트는 메시 위에 그대로).
|
|
// FI 가 나중에 자기 설정으로 갱신하면(확장 등) 바뀐 칸만 30°·0.5 로 돌아가므로 → FI 갱신이 끝나는 순간을 지켜보다가(`Co_Watch`) 다시 굽는다.
|
|
// ■ 소스 수집·범위는 AI Navigation 2.0.14 `NavMeshSurface` 의 비공개 `CollectSources`·`CalculateWorldBounds` 를 그대로 부른다(동작 동일 보장).
|
|
// 못 찾으면(패키지 변경) FI 기본 갱신(`UpdateNavMesh`)으로 돌아간다. FI 코드 0줄.
|
|
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using Unity.AI.Navigation;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using FIIslandManager = CryingSnow.FarmingIsland.IslandManager;
|
|
|
|
namespace WL.Island
|
|
{
|
|
public static class WLIslandNavMesh
|
|
{
|
|
// ── 진단(프로브가 읽는다)
|
|
public static int Requests, Updates, Skipped, CustomBuilds, FiReapplies;
|
|
public static string LastLog = "", LastBuildLog = "";
|
|
|
|
static bool s_pending, s_watching;
|
|
static string s_why = "";
|
|
static NavMeshData s_ours; // 마지막으로 이쪽이 구워 끼운 데이터
|
|
static MethodInfo s_collect, s_bounds;
|
|
static FieldInfo s_fiBusy; // FI IslandManager.navMeshUpdateCoroutine(비공개) — FI 갱신 진행 중이면 null 아님
|
|
static bool s_reflTried;
|
|
|
|
/// <summary>FI 섬 내비메시 다시 굽기를 예약한다(2프레임 뒤 · 같은 사이의 요청은 합친다).</summary>
|
|
public static void RequestUpdate(string why)
|
|
{
|
|
Requests++;
|
|
s_why = string.IsNullOrEmpty(s_why) ? why : s_why + " + " + why;
|
|
EnsureWatch();
|
|
if (s_pending) return;
|
|
var runner = WLIslandBridge.Runner;
|
|
if (runner == null) { Run(); return; }
|
|
s_pending = true;
|
|
runner.StartCoroutine(Co_Run());
|
|
}
|
|
|
|
static IEnumerator Co_Run()
|
|
{
|
|
yield return null;
|
|
yield return null;
|
|
s_pending = false;
|
|
Run();
|
|
}
|
|
|
|
static void Run()
|
|
{
|
|
string why = s_why; s_why = "";
|
|
var im = FIIslandManager.Instance;
|
|
var surf = im != null ? im.GetComponent<NavMeshSurface>() : null;
|
|
if (surf == null || surf.navMeshData == null) { Skipped++; LastLog = "건너뜀(FI 내비메시 첫 베이크 전) — " + why; return; }
|
|
Updates++;
|
|
if (TryCustomBuild(surf)) { LastLog = "섬 내비메시 다시 굽기 #" + Updates + " — " + why + " · " + LastBuildLog; return; }
|
|
surf.UpdateNavMesh(surf.navMeshData);
|
|
LastLog = "FI 섬 내비메시 갱신 #" + Updates + " — " + why + (string.IsNullOrEmpty(LastBuildLog) ? "" : " (" + LastBuildLog + ")");
|
|
}
|
|
|
|
static bool CustomEnabled(out float slope, out float radius)
|
|
{
|
|
var cfg = WL.Look.Farm.WLIslandLookSettings.Instance;
|
|
slope = cfg != null ? cfg.beachWalkNavSlope : 0f;
|
|
radius = cfg != null ? cfg.beachWalkNavRadius : 0f;
|
|
return cfg != null && cfg.enabled_ != 0 && cfg.beachWalkEnabled != 0 && (slope > 0f || radius > 0f);
|
|
}
|
|
|
|
static bool Reflect()
|
|
{
|
|
if (!s_reflTried)
|
|
{
|
|
s_reflTried = true;
|
|
var bf = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
s_collect = typeof(NavMeshSurface).GetMethod("CollectSources", bf, null, System.Type.EmptyTypes, null);
|
|
s_bounds = typeof(NavMeshSurface).GetMethod("CalculateWorldBounds", bf, null, new[] { typeof(List<NavMeshBuildSource>) }, null);
|
|
s_fiBusy = typeof(FIIslandManager).GetField("navMeshUpdateCoroutine", bf);
|
|
}
|
|
return s_collect != null && s_bounds != null;
|
|
}
|
|
|
|
/// <summary>같은 소스·같은 에이전트 종류로 경사·반지름만 바꿔 통째로 굽고 바꿔 끼운다. 못 하면 false(→ FI 기본 갱신).</summary>
|
|
static bool TryCustomBuild(NavMeshSurface surf)
|
|
{
|
|
float slope, radius;
|
|
if (!CustomEnabled(out slope, out radius)) { LastBuildLog = ""; return false; }
|
|
if (!Reflect()) { LastBuildLog = "다시 굽기 불가(NavMeshSurface 비공개 메서드 없음 · 패키지 변경?) → FI 기본 갱신"; return false; }
|
|
List<NavMeshBuildSource> sources;
|
|
Bounds bounds;
|
|
try
|
|
{
|
|
sources = (List<NavMeshBuildSource>)s_collect.Invoke(surf, null);
|
|
if (surf.collectObjects != CollectObjects.Volume) bounds = (Bounds)s_bounds.Invoke(surf, new object[] { sources });
|
|
else bounds = new Bounds(surf.center, new Vector3(Mathf.Abs(surf.size.x), Mathf.Abs(surf.size.y), Mathf.Abs(surf.size.z)));
|
|
}
|
|
catch (System.Exception ex) { LastBuildLog = "소스 수집 실패 — " + ex.Message + " → FI 기본 갱신"; return false; }
|
|
|
|
var st = surf.GetBuildSettings();
|
|
float slope0 = st.agentSlope, radius0 = st.agentRadius;
|
|
if (slope > 0f) st.agentSlope = slope;
|
|
if (radius > 0f) st.agentRadius = radius;
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
var data = NavMeshBuilder.BuildNavMeshData(st, sources, bounds, surf.transform.position, surf.transform.rotation);
|
|
float ms = (float)sw.Elapsed.TotalMilliseconds;
|
|
if (data == null) { LastBuildLog = "다시 굽기 실패(null) → FI 기본 갱신"; return false; }
|
|
|
|
data.name = surf.gameObject.name;
|
|
var old = surf.navMeshData;
|
|
surf.RemoveData();
|
|
surf.navMeshData = data;
|
|
surf.AddData();
|
|
if (old != null && old != data && old.GetInstanceID() < 0) Object.Destroy(old); // 런타임에 구운 옛 데이터만 버린다(에셋은 건드리지 않음)
|
|
s_ours = data;
|
|
CustomBuilds++;
|
|
LastBuildLog = "경사 " + slope0.ToString("F0") + "→" + st.agentSlope.ToString("F0") + "° · 반지름 " + radius0.ToString("F2") + "→" + st.agentRadius.ToString("F2") +
|
|
" m · 소스 " + sources.Count + " · " + ms.ToString("F1") + " ms";
|
|
return true;
|
|
}
|
|
|
|
// ── FI 가 자기 설정으로 다시 구운 뒤 → 우리 설정으로 되돌린다 ─────────────────
|
|
static void EnsureWatch()
|
|
{
|
|
if (s_watching) return;
|
|
var runner = WLIslandBridge.Runner;
|
|
if (runner == null) return;
|
|
s_watching = true;
|
|
runner.StartCoroutine(Co_Watch());
|
|
}
|
|
|
|
static IEnumerator Co_Watch()
|
|
{
|
|
bool wasBusy = false;
|
|
while (true)
|
|
{
|
|
yield return null;
|
|
var im = FIIslandManager.Instance;
|
|
if (im == null) { wasBusy = false; continue; }
|
|
float slope, radius;
|
|
if (!CustomEnabled(out slope, out radius) || !Reflect()) { wasBusy = false; continue; }
|
|
var surf = im.GetComponent<NavMeshSurface>();
|
|
if (surf == null || surf.navMeshData == null) continue;
|
|
bool busy = s_fiBusy != null && s_fiBusy.GetValue(im) != null;
|
|
if (wasBusy && !busy) { FiReapplies++; RequestUpdate("FI 갱신 뒤 재적용"); }
|
|
else if (!busy && !s_pending && s_ours != surf.navMeshData) { FiReapplies++; RequestUpdate("FI 새 내비메시"); }
|
|
wasBusy = busy;
|
|
}
|
|
}
|
|
}
|
|
}
|