Files
2026-02-27 17:44:21 +08:00

316 lines
13 KiB
C#

// ╔════════════════════════════════════════════════════════════════╗
// ║ Copyright © 2025 NWH Coding d.o.o. All rights reserved. ║
// ║ Licensed under Unity Asset Store Terms of Service: ║
// ║ https://unity.com/legal/as-terms ║
// ║ Use permitted only in compliance with the License. ║
// ║ Distributed "AS IS", without warranty of any kind. ║
// ╚════════════════════════════════════════════════════════════════╝
#region
using NWH.DWP2.WaterObjects;
using UnityEngine;
#endregion
namespace NWH.DWP2.WaterData
{
/// <summary>
/// Base class for providing water surface data to WaterObjects.
/// Implementations provide water height, flow velocity, and surface normal information.
/// Uses a trigger collider to automatically detect and register WaterObjects that enter the water.
/// Inherit from this class to create adapters for different water systems.
/// </summary>
public abstract class WaterDataProvider : MonoBehaviour
{
protected float[] _singleHeightArray;
protected Vector3[] _singlePointArray;
protected Collider _triggerCollider;
public virtual void Awake()
{
_singleHeightArray = new float[1];
_singlePointArray = new Vector3[1];
_triggerCollider = GetComponent<Collider>();
if (_triggerCollider == null)
{
// Debug.LogWarning("WaterDataProvider requires a Collider with 'Is Trigger' ticked to be present " +
// "on the same GameObject to act as a trigger volume. Creating one.");
_triggerCollider = gameObject.AddComponent<SphereCollider>();
_triggerCollider.isTrigger = true;
((SphereCollider)_triggerCollider).radius = 1000000f;
}
if (!_triggerCollider.isTrigger)
{
Debug.LogWarning("WaterDataProvider Collider has to have 'Is Trigger' ticked. Fixing.");
_triggerCollider.isTrigger = true;
}
}
private void OnDrawGizmos()
{
Gizmos.color = Color.green;
Gizmos.DrawSphere(transform.position, 0.1f);
}
private void OnTriggerEnter(Collider other)
{
// Assign the current water data provider
Rigidbody targetRigidbody = other.attachedRigidbody;
if (targetRigidbody != null)
{
WaterObject[] targetWaterObjects = targetRigidbody.GetComponentsInChildren<WaterObject>();
for (int i = 0; i < targetWaterObjects.Length; i++)
{
targetWaterObjects[i].OnEnterWaterDataProvider(this);
}
}
}
private void OnTriggerExit(Collider other)
{
// Assign the current water data provider
Rigidbody targetRigidbody = other.attachedRigidbody;
if (targetRigidbody != null)
{
WaterObject[] targetWaterObjects = targetRigidbody.GetComponentsInChildren<WaterObject>();
for (int i = 0; i < targetWaterObjects.Length; i++)
{
targetWaterObjects[i].OnExitWaterDataProvider(this);
}
}
}
/// <summary>
/// Does this water system support water height queries?
/// </summary>
/// <returns> True if it does, false if it does not. </returns>
public abstract bool SupportsWaterHeightQueries();
/// <summary>
/// Does this water system support water normal queries?
/// </summary>
/// <returns> True if it does, false if it does not. </returns>
public abstract bool SupportsWaterNormalQueries();
/// <summary>
/// Does this water system support water velocity queries?
/// </summary>
/// <returns> True if it does, false if it does not. </returns>
public abstract bool SupportsWaterFlowQueries();
/// <summary>
/// Returns water height at each given point.
/// Override this method to provide water height data from your water system.
/// </summary>
/// <param name="waterObject">WaterObject requesting the data.</param>
/// <param name="points">Position array in world coordinates.</param>
/// <param name="waterHeights">Water height array in world coordinates. Corresponds to positions.</param>
public virtual void GetWaterHeights(WaterObject waterObject, ref Vector3[] points, ref float[] waterHeights)
{
// Do nothing. This will use the initial values of water heights (0).
}
/// <summary>
/// Returns water flow velocity at each given point.
/// Override this method to provide water flow data from your water system.
/// Flow should be in world space and relative to the world, not the WaterObject.
/// </summary>
/// <param name="waterObject">WaterObject requesting the data.</param>
/// <param name="points">Position array in world coordinates.</param>
/// <param name="waterFlows">Water flow velocity array in world coordinates. Corresponds to positions.</param>
public virtual void GetWaterFlows(WaterObject waterObject, ref Vector3[] points, ref Vector3[] waterFlows)
{
// Do nothing. This will use the initial values of water velocities (0,0,0).
}
/// <summary>
/// Returns water surface normals at each given point.
/// Override this method to provide water normal data from your water system.
/// </summary>
/// <param name="waterObject">WaterObject requesting the data.</param>
/// <param name="points">Position array in world coordinates.</param>
/// <param name="waterNormals">Water surface normal array in world coordinates. Corresponds to positions.</param>
public virtual void GetWaterNormals(WaterObject waterObject, ref Vector3[] points, ref Vector3[] waterNormals)
{
// Do nothing. This will use the initial values of water normals (0,0,0).
}
/// <summary>
/// Queries water data based on enabled flags.
/// Calls the appropriate getter methods based on which data types are requested.
/// </summary>
/// <param name="waterObject">WaterObject requesting the data.</param>
/// <param name="points">Position array in world coordinates.</param>
/// <param name="waterHeights">Output water heights array.</param>
/// <param name="waterFlows">Output water flows array.</param>
/// <param name="waterNormals">Output water normals array.</param>
/// <param name="useWaterHeight">Should water heights be queried.</param>
/// <param name="useWaterNormals">Should water normals be queried.</param>
/// <param name="useWaterFlow">Should water flows be queried.</param>
public void GetWaterHeightsFlowsNormals(WaterObject waterObject, ref Vector3[] points, ref float[] waterHeights,
ref Vector3[] waterFlows, ref Vector3[] waterNormals, bool useWaterHeight, bool useWaterNormals,
bool useWaterFlow)
{
if (useWaterHeight)
{
GetWaterHeights(waterObject, ref points, ref waterHeights);
}
if (useWaterFlow && SupportsWaterFlowQueries())
{
GetWaterFlows(waterObject, ref points, ref waterFlows);
}
if (useWaterNormals && SupportsWaterNormalQueries())
{
GetWaterNormals(waterObject, ref points, ref waterNormals);
}
}
/// <summary>
/// Returns water height at a single point.
/// Less efficient than batch queries but useful for one-off checks.
/// </summary>
/// <param name="waterObject">WaterObject requesting the data.</param>
/// <param name="point">Position in world coordinates.</param>
/// <returns>Water height at the given point.</returns>
public virtual float GetWaterHeightSingle(WaterObject waterObject, Vector3 point)
{
_singlePointArray[0] = point;
GetWaterHeights(waterObject, ref _singlePointArray, ref _singleHeightArray);
return _singleHeightArray[0];
}
/// <summary>
/// Checks if a point is underwater.
/// Accounts for wave height when water normals are supported.
/// </summary>
/// <param name="waterObject">WaterObject making the query.</param>
/// <param name="worldPoint">Position to check in world coordinates.</param>
/// <returns>True if the point is below the water surface.</returns>
public bool PointInWater(WaterObject waterObject, Vector3 worldPoint)
{
return GetWaterHeight(waterObject, worldPoint) > worldPoint.y;
}
/// <summary>
/// Returns water height at the given world position.
/// </summary>
/// <param name="waterObject">WaterObject making the query.</param>
/// <param name="worldPoint">Position in world coordinates.</param>
/// <returns>Water surface height at the given point.</returns>
public float GetWaterHeight(WaterObject waterObject, Vector3 worldPoint)
{
return GetWaterHeightSingle(waterObject, worldPoint);
}
}
}
#if UNITY_EDITOR
namespace NWH.DWP2.WaterData
{
using NWH.NUI;
using UnityEditor;
/// <summary>
/// Base editor for WaterDataProvider implementations.
/// Draws common UI elements (capabilities, trigger volume) and provides virtual methods for derived editors.
/// </summary>
public class WaterDataProviderEditor : DWP2NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI()) return false;
var provider = (WaterDataProvider)target;
DrawStatus(provider);
DrawTriggerVolume(provider);
DrawCapabilities(provider);
DrawSettings(provider);
drawer.EndEditor(this);
return true;
}
/// <summary>
/// Override to draw provider-specific status information.
/// </summary>
protected virtual void DrawStatus(WaterDataProvider provider)
{
}
/// <summary>
/// Draws the trigger volume information.
/// </summary>
protected virtual void DrawTriggerVolume(WaterDataProvider provider)
{
drawer.BeginSubsection("Trigger Volume");
var collider = provider.GetComponent<UnityEngine.Collider>();
if (collider != null)
{
string colliderType = collider.GetType().Name;
string size = collider switch
{
UnityEngine.BoxCollider box => $"{box.size.x:F1} x {box.size.y:F1} x {box.size.z:F1}",
UnityEngine.SphereCollider sphere => $"Radius: {sphere.radius:F1}",
UnityEngine.CapsuleCollider capsule => $"Radius: {capsule.radius:F1}, Height: {capsule.height:F1}",
UnityEngine.MeshCollider mesh => mesh.sharedMesh != null ? mesh.sharedMesh.name : "No mesh",
_ => ""
};
drawer.Info($"{colliderType}{(size.Length > 0 ? $" ({size})" : "")}");
if (!collider.isTrigger)
{
drawer.Info("Collider is not set as trigger!", MessageType.Warning);
}
}
else
{
drawer.Info("No collider found. A large sphere trigger will be auto-created at runtime.", MessageType.Warning);
}
drawer.EndSubsection();
}
/// <summary>
/// Draws the capabilities section showing supported query types.
/// </summary>
protected virtual void DrawCapabilities(WaterDataProvider provider)
{
drawer.BeginSubsection("Capabilities");
drawer.Label($"Water Height: {(provider.SupportsWaterHeightQueries() ? "Yes" : "No")}");
drawer.Label($"Water Normal: {(provider.SupportsWaterNormalQueries() ? "Yes" : "No")}");
drawer.Label($"Water Flow: {(provider.SupportsWaterFlowQueries() ? "Yes" : "No")}");
drawer.EndSubsection();
}
/// <summary>
/// Override to draw provider-specific settings.
/// </summary>
protected virtual void DrawSettings(WaterDataProvider provider)
{
}
public override bool UseDefaultMargins() => false;
}
}
#endif