Skip to content

UseableStructure — Building Placement

UseableStructure is the placement engine for structure items in Unturned. Unlike UseableBarricade with its 15+ bespoke placement strategies, UseableStructure delegates entirely to UseableHousingUtils — a unified grid-based placement system built around housing connection validation. The class manages foundation height offsets, rotation presets, client-side preview with green/red highlighting, server-authoritative validation via EHousingPlacementResult, and an animation-timed construct cycle with sound and alert triggers.

Source code location: Unturned/Useable/UseableStructure.cs

Architecture Overview

UseableStructure extends Useable and relies on UseableHousingUtils for all placement logic. The class uses 14 private fields to track its state machine across the equip → preview → validate → animate → spawn lifecycle. Key architectural components:

  • placementPreviewTransform: Stripped-down preview ghost for visual placement feedback.
  • UseableHousingUtils.ValidatePendingPlacement: Server-authoritative grid validation.
  • UseableHousingUtils.FindPlacement: Client-side placement projection.
  • Housing connection system: Grid-snapped placement enforced by HousingConnections.

Equip and Preview

Preview Creation

csharp
if (channel.IsLocalPlayer)
{
    isPlacementPreviewValid = false;
    placementPreviewTransform = UseableHousingUtils.InstantiatePlacementPreview(equippedStructureAsset);
}

The preview is highlighted green/red based on validity via HighlighterTool.help().

Field Layout

UseableStructure uses 14 private fields:

FieldTypePurpose
placementPreviewTransformTransformGhost preview object
isPlacementPreviewValidboolCurrent highlight state
useAnimationStartTimedoubleTime.timeAsDouble at animation start
useAnimationDurationfloatLength of "Use" clip
isWaitingForSoundTriggerboolSound hasn't fired yet
isUseAnimationPlayingboolAnimation in progress
hasServerReceivedBuildRequestboolServer received RPC
isServerBuildRequestInitiallyApprovedboolPre-validation result
pendingPlacementPositionVector3Computed placement point
pendingPlacementYawfloatComputed rotation
animatedRotationOffsetfloatLerp-smoothed offset
customRotationOffsetfloatPlayer rotation input
foundationPositionOffsetfloatScroll wheel height
serverPlacementPosition / serverPlacementYawVector3 / floatServer-side copies

Update Loop — tick()

csharp
if (!isUseAnimationPlaying)
{
    bool isPlacementValid = UpdatePendingPlacement();
    if (isPlacementPreviewValid != isPlacementValid)
    {
        isPlacementPreviewValid = isPlacementValid;
        HighlighterTool.help(placementPreviewTransform, isPlacementPreviewValid);
    }
}

Foundation Height Offset

Scroll wheel adjusts foundation height:

csharp
foundationPositionOffset = Mathf.Clamp(
    foundationPositionOffset + (scrollWheelInput * FOUNDATION_MOUSE_SCROLL_MULTIPLIER),
    FOUNDATION_MIN_OFFSET, FOUNDATION_MAX_OFFSET);

Rotation System

Secondary input rotates via presets based on EConstruct:

csharp
if (equippedStructureAsset.construct == EConstruct.FLOOR || ...)
    delta = 90.0f;
else if (equippedStructureAsset.construct == EConstruct.RAMPART || ...)
    delta = 180.0f;
else
    delta = 30.0f;

if (InputEx.GetKey(KeyCode.LeftShift))
    delta *= -1.0f;
customRotationOffset += delta;

The animatedRotationOffset interpolates toward customRotationOffset using Mathf.Lerp for smooth visual rotation.

Placement Flow — startPrimary()

csharp
public override bool startPrimary()
{
    if (Dedicator.IsDedicatedServer ? isServerBuildRequestInitiallyApproved : UpdatePendingPlacement())
    {
        if (channel.IsLocalPlayer)
            SendBuildStructure.Invoke(GetNetId(), ENetReliability.Reliable,
                pendingPlacementPosition, pendingPlacementYaw + customRotationOffset);
        player.equipment.isBusy = true;
        PlayUseAnimation();
        if (Provider.isServer)
            SendPlayConstruct.Invoke(GetNetId(), ENetReliability.Unreliable,
                channel.GatherRemoteClientConnectionsExcludingOwner());
    }
}

The SendPlayConstruct RPC broadcasts the construct animation to other clients for visual syncing.

Client-Side Placement Projection — UpdatePendingPlacement

csharp
private bool UpdatePendingPlacement()
{
    if (!UseableHousingUtils.FindPlacement(equippedStructureAsset, player,
        customRotationOffset, foundationPositionOffset,
        out pendingPlacementPosition, out pendingPlacementYaw))
        return false;

    if (!UseableHousingUtils.IsPendingPositionValid(player, pendingPlacementPosition))
        return false;

    return true;
}

Server Validation — ReceiveBuildStructure

csharp
if ((newPoint - player.look.aim.position).sqrMagnitude < HousingConnections.MAX_PLACEMENT_SQR_DISTANCE)
{
    serverPlacementPosition = newPoint;
    serverPlacementYaw = newAngle;
    if (!UseableHousingUtils.IsPendingPositionValid(player, serverPlacementPosition))
        isServerBuildRequestInitiallyApproved = false;
    else
    {
        string obstructionHint = null;
        EHousingPlacementResult result = UseableHousingUtils.ValidatePendingPlacement(
            equippedStructureAsset, ref serverPlacementPosition,
            serverPlacementYaw, ref obstructionHint);
        isServerBuildRequestInitiallyApproved = (result == EHousingPlacementResult.Success);
    }
}

Finalize — simulate()

After the use animation completes, simulate() double-checks validity and spawns:

csharp
if (!UseableHousingUtils.IsPendingPositionValid(player, serverPlacementPosition))
{
    player.equipment.dequip();
}
else
{
    EHousingPlacementResult result = UseableHousingUtils.ValidatePendingPlacement(
        equippedStructureAsset, ref serverPlacementPosition,
        serverPlacementYaw, ref obstructionHint);
    if (result != EHousingPlacementResult.Success)
        player.equipment.dequip();
    else
    {
        StructureManager.dropStructure(
            new Structure(asset, asset.health),
            serverPlacementPosition, 0, serverPlacementYaw, 0,
            channel.owner.playerID.steamID.m_SteamID,
            player.quests.groupID.m_SteamID);
        player.equipment.use();
    }
}

The double-validation pattern (client pre-check + server authoritative check) prevents race conditions and hacked clients.

Sound and Alert

Sound playback at 80% through the animation:

csharp
if (isWaitingForSoundTrigger && HasReachedSoundTrigger)
{
    isWaitingForSoundTrigger = false;
    if (!Dedicator.IsDedicatedServer)
        player.playSound(equippedStructureAsset.use);
    if (Provider.isServer)
        AlertTool.alert(transform.position, 8);
}

The 8-meter alert radius notifies nearby zombies of building activity.

Housing Connection Validation

UseableHousingUtils.ValidatePendingPlacement performs:

  1. Pillar connection check — Must connect to at least one existing pillar or foundation.
  2. Overlap check — No existing structure occupies the same grid space.
  3. Terrain clearance — Footprint not blocked by terrain.
  4. Clip volume check — Rejects no-build volumes.
  5. Safezone check — Rejects safezones unless explicitly allowed.

Key Design Insights

  1. Delegated placementUseableStructure delegates entirely to UseableHousingUtils, a clean contrast to UseableBarricade's monolithic approach.
  2. Double validation — Client pre-check + server authoritative re-check prevents desync.
  3. Animation-timed state machine — Sound triggers at 80%, spawn finalizes at 100%, ensuring proper visual sequencing.
  4. Rotation presets — Ramparts rotate 180°, floors 90°, others 30° — appropriate scale per construct type.
  5. Foundation offset — Scroll wheel adjusts height, perpendicular to player look direction for intuitive control.
  6. Alert integrationAlertTool.alert(8) makes building a noisy activity that attracts nearby AI.

Document history