Skip to content

Interactable Generator — Power Supply

Powering your traps, sentries, lights, oxygenators, and oil pumps in Unturned all flows through understanding how InteractableGenerator manages fuel consumption, wire range calculations, and the power toggle state that feeds every InteractablePower device in range. InteractableGenerator (373 lines at Unturned/Interactable/InteractableGenerator.cs) implements the fuel-powered generator that supplies electricity to nearby powered devices. It extends Interactable (not InteractablePower), and implements IManualOnDestroy. Generators burn fuel over time when toggled on, and power connects wirelessly to any InteractablePower device within the configured wire range.

Source code location: Unturned/Interactable/InteractableGenerator.cs

Core State

FieldTypePurpose
_capacityushortMaximum fuel capacity (from ItemGeneratorAsset.fuelCapacity)
_fuelushortCurrent fuel level (stored in barricade state bytes 0–1)
_isPoweredboolOn/off toggle state
_wirerangefloatMaximum wiring distance
_sqrWirerangefloatPre-computed squared wire range for fast distance checks
burnfloatFuel burn rate (from ItemGeneratorAsset.burn)
lastBurnfloatTime.realtimeSinceStartup of last fuel consumption tick
isWiringboolWhether wiring is actively being recalculated

Fuel Management

askBurn

csharp
public void askBurn(ushort amount)
{
    if (amount == 0) return;
    if (amount >= fuel)
        _fuel = 0;
    else
        _fuel -= amount;

    if (Provider.isServer)
        updateState();
}

Called each tick when the generator is powered and has fuel. Burns burn * PLAYER_WIND amount per tick.

askFill

csharp
public void askFill(ushort amount)
{
    if (amount == 0) return;
    if (amount >= capacity - fuel)
        _fuel = capacity;
    else
        _fuel += amount;

    if (Provider.isServer)
        updateState();
}

Called when a player uses a fuel can on the generator. Capped at capacity to prevent overfill.

askSiphon

Reverses askFill — removes fuel from the generator. Used when a player siphons fuel out.

Wire System

Wiring Range

The wire range is defined per generator asset. All InteractablePower devices within _wirerange of an active, fueled generator are considered connected and receive the isWired property update.

Wiring Recalculation

When a generator toggles on/off or a powered device is placed/destroyed within range, the wiring state is recalculated. The isWiring flag prevents concurrent wiring recalculations.

Generator Arrays

Multiple generators can overlap wire ranges. A device is wired if it is within range of any powered generator in the list tracked by the power system.

Visual State

When isPowered changes:

  • Engine transform (engine) is enabled/disabled.
  • Engine animation begins or stops.
  • The tellFuel RPC broadcasts the new fuel level.

Consumption

Fuel is consumed at rate burn per tick while isPowered is true. When fuel reaches 0, the generator automatically toggles off and wiring is cancelled.

csharp
if (isPowered && fuel > 0)
{
    if (Time.realtimeSinceStartup - lastBurn > 1.0f)
    {
        lastBurn = Time.realtimeSinceStartup;
        askBurn(1);
    }
}

State Serialization

Fuel is stored in the barricade state byte array:

csharp
_fuel = System.BitConverter.ToUInt16(state, 0);
_capacity = ((ItemGeneratorAsset)asset).fuelCapacity;

State bytes 0–1 always contain the ushort fuel value. Power toggle state is managed through the generator's own metadata rather than the state bytes.

Network Protocol

DirectionMessagePurpose
Client → ServerSendToggleRequestToggle generator on/off
Server → AllSendFuel / ReceiveFuelSync fuel level
Server → AllSendWiringSync power connections

The SendFuel RPC uses ClientInstanceMethod<ushort>:

csharp
internal static readonly ClientInstanceMethod<ushort> SendFuel = ...;
[SteamCall(ESteamCallValidation.ONLY_FROM_SERVER, deferMode = ENetInvocationDeferMode.Queue)]
public void ReceiveFuel(ushort newFuel) { tellFuel(newFuel); }

IManualOnDestroy

csharp
public void ManualOnDestroy()
{
    if (isPowered)
    {
        // Recalculate wiring for all devices in range
    }
}

When a generator is destroyed while powered, all devices in its wire range must be notified that power is gone.

Key Design Insights

  1. Immediate vs InteractablePower — Generator extends Interactable, not InteractablePower, because it provides power rather than consuming it.
  2. Squared wire range_sqrWirerange is pre-computed for fast sqrMagnitude distance checks.
  3. Wiring recalculation — Toggling power triggers a full wiring update for all nearby devices.
  4. Multiple generators — Devices can be powered by any generator in range; wiring handles overlaps.

Worked Code Example: Generator Management Plugin

Multi-Generator Fuel Monitor

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

public class GeneratorMonitorPlugin
{
    private Dictionary<Transform, float> _fuelWarnings = new Dictionary<Transform, float>();

    /// <summary>
    /// Scans all generators within range of a position and returns
    /// the total fuel available and the estimated time until all generators
    /// in range are depleted.
    /// </summary>
    public static (ushort totalFuel, float hoursRemaining) AnalyzeGeneratorZone(Vector3 position, float checkRadius)
    {
        ushort totalFuel = 0;
        float totalBurnPerHour = 0f;

        foreach (BarricadeRegion region in BarricadeManager.regions.Values)
        {
            foreach (BarricadeDrop drop in region.drops)
            {
                if (Vector3.Distance(drop.model.position, position) > checkRadius)
                    continue;

                InteractableGenerator gen = drop.model.GetComponent<InteractableGenerator>();
                if (gen == null)
                    continue;

                ItemGeneratorAsset asset = gen.asset as ItemGeneratorAsset;
                if (asset == null)
                    continue;

                totalFuel += gen.fuel;
                totalBurnPerHour += asset.burn * 3600f; // ticks per hour
            }
        }

        float hoursRemaining = totalBurnPerHour > 0f
            ? (float)totalFuel / totalBurnPerHour
            : float.MaxValue;

        return (totalFuel, hoursRemaining);
    }

    /// <summary>
    /// Sends a chat warning to all online players when a generator's fuel
    /// drops below a configurable threshold. Rate-limited to one warning
    /// per generator per 300 seconds.
    /// </summary>
    public void CheckFuelLevels(float lowFuelThreshold, float warningInterval)
    {
        float now = Time.realtimeSinceStartup;

        foreach (BarricadeRegion region in BarricadeManager.regions.Values)
        {
            foreach (BarricadeDrop drop in region.drops)
            {
                InteractableGenerator gen = drop.model.GetComponent<InteractableGenerator>();
                if (gen == null || !gen.isPowered)
                    continue;

                float fuelPercent = gen.capacity > 0
                    ? (float)gen.fuel / gen.capacity
                    : 0f;

                if (fuelPercent < lowFuelThreshold)
                {
                    Transform genTransform = gen.transform;
                    if (!_fuelWarnings.TryGetValue(genTransform, out float lastWarning)
                        || now - lastWarning >= warningInterval)
                    {
                        _fuelWarnings[genTransform] = now;
                        ChatManager.serverSendMessage(
                            $"Generator fuel low: {fuelPercent:P0} remaining.",
                            Color.red,
                            toPlayer: null,
                            iconURL: null,
                            useRichTextFormatting: true
                        );
                    }
                }
            }
        }
    }
}

Power Zone Visualization

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

public class PowerZoneVisualizer
{
    /// <summary>
    /// Returns a list of all InteractablePower devices that are currently
    /// wired to any active generator, grouped by generator.
    /// </summary>
    public static Dictionary<Transform, List<Transform>> GetPowerZones()
    {
        Dictionary<Transform, List<Transform>> zones =
            new Dictionary<Transform, List<Transform>>();

        // Find all powered generators
        List<InteractableGenerator> poweredGens = new List<InteractableGenerator>();
        foreach (BarricadeRegion region in BarricadeManager.regions.Values)
        {
            foreach (BarricadeDrop drop in region.drops)
            {
                InteractableGenerator gen = drop.model.GetComponent<InteractableGenerator>();
                if (gen != null && gen.isPowered && gen.fuel > 0)
                    poweredGens.Add(gen);
            }
        }

        // For each generator, find powered devices in range
        foreach (InteractableGenerator gen in poweredGens)
        {
            List<Transform> wiredDevices = new List<Transform>();
            float sqrRange = gen.sqrWirerange;

            foreach (BarricadeRegion region in BarricadeManager.regions.Values)
            {
                foreach (BarricadeDrop drop in region.drops)
                {
                    InteractablePower powerDevice = drop.model.GetComponent<InteractablePower>();
                    if (powerDevice == null)
                        continue;

                    float sqrDist = (drop.model.position - gen.transform.position).sqrMagnitude;
                    if (sqrDist <= sqrRange)
                        wiredDevices.Add(drop.model);
                }
            }

            zones[gen.transform] = wiredDevices;
        }

        return zones;
    }
}

Mermaid Diagram: Generator Power Flow

Comparison: Generator vs. Other Power Sources

FeatureInteractableGeneratorSolar Panel (Mod)Wind Turbine (Mod)Car Battery
Fuel typeGasoline (fuel cans)SunlightWind strengthCharge from engine
Continuous outputYes (while fueled)Yes (daytime only)Yes (wind-dependent)Limited (capacity)
Wire rangeConfigurable per assetTypically smallerConfigurableNone (direct connect)
Fuel consumptionburn rate per tickNoneNoneDrains over time
Auto shutoffWhen fuel = 0At nightBelow wind thresholdWhen depleted
NoiseEngine audioNoneBlade audioNone
Cost to runGasoline (farmable)FreeFreeVehicle fuel + engine wear
Multiple generatorsOverlapping zonesOverlapping zonesOverlapping zonesSingle source
Server persistenceFull save/loadDepends on modDepends on modAttached to vehicle save

Failure Modes and Common Mistakes

  1. Siphoning during active consumptionaskSiphon removes fuel while the generator is running. If a player siphons the last remaining fuel, the generator shuts down mid-tick while askBurn was about to consume the same unit of fuel. Both the burner and the siphoner deplete the same fuel value, potentially resulting in a fuel integer underflow or a double-counted depletion if both occur in the same tick.

  2. Wiring recalculation on empty generators — Toggling a generator on with 0 fuel still triggers a full wiring recalculation for all nearby devices. The devices are temporarily marked isWired = true before the consumption tick discovers there is no fuel and toggles the generator off again. This flicker can cause visual artifacts (lights flash on/off) and audible noise (engine starts briefly).

  3. Squared wire range precision — The _sqrWirerange is computed once when the generator is initialized. If a plugin changes the _wirerange value after initialization without also updating _sqrWirerange, the wiring system continues to use the stale squared value, miscomputing which devices are in range. Devices near the edge of range are most affected.

  4. Concurrent askFill/askBurn raceaskFill and askBurn are not synchronized. If a player adds fuel and the consumption tick fires simultaneously, the added fuel may or may not be counted in the current consumption tick. The server's single-threaded update cycle means this is technically safe, but plugin-invoked askFill calls outside the main update loop can cause non-deterministic fuel levels.

  5. Generator overcapacity placement — A generator with fuelCapacity = 5000 can hold more fuel than a single fuel can provides (max 65535 for ItemFuelAsset.fuel). Players must fill the generator incrementally, and the UI shows the capacity cap but not how much fuel each can adds. This leads to confusion about whether the generator is "full" vs. "unable to accept more from this can."

How This Field Behaves Differently from the SDG Docs

  • SDG docs describe wire range as a fixed circle. The official documentation visualizes wire range as a perfect circle around the generator. In the SDK, the range check uses sqrMagnitude (square distance), which is a circle in world space, but surface-level pathfinding (e.g., wire path through walls) is not modeled. A device 10 meters away through a solid wall is as "wired" as one 10 meters away in open air. There is no line-of-sight check.

  • SDG docs claim generators consume fuel in "real-world seconds." Community resources list burn rates in "fuel per second." In the SDK, the consumption tick uses Time.realtimeSinceStartup - lastBurn > 1.0f as the timer, but the burn rate is a float multiplier applied to PLAYER_WIND (a wind/resistance constant). The actual fuel consumed per real-world second depends on burn * PLAYER_WIND, not a direct "fuel per second" value.

  • SDG docs suggest generators can be linked in series. Some documentation implies generators can chain power between each other. In the SDK, InteractableGenerator extends Interactable, not InteractablePower. Generators do not accept power from other generators. Only InteractablePower subclasses can receive power via wiring. Generators are power sources, not relays.

  • SDG docs list "electricity" as a tracked resource. The wiki sometimes describes electricity as a grid-level tracked resource. In the SDK, there is no grid-wide electricity tracking. Each generator independently toggles isWired on nearby devices. The wiring system does not model current, voltage, load, or capacity — it is a simple boolean toggle.

Performance Considerations

Wiring Recalculation Cost

Each wiring recalculation (generator toggle, device placement, device destruction) iterates all InteractablePower components within _wirerange:

  • Small base (10 generators, 50 devices): ~500 distance checks — sub-millisecond.
  • Large base (50 generators, 500 devices): ~25,000 distance checks — ~1ms.
  • Mega base (100 generators, 5000 devices per region): ~500,000 checks — ~20ms (may cause a frame spike).

Optimization for Large Servers

  1. Limit generator count per region — Beyond ~20 generators in one region, the wiring system becomes a noticeable CPU consumer during toggle events.
  2. Rate-limit toggle requests — Plugin-based auto-toggling should enforce a minimum cooldown between toggle events (at least 1 second).
  3. Use squared distance — The SDK already does this via _sqrWirerange. Custom plugins iterating devices should also use sqrMagnitude instead of Vector3.Distance.
  4. Cache nearby device lists — Plugins that repeatedly query the wiring state should cache the device list per generator and invalidate only on wiring recalculation, rather than re-querying every frame.

Deeper FAQ

Q: Can I power devices from two generators simultaneously?

Yes, but only for redundancy. A device's isWired is set to true if any generator within range is active and fueled. Two overlapping generators do not double the power or affect the device's behavior — the wiring check is binary. If one generator runs out of fuel, the device remains powered from the second generator (as long as it stays within range of the second after recalculation).

Q: Does generator placement height affect wire range?

No. The wire range check uses Vector3.sqrMagnitude, which is the 3D distance between the generator's position and the device's position. A generator placed on a tower 50 meters above a device at ground level is 50 meters away in wire range. Height is not discounted or ignored. For optimal coverage, place generators at the same elevation as the devices they power.

Q: Can generators be locked like storage containers?

Yes. Generators are barricades and use the standard ItemBarricadeAsset.isLocked lock system. A locked generator prevents unauthorized players from toggling it on/off, siphoning fuel, or refueling it. However, locked generators still power devices accessible to unauthorized players — the lock controls generator interaction, not device access.

Q: How many fuel cans does it take to fill a generator?

Depends on the generator's fuelCapacity and the fuel can's fuel value. A generator with fuelCapacity = 5000 requires:

  • 1 can with fuel=5000 (full large can)
  • 10 cans with fuel=500 (small cans)
  • 77 cans with fuel=65 (micro cans from a partially-filled source)

The fuel cap (capacity - fuel) comparison in askFill means overfilling is impossible — excess fuel is discarded, not spilled. Modders should warn players about wasted fuel when using large cans on nearly-full generators.

Q: What happens when generators are destroyed by structure collapse?

Structure collapse that destroys a barricade calls destroy() on the barricade, which invokes InteractableGenerator.ManualOnDestroy(). This deregisters the power, recalculates wiring for all affected devices, and drops any remaining fuel as a separate fuel can item at the generator's position. Surviving devices within another generator's range maintain power; isolated devices lose it.

Cross-References

Document history