Project_WL/Assets/WL/Scripts/Player/WaterProbe.cs

109 lines
4.6 KiB
C#

using System.Collections.Generic;
using UnityEngine;
namespace WL.Player
{
/// <summary>
/// 물 판정기 (PD #704 범위확정 1 — 수영).
///
/// 씬에 물 볼륨/트리거 콜라이더를 새로 깔지 않고, 이미 있는 물 오브젝트의 렌더러 경계만 읽어서 판정한다.
/// (실측: BigIsland 의 물 = Ocean 1개 + 강 LOD 4개 뿐이라 이 방식이 가장 싸고 씬을 건드리지 않는다)
///
/// 판정 = 발밑 좌표가 어떤 물 오브젝트의 XZ 범위 안에 있고, 그 수면보다 아래로 내려간 깊이가
/// PlayerMoveSettings.swimEnterDepth 이상이면 수영. 물이 겹치면(바다 위에 강) 더 높은 수면을 쓴다.
///
/// 셰이더 이름·판정 수치는 모두 외부화한다(코드 상수 금지 · C45).
/// </summary>
[DisallowMultipleComponent]
public class WaterProbe : MonoBehaviour
{
[Header("물 인식")]
[Tooltip("이 셰이더를 쓰는 머티리얼이 붙은 렌더러를 물로 본다")]
[SerializeField] private string waterShaderName = "Shader Graphs/ToonWaterU";
[Tooltip("비워 두면 씬 전체에서 자동으로 찾는다. 직접 지정하면 그 렌더러만 물로 본다")]
[SerializeField] private Renderer[] explicitWaterRenderers;
[Tooltip("시작할 때 찾은 물 목록을 콘솔에 남긴다")]
[SerializeField] private bool logDiscovery = true;
private readonly List<Renderer> _water = new List<Renderer>();
/// <summary>찾은 물 오브젝트 수. 0 이면 수영 판정이 절대 발생하지 않는다.</summary>
public int WaterCount { get { return _water.Count; } }
private void Awake() { Rescan(); }
/// <summary>물 오브젝트 목록을 다시 수집한다. 씬에 물을 추가·제거했다면 호출한다.</summary>
public void Rescan()
{
_water.Clear();
if (explicitWaterRenderers != null && explicitWaterRenderers.Length > 0)
{
foreach (var r in explicitWaterRenderers)
if (r != null) _water.Add(r);
}
else
{
var all = Object.FindObjectsByType<Renderer>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (var r in all)
{
var m = r.sharedMaterial;
if (m == null || m.shader == null) continue;
if (m.shader.name != waterShaderName) continue;
_water.Add(r);
}
}
if (logDiscovery)
{
var sb = new System.Text.StringBuilder();
sb.Append("[WaterProbe] 물 오브젝트 ").Append(_water.Count).Append("개 인식");
for (int i = 0; i < _water.Count; i++)
{
var b = _water[i].bounds;
sb.Append(" | ").Append(_water[i].gameObject.name)
.Append(" 수면y=").Append(b.max.y.ToString("F2"))
.Append(" XZ=").Append(b.min.x.ToString("F0")).Append("~").Append(b.max.x.ToString("F0"))
.Append(",").Append(b.min.z.ToString("F0")).Append("~").Append(b.max.z.ToString("F0"));
}
Debug.Log(sb.ToString(), this);
}
}
/// <summary>
/// 이 좌표의 XZ 를 덮는 물이 있으면 그 수면 높이를 돌려준다(겹치면 가장 높은 수면).
/// </summary>
public bool TryGetSurfaceY(Vector3 worldPos, out float surfaceY, out string zoneName)
{
surfaceY = 0f;
zoneName = null;
bool found = false;
for (int i = 0; i < _water.Count; i++)
{
var r = _water[i];
if (r == null) continue;
var b = r.bounds;
if (worldPos.x < b.min.x || worldPos.x > b.max.x) continue;
if (worldPos.z < b.min.z || worldPos.z > b.max.z) continue;
if (!found || b.max.y > surfaceY)
{
surfaceY = b.max.y;
zoneName = r.gameObject.name;
found = true;
}
}
return found;
}
/// <summary>발밑(feetY)이 수면 아래로 잠긴 깊이(m). 물이 없거나 물 위면 음수/0.</summary>
public float GetSubmergeDepth(Vector3 feetPos)
{
float surface;
string zone;
if (!TryGetSurfaceY(feetPos, out surface, out zone)) return float.NegativeInfinity;
return surface - feetPos.y;
}
}
}