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:
- Reflection: A
CamerawithRenderTexturecaptures the scene reflected across the water plane. - Foam: A scrolling foam texture at the water's edge.
- Specular: Blinn-Phong specular highlight with time-of-day-dependent intensity.
- 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:
- Position: Reflected across the water plane (Y = 2 × seaLevel − camera.Y).
- Rotation: Mirrored (X and Z rotation inverted).
- Culling: By
LayerMasks.WATERto avoid rendering the water plane itself. - 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:
| Effect | Component | Description |
|---|---|---|
| Blue-tinted fog | Scene fog override | Underwater ambient fog |
| Bubbles | _bubbles particle system | Bubble particle spray |
| Muffled audio | waterAudio source | Underwater audio ambiance |
| Screen overlay | Post-processing | Blurred 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:
| System | Integration |
|---|---|
| Bubbles | UpdateBubblesActive() toggles bubble particles based on seaLevel and player state |
| Audio | waterAudio source plays underwater ambiance |
| Visual | reflectionCamera renders the water reflection |
| Fog | Underwater fog overrides the level's ambient fog with blue-tinted fog |
| Sort order | DynamicWaterTransparentSort and WaterHeightTransparentSort handle transparent object rendering order |
Transparency Sort
csharp
DynamicWaterTransparentSort
WaterHeightTransparentSortThese 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
seaLeveland localWaterVolumesurfaces.
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 dataThe 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
FoamColorfrom the activeLightingInfo.
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
| Feature | Legacy Water Plane | Modern WaterVolume | Custom Water Shader (Mod) |
|---|---|---|---|
| Coordinate system | Single global seaLevel Y value | Per-volume polygon boundary | Shader-defined |
| Multi-elevation | No (single plane) | Yes (independent volumes) | Yes |
| Reflection camera | Global reflectionCamera | None (volume-based) | Per-instance possible |
| Foam rendering | Global FoamColor per LightingInfo | None | Custom |
| Performance | Single reflection render per N frames | Distance-check per volume per frame | Shader complexity |
| Level editing | seaLevel float in environment config | Editor-placed volume instances | Requires material setup |
| Boat/buoyancy | Legacy seaLevel-based Buoyancy | WaterVolume support in Buoyancy | May need custom |
| Audio | Global waterAudio source | None | None |
| Underground water | No (global plane cuts through terrain) | Yes (volume can be at any Y) | Yes |
| Dedicated server | All rendering disabled | Collider-only for gameplay | Disabled |
Failure Modes and Common Mistakes
Reflection camera thrashing — If
seaLevelchanges 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.Legacy water and modern volumes competing — If both
Use_Legacy_Wateris enabled andWaterVolumeinstances exist in the level, theisPositionUnderwatercheck prioritizes one over the other inconsistently depending on player position. This causes flickering underwater effects as the player moves between detection methods.Night vision reflection mismatch — The
reflectionMapVisionrender texture uses different color filtering thanreflectionMap. If thereflectionIndexround-robin desynchronizes between the two textures (e.g., a plugin advances the index without updating both), night vision reflections show incorrect color grading.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.
Buoyancy ignoring per-volume water — The
Buoyancycomponent primarily checks againstseaLevel. If a player is in aWaterVolumeat elevation 200 butseaLevelis at 50, the buoyancy force may not apply becauseseaLevelis far below. The buoyancy code must explicitly iterateWaterVolumeManagervolumes for proper multi-elevation support.
How This Field Behaves Differently from the SDG Docs
SDG docs present
seaLevelas a level-editor constant. The community tutorials treatseaLevelas a value set once during level creation and never changed. In the SDK,seaLevelis 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
WaterVolumeinstances 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
reflectionIndexround-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
WaterQualitysettings. 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 thereflectionCamerarendering resolution is derived from screen resolution and theQualitySettings.masterTextureLimitsetting.
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
reflectionIndexcycling 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
isPositionUnderwaterfunctional 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
- Level Foliage System — Foliage interacts with water through buoyancy and the water's edge foam rendering.
- Interactable Oxygenator — Air Supply System — Air replenishment in underwater zones through oxygenator bubbles.
- Level Terrain Generation — Terrain material and height data determine water boundary interactions.
- Architecture Deferred Rendering / Post-Processing — How the deferred rendering pipeline handles water transparency sorting.
