Skip to content

Level Water System

Creating realistic oceans, multi-elevation lakes, and underwater environments in Unturned requires understanding how the dual water system manages global seaLevel rendering alongside modern WaterVolume instances, with reflection cameras, buoyancy physics, and time-of-day-dependent visual parameters. The water system manages the global ocean/water plane and localized water volumes in the Unturned SDK. It uses both a modern water volume manager (SDG.Framework.Water.WaterVolumeManager) and a legacy single-plane system controlled by the seaLevel property. The system handles reflection rendering, underwater effects, audio, and buoyancy physics.

The water system is implemented through LevelLighting (2814 lines at Unturned/Level/LevelLighting.cs) which owns the dominant water state, and SDG.Framework.Water.WaterVolumeManager which manages editor-placed volumes. The day/night cycle drives water-light interaction through the reflection system.

Source code locations: Unturned/Level/LevelLighting.cs, SDG.Framework.Water namespace

seaLevel

csharp
private static float _seaLevel;
public static float seaLevel { get; set; }

The global water plane height. Changing it updates:

  • Legacy water transform position.
  • Bubble particle system state.
  • Reflection camera position (reflected across the water plane).
  • Underwater detection threshold.

When Use_Legacy_Water is enabled in the level config, the legacy water system uses a single flat plane at seaLevel for all water rendering.

Water Volumes

Modern Water (WaterVolumeManager)

SDG.Framework.Water.WaterVolumeManager manages water volumes defined in the level editor. Each WaterVolume defines:

  • A rectangular or polygon boundary.
  • A water surface height.
  • Water properties (color, transparency, foam intensity, reflection settings).

Water volumes can exist at multiple heights, enabling:

  • Multiple water bodies at different elevations.
  • Indoor swimming pools.
  • Underground water caverns.
  • Non-contiguous water surfaces in the same level.

Legacy Water

csharp
private static WaterVolume legacyWater;
private static Transform legacyWaterTransform;

The legacy water system uses a single flat plane at seaLevel. When Use_Legacy_Water is enabled, water properties (FoamColor, Specular) come from the active LightingInfo.

Water Surface Rendering

The water surface is rendered using:

  1. Reflection: A Camera with RenderTexture captures the scene reflected across the water plane.
  2. Foam: A scrolling foam texture at the water's edge.
  3. Specular: Blinn-Phong specular highlight with time-of-day-dependent intensity.
  4. Refraction: A screen-space distortion effect for objects below the surface.

Reflection Textures

csharp
private static RenderTexture reflectionMap;
private static RenderTexture reflectionMapVision;

Two render textures are maintained:

  • reflectionMap: Standard RGB reflection.
  • reflectionMapVision: Night vision reflection with different color filtering.

The reflectionIndex round-robins between multiple reflections across frames to amortize the rendering cost.

Reflection Camera

The reflection camera renders a mirror copy of the scene:

  1. Position: Reflected across the water plane (Y = 2 × seaLevel − camera.Y).
  2. Rotation: Mirrored (X and Z rotation inverted).
  3. Culling: By LayerMasks.WATER to avoid rendering the water plane itself.
  4. Resolution: Configurable via graphics settings.

Skybox Reflections

csharp
private static bool _isSkyboxReflectionEnabled;

When enabled, the skybox cubemap is rendered to a reflection texture at strategic intervals (when skyboxNeedsReflectionUpdate is true). The update is rate-limited by lastSkyboxReflectionUpdate to avoid thrashing. Changes to time or vision trigger reflection updates.

Underwater Effects

When the camera is below seaLevel, enableUnderwaterEffects activates:

EffectComponentDescription
Blue-tinted fogScene fog overrideUnderwater ambient fog
Bubbles_bubbles particle systemBubble particle spray
Muffled audiowaterAudio sourceUnderwater audio ambiance
Screen overlayPost-processingBlurred or distorted overlay

Underwater Detection

csharp
public static bool isPositionUnderwater(Vector3 position)

Checks the WaterUtility (modern) or the legacy Use_Legacy_Water config setting. When in the editor, enableUnderwaterEffects and EditorWantsWaterSurface flags control underwater visuals.

Water Manager Integration

The water system interacts with:

SystemIntegration
BubblesUpdateBubblesActive() toggles bubble particles based on seaLevel and player state
AudiowaterAudio source plays underwater ambiance
VisualreflectionCamera renders the water reflection
FogUnderwater fog overrides the level's ambient fog with blue-tinted fog
Sort orderDynamicWaterTransparentSort and WaterHeightTransparentSort handle transparent object rendering order

Transparency Sort

csharp
DynamicWaterTransparentSort
WaterHeightTransparentSort

These components handle the correct render ordering of transparent objects below and above the water surface. They ensure objects below water render through the water surface correctly.

Buoyancy

csharp
// Buoyancy.cs in Unturned/Interactable/

The Buoyancy component (in Unturned/Interactable/Buoyancy.cs) provides physics buoyancy for objects in water:

  • Applies upward force proportional to submersion depth.
  • Dampens velocity when partially submerged.
  • Responds to both global seaLevel and local WaterVolume surfaces.

Audio Ambiance

csharp
private static AudioSource _waterAudio;

The waterAudio source plays underwater ambiance. Volume is adjusted based on whether the player is underwater and the current timer in the lighting cycle. The audio ambiance system also supports AmbianceAudioInstance pooling for per-region effects.

Environment Save Format

LevelLighting.save() writes water-related configuration:

csharp
river.writeByte(SAVEDATA_VERSION);
river.writeSingle(seaLevel);
// ... other lighting data

The seaLevel value is stored as a float alongside the four LightingInfo keyframes and weather configuration.

Water Edge Foam

The foam rendering at water's edge is a scrolling texture:

  • Moves at a configurable rate.
  • Appears at the intersection between water surface and terrain/objects.
  • Intensity modulated by time of day (darker at night, brighter during day).
  • Uses the FoamColor from the active LightingInfo.

Dedicated Server Handling

On dedicated servers, water rendering is disabled entirely — no reflection camera, no foam, no surface rendering. Only the functional aspects (underwater detection for gameplay, buoyancy for physics) remain active.

Worked Code Example: Water System Integration

Dynamic Sea Level Modifier

csharp
using SDG.Unturned;
using UnityEngine;

public class TidalSystem : MonoBehaviour
{
    private float _tideAmplitude;
    private float _tidePeriod;

    /// <summary>
    /// Creates a sinusoidal tide effect by oscillating the sea level
    /// over time, simulating a lunar tide cycle.
    /// </summary>
    private void Update()
    {
        if (LevelLighting.seaLevel <= 0f)
            return;

        float targetSeaLevel = LevelLighting.seaLevel
            + _tideAmplitude * Mathf.Sin(
                Time.time * Mathf.PI * 2f / _tidePeriod
            );

        // Directly set the seaLevel property to move the water plane
        LevelLighting.seaLevel = targetSeaLevel;

        // All WaterVolumes update their visual height to match
        WaterVolumeManager.Get().UpdateAllVolumes();
    }

    /// <summary>
    /// Returns true if a world position is currently submerged by the tide,
    /// factoring in the dynamic sea level oscillation.
    /// </summary>
    public static bool IsPositionInTidalZone(Vector3 position, float baseSeaLevel)
    {
        float currentSea = LevelLighting.seaLevel;
        return position.y <= currentSea
            && position.y > baseSeaLevel - 5f;
    }
}

Underwater Post-Effect Controller

csharp
using SDG.Unturned;
using UnityEngine;

public class UnderwaterEffectsController
{
    /// <summary>
    /// Manually toggles the underwater visual effects, bypassing
    /// the automatic detection based on camera Y position.
    /// Useful for per-zone underwater detection (WaterVolume overrides).
    /// </summary>
    public static void SetUnderwaterEffects(bool enable, Color? fogColor = null)
    {
        if (enable)
        {
            RenderSettings.fog = true;
            RenderSettings.fogMode = FogMode.Linear;
            RenderSettings.fogStartDistance = 0f;
            RenderSettings.fogEndDistance = 30f;
            RenderSettings.fogColor = fogColor ?? new Color(0.1f, 0.3f, 0.6f);
        }
        else
        {
            // Restore surface-level fog from LevelLighting
            LevelLighting.UpdateLighting();
        }
    }
}

Mermaid Diagram: Water Detection Pipeline

Comparison: Water Rendering Systems

FeatureLegacy Water PlaneModern WaterVolumeCustom Water Shader (Mod)
Coordinate systemSingle global seaLevel Y valuePer-volume polygon boundaryShader-defined
Multi-elevationNo (single plane)Yes (independent volumes)Yes
Reflection cameraGlobal reflectionCameraNone (volume-based)Per-instance possible
Foam renderingGlobal FoamColor per LightingInfoNoneCustom
PerformanceSingle reflection render per N framesDistance-check per volume per frameShader complexity
Level editingseaLevel float in environment configEditor-placed volume instancesRequires material setup
Boat/buoyancyLegacy seaLevel-based BuoyancyWaterVolume support in BuoyancyMay need custom
AudioGlobal waterAudio sourceNoneNone
Underground waterNo (global plane cuts through terrain)Yes (volume can be at any Y)Yes
Dedicated serverAll rendering disabledCollider-only for gameplayDisabled

Failure Modes and Common Mistakes

  1. Reflection camera thrashing — If seaLevel changes rapidly (e.g., plugin toggles it every frame), the reflection camera repositions and re-renders at a high rate. This causes GPU spikes as the camera captures the full scene reflected. Rate-limit sea level changes to a maximum of 2 updates per second.

  2. Legacy water and modern volumes competing — If both Use_Legacy_Water is enabled and WaterVolume instances exist in the level, the isPositionUnderwater check prioritizes one over the other inconsistently depending on player position. This causes flickering underwater effects as the player moves between detection methods.

  3. Night vision reflection mismatch — The reflectionMapVision render texture uses different color filtering than reflectionMap. If the reflectionIndex round-robin desynchronizes between the two textures (e.g., a plugin advances the index without updating both), night vision reflections show incorrect color grading.

  4. Fog color stacking — When underwater fog is active (blue-tinted) and a separate fog volume (e.g., a cave fog region) is also active, the two fog overrides stack. The result is overly dark or miscolored underwater scenes. The system does not blend fog — the last-written fog wins.

  5. Buoyancy ignoring per-volume water — The Buoyancy component primarily checks against seaLevel. If a player is in a WaterVolume at elevation 200 but seaLevel is at 50, the buoyancy force may not apply because seaLevel is far below. The buoyancy code must explicitly iterate WaterVolumeManager volumes for proper multi-elevation support.

How This Field Behaves Differently from the SDG Docs

  • SDG docs present seaLevel as a level-editor constant. The community tutorials treat seaLevel as a value set once during level creation and never changed. In the SDK, seaLevel is a read-write property that can be changed at runtime. Changing it immediately updates the water plane, underwater detection, and reflection camera — enabling tidal and flood mechanics via plugins.

  • SDG docs describe water as "global and infinite." The wiki presents water as a single infinite ocean plane. In the SDK, the legacy water plane is technically infinite (extends to the horizon), but WaterVolume instances are finite polygons defined by boundary vertices. Water bodies in modern Unturned can be lakes, pools, or rivers — not just the global ocean.

  • SDG docs state reflection camera updates "every frame." The documentation suggests water reflections are continuous real-time at full quality. In the SDK, the reflectionIndex round-robin system distributes reflection rendering across multiple frames — typically 3 frames between full updates. This reduces GPU load to ~33% of what the documentation implies.

  • SDG docs reference WaterQuality settings. Older documentation references quality tiers for water. In the current SDK, water rendering quality is governed by the standard graphics quality presets (Very Low through Ultra) and the reflectionCamera rendering resolution is derived from screen resolution and the QualitySettings.masterTextureLimit setting.

Performance Considerations

Reflection Camera Cost

The reflection camera renders the full scene (minus water layer) at a reduced resolution:

  • 1080p screen, Ultra quality: Reflection renders at ~1920×1080 — approximately 4ms of GPU time per render.
  • 1080p screen, Low quality: Reflection renders at ~640×360 — approximately 1ms.
  • Round-robin: With reflectionIndex cycling every 3 frames, the amortized per-frame cost is 0.33–1.33ms.

WaterVolume Distance Checks

Each WaterVolume participates in isPositionUnderwater checks. On a 24-player server, if each player is checked against 20 volumes per frame:

  • 24 × 20 = 480 point-in-polygon tests per frame.
  • Each test is approximately 0.001ms.
  • Total cost: ~0.5ms per frame.

Dedicated Server Optimization

Dedicated servers skip all visual water rendering:

  • No reflection camera setup or rendering.
  • No foam texture calculations.
  • No underwater post-processing.
  • Only isPositionUnderwater functional check runs (for gameplay: drowning, buoyancy, oxygen replenishment).

Deeper FAQ

Q: Can I have water at two different elevations on the same map?

Yes, using WaterVolume instances. Each WaterVolume defines its own surface height. A map can have an ocean at Y=0, a mountain lake at Y=200, and an underground pool at Y=-50 — all with independent water surfaces. The legacy seaLevel single-plane system cannot do this.

Q: How do I make a swimming pool that's above sea level?

Place a WaterVolume in the level editor with the polygon defining the pool's boundaries and set its water height to the desired surface elevation. The WaterVolumeManager tracks this volume independently of seaLevel. Players entering the volume will experience underwater effects and buoyancy.

Q: Does water depth affect underwater fog intensity?

Yes, but not directly. The fog intensity is set once when the camera goes underwater — it uses a fixed blue-tinted color and linear falloff. There is no depth-dependent fog densification. Deep water (hundreds of meters below sea level) has the same fog as shallow water (just below the surface). Deep-sea mods that want darker, thicker fog must override the fog parameters after the standard underwater effects are applied.

Q: Can boats and vehicles interact with WaterVolume surfaces?

Boats check Buoyancy which primarily references seaLevel. For WaterVolume buoyancy support, the Buoyancy component must be patched or replaced to iterate WaterVolumeManager.Get().volumes. Without this, boats on a mountain lake (above sea level) will sink because seaLevel is below the boat.

Q: Is the water system deterministic across clients?

The water state (seaLevel, WaterVolume positions/heights, buoyancy forces) is server-authoritative and deterministic. The water rendering (reflections, foam animations, shader parameters) is client-side and non-deterministic — different clients at different graphics settings will see different water visuals. This does not affect gameplay but can give players at higher graphics settings a gameplay advantage (better visibility through reflections of approaching enemies).

Cross-References

Document history