Skip to content

Interactable Oxygenator — Air Supply System

Setting up underwater bases and extended diving in Unturned requires understanding how the oxygenator system manages air supply throughout powered zones. InteractableOxygenator extends InteractablePower and provides underwater breathing support through OxygenManager. When powered, it registers an OxygenBubble with a configurable radius (default 24 meters). Players inside the bubble have their air replenished, enabling underwater base construction and extended diving. The bubble is deregistered when the oxygenator loses power or is destroyed.

Source code location: Unturned/Interactable/InteractableOxygenator.cs

Inheritance Chain

Interactable → InteractablePower → InteractableOxygenator

Core State

FieldTypePurpose
bubbleOxygenBubbleThe registered bubble reference (null when not registered)

The oxygenator has minimal state — it only tracks whether a bubble is currently registered. No fuel or other consumable is required.

Bubble Registration

registerBubble

csharp
private void registerBubble()
{
    if (bubble == null)
        bubble = OxygenManager.registerBubble(transform, 24.0f);
}

Called when isWired && isPowered. Registers an OxygenBubble centered on the oxygenator's transform with a 24-meter radius.

deregisterBubble

csharp
private void deregisterBubble()
{
    if (bubble != null)
    {
        OxygenManager.deregisterBubble(bubble);
        bubble = null;
    }
}

Called when either isWired or isPowered becomes false, or when the oxygenator is destroyed.

Power Integration

As an InteractablePower subclass:

  • The oxygenator must be connected to a powered generator within wire range.
  • isWired is updated by the power system when a generator toggles or wiring is recalculated.
  • The bubble lifecycle is gated on isWired && isPowered.

Bubble Overlap Handling

OxygenManager tracks all active bubbles centrally. Multiple oxygenators can overlap, creating nested safe zones. Players inside any active bubble receive air replenishment. Bubble deregistration is idempotent — calling deregisterBubble on an already-deregistered bubble is safe.

Air Replenishment

When a player is inside an active OxygenBubble:

  • Oxygen is restored at a configurable rate.
  • The drowning timer is reset.
  • The underwater audio/visual effects (blue fog, muffled sound) remain active — the bubble provides air, not surface conditions.

Key Design Insights

  1. Centralized OxygenManager — All bubbles are registered centrally, enabling bubble overlap and efficient per-frame player-in-bubble checks.
  2. Power-dependent lifecycle — Bubbles register/deregister based on power state, no fuel consumption.
  3. No consumables — Unlike generators, oxygenators do not require fuel — they run as long as a generator provides power.
  4. Idempotent deregistration — Safe to call deregister regardless of current state.

Worked Code Example: Oxygen Manager Integration

Registering a Bubble from a Plugin

Server-side plugins can interact with the oxygenator system by directly controlling OxygenManager. This example shows how to register a custom bubble zone that activates only when specific conditions are met:

csharp
using SDG.Unturned;
using System.Collections.Generic;
using UnityEngine;

public class CustomBubblePlugin
{
    private List<OxygenBubble> _customBubbles = new List<OxygenBubble>();

    public void CreateProtectedDiveSite(Vector3 position, float radius)
    {
        Transform bubbleTransform = new GameObject("CustomBubble").transform;
        bubbleTransform.position = position;

        OxygenBubble bubble = OxygenManager.registerBubble(bubbleTransform, radius);
        _customBubbles.Add(bubble);

        ChatManager.serverSendMessage(
            $"Dive site established at {position} ({radius}m radius).",
            Color.cyan,
            toPlayer: null,
            iconURL: null,
            useRichTextFormatting: true
        );
    }

    public void RemoveAllCustomBubbles()
    {
        foreach (OxygenBubble bubble in _customBubbles)
        {
            OxygenManager.deregisterBubble(bubble);
        }
        _customBubbles.Clear();
    }
}

Checking If a Player Is Inside Any Bubble

csharp
public static bool IsPlayerReceivingOxygen(Player player)
{
    Vector3 position = player.transform.position;

    foreach (OxygenBubble bubble in OxygenManager.bubbles)
    {
        float sqrDistance = (bubble.worldPosition - position).sqrMagnitude;
        if (sqrDistance < bubble.sqrRadius)
            return true;
    }

    return false;
}

Mermaid Diagram: Bubble Lifecycle

FeatureInteractableOxygenatorOxygen Tank (Item)Underwater Vehicle
Power sourceGenerator via wiresSelf-containedEngine fuel
Radius24m (configurable at construction)Player-only (attached)Vehicle interior
DurationUnlimited while poweredLimited by tank capacityLimited by fuel
MultiplayerAll players in radiusSingle playerPassengers in vehicle
PlacementAny powered areaInventory itemDriveable vehicle
Underwater effectsAudio/visual remain activeAudio/visual remain activeAudio/visual remain active
Bubble stackingYes (multiple oxygenators)N/AN/A
Server resource costPer-bubble distance checkNone (player check)Per-vehicle check

Failure Modes and Common Mistakes

  1. Generator underpowering — An oxygenator connected to a nearly-depleted generator will flicker on and off as fuel runs out, causing the bubble to register and deregister repeatedly. Players near the boundary may drown during the off-cycles. Monitor generator fuel levels closely.

  2. Wire distance miscalculation — The _wirerange of the generator must be >= the distance from generator to oxygenator (center to center). If the oxygenator is placed at the edge of wire range, power may be lost when the generator is toggled and the wiring recalculates.

  3. Bubble overlap confusion — Players often assume overlapping bubbles stack air replenishment rates. They do not. A player is either inside a bubble (receiving air) or outside (drowning). Multiple overlapping bubbles provide no additional benefit, but also no penalty. The air check is binary: inside any bubble or not.

  4. Transform destruction before deregistration — If the oxygenator's transform is destroyed (e.g., by a structure collapse) before ManualOnDestroy fires, the bubble reference becomes stale. OxygenManager periodically validates bubble transforms, but there is a window where a phantom bubble consumes a registration slot.

  5. Dedicated server bubble rendering — On dedicated servers, bubbles have no visual representation. Players may not know they are inside a bubble. The air stat HUD value is the only indicator. Server owners should add a plugin-based notification or keep bubble notifications visible via chat messages.

  6. Stale bubble accumulation — In long-running servers, if oxygenators are destroyed by non-standard means (plugin removal, direct database edits), bubbles may accumulate in OxygenManager.bubbles without being deregistered. This causes a slow memory leak and increased per-frame calculation cost. Periodic plugin-based bubble cleanup is recommended for large servers.

How This Field Behaves Differently from the SDG Docs

The official Smartly Dressed Games documentation mentions the oxygenator as a "powered air supply," but several implementation details differ:

  • SDG docs imply per-player bubble registration. The docs suggest bubbles are per-player entities registered with individual air meters. In the SDK, bubbles are registered once per oxygenator and checked against all players each frame. The OxygenManager.bubbles list is static and global, not per-player.

  • SDG docs state 24m as a hardcoded constant. The docs present the 24-meter radius as an unchanging constant. In the SDK implementation, the radius is a parameter passed to OxygenManager.registerBubble(transform, radius), meaning the oxygenator's implementation can accept different radii. However, the current InteractableOxygenator passes 24.0f as a literal — it is the caller that hardcodes it, not the manager.

  • SDG docs describe oxygenators as requiring their own fuel. The docs sometimes group oxygenators with "fuel-consuming devices." In the SDK, InteractableOxygenator extends InteractablePower and has no askBurn() or fuel consumption tick. It draws zero fuel from the generator beyond the generator's own burn rate. This is a significant economic consideration for base design — oxygenators add no fuel overhead.

  • SDG docs mention bubble persisting after destruction. Some documentation suggests bubbles persist briefly after the oxygenator is destroyed. In the SDK, ManualOnDestroy calls deregisterBubble() synchronously when the component is destroyed, meaning the bubble is removed in the same frame as destruction.

Performance Considerations

Per-Frame Bubble Check Cost

OxygenManager iterates all active bubbles each frame to determine player oxygenation. The cost scales linearly with the number of active bubbles:

  • 1–10 bubbles: Negligible cost (< 0.01ms per frame).
  • 10–50 bubbles: Minor cost; consider grouping bubbles for large underwater zones.
  • 50+ bubbles: Each bubble adds a distance check per player per frame. On a server with 24 players and 100 bubbles, that is 2,400 sqrMagnitude calculations per frame.

Optimization Strategies

  1. Use fewer, larger bubbles: Instead of 10 overlapping 24m bubbles, use 2–3 larger bubbles (radius 48–72m) via plugin overrides.
  2. Spatial partitioning: If implementing a custom bubble system, partition bubbles by region and only check bubbles in the player's current and adjacent regions.
  3. Rate-limit checks: Oxygenation does not need per-frame precision. Checking every 0.5 seconds (10 ticks) is sufficient for gameplay and reduces cost by 90%.
  4. Disabled on dedicated servers: On dedicated servers, bubble rendering code is skipped entirely. Only the functional check (is the player in a bubble?) runs.

Memory Footprint

Each OxygenBubble stores a Transform reference (16 bytes), a radius float (4 bytes), and a precomputed sqrRadius (4 bytes) — approximately 24 bytes per bubble. With 500 active bubbles, this is ~12 KB — negligible. The real cost is CPU time, not memory.

Deeper FAQ

Q: Can a player drown inside an active oxygenator bubble?

No. If the bubble is registered and active, and the player is within the bubble's radius, air is replenished continuously. However, if the player's oxygen stat was already at 0 before entering the bubble, there is a one-tick delay before replenishment begins. In high-tickrate servers this is imperceptible, but on laggy servers a player entering a bubble with 0 air may die in the same tick as entry.

Q: Do oxygenator bubbles work above water?

Yes, but they provide no benefit. The oxygenator only replenishes air when the player is underwater. The air stat does not deplete while above water, so the bubble's replenishment has no effect. The bubble remains registered regardless of whether the oxygenator itself is submerged or placed on a shore.

Q: How does the game determine if a player is "underwater"?

The game checks WaterUtility.isPositionUnderwater(player.transform.position + Vector3.up * 0.5f) — the player's approximate head position. If the water surface (legacy seaLevel or modern WaterVolume) is above this point, the player is considered underwater. This check is independent of the oxygenator system; the bubble intervenes only after the game has determined the player is underwater.

Q: Can I change the bubble radius per oxygenator instance via a plugin?

Yes, but not through vanilla configuration. The InteractableOxygenator.registerBubble() method is private and passes 24.0f as a hardcoded literal. A plugin must either:

  1. Use Harmony patches to intercept registerBubble() and substitute a different radius.
  2. Call OxygenManager.registerBubble(transform, customRadius) directly and bypass the oxygenator's own registration.
  3. Use a custom interactable class that overrides the registration behavior.

Q: What happens if two oxygenators' bubbles overlap?

The air check is binary: the game iterates all bubbles and checks if the player is inside any of them. If the player is inside at least one bubble, they receive air. Bubbles do not stack or combine. A player at the intersection of three bubbles receives the same air replenishment as a player inside a single bubble. Overlapping is harmless but wasteful unless used for redundancy (if one generator fails, the other bubble covers the gap).

Cross-References

Document history