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
| Field | Type | Purpose |
|---|---|---|
_capacity | ushort | Maximum fuel capacity (from ItemGeneratorAsset.fuelCapacity) |
_fuel | ushort | Current fuel level (stored in barricade state bytes 0–1) |
_isPowered | bool | On/off toggle state |
_wirerange | float | Maximum wiring distance |
_sqrWirerange | float | Pre-computed squared wire range for fast distance checks |
burn | float | Fuel burn rate (from ItemGeneratorAsset.burn) |
lastBurn | float | Time.realtimeSinceStartup of last fuel consumption tick |
isWiring | bool | Whether 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
tellFuelRPC 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
| Direction | Message | Purpose |
|---|---|---|
| Client → Server | SendToggleRequest | Toggle generator on/off |
| Server → All | SendFuel / ReceiveFuel | Sync fuel level |
| Server → All | SendWiring | Sync 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
- Immediate vs InteractablePower — Generator extends
Interactable, notInteractablePower, because it provides power rather than consuming it. - Squared wire range —
_sqrWirerangeis pre-computed for fastsqrMagnitudedistance checks. - Wiring recalculation — Toggling power triggers a full wiring update for all nearby devices.
- 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
| Feature | InteractableGenerator | Solar Panel (Mod) | Wind Turbine (Mod) | Car Battery |
|---|---|---|---|---|
| Fuel type | Gasoline (fuel cans) | Sunlight | Wind strength | Charge from engine |
| Continuous output | Yes (while fueled) | Yes (daytime only) | Yes (wind-dependent) | Limited (capacity) |
| Wire range | Configurable per asset | Typically smaller | Configurable | None (direct connect) |
| Fuel consumption | burn rate per tick | None | None | Drains over time |
| Auto shutoff | When fuel = 0 | At night | Below wind threshold | When depleted |
| Noise | Engine audio | None | Blade audio | None |
| Cost to run | Gasoline (farmable) | Free | Free | Vehicle fuel + engine wear |
| Multiple generators | Overlapping zones | Overlapping zones | Overlapping zones | Single source |
| Server persistence | Full save/load | Depends on mod | Depends on mod | Attached to vehicle save |
Failure Modes and Common Mistakes
Siphoning during active consumption —
askSiphonremoves fuel while the generator is running. If a player siphons the last remaining fuel, the generator shuts down mid-tick whileaskBurnwas about to consume the same unit of fuel. Both the burner and the siphoner deplete the samefuelvalue, potentially resulting in a fuel integer underflow or a double-counted depletion if both occur in the same tick.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 = truebefore 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).Squared wire range precision — The
_sqrWirerangeis computed once when the generator is initialized. If a plugin changes the_wirerangevalue 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.Concurrent askFill/askBurn race —
askFillandaskBurnare 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-invokedaskFillcalls outside the main update loop can cause non-deterministic fuel levels.Generator overcapacity placement — A generator with
fuelCapacity = 5000can hold more fuel than a single fuel can provides (max 65535 forItemFuelAsset.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.0fas the timer, but theburnrate is a float multiplier applied toPLAYER_WIND(a wind/resistance constant). The actual fuel consumed per real-world second depends onburn * 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,
InteractableGeneratorextendsInteractable, notInteractablePower. Generators do not accept power from other generators. OnlyInteractablePowersubclasses 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
isWiredon 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
- Limit generator count per region — Beyond ~20 generators in one region, the wiring system becomes a noticeable CPU consumer during toggle events.
- Rate-limit toggle requests — Plugin-based auto-toggling should enforce a minimum cooldown between toggle events (at least 1 second).
- Use squared distance — The SDK already does this via
_sqrWirerange. Custom plugins iterating devices should also usesqrMagnitudeinstead ofVector3.Distance. - 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
- Interactable Oxygenator — Air Supply System — Oxygenator registers/deregisters its bubble based on generator power state.
- Interactable Trap — Trigger Damage System — Traps require generator power via
InteractablePowerwiring. - ItemFuelAsset — Fuel Can Items — Fuel can item definition and how it interacts with
askFill. - ItemGeneratorAsset — Generator Asset Definition — Asset-level fuel capacity, burn rate, and wire range configuration.
