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:
| Field | Type | Purpose |
|---|---|---|
placementPreviewTransform | Transform | Ghost preview object |
isPlacementPreviewValid | bool | Current highlight state |
useAnimationStartTime | double | Time.timeAsDouble at animation start |
useAnimationDuration | float | Length of "Use" clip |
isWaitingForSoundTrigger | bool | Sound hasn't fired yet |
isUseAnimationPlaying | bool | Animation in progress |
hasServerReceivedBuildRequest | bool | Server received RPC |
isServerBuildRequestInitiallyApproved | bool | Pre-validation result |
pendingPlacementPosition | Vector3 | Computed placement point |
pendingPlacementYaw | float | Computed rotation |
animatedRotationOffset | float | Lerp-smoothed offset |
customRotationOffset | float | Player rotation input |
foundationPositionOffset | float | Scroll wheel height |
serverPlacementPosition / serverPlacementYaw | Vector3 / float | Server-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:
- Pillar connection check — Must connect to at least one existing pillar or foundation.
- Overlap check — No existing structure occupies the same grid space.
- Terrain clearance — Footprint not blocked by terrain.
- Clip volume check — Rejects no-build volumes.
- Safezone check — Rejects safezones unless explicitly allowed.
Key Design Insights
- Delegated placement —
UseableStructuredelegates entirely toUseableHousingUtils, a clean contrast toUseableBarricade's monolithic approach. - Double validation — Client pre-check + server authoritative re-check prevents desync.
- Animation-timed state machine — Sound triggers at 80%, spawn finalizes at 100%, ensuring proper visual sequencing.
- Rotation presets — Ramparts rotate 180°, floors 90°, others 30° — appropriate scale per construct type.
- Foundation offset — Scroll wheel adjusts height, perpendicular to player look direction for intuitive control.
- Alert integration —
AlertTool.alert(8)makes building a noisy activity that attracts nearby AI.
