Conveyance/BridgePlacer.cs
using Sandbox;
using System.Collections.Generic;
/// <summary>
/// Places N clones of a BridgePlank prefab in a row between StartAnchor and EndAnchor
/// to form a rope-bridge style assembly.
///
/// PHILOSOPHY: This script does NOT create any joints from code. Each plank instance is
/// a self-contained prefab with its own anchors and springs already wired up. We just
/// clone the prefab at calculated positions and let each clone's internal physics work
/// independently. No plank-to-plank constraints, no shared anchors — just N independent
/// suspended planks that visually form a bridge.
///
/// SETUP:
/// 1. Build one BridgePlank prefab manually (per the bridge plank guide). Confirm it
/// works in isolation.
/// 2. Save it as a prefab asset.
/// 3. Add this component to an empty GameObject called "Bridge".
/// 4. Place two empty GameObjects "StartAnchor" and "EndAnchor" in the scene where
/// the bridge should start and end.
/// 5. Drag the BridgePlank prefab into the PlankPrefab slot.
/// 6. Drag the start/end anchors into their slots.
/// 7. Set PlankCount and SagAmount to taste.
/// 8. Click "Build Bridge".
///
/// REBUILD: Clicking Build again destroys existing planks (tracked internally) and
/// rebuilds with current parameters. Safe to iterate on.
///
/// COORDINATE NOTE: planks are placed in a straight line between the two anchors,
/// then optionally shifted downward in a parabolic sag for that authentic rope-bridge
/// droop. Each plank is rotated to face along the bridge direction so it sits naturally.
/// </summary>
public sealed class BridgePlacer : Component
{
/// <summary>
/// The BridgePlank prefab. Must contain its own anchors, plank rigidbody, and springs
/// pre-wired in the editor. This script does NOT modify the prefab's internals.
/// </summary>
[Property] public GameObject PlankPrefab { get; set; }
/// <summary>
/// Empty GameObject marking the start of the bridge. The first plank is placed here.
/// </summary>
[Property] public GameObject StartAnchor { get; set; }
/// <summary>
/// Empty GameObject marking the end of the bridge. The last plank is placed here.
/// </summary>
[Property] public GameObject EndAnchor { get; set; }
/// <summary>
/// Number of planks to spawn between (and including) the start and end positions.
/// Below ~4 looks sparse; above ~12 starts to feel dense. 6–8 is the rope-bridge sweet spot.
/// </summary>
[Property, Range( 2, 30 )] public int PlankCount { get; set; } = 8;
/// <summary>
/// How much the bridge dips in the middle, in world units. 0 = perfectly horizontal
/// line, looks artificial. Real rope bridges sag — try 30–80 depending on bridge length.
/// The sag follows a parabolic curve, deepest at the midpoint.
/// </summary>
[Property, Range( 0, 200 )] public float SagAmount { get; set; } = 40f;
/// <summary>
/// Internal tracking of the GameObjects this script has created, so we can clean them
/// up on rebuild without nuking unrelated bridge decorations the user might add.
/// </summary>
private readonly List<GameObject> spawnedPlanks = new();
/// <summary>
/// Wipe existing planks and stamp PlankCount new ones in a row.
/// </summary>
[Button( "Build Bridge" )]
public void Build()
{
if ( !PlankPrefab.IsValid() )
{
Log.Warning( $"{nameof( BridgePlacer )}: PlankPrefab is not set." );
return;
}
if ( !StartAnchor.IsValid() || !EndAnchor.IsValid() )
{
Log.Warning( $"{nameof( BridgePlacer )}: StartAnchor or EndAnchor is not set." );
return;
}
// Step 1: clean up any planks from a previous build.
ClearExisting();
// Step 2: compute the geometry.
var startPos = StartAnchor.WorldPosition;
var endPos = EndAnchor.WorldPosition;
var bridgeVector = endPos - startPos;
var bridgeLength = bridgeVector.Length;
if ( bridgeLength < 1f )
{
Log.Warning( $"{nameof( BridgePlacer )}: Start and End anchors are at the same position." );
return;
}
// Bridge direction (used to orient each plank along the bridge axis).
// LookAt produces a rotation whose Forward points toward the target. We want planks
// to face "across" the bridge — their long axis pointing from start to end. So we
// take a LookAt and use it directly for plank rotation.
var bridgeRotation = Rotation.LookAt( bridgeVector.Normal );
// Step 3: place planks evenly spaced from start to end. The parameter t goes 0 → 1
// as we walk from start to end. We include both endpoints, so the first plank sits
// at t=0 (start) and the last at t=1 (end). With N planks, the step is 1/(N-1).
for ( int i = 0; i < PlankCount; i++ )
{
// Linear interpolation parameter, 0 at first plank, 1 at last.
float t = PlankCount == 1 ? 0.5f : (float)i / ( PlankCount - 1 );
// Position along the straight line from start to end.
var linearPos = Vector3.Lerp( startPos, endPos, t );
// Parabolic sag: maximum at t=0.5 (midpoint), zero at endpoints.
// Formula: 1 - (2t - 1)^2 is a parabola that peaks at t=0.5 with value 1.
// Using x*x instead of MathF.Pow because s&box sandboxes System.MathF out.
float u = 2f * t - 1f;
float sagFactor = 1f - u * u;
var sagOffset = Vector3.Down * SagAmount * sagFactor;
var plankPos = linearPos + sagOffset;
// Step 4: clone the prefab at this position with the bridge's facing rotation.
// The clone includes the plank's anchors as children, so they move with it —
// each plank's springs reference its own anchors (relative GUIDs in the prefab),
// so no cross-wiring happens.
var plank = PlankPrefab.Clone( plankPos, bridgeRotation );
// Parent under this GameObject for tidiness. Setting parent moves the object,
// so we set parent first if we want to preserve world position — actually,
// SetParent has an option for this. Default is to keep world transform.
plank.SetParent( GameObject, true );
// Name it for easier debugging in the hierarchy.
plank.Name = $"Plank_{i:D2}";
spawnedPlanks.Add( plank );
}
Log.Info( $"{nameof( BridgePlacer )}: built bridge with {PlankCount} planks over {bridgeLength:F0} units." );
}
/// <summary>
/// Destroy all planks this script has created. Does not touch anything else under
/// the bridge GameObject (e.g. LineRenderers for the rope visuals are safe).
/// </summary>
[Button( "Clear Bridge" )]
public void ClearExisting()
{
foreach ( var p in spawnedPlanks )
{
if ( p.IsValid() )
p.Destroy();
}
spawnedPlanks.Clear();
}
}