UseableBarricade — Building Placement
Advanced60-90 minutesWindowsVisual StudioU3 SDK
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 modelguide— 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); // StandardBuild 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) dotLADDER
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 blockedFREEFORM / 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:
- Safezone check —
player.movement.isSafe && !isSafeInfo.CurrentlyAllowsBuilding - Vehicle allowance —
allowedToPlaceOnVehicleconfig gate - Beacon nav check —
LevelNavigation.checkSafeFakeNav() - Beacon difficulty — zombie difficulty asset can disallow
- Claim check —
ClaimManager.checkCanBuild()(unlessbypassClaim) - Vehicle claim —
ClaimManager.canBuildOnVehicle() - Clip volume —
PlayerClipVolumeManagerno-build volumes - Bed validation — arena mode block + kill volume overlap
- Vehicle mobility —
EVehicleBuildablePlacementRuleper asset - Freeform config — global disable except admins
- Occupied vehicle — cannot build on occupied seats
- In-vehicle player — cannot place while seated
- Spawn proximity —
LevelPlayers.checkCanBuild() - Build request dedup —
BuildRequestManager.canBuildAt() - 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
| Event | Action |
|---|---|
| Vehicle spawned | Region created lazily on first barricade placement |
| Vehicle destroyed | trimPlant drops all barricades; region destroyed |
| Vehicle exploded | uprootPlant for main vehicle + all train cars |
Key Design Insights
- Static collider pools —
checkCollidersarray avoids per-frame allocations. - Dedicated server separation — Server paths skip visual/audio and use pre-validated
isValid. - Rate limiting — Server RPCs enforce
ratelimitHz(10 for placement). - Localized hints —
PlayerUI.hint(null, EPlayerMessage.*)provides consistent failure feedback. - Pose vs. position — Barricades store absolute world position; vehicle barricades store parent-relative.
- Build request dedup —
BuildRequestManageratomically registers pending builds. - Material-driven validation — Farm/oil use
PhysicMaterialCustomDatafor soil/oil checks. - Door swing clearance — Separate sphere overlaps at hinge swing points.
