A component that makes ripples on the water surface under an object hovering above it, like a helicopter's downwash. Each update it measures the object against the water height, computes a squared falloff intensity over the configured Reach, and at a fixed Interval adds a ripple at a random point inside Radius. Skips objects that have a parent, and skips anything at or below the surface or above Reach.
using System;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// Stirs the water under something hovering above it without touching it - a helicopter's downwash.
/// Ripples land scattered in a patch under the object and build as it descends, stopping entirely
/// once it climbs out of reach.
///
/// Nothing else ripples the water from the air: <see cref="Buoyancy"/> and
/// <see cref="WaterRippleEmitter"/> both wait until something is genuinely in the water. Put this on
/// whatever should disturb the surface from above, and leave it off everything else.
/// </summary>
[Title("Water Downwash")]
[Category("Water Tool 2")]
[Icon("air")]
public sealed class WaterDownwash : Component
{
/// <summary>
/// How far above the surface the downwash still reaches. Nothing happens higher than this, and
/// the effect builds as the object comes down through it.
/// </summary>
[Property, Group("Downwash")] public float Reach { get; set; } = 600.0f;
/// <summary>
/// Ripple strength just above the surface, falling away to nothing at <see cref="Reach"/>.
/// </summary>
[Property, Group("Downwash")] public float Strength { get; set; } = 0.35f;
/// <summary>
/// How wide a patch the ripples scatter over. Rotor wash churns an area rather than ringing out
/// from a point, so each ripple lands somewhere random within this.
/// </summary>
[Property, Group("Downwash")] public float Radius { get; set; } = 150.0f;
/// <summary>
/// Seconds between ripples. The water holds only 64 at once across the whole scene, so a dense
/// trail here crowds out the boats and splashes everywhere else.
/// </summary>
[Property, Group("Downwash")] public float Interval { get; set; } = 0.06f;
[Property, Group("Downwash"), Range(20.0f, 400.0f)] public float Wavelength { get; set; } = 90.0f;
[Property, Group("Downwash"), Range(10.0f, 500.0f)] public float RingWidth { get; set; } = 70.0f;
/// <summary>The point measured against the surface, from the object - the rotor hub, say.</summary>
[Property, Group("General")] public Vector3 SampleOffset { get; set; } = Vector3.Zero;
private float m_Timer;
private Vector3 SamplePosition => WorldPosition + WorldRotation * SampleOffset;
/// <summary>
/// How hard the downwash is hitting the water right now, 0 out of reach and 1 just above the
/// surface. Useful for driving a spray effect or a rotor sound off the same falloff.
/// </summary>
public float Intensity { get; private set; }
protected override void OnUpdate()
{
Intensity = 0.0f;
// Something carried by something else leaves the rippling to whatever carries it.
if (GameObject.Parent != Scene)
return;
Vector3 position = SamplePosition;
float waterHeight = WaterNetwork.GetWaterHeightAt(position);
if (waterHeight <= float.MinValue)
return;
float above = position.z - waterHeight;
// Strictly from the air. Once it is in the water that is buoyancy's and the ripple emitter's
// business, and doubling up would only eat the ripple budget.
if (above < 0.0f || above > Reach)
return;
// Squared, so the water stays calm over most of the approach and then churns as it settles
// in, rather than rippling faintly the whole way down.
float falloff = 1.0f - (above / float.Max(Reach, 0.001f));
Intensity = falloff * falloff;
m_Timer -= Time.Delta;
if (m_Timer > 0.0f)
return;
m_Timer = float.Max(Interval, 0.01f);
// Scattered over the disc rather than always dead centre, so it churns a patch instead of
// ringing out from one point. Square-rooting the radius keeps the spread even across the
// area; without it they bunch up in the middle.
float angle = Game.Random.Float(0.0f, MathF.Tau);
float distance = Radius * MathF.Sqrt(Game.Random.Float(0.0f, 1.0f));
Vector3 spot = position + new Vector3(MathF.Cos(angle) * distance, MathF.Sin(angle) * distance, 0.0f);
WaterNetwork.AddRipple(spot.WithZ(waterHeight), Strength * Intensity, Wavelength, RingWidth);
}
}