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
AssignedSlotIndexassigned at spawn -
Slots are calculated via
CalculateFormationSlotsAtbased 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);
}
