Gameplay/WaterRippleEmitter.cs

Component that emits water ripples when the GameObject crosses the water surface or moves across it. Samples a point offset from the object, detects entry impacts to spawn splashes and periodically emits wake ripples while moving on the surface.

NetworkingFile Access
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;
	}
}