terminalCode Example

Sandbox: Stacker tool — duplicate objects in a line or arc with StackAlignMode

calendar_today May 4, 2026 schedule ~1 min read person patrickjr verified 50

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

Was this helpful?