Regiment Formation system

Does someone know how to make a dynamic regiment formation system like in the Total War Series Games?

I’m trying to make a basic formation system that uses a rectangle formation, but my soldiers randomly switch positions while moving or rotating.

What I want to achieve:

Soldiers should always keep their fixed slot position. If counted from left to right, row by row (from behind the formation), Soldier 1 is always front-left, Soldier 2 front-second, etc. This should stay consistent no matter which direction the formation faces. The only exception would be a 180° turn where the back row becomes the front row.

Current setup:

  • Each soldier has a fixed AssignedSlotIndex assigned at spawn

  • Slots are calculated via CalculateFormationSlotsAt based on destination and target rotation

  • Soldiers are sent to their slot via MoveToFormationSlotWithSpeed

The problem: When the formation rotates (e.g. 90°), soldiers on the left and right side swap places, which makes no sense visually.

Below an example of what im trying to accomplish.

My current code:

// Slot calculation

TArray APTWBaseUnit::CalculateFormationSlotsAt(
const FVector& Origin, const FRotator& Rotation, int32 OverrideCols) const
{
TArray Slots;

const int32 Cols = (OverrideCols > 0)
    ? OverrideCols
    : FMath::CeilToInt(FMath::Sqrt((float)SoldierCount));
const int32 Rows = FMath::CeilToInt((float)SoldierCount / Cols);

const FVector Forward = FRotationMatrix(Rotation).GetScaledAxis(EAxis::X);
const FVector Right = FRotationMatrix(Rotation).GetScaledAxis(EAxis::Y);

const float TotalDepth = (Rows - 1) * FormationSpacingX;
const float TotalWidth = (Cols - 1) * FormationSpacingY;
const FVector CenterOffset = Forward * (TotalDepth * 0.5f);

const FVector WorldEast = FVector(0.f, 1.f, 0.f);
const float RightDot = FVector::DotProduct(Right, WorldEast);
const float ColDir = (RightDot >= 0.f) ? 1.f : -1.f;

for (int32 Row = 0; Row < Rows; Row++)
    for (int32 Col = 0; Col < Cols; Col++)
    {
        if (Slots.Num() >= SoldierCount) break;
        Slots.Add(Origin
            - CenterOffset
            + Forward * Row * FormationSpacingX
            + Right * ColDir * (Col - (Cols - 1) * 0.5f) * FormationSpacingY);
    }
return Slots;

}

// Slot assignment

void APTWBaseUnit::AssignSlotsToSoldiers(const TArray& Slots)
{
if (Soldiers.Num() == 0 || Slots.Num() == 0) return;

const int32 Count = FMath::Min(Soldiers.Num(), Slots.Num());

for (int32 i = 0; i < Count; i++)
{
    if (!Soldiers[i]) continue;
    const int32 Idx = FMath::Min(Soldiers[i]->AssignedSlotIndex, Slots.Num() - 1);
    Soldiers[i]->MoveToFormationSlotWithSpeed(Slots[Idx], 1.f);
}

}

// Move order

void APTWBaseUnit::OrderMoveTo(const FVector& Destination,
const FRotator& TargetRotation, int32 OverrideCols)
{
MoveDestination = Destination;
MoveTargetRotation = TargetRotation;
CurrentCommand = EPTWUnitCommand::Moving;

// save cols for UpdateFormationSlots
if (OverrideCols > 0)
    CurrentFormationCols = OverrideCols;

SetActorRotation(TargetRotation);

TArray<FVector> Slots = CalculateFormationSlotsAt(
    Destination, TargetRotation, CurrentFormationCols);
AssignSlotsToSoldiers(Slots);

}