Ultimate Water 2D / Docs v1.0.0Web demo Back to website
DiffusionWorks/Documentation/Runtime controls and scripting recipes

Runtime controls and scripting recipes

Water Body Runtime Control is an example

The imported Water Body Runtime Control (WaterBodyRuntimeControl) demonstrates how a script can read, change, interpolate, and restore water values during Play mode. It is sample code for learning and prototyping, not a general runtime utility intended to be added unchanged to a finished game.

The example can fade surface and underwater colors, transition between ambient-wave configurations, flatten the water before changing wave motion, and restore the values it captured. Study its material-property checks and transition logic, then create a focused component for your game that changes only the values your mechanic needs. A weather system might blend calm and storm waves, while an area transition might fade the underwater color and distortion together. Keeping that logic in your own system gives you control over timing, cancellation, state ownership, and interaction with save data.

The sample exposes these public actions so buttons, timelines, or another script can demonstrate the transitions:

Action Demonstrated behavior
FadeToTargetColors() / FadeToOriginalColors() Blends between the configured target colors and the values captured from the water.
ToggleColors() Switches between those two color states.
FadeToTargetWaves() Blends to the configured amplitude, speed, frequency, mode, and direction. FadeToNewWaves() and ActivateWaves() are aliases used by sample hookups.
DeactivateWaves() Smoothly reduces ambient-wave amplitude to zero.
RestoreOriginalWaves() / ToggleWaves() Restores the captured wave configuration or toggles between wave states.
CaptureCurrentAsOriginal() Replaces the remembered baseline with the water’s current runtime values.
RestoreCapturedValues() Immediately restores the remembered colors and waves.

These methods document the sample’s behavior, not a component that every finished game should depend on. Create a smaller game-specific controller and expose only the transitions your weather, level, timeline, or gameplay system owns.

When changing colors at runtime, operate on the water body’s runtime material instances rather than editing a shared material asset. Check Material.HasProperty(...) because the standard 2D, 2.5D, and Fake Perspective materials do not expose the same shader properties. When changing ambient waves, assign the public wave properties and call ApplyMaterials() so the rendered water receives the new values.

Runtime water values used by the sample

These are the water-body values used by the sample controller and by common water transitions in version 1.0:

Member Type Purpose
WaveAmplitude float Maximum vertical displacement of the ambient wave. Set it to zero to flatten ambient waves.
WaveSpeed float Phase speed of the ambient wave.
WaveFrequency float Horizontal wave frequency; larger values place crests closer together.
WaveMode AmbientWaveMode Selects the ambient wave shape.
WaveDirection WaveTravelDirection Selects the direction in which the ambient wave travels.
SurfaceMaterialInstance Material Runtime surface-material instance used by standard and perspective water when available.
UnderwaterMaterialInstance Material Runtime underwater-material instance.
OrthoFakeSurfaceMaterialInstance Material Runtime material instance for the Fake Perspective surface strip.
ApplyMaterials() method Reapplies the water’s configured values and material state after a runtime change.
MaterialsRefreshed event Signals that water materials were recreated or refreshed; use it when a long-lived runtime override must be reapplied.

Runtime color properties

WaterBodyRuntimeControl checks the following shader properties. A material only needs the properties relevant to its water type.

Shader property Visual area
_WaterColor Main surface color.
_BaseFarColor Far or secondary color on the perspective surface.
_UnderwaterPlaneColor Color beneath the perspective surface plane.
_UnderwaterTopColor Underwater color near the waterline.
_UnderWaterBottomColor Underwater color toward the bottom of the volume.
_SurfaceColor Fake Perspective surface-strip color.
_UnderwaterColor Fake Perspective underwater color.

Cache property IDs with Shader.PropertyToID, confirm support with HasProperty, and interpolate from a captured start value. Avoid writing every frame when no transition is active.

C#
using UnityEngine;

public sealed class CalmWaterTransition : MonoBehaviour
{
    [SerializeField] private DW_UWS2D.WaterLodController water;
    [SerializeField] private float calmAmplitude = 0.08f;
    [SerializeField] private float transitionTime = 1.5f;

    private Coroutine transition;

    public void FadeToCalm()
    {
        if (transition != null)
            StopCoroutine(transition);

        transition = StartCoroutine(FadeAmplitude(calmAmplitude));
    }

    private System.Collections.IEnumerator FadeAmplitude(float target)
    {
        float start = water.WaveAmplitude;
        float elapsed = 0f;

        while (elapsed < transitionTime)
        {
            elapsed += Time.deltaTime;
            float t = Mathf.SmoothStep(0f, 1f, elapsed / transitionTime);
            water.WaveAmplitude = Mathf.Lerp(start, target, t);
            water.ApplyMaterials();
            yield return null;
        }

        water.WaveAmplitude = target;
        water.ApplyMaterials();
        transition = null;
    }
}

This example intentionally controls one value. Extend the same pattern with the specific wave or material values owned by your game rather than exposing every Inspector setting through one global runtime controller.

Spawn a ripple at a point

Attach this component to any GameObject, assign the water body you want to affect, then call SplashAt(worldPosition) from your own gameplay code. The position must be appropriate for the assigned water surface.

C#
using DW_UWS2D;
using UnityEngine;

public sealed class ManualWaterSplash : MonoBehaviour
{
    // This script does not need to be attached to the water root.
    // Assign the WaterPhysics component from this or another water body.
    [SerializeField] private WaterPhysics water;

    public void SplashAt(Vector3 worldPosition)
    {
        if (water == null || !water.waterInteractionsEnabled)
            return;
        if (water.IsPointExcluded(worldPosition))
            return;

        var ripple = water.GetDefaultParams();
        ripple.amplitude = 0.4f;
        water.SpawnRipple(worldPosition, ripple);
    }
}

Call this for an intentional gameplay effect. Avoid generating extra manual ripples on top of automatic entry/wake behavior unless that combination is intended.

Sample water at a custom point

After refreshing a configured sensor, use sensor.GetSampleAt(sampleTransform). Check sample.isInsideWater and sample.isSubmerged before treating the sample as underwater. For a known built-in water body, use GetWaterSampleAtWorldPoint(worldPosition).

GetWaterHeightWorld(...) samples the displaced height; GetAmbientWaterHeightWorld(...) is useful for a surface target that should follow ambient waves without impact-ripple displacement. Current and ripple readings remain separate so each controller can choose its own response. For character code, prefer CurrentRaw; WaterCharacter2D forwards it unchanged and does not perform time integration.

This probe uses a configured sensor to test a deliberate point such as a character’s hand, camera, or interaction origin. The sensor remains responsible for finding the current water body.

C#
using DW_UWS2D;
using UnityEngine;

[RequireComponent(typeof(WaterSensor2D))]
public sealed class UnderwaterPointProbe : MonoBehaviour
{
    [SerializeField] private Transform probe;

    private WaterSensor2D sensor;
    public bool ProbeIsUnderwater { get; private set; }

    private void Awake() => sensor = GetComponent<WaterSensor2D>();

    private void FixedUpdate()
    {
        if (probe == null)
        {
            ProbeIsUnderwater = false;
            return;
        }

        // Let the sensor's normal update own Refresh(); this is an extra query.
        var sample = sensor.GetSampleAt(probe);
        ProbeIsUnderwater = sample.isInsideWater && sample.isSubmerged;
    }
}

Follow the displaced or ambient surface

Use the displaced height for an object that should react to impact ripples. Use the ambient height when an object should follow only the broad background waves. This example can switch between the two behaviors.

C#
using DW_UWS2D;
using UnityEngine;

public sealed class WaterSurfaceFollower : MonoBehaviour
{
    [SerializeField] private WaterPhysics water;
    [SerializeField] private bool includeImpactRipples = true;
    [SerializeField] private float heightOffset = 0.15f;
    [SerializeField] private float followSpeed = 8f;

    private void LateUpdate()
    {
        if (water == null)
            return;

        Vector3 position = transform.position;
        float surfaceY = includeImpactRipples
            ? water.GetWaterHeightWorld(position)
            : water.GetAmbientWaterHeightWorld(position);

        position.y = Mathf.Lerp(
            position.y,
            surfaceY + heightOffset,
            1f - Mathf.Exp(-followSpeed * Time.deltaTime));
        transform.position = position;
    }
}

For physics objects, do not assign the transform this way. Use DWBuoyancy2D, a floating-platform component, or apply the sampled result through your Rigidbody movement path.

Attach entry VFX or audio

For a splash effect, subscribe to WaterEntered and use surfacePoint, impact01, and enteredThroughSurface. Pair C# subscriptions in OnEnable with unsubscriptions in OnDisable. If you need events for all wet/dry changes, use StateChanged as well.

The sample player exposes an optional entry splash prefab and sizing/offset controls. Its splash helper shows how to scale by entry speed, reject excluded points, and destroy the one-shot effect after its lifetime. These are sample choices you can adapt to your own pooling, audio, or VFX systems.

C#
using DW_UWS2D;
using UnityEngine;

[RequireComponent(typeof(WaterSensor2D))]
public sealed class WaterEntryEffects : MonoBehaviour
{
    [SerializeField] private ParticleSystem splashPrefab;
    [SerializeField] private AudioSource audioSource;
    [SerializeField] private AudioClip splashClip;

    private WaterSensor2D sensor;

    private void Awake() => sensor = GetComponent<WaterSensor2D>();
    private void OnEnable() => sensor.WaterEntered += HandleWaterEntered;
    private void OnDisable() => sensor.WaterEntered -= HandleWaterEntered;

    private void HandleWaterEntered(WaterSensorEventData data)
    {
        if (!data.enteredThroughSurface)
            return;

        if (splashPrefab != null)
        {
            ParticleSystem splash = Instantiate(
                splashPrefab,
                data.surfacePoint,
                Quaternion.identity);

            // A harder entry produces a larger one-shot effect.
            splash.transform.localScale *= Mathf.Lerp(0.65f, 1.35f, data.impact01);
        }

        if (audioSource != null && splashClip != null)
            audioSource.PlayOneShot(splashClip, Mathf.Lerp(0.35f, 1f, data.impact01));
    }
}

Use a pool instead of Instantiate for frequent effects. WaterEntered already represents a surface entry, but retaining the enteredThroughSurface guard makes the intent explicit when adapting the handler.

Report velocity from a custom mover

When a controller already calculates its final world-space velocity, pass that value to MotionTracker2D instead of asking the tracker to estimate it from transform changes. Call SetExternalVelocity(...) in the same loop that owns movement.

C#
using DW_UWS2D;
using UnityEngine;

[RequireComponent(typeof(MotionTracker2D))]
public sealed class KinematicWaterMover : MonoBehaviour
{
    [SerializeField] private float speed = 4f;

    private MotionTracker2D motionTracker;
    private Vector2 moveInput;

    private void Awake() => motionTracker = GetComponent<MotionTracker2D>();

    public void SetMoveInput(Vector2 input) => moveInput = input;

    private void FixedUpdate()
    {
        Vector2 velocity = Vector2.ClampMagnitude(moveInput, 1f) * speed;

        // Replace this with your collision-aware kinematic move.
        transform.position += (Vector3)(velocity * Time.fixedDeltaTime);
        motionTracker.SetExternalVelocity(velocity);
    }
}

No special sampling mode is required when you call SetExternalVelocity(...); the supplied value wins for that frame. If the controller can be blocked by collisions, report the post-collision velocity rather than the requested input velocity so splash and wake strength match the movement that actually occurred.