Skip to content

VehicleManager — Vehicle Spawning and Physics

The VehicleManager is the server-authoritative manager responsible for the complete lifecycle of all InteractableVehicle instances in an Unturned level. It owns vehicle spawning and despawn, fuel and damage sync, passenger management, tire damage, lock and skin state, the respawn timer system, and the batched state replication protocol that keeps clients synchronized with vehicle positions, rotations, velocities, and suspension states. At 3661 lines in Unturned/Managers/VehicleManager.cs, it is one of the largest manager classes in the SDK.

VehicleManager extends SteamCaller and follows the same static-delegate pattern as the other runtime managers: a static manager field, a public static instance accessor, and all public API methods are static. The manager maintains a single List<InteractableVehicle> _vehicles that holds every spawned vehicle in the level, and a monotonically incrementing highestInstanceID counter that assigns unique network instance IDs.

Source file: Unturned/Managers/VehicleManager.cs — 3661 lines. This article references API surface, event delegates, and internal implementation visible in the U3 SDK.

Who this article is for

This article is written for plugin and module developers who need to spawn, damage, repair, or manage vehicles programmatically. It presupposes familiarity with the SteamCaller network transport layer, NetId ownership, and the Unturned asset system. If you are new to the SDK manager architecture, read ItemManager — Item Spawning first.

What you'll learn

  • The vehicle spawn/despawn lifecycle, including spawnVehicleInternal and SpawnVehicleV3
  • The fuel, health, battery, and tire damage model
  • Passenger enter/exit/swap seat management
  • The respawn timer and respawn queue system
  • The vehicle state replication protocol: batched position/rotation/velocity sync with sequenced packets
  • Tire and wheel suspension replication
  • Lock, skin, headlight, siren, and horn state synchronization
  • Plugin hook points: damage, repair, enter, exit, lockpick, carjack, siphon

Vehicle list and instance ID allocation

The manager holds all vehicles in a static list:

csharp
private static List<InteractableVehicle> _vehicles;
public static List<InteractableVehicle> vehicles => _vehicles;

Every vehicle receives a unique instance ID at spawn time. The counter is never decremented; IDs are unique for the lifetime of the server process.

csharp
private static uint highestInstanceID;
private static uint allocateInstanceID()
{
    return ++highestInstanceID;
}

The findVehicleByNetInstanceID method performs a linear scan across the vehicles list. At the scale of most Unturned levels (hundreds of vehicles, not thousands) this is acceptable, but plugin developers working with large vehicle counts should cache instance ID lookups.

Spawn limits by level size

The maxInstances property reads from Provider.modeConfigData.Vehicles.Max_Instances_* keyed by ELevelSize:

Level SizeMax Instances
TINYConfig value
SMALLConfig value
MEDIUMConfig value
LARGEConfig value
INSANEConfig value

Vehicles that exceed the configured maximum are not prevented from spawning by the manager itself — the limit is advisory and enforced by the respawn system, which skips spawning new vehicles when the count is at the cap.

Spawning vehicles

spawnVehicleInternal — The internal spawn pipeline

The spawnVehicleInternal method is the common entry point called by the public spawnVehicleV2 and spawnLockedVehicleForPlayerV2 overloads. It resolves the asset (handling VehicleRedirectorAsset indirection), determines paint color (from the caller or from the redirector), determines lock state (locked if an owner is specified), and delegates to SpawnVehicleV3.

csharp
internal static InteractableVehicle spawnVehicleInternal(Asset asset, Vector3 point,
    Quaternion angle, CSteamID owner, CSteamID groupId, Color32? preferredColor)

The redirector resolution logic:

  1. If the passed asset is a VehicleRedirectorAsset, resolve its TargetVehicle GUID.
  2. If no preferredColor was provided but the redirector has a SpawnPaintColor, use that.
  3. Otherwise fall through to the caller's color or zero.

Lock state is determined by whether the owner is CSteamID.Nil: if an owner is specified, the vehicle spawns locked and the group ID is set.

SpawnVehicleV3 — Full-parameter spawn

SpawnVehicleV3 is the most complete spawn method. It accepts every configurable vehicle parameter:

csharp
public static InteractableVehicle SpawnVehicleV3(VehicleAsset asset, ushort skinID,
    ushort mythicID, float roadPosition, Vector3 point, Quaternion angle, bool sirens,
    bool blimp, bool headlights, bool taillights, ushort fuel, ushort health,
    ushort batteryCharge, CSteamID owner, CSteamID group, bool locked, byte[][] turrets,
    byte tireAliveMask, Color32 paintColor)

The method:

  1. Claims a block of NetIds from NetIdRegistry.ClaimBlock.
  2. Calls manager.addVehicle to instantiate the prefab and configure it.
  3. Sends the full vehicle state to all remote clients via SendSingleVehicle.Invoke.
  4. Calls vehicle.NotifyFirstSpawned() to trigger the spawned event.

The batteryCharge parameter has three modes:

  • 0 — spawn without a battery
  • ushort.MaxValue — randomly spawn a battery per asset configuration
  • Any other value — force a specific battery charge

Tire alive mask randomization

csharp
public static byte getVehicleRandomTireAliveMask(VehicleAsset asset)

When asset.canTiresBeDamaged is true, each of the 8 bit positions is randomly set to 1 with probability Provider.modeConfigData.Vehicles.Has_Tire_Chance. When tires cannot be damaged, the mask is byte.MaxValue (all tires present).

Damage and repair

vehicle damage pipeline

csharp
public static void damage(InteractableVehicle vehicle, float damage, float times,
    bool canRepair, CSteamID instigatorSteamID, EDamageOrigin damageOrigin)

The damage pipeline:

  1. Null-check vehicle and asset. Return silently on null.
  2. Check invulnerability flags: if the vehicle is not vulnerable to damage, explosions, or environment, log an error and return.
  3. Apply the config-defined Armor_Multiplier to times.
  4. Calculate totalDamage = (ushort)(damage * times).
  5. Fire onDamageVehicleRequested delegate. Plugins can modify totalDamage and canRepair, or cancel via shouldAllow.
  6. If allowed and totalDamage >= 1, call vehicle.askDamage(totalDamage, canRepair).

Tire damage

csharp
public static void damageTire(InteractableVehicle vehicle, int tireIndex,
    CSteamID instigatorSteamID, EDamageOrigin damageOrigin)

Tire damage fires onDamageTireRequested and, if allowed, calls vehicle.askDamageTire(tireIndex).

Repair

csharp
public static void repair(InteractableVehicle vehicle, float damage, float times,
    CSteamID instigatorSteamID)

The repair pipeline:

  1. Check vehicle is not exploded and not already fully repaired.
  2. Fire onRepairVehicleRequested — plugins can modify the heal amount or cancel.
  3. Call vehicle.askRepair(amount).

Fuel system

Fuel is synchronized through sendVehicleFuel which invokes SendVehicleFuel to all clients. The client-side ReceiveVehicleFuel updates the fuel gauge on the client copy of the vehicle.

The siphonFromVehicle method allows players to extract fuel:

csharp
public static ushort siphonFromVehicle(InteractableVehicle vehicle,
    Player instigatingPlayer, ushort desiredAmount)

It fires onSiphonVehicleRequested for plugin interception, clamps the desired amount to available fuel, calls vehicle.askBurnFuel, and invokes sendVehicleFuel to broadcast the updated fuel level.

Passenger management

Enter vehicle

The enterVehicle static method sends an enter request to the server:

csharp
public static void enterVehicle(InteractableVehicle vehicle)

It packs the vehicle's physics profile hash and engine type into the SendEnterVehicleRequest RPC. The server validates the request and, if accepted, broadcasts SendEnterVehicle which calls vehicle.addPlayer(seat, player).

Exit vehicle

csharp
public static void exitVehicle()

Sends the player's current rigidbody velocity with the exit request. The server broadcasts SendExitVehicle which calls vehicle.removePlayer(seat, point, angle, forceUpdate).

Swap seat

csharp
public static void swapVehicle(byte toSeat)

Sends SendSwapVehicleRequest. On acceptance, the server broadcasts SendSwapVehicleSeats which calls vehicle.swapPlayer(fromSeat, toSeat).

Delegate hooks

DelegateSignatureFires
onEnterVehicleRequested(Player, InteractableVehicle, ref bool)Before player enters
onExitVehicleRequested(Player, InteractableVehicle, ref bool, ref Vector3, ref float)Before player exits
onSwapSeatRequested(Player, InteractableVehicle, ref bool, byte, ref byte)Before seat swap
onVehicleLockpicked(InteractableVehicle, Player, ref bool)When lockpick attempt occurs
onVehicleCarjacked(InteractableVehicle, Player, ref bool, ref Vector3, ref Vector3)When empty vehicle is shoved

Lock and skin state

Lock state

The lock state is toggled through ReceiveVehicleLockRequest (rate-limited at 4 Hz). The server validates that the requesting player is the driver and that the vehicle can be locked. The driver's Steam ID and group ID are used as lock credentials. The lock state is broadcast via SendVehicleLockState to all clients.

csharp
public static void ServerSetVehicleLock(InteractableVehicle vehicle, CSteamID ownerID,
    CSteamID groupID, bool isLocked)

Unlocking via lockpick calls unlockVehicle, which fires onVehicleLockpicked and — if allowed — clears the lock state via ServerSetVehicleLock with CSteamID.Nil.

Skin state

The skin system supports economy cosmetic items. ReceiveVehicleSkinRequest (rate-limited at 2 Hz) reads the player's equipped skin items from the economy service and broadcasts the skin/mythic ID pair via SendVehicleSkin.

State replication protocol

The most performance-sensitive system in VehicleManager is the batched state replication sent via SendVehicleStates.

The sequenced batch

csharp
private static uint seq;
private static readonly ClientStaticMethod SendVehicleStates = ClientStaticMethod.Get(ReceiveVehicleStates);

The server periodically gathers all vehicle states into a single packet with:

  • A monotonically increasing sequence number (seq)
  • A count of vehicles in this batch
  • Per-vehicle: instanceID, compressed position (ClampedVector3 with POSITION_FRAC_BIT_COUNT), compressed rotation (ReadQuaternion with ROTATION_BIT_COUNT), speed, forward velocity, steering input, and velocity input

The client rejects outdated sequence numbers (newSeq <= seq) to prevent stale state from overwriting newer data.

High-quality detail flag

Each vehicle in the batch carries an includesHighQualityDetails bit. When set, the packet additionally includes per-wheel suspension state (compressed to 4 bits) and ground material NetId. When not set, wheel states default to full extension.

For vehicles that use UsesEngineRpmAndGears, the HQ detail includes the current gear index and normalized engine RPM.

Wheel suspension replication

The asset's replicatedWheelIndices array determines which wheels get their suspension state serialized. At initial spawn, all replicated wheel states are included. During state updates, only the compressed suspension value and ground material are sent (when HQ detail is enabled).

Respawn system

The respawn timer is managed through a round-robin index:

csharp
private static ushort respawnVehicleIndex;
private static float lastTick;

The tick method iterates one vehicle per tick (checking respawnVehicleIndex), advancing the index each frame. If the current vehicle is dead and its respawn timer has elapsed, the vehicle is destroyed and a new one is spawned at its original position using the original spawn table.

The respawn delay and the decision to respawn are governed by Provider.modeConfigData.Vehicles.

Destroying vehicles

Single vehicle destruction

csharp
public static void askVehicleDestroy(InteractableVehicle vehicle)

Server-side only. Forces all players out of the vehicle, broadcasts SendDestroySingleVehicle, and removes the vehicle from the list.

Bulk destruction

csharp
public static void askVehicleDestroyAll()

Iterates all vehicles from last to first, force-removes all players, then broadcasts SendDestroyAllVehicles.

DestroyVehicleCommon

Both destruction paths converge on DestroyVehicleCommon:

  1. Force-exit any player whose movement.getVehicle() still references the destroyed vehicle (edge case where enter/exit and destroy race on the same frame).
  2. Uproot any barricades planted on the vehicle and on any train cars.
  3. Set vehicle.IsPendingDestroy = true.
  4. Fire OnPreDestroyVehicle event.
  5. Release the NetId and transform registration.
  6. Call EffectManager.ClearAttachments for any effects parented to the vehicle.
  7. Destroy the GameObject.

Decay system

Vehicles have an optional decay system controlled by SAVEDATA_VERSION_ADDED_DECAY = 13. The serversideData stores the lastActive timestamp. When a vehicle has not been interacted with (no player entry, no damage) for a configurable duration, it decays:

  1. Health is reduced periodically by a decay amount.
  2. When health reaches zero, the vehicle enters the exploded state.
  3. Decay only applies to naturally-spawned vehicles (isNaturalSpawned from SAVEDATA_VERSION_ADDED_NATURAL_SPAWNED = 17).
  4. Configuration keys from Provider.modeConfigData.Vehicles control decay rate, delay, and whether decay is enabled.

getVehiclesInRadius

csharp
public static void getVehiclesInRadius(Vector3 center, float sqrRadius,
    List<InteractableVehicle> result)

Iterates all vehicles, skipping dead vehicles, and checks squared distance. Because the check iterates the entire vehicles list, performance degrades linearly with total vehicle count. For large maps with many vehicles, consider calling this infrequently or caching results.

Natural spawn tracking

SAVEDATA_VERSION_ADDED_NATURAL_SPAWNED = 17 added the isNaturalSpawned flag. This flag is set on vehicles that were spawned by the natural respawn system (as opposed to admin-spawned or plugin-spawned vehicles). Only naturally-spawned vehicles are eligible for decay and respawn timer management.

Train vehicle handling

Vehicles with EEngine.TRAIN use a special position packing scheme:

csharp
if (vehicle.asset.engine == EEngine.TRAIN)
{
    position = InteractableVehicle.PackRoadPosition(vehicle.roadPosition);
}
else
{
    position = vehicle.transform.position;
}

Train position is packed into a Vector3 where the X coordinate stores the road path position. Train cars are linked via the trainCars array, and barricade uprooting processes each car individually during vehicle destruction.

Engine RPM and gear replication

Vehicles with UsesEngineRpmAndGears (sports cars, performance vehicles) replicate:

  1. Current gear: Packed as a signed integer (-1 = reverse, 0 = neutral, 1-N = forward gears). The gear is clamped to the asset's forwardGearRatios array length.
  2. Engine RPM: Normalized to [0, 1] via 4-bit unsigned normalized float. Deserialized as Mathf.Lerp(EngineIdleRpm, EngineMaxRpm, normalizedRpm).

When HQ detail is not included in the state update, the vehicle defaults to gear 1 and idle RPM.

Edge cases in passenger management

Concurrent enter/exit races

DestroyVehicleCommon handles the case where a player has requested to enter or exit a vehicle on the same frame the server is destroying it:

csharp
foreach (SteamPlayer client in Provider.clients)
{
    Player player = client.player;
    if (player.movement.getVehicle() == vehicle)
    {
        Debug.Assert(player.movement.hasPendingVehicleChange,
            "Player should already be exiting vehicle");
        player.movement.ApplyPendingVehicleChange();
        Debug.Assert(player.movement.getVehicle() == null,
            "Player should no longer be in vehicle");
    }
}

This ensures no player component remains parented to a destroyed vehicle's transform.

Train vehicle barricade management

Trains consist of multiple linked cars. Each car can have barricades planted on it. When a train is destroyed, the manager must uproot barricades from all cars:

csharp
if (vehicle.trainCars != null)
{
    for (int carIndex = 1; carIndex < vehicle.trainCars.Length; ++carIndex)
    {
        BarricadeManager.uprootPlant(vehicle.trainCars[carIndex].root);
    }
}

Car index 0 is the primary locomotive (vehicle.transform). Cars at index 1+ are separate transforms stored in the trainCars array. Each car independently tracks its barricade region.

Vehicle search by type

Beyond the instance-ID-based lookup, vehicles can be searched by type using getVehiclesInRadius and filtering by asset. For targeted queries:

csharp
public static InteractableVehicle findVehicleByNetInstanceID(uint instanceID)

This is the primary lookup method used by all state update RPCs. Because it scans the entire vehicles list, it is O(n). For state updates that process 50+ vehicles per packet, this linear scan is acceptable because the list is typically small (hundreds, not thousands).

Plugin integration patterns

Damage interception

To create invulnerable vehicles for a specific game mode:

csharp
VehicleManager.onDamageVehicleRequested += (instigator, vehicle, ref damage,
    ref canRepair, ref allow, origin) =>
{
    if (vehicle.asset.GUID == myInvulnerableVehicleGuid)
        allow = false;
};

Custom respawn logic

To control where and when vehicles respawn:

csharp
// Disable natural respawn by setting all timers to max
// Then manually call SpawnVehicleV3 when needed
VehicleManager.SpawnVehicleV3(asset, 0, 0, 0f, point, angle,
    false, false, false, false,
    asset.fuel, asset.health, 10000,
    CSteamID.Nil, CSteamID.Nil, false,
    null, byte.MaxValue, paintColor);

Fuel siphon interception

csharp
VehicleManager.onSiphonVehicleRequested += (vehicle, player,
    ref allow, ref desiredAmount) =>
{
    // Allow only half the requested amount
    desiredAmount = (ushort)(desiredAmount / 2);
};

Vehicle writing and reading

WriteVehicle — Serialization

The WriteVehicle method serializes the full state of a vehicle for initial replication:

  • Asset GUID
  • Skin ID and mythic ID
  • Position (or packed road position for TRAIN engine type)
  • Rotation (compressed quaternion)
  • Boolean flags: sirens, blimp floating, headlights, taillights
  • Fuel, exploded flag, health, battery charge
  • Lock owner, lock group, locked flag
  • Passenger list (Steam IDs for each seat; CSteamID.Nil for empty seats)
  • Instance ID
  • Tire alive mask
  • NetId
  • Paint color
  • Replicated wheel suspension states

ReceiveSingleVehicle — Client deserialization

The client deserializes the vehicle from the incoming packet, calls manager.addVehicle to instantiate it, and then reads the replicated wheel suspension states to teleport the wheels into their correct positions.

Plugin integration summary

HookWhen to use
onDamageVehicleRequestedIntercept or modify vehicle damage
onRepairVehicleRequestedIntercept or modify vehicle repair
onDamageTireRequestedIntercept or cancel tire damage
onEnterVehicleRequestedBlock or allow vehicle entry
onExitVehicleRequestedBlock or modify vehicle exit
onSwapSeatRequestedRedirect seat changes
onVehicleLockpickedIntercept lockpick attempts
onVehicleCarjackedModify carjack force/torque
onSiphonVehicleRequestedIntercept fuel siphoning
OnToggleVehicleLockRequestedIntercept lock toggle
OnPreDestroyVehicleClean up before vehicle destruction

Serialization version history

ConstantVersionChange
SAVEDATA_VERSION_ADDED_DECAY13Added decay system
SAVEDATA_VERSION_REPLACED_ID_WITH_GUID14Replaced legacy IDs with GUIDs
SAVEDATA_VERSION_BATTERY_GUID15Battery GUID support
SAVEDATA_VERSION_ADDED_PAINT_COLOR16Paint color field
SAVEDATA_VERSION_ADDED_NATURAL_SPAWNED17Natural spawn tracking

The addVehicle instantiation method

The internal addVehicle method is the factory for all InteractableVehicle instances. It is called from both SpawnVehicleV3 (server-originated spawn) and ReceiveSingleVehicle (client-side deserialization). The method:

  1. Resolves the VehicleAsset from the provided GUID.
  2. Selects the prefab based on build context:
    • Dedicated server: Vehicle_Dedicated (no renderers, no audio).
    • Listen server: Vehicle_Server (minimal visuals).
    • Client: Full prefab with all components.
  3. Instantiates the prefab at the configured position and rotation.
  4. Assigns the vehicle's identity:
    • instanceID from allocateInstanceID().
    • skinID and mythicID for cosmetic item display.
    • roadPosition for train alignment.
  5. Applies vehicle state:
    • Fuel, health, battery charge, exploded flag.
    • Lock status: lockedOwner, lockedGroup, isLocked.
    • tireAliveMask: Each bit indicates whether a tire is present.
    • Paint color: 32-bit RGBA stored as Color32.
    • Turret data: byte[][] where each entry is the turret's ammo/state array.
  6. Registers the vehicle:
    • Assigns the NetId block (NETIDS_PER_VEHICLE IDs per vehicle).
    • Registers the transform at NetId + 1 for collision network sync.
    • Adds the vehicle to the _vehicles list.
  7. Initializes decay state:
    • Sets lastActive timestamp.
    • Clears decay flags for newly spawned vehicles.

Network ID allocation

Each vehicle consumes a contiguous block of NetId values:

csharp
NetId netId = NetIdRegistry.ClaimBlock(NETIDS_PER_VEHICLE);

The allocation ensures:

  • netId + 0 — Primary NetId for the InteractableVehicle component.
  • netId + 1 — Transform NetId registered at NetIdRegistry.AssignTransform.

During destruction, the transform NetId is released before the primary NetId:

csharp
NetIdRegistry.ReleaseTransform(vehicle.GetNetId() + 1, vehicle.transform);
vehicle.ReleaseNetId();

Server-side initial state to clients

SendInitialGlobalState handles the batched transmission of all vehicles to a newly connected client. The batching at 50 vehicles per packet prevents network congestion:

csharp
const int MAX_TELLVEHICLES_PER_PACKET = 50;

For a map with 200 vehicles, this means 4 packets instead of 1 large one. Each packet is sent reliably (ENetReliability.Reliable). After all vehicle packets are sent, BarricadeManager.SendVehicleRegions transmits the barricades planted on vehicles.

Physics profile hash sync

When entering a vehicle, the client sends the vehicle's PhysicsProfileAsset hash to the server:

csharp
VehiclePhysicsProfileAsset physicsProfile = vehicle.asset.physicsProfileRef.Find();
byte[] physicsProfileHash = physicsProfile != null ? physicsProfile.hash : new byte[0];
SendEnterVehicleRequest.Invoke(ENetReliability.Unreliable, vehicle.instanceID,
    vehicle.asset.hash, physicsProfileHash, (byte)vehicle.asset.engine);

This allows the server to validate that the client has the correct physics profile for the vehicle. Mismatched profiles can cause desync in vehicle handling.

Headlight battery drain prevention

The client-side sendVehicleHeadlights method includes a defensive check:

csharp
if (!vehicle.asset.hasHeadlights)
{
    if (wantsHeadlightsOn) return;
}

This prevents clients from accidentally draining the battery by toggling headlights on vehicles that don't visually support them. The server does not know whether a vehicle has headlights (the asset may differ between client and server due to mod mismatch), so this is a client-side safeguard.

Key type references

  • InteractableVehicle — The runtime vehicle component on the spawned prefab
  • VehicleAsset — The asset definition (2809 lines) with speed, fuel, health, turret, wheel, and engine configuration
  • VehicleRedirectorAsset — Redirector asset that maps a legacy ID to a target vehicle with optional paint color override
  • VehiclePhysicsProfileAsset — Physics tuning asset with mass, drag, friction, suspension, and torque profiles
  • Passenger — Seat occupant data: player reference and seat index
  • Wheel — Per-wheel data: suspension state, ground material, steering mode

addVehicle — Internal instantiation

The addVehicle method (called by both SpawnVehicleV3 and ReceiveSingleVehicle) is the single point of InteractableVehicle creation. It:

  1. Resolves the VehicleAsset from the GUID.
  2. Loads the appropriate prefab (dedicated server prefab vs client prefab).
  3. Instantiates the prefab at the given position and rotation.
  4. Applies the asset's configuration: sets instanceID, skinID, mythicID, roadPosition, fuel, health, battery charge, isExploded flag, lock state (owner + group + locked flag), tireAliveMask, paint color, and NetId.
  5. Configures turret ammunition from the turret data arrays.
  6. Applies decay data and last active timestamp.
  7. Assigns the vehicle to the global vehicles list.

The dedicated server uses a stripped-down prefab (Vehicle_Dedicated) that lacks renderers, audio sources, and other client-only components. The client prefab includes the full visual and audio model.

Server-side enter/exit handling

Enter request processing

ReceiveEnterVehicleRequest validates:

  1. The requesting player is connected and alive.
  2. The vehicle exists, is not dead, and is not full.
  3. The player is within interaction range.
  4. The vehicle is not locked against the player (or the player has ownership).

If validation passes, the server broadcasts SendEnterVehicle with the vehicle instance ID, seat index, and player Steam ID.

Exit request processing

ReceiveExitVehicleRequest validates:

  1. The requesting player is in the vehicle.
  2. The exit point is safe (the player will not be teleported inside terrain or a barricade).

On accept, the server broadcasts SendExitVehicle with the exit position and yaw.

Seat swap

ReceiveSwapSeatRequest validates:

  1. The target seat exists on the vehicle.
  2. The target seat is empty (or the player can swap through occupied seats).
  3. The onSwapSeatRequested delegate does not redirect.

Force remove all players

forceRemoveAllPlayers is called during vehicle destruction. It iterates all passengers, calls removePlayer on each seat, and asserts isEmpty afterward. Developers must be aware of the edge case where a player's movement.getVehicle() still references the vehicle mid-change — DestroyVehicleCommon handles this by applying any pending vehicle changes before uprooting barricades.

The vehicle tick and respawn loop

The tick method is called every frame from the game's update loop:

csharp
private static float lastTick;

On each tick, the manager processes the vehicle at respawnVehicleIndex:

  1. If the vehicle is not dead, skip.
  2. If the vehicle was recently destroyed (exploded), check if the explosion timer has elapsed.
  3. If the vehicle should respawn, destroy the existing dead vehicle and spawn a new one using the original spawn table data.
  4. Advance respawnVehicleIndex wrapping around to 0 when it reaches vehicles.Count.

The respawn system relies on shouldRespawnReloadedVehicles flag. When a vehicle asset is replaced (via hot reload), this flag triggers respawn of all vehicles of that type.

Battery system

The battery is a separate entity from the vehicle's fuel system. The batteryCharge field (ushort) represents the battery's current charge level, and ReceiveVehicleBatteryCharge synchronizes it independently from fuel.

The battery GUID system (SAVEDATA_VERSION_BATTERY_GUID = 15) allows vehicles to reference a specific battery item asset for the battery that spawns with the vehicle.

Headlight, siren, blimp, and horn synchronization

Headlights

ReceiveToggleVehicleHeadlights (rate-limited at 10 Hz) validates:

  1. The requesting player is alive and in the vehicle.
  2. The vehicle supports headlights (asset.hasHeadlights).
  3. The player is the driver.
  4. The wantsHeadlightsOn state differs from the current state.
  5. The vehicle has enough battery charge (checked via canTurnOnLights).

On acceptance, SendVehicleHeadlights broadcasts the new state.

Sirens and blimp

Sirens (SendVehicleSirens) and blimp float state (SendVehicleBlimp) are synchronized via their respective RPCs. The client determines the bonus mode (0 = siren, 1 = hook, 2 = blimp) in sendVehicleBonus because the server does not know which features the vehicle asset supports.

Horn

SendVehicleHorn is a simple event notification — it has no on/off toggle, just a "play the horn" signal.

Physics profile and carjack mechanics

The carjackVehicle method applies force to an empty vehicle's rigidbody:

csharp
public static void carjackVehicle(InteractableVehicle vehicle,
    Player instigatingPlayer, Vector3 force, Vector3 torque)

The force is scaled by:

  1. physicsProfile.carjackForceMultiplier from the VehiclePhysicsProfileAsset (if present).
  2. vehicle.asset.carjackForceMultiplier.

The onVehicleCarjacked delegate allows plugins to modify force and torque or cancel the action entirely.

Wheel suspension and ground material

During state updates with HQ detail enabled, each replicated wheel receives:

  • replicatedSuspensionState: a 4-bit normalized float where 0 = fully compressed and 1 = fully extended.
  • replicatedGroundMaterial: a PhysicsMaterialNetId that identifies the surface type (concrete, grass, gravel, metal, etc.).

These values drive the vehicle's visual suspension animation and tire friction during rendering.

Initial replication to new clients

SendInitialGlobalState sends vehicles to newly connecting clients in batches of 50:

csharp
const int MAX_TELLVEHICLES_PER_PACKET = 50;
int requiredPacketCount = ((totalVehicleCount - 1) / MAX_TELLVEHICLES_PER_PACKET) + 1;

This prevents a single massive packet from overwhelming the new client. After all vehicle batches are sent, BarricadeManager.SendVehicleRegions is called to send the barricades planted on vehicles.

Serialization version history