Skip to content

UseableBarricade — Building Placement

UseableBarricade is the client-side placement engine for all barricade items in Unturned. At approximately 1,200 lines, it is a monolithic system handling 15+ EBuild types with individual raycasting, preview rendering, and validation logic. It activates when the player equips any ItemBarricadeAsset and manages the full lifecycle from equipping → previewing → positioning → rotating → server validation → spawning.

Source code location: Unturned/Useable/UseableBarricade.cs

Architecture Overview

UseableBarricade extends Useable and maintains three distinct transforms during placement:

  • help — the full placement preview model
  • guide — the root for hierarchy traversal (used for recursive highlighting on sentries)
  • arrow — an arrow indicator showing rotation

The class uses static Collider[] pools to avoid per-frame allocations in overlap checks, and separates dedicated server code paths that skip all visual/audio work.

Equip and Preview Initialization

When equip() is called, the class determines the animation length and initializes the preview ghost:

csharp
public override void equip()
{
    player.animator.play("Equip", true);
    useTime = player.animator.GetAnimationLength("Use");
}

The preview ghost is loaded from the barricade asset's placementPreviewRef (or a full barricade fallback). Colliders are destroyed to prevent physics interference. Bounds are computed for overlap detection:

csharp
if (col != null)
{
    boundsUse = true;
    boundsCenter = help.InverseTransformPoint(col.bounds.center);
    boundsExtents = col.bounds.extents;
    Destroy(col);
}
boundsOverlap = boundsExtents + new Vector3(0.5f, 0.5f, 0.5f);

The overlap padding (0.5) prevents adjacent placement too close to the buildable.

The arrow rotation varies by build type:

csharp
if (equippedBarricadeAsset.build == EBuild.DOOR || ... || EBuild.HATCH)
    arrow.localRotation = Quaternion.identity; // Flat on ground
else if (equippedBarricadeAsset.build == EBuild.MANNEQUIN)
    arrow.localEulerAngles = new Vector3(-90.0f, 0.0f, 0.0f);
else
    arrow.localRotation = Quaternion.Euler(90, 0, 0); // Standard

Build Type Dispatch — checkSpace()

The core placement logic dispatches based on equippedBarricadeAsset.build (the EBuild enum):

FORTIFICATION / SHUTTER / GLASS

Target "Slot" logic objects embedded in structure prefabs. Uses Raycast with SLOTS_INTERACT mask:

csharp
if (Mathf.Abs(Vector3.Dot(colliderTransform.right, Vector3.up)) > 0.5f)
{
    angle_y = Quaternion.LookRotation(colliderTransform.forward).eulerAngles.y;
    if (Vector3.Dot(MainCamera.instance.transform.forward, colliderTransform.forward) < 0.0f)
        angle_y += 180.0f;
}

For barricade/structure parented slots, the point snaps exactly:

csharp
if (colliderTransform.parent.CompareTag("Barricade") || colliderTransform.parent.CompareTag("Structure"))
{
    point = colliderTransform.position - (hit.normal * equippedBarricadeAsset.offset);
}

BARRICADE / TANK / STORAGE / GENERATOR / BED (etc.)

These 20+ build types use SphereCast with BARRICADE_INTERACT mask. Surface normal filtering:

csharp
if (hit.normal.y < 0.01f) { /* blocked */ }
if (hit.normal.y > 0.75)
    point = hit.point + (hit.normal * equippedBarricadeAsset.offset);
else
    point = hit.point + (Vector3.up * equippedBarricadeAsset.offset);

If near-horizontal (normal.y > 0.75), offset along normal. If sloped, offset strictly upward.

VEHICLE

Uses Raycast upward with BARRICADE_INTERACT. Linecast prevents ceiling clip:

csharp
bool hitAnything = Physics.Linecast(hit.point, point, out hitInfo, RayMasks.BLOCK_BARRICADE);
if (hitAnything) { /* blocked */ }

DOOR

Targets named "Door" transforms with SLOTS_INTERACT. Overlap uses BLOCK_FRAME:

csharp
if (Physics.OverlapSphereNonAlloc(point, equippedBarricadeAsset.radius,
    checkColliders, RayMasks.BLOCK_FRAME) > 0) { /* blocked */ }

HATCH

Four-directional dot-product comparison determines opening side:

csharp
float dot_0 = Vector3.Dot(MainCamera.instance.transform.forward, hit.transform.forward);
float dot_1 = Vector3.Dot(MainCamera.instance.transform.forward, hit.transform.right);
// ... pick lowest (most negative) dot

LADDER

Dual-mode: "Climb" logic object snap OR wall placement with left/right clearance checks:

csharp
if (Physics.OverlapSphereNonAlloc(point + (Quaternion.Euler(0, angle_y, 0) * Vector3.right * 0.5f),
    0.1f, checkColliders, RayMasks.BLOCK_BARRICADE) > 0)
    return false; // Right side blocked

FREEFORM / SENTRY_FREEFORM

Three-axis rotation via composed quaternion:

csharp
Quaternion rotation = Quaternion.Euler(0, angle_y + rotate_y, 0);
rotation *= Quaternion.Euler(-90 + angle_x + rotate_x, 0, 0);
rotation *= Quaternion.Euler(0, angle_z + rotate_z, 0);

FARM / OIL

Material validation via PhysicMaterialCustomData:

csharp
string hitMaterialName = PhysicsTool.GetMaterialName(hit);
if (hit.transform.CompareTag("Ground"))
{
    if (!PhysicMaterialCustomData.IsArable(hitMaterialName))
        { PlayerUI.hint(null, EPlayerMessage.SOIL); return false; }
}
if (!PhysicMaterialCustomData.HasOil(hitMaterialName))
    { PlayerUI.hint(null, EPlayerMessage.OIL); return false; }

Validation Pipeline — checkClaims()

csharp
private bool checkClaims()

Multi-step validation:

  1. Safezone checkplayer.movement.isSafe && !isSafeInfo.CurrentlyAllowsBuilding
  2. Vehicle allowanceallowedToPlaceOnVehicle config gate
  3. Beacon nav checkLevelNavigation.checkSafeFakeNav()
  4. Beacon difficulty — zombie difficulty asset can disallow
  5. Claim checkClaimManager.checkCanBuild() (unless bypassClaim)
  6. Vehicle claimClaimManager.canBuildOnVehicle()
  7. Clip volumePlayerClipVolumeManager no-build volumes
  8. Bed validation — arena mode block + kill volume overlap
  9. Vehicle mobilityEVehicleBuildablePlacementRule per asset
  10. Freeform config — global disable except admins
  11. Occupied vehicle — cannot build on occupied seats
  12. In-vehicle player — cannot place while seated
  13. Spawn proximityLevelPlayers.checkCanBuild()
  14. Build request dedupBuildRequestManager.canBuildAt()
  15. Underwater — campfires/torches blocked

Each rejection sends a localized EPlayerMessage hint.

Overlap Detection — check()

After checkClaims() passes, validates physical overlap via Physics.OverlapBoxNonAlloc. Doors additionally check swing clearance:

csharp
Vector3 leftCenter = realPoint + (boundsRotation * new Vector3(-boundsExtents.x, 0, boundsExtents.x));
if (Physics.OverlapSphereNonAlloc(leftCenter, 0.75f, checkColliders, RayMasks.BLOCK_DOOR_OPENING) > 0)
    return false;

Networked Placement — startPrimary()

csharp
public override bool startPrimary()
{
    if (Dedicator.IsDedicatedServer ? isValid : check())
    {
        if (channel.IsLocalPlayer)
        {
            if (parent != null)
                SendBarricadeVehicle.Invoke(...);
            else
                SendBarricadeNone.Invoke(...);
        }
        player.equipment.isBusy = true;
        build();
        return true;
    }
}

Server uses pre-validated isValid from ReceiveBarricadeVehicle / ReceiveBarricadeNone. Client runs check() locally.

Vehicle Placement

Barricade placement on vehicles uses SendBarricadeVehicle with position relative to parent:

csharp
SendBarricadeVehicle.Invoke(GetNetId(), ENetReliability.Reliable,
    parent.InverseTransformPoint(point),
    angle_x + rotate_x, angle_y + rotate_y - parent.localRotation.eulerAngles.y,
    angle_z + rotate_z, parentRegion._netId);

Server validates distance from hull:

csharp
if (testVehicle.getSqrDistanceFromHull(worldspaceTestPoint) > sqrMaxDistanceFromHull)
    { return; }

VehicleBarricadeRegion identified by NetId rather than old plant/region coordinates.

Timer Properties

csharp
private bool isUseable => Time.realtimeSinceStartup - startedUse > useTime;
private bool isBuildable => Time.realtimeSinceStartup - startedUse > useTime * 0.8f;

Placement available at 80% through the animation. Full usability at 100%.

Server-Side Received Methods

ReceiveBarricadeVehicle

Validates: wasAsked guard, config allows, NetId resolves, distance-from-hull, claims via checkClaims(). Rate limited at 10 Hz.

ReceiveBarricadeNone

Distance check against player aim position:

csharp
if ((newPoint - player.look.aim.position).sqrMagnitude < 256)
    isValid = checkClaims();

pendingBuildHandle = BuildRequestManager.registerPendingBuild(point) — atomic spot registration prevents race conditions.

Vehicle Region Lifecycle

EventAction
Vehicle spawnedRegion created lazily on first barricade placement
Vehicle destroyedtrimPlant drops all barricades; region destroyed
Vehicle explodeduprootPlant for main vehicle + all train cars

Key Design Insights

  1. Static collider poolscheckColliders array avoids per-frame allocations.
  2. Dedicated server separation — Server paths skip visual/audio and use pre-validated isValid.
  3. Rate limiting — Server RPCs enforce ratelimitHz (10 for placement).
  4. Localized hintsPlayerUI.hint(null, EPlayerMessage.*) provides consistent failure feedback.
  5. Pose vs. position — Barricades store absolute world position; vehicle barricades store parent-relative.
  6. Build request dedupBuildRequestManager atomically registers pending builds.
  7. Material-driven validation — Farm/oil use PhysicMaterialCustomData for soil/oil checks.
  8. Door swing clearance — Separate sphere overlaps at hinge swing points.

Document history