terminalCode Example
Sandbox: Stacker tool — duplicate objects in a line or arc with StackAlignMode
Stacker Tool — Duplicate Objects in a Line or Arc
The Stacker tool clones a selected object N times along a configurable axis with optional rotation and gap. It supports both world-space and object-local alignment modes.
CSHARP
[Icon( "📚" )]
[ClassName( "stacker" )]
[Group( "#tool.group.building" )]
[Title( "#tool.name.stacker" )]
public class StackerTool : ToolMode
{
private const int MaxStackCount = 50;
public override IEnumerable<string> TraceIgnoreTags => ["player", "constraint", "collision"];
[Property, Sync, Range( 1, MaxStackCount )]
public int Count { get; set; } = 3;
[Property, Sync, Range( -128, 128 )]
public float PositionOffset { get; set; } = 0f; // Extra gap between copies
[Property, Sync]
public bool FreezeAll { get; set; } = true; // Freeze spawned copies
[Property, Sync]
public StackAlignMode AlignMode { get; set; } = StackAlignMode.Object;
// Direction to stack along (cycles through X/Y/Z)
[Property, Sync]
public Vector3 Direction { get; set; } = Vector3.Up;
// Per-copy rotation offset (for arc stacking)
[Property, Sync]
public Angles AngleOffset { get; set; } = Angles.Zero;
protected override void OnStart()
{
base.OnStart();
RegisterAction( ToolInput.Primary, () => "#tool.hint.stacker.stack", OnStack );
RegisterAction( ToolInput.Secondary, () => "#tool.hint.stacker.cycle_alignment", CycleAlignment );
RegisterAction( ToolInput.Reload, () => "#tool.hint.stacker.cycle_direction", CycleDirection );
}
}Stack creation
CSHARP
[Rpc.Host]
void SpawnStack( GameObject source )
{
var basePos = source.WorldPosition;
var baseRot = source.WorldRotation;
var bounds = source.GetBounds();
var localDir = Direction; // e.g. Vector3.Up
var stepAngle = Rotation.From( AngleOffset );
var undo = Player.Undo.Create();
undo.Name = "Stack";
var prevPos = basePos;
var prevRot = baseRot;
for ( int i = 0; i < Count; i++ )
{
// Compute rotation for this copy
Rotation copyRot;
if ( AlignMode == StackAlignMode.Object )
copyRot = prevRot * stepAngle;
else
copyRot = stepAngle * prevRot; // World-space rotation
// Compute step direction from previous copy's orientation
Vector3 stepAxis = AlignMode == StackAlignMode.Object
? prevRot * localDir
: localDir;
// Step distance = bounds extent along step axis + gap
var extent = bounds.Size.Dot( stepAxis.Abs() ) * 0.5f;
var copyPos = prevPos + stepAxis * (extent * 2 + PositionOffset);
var clone = source.Clone( new CloneConfig
{
Transform = new Transform( copyPos, copyRot, source.WorldScale ),
StartEnabled = true
} );
clone.Tags.Add( "removable" );
if ( FreezeAll )
{
var rb = clone.GetComponent<Rigidbody>();
if ( rb.IsValid() ) rb.MotionEnabled = false;
}
clone.NetworkSpawn( true, null );
undo.Add( clone );
prevPos = copyPos;
prevRot = copyRot;
}
}StackAlignMode
CSHARP
public enum StackAlignMode
{
World, // Stack along world-space axes regardless of object orientation
Object // Stack along the target object's local axes
}Key points
- Max stack count is 50 — enforced in the tool, not the limits system
- FreezeAll = true by default — spawned copies have Rigidbody.MotionEnabled = false
- Secondary click cycles alignment mode (World/Object); Reload cycles the stack direction (X/Y/Z)
- AngleOffset enables arc stacking — each copy is rotated by this amount relative to the previous
- All clones are tagged "removable" and added to a single undo entry
- TraceIgnoreTags includes "constraint" and "collision" — the tool targets the base prop, not its constraints
Was this helpful?