Component that emits water ripples when the GameObject crosses into water or while moving across the surface. Samples a point offset from the object, detects entry impact speed to create splash rings, and emits periodic wakes based on horizontal velocity.
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// Ripples on the water when this object drops through the surface, and if wanted while it moves across it. For
/// anything without a <see cref="Buoyancy"/> (players, NPCs, projectiles, debris): its velocity is worked out from its
/// own movement, so it works with any controller and needs no rigidbody.
/// </summary>
[Title("Water Ripple Emitter")]
[Category("Water Tool 2")]
[Icon("water")]
public sealed class WaterRippleEmitter : Component
{
[Property, Group("Entry")] public bool EmitOnEntry { get; set; } = true;
[Property, Group("Entry")] public float EntryStrength { get; set; } = 0.2f;
/// <summary>
/// Ring spacing of the splash: smaller makes more, tighter rings.
/// </summary>
[Property, Group("Entry"), Range(20.0f, 400.0f)] public float EntryWavelength { get; set; } = 120.0f;
/// <summary>
/// Ring size of the splash: larger makes a bigger, broader ripple.
/// </summary>
[Property, Group("Entry"), Range(10.0f, 500.0f)] public float EntryRingWidth { get; set; } = 50.0f;
/// <summary>
/// The downward speed (units/s) needed to splash. 0 ripples on any crossing.
/// </summary>
[Property, Group("Entry")] public float MinImpactSpeed { get; set; } = 40.0f;
[Property, Group("Wake")] public bool EmitWake { get; set; } = false;
[Property, Group("Wake")] public float WakeStrength { get; set; } = 0.1f;
[Property, Group("Wake"), Range(20.0f, 400.0f)] public float WakeWavelength { get; set; } = 120.0f;
[Property, Group("Wake"), Range(10.0f, 500.0f)] public float WakeRingWidth { get; set; } = 50.0f;
/// <summary>
/// The horizontal speed (units/s) below which no wake is left.
/// </summary>
[Property, Group("Wake")] public float WakeMinSpeed { get; set; } = 1.0f;
[Property, Group("Wake")] public float WakeInterval { get; set; } = 0.0333f;
/// <summary>
/// The point tested against the surface, from the object (its feet, say).
/// </summary>
[Property, Group("General")] public Vector3 SampleOffset { get; set; } = Vector3.Zero;
private bool m_Initialized;
private bool m_WasBelowSurface;
private Vector3 m_LastPosition;
private float m_WakeTimer;
private Vector3 SamplePosition => WorldPosition + WorldRotation * SampleOffset;
protected override void OnEnabled()
{
m_LastPosition = SamplePosition;
m_WasBelowSurface = false;
m_Initialized = false;
}
protected override void OnUpdate()
{
// Something carried by something else (a player in a boat) leaves the rippling to what carries it.
if (GameObject.Parent != Scene)
return;
Vector3 position = SamplePosition;
Vector3 velocity = Time.Delta > 0.0f ? (position - m_LastPosition) / Time.Delta : Vector3.Zero;
m_LastPosition = position;
float waterHeight = WaterNetwork.GetWaterHeightAt(position);
if (waterHeight <= float.MinValue)
{
m_WasBelowSurface = false;
return;
}
bool belowSurface = position.z <= waterHeight;
// The first frame over water only takes note, so something spawned in the water doesn't splash.
if (!m_Initialized)
{
m_WasBelowSurface = belowSurface;
m_Initialized = true;
return;
}
if (EmitOnEntry && belowSurface && !m_WasBelowSurface)
{
float impactSpeed = float.Max(0.0f, -velocity.z);
if (impactSpeed >= MinImpactSpeed)
{
float strength = (impactSpeed / 150.0f).Clamp(0.3f, 2.5f) * EntryStrength;
WaterNetwork.AddRipple(position.WithZ(waterHeight), strength, EntryWavelength, EntryRingWidth);
}
}
m_WasBelowSurface = belowSurface;
if (!EmitWake || !belowSurface || velocity.WithZ(0.0f).Length < WakeMinSpeed)
return;
m_WakeTimer -= Time.Delta;
if (m_WakeTimer > 0.0f)
return;
WaterNetwork.AddRipple(position.WithZ(waterHeight), WakeStrength, WakeWavelength, WakeRingWidth);
m_WakeTimer = WakeInterval;
}
}