Game Dev and more
Game Dev and more
  • Blog
  • Game Dev
  • AI

Beyond A-Move: Creating Tactical Formation Movement in Unity

  • Home
  • Game Dev
  • Beyond A-Move: Creating Tactical Formation Movement in Unity
A screenshot from the game "Antara: Reclaim the Wastes," showing a fleet of five stylized, low-poly airships flying in a diagonal line formation over a vast, orange, canyon-like desert. In the background, there are a few simple, angular buildings next to a lone purple airship. The user interface, including a minimap, is visible in the corners.
  • July 10, 2025
  • tsartsaris
  • 826 Views

If you’ve ever worked on a real-time strategy game, you’ve faced the “beehive problem.” You select a group of units, right-click a destination, and they all rush towards the same point, clumping up like angry bees. The units in the back crash into the ones in front, and they arrive in a disorganized blob. It works, but it lacks any sense of tactical precision.

For my upcoming city-builder/RTS hybrid, Antara: Reclaim the Wastes, I needed something better. I wanted my airship fleets to move with purpose—to hold a line, a wedge, or a defensive square as they traveled. Today, I’m going to break do

The Core Philosophy: Arrive Together

Before

NUMBERS MATTER in combat sometimes. Working on multi-unit combat. The goal is to blend classic RTS controls with a deep city-building core. #devlog #indiegamedev #strategygame #andara #indiegame #steampunk #airship #gamedevelopment #citybuilder #RTS #madewithunity #indiedevhour pic.twitter.com/nEJh1kqFyk

— Sotiris (@Tsartsaris) July 3, 2025

The fundamental principle is simple: To have units arrive at their formation positions simultaneously, they must all travel for the exact same amount of time.

This means the unit with the longest journey dictates the pace for everyone else. If the rearmost ship in a formation has to travel 100 meters, and a front-line ship only has to travel 60, the front-line ship must slow down so they both arrive at the same time.

Our system will achieve this in four steps:

  1. Define the Formation Shape: Store the formation positions in a data-only asset.
  2. Assign Slots: Give each unit a target position within that formation.
  3. Calculate Path Time: Find the unit with the longest path and calculate the time it will take to arrive.
  4. Coordinate Speed: Calculate the precise speed each individual unit needs to travel its own path within that same timeframe.

The Components of the System

Our system is broken into three key parts: a data container for the formation, a controller for individual units, and a central “brain” that coordinates the group.

1. The Blueprint: FormationAsset.cs (The ScriptableObject)

First, we need a way to define different formations that a designer can easily create and tweak. ScriptableObjects are perfect for this. They are data containers that live in your project files, not in a scene.

// In file: FormationAsset.cs
using UnityEngine;

[CreateAssetMenu(fileName = "NewFormation", menuName = "Formations/Formation Asset")]
public class FormationAsset : ScriptableObject
{
    [Tooltip("The local-space positions for each slot in the formation. (0,0,0) is the center.")]
    public Vector3[] points;

    [Tooltip("The spacing multiplier between units in the formation.")]
    public float spacing = 3f;
}

This gives us a reusable asset in the editor where we can define a shape by just arranging a few points in an array.

 The Unity Inspector window showing a "Wedge Formation" asset, which is a FormationAsset ScriptableObject. The "Points" array is expanded, showing five Vector3 elements that define the V-shape of the formation. A "Spacing" multiplier is set to 3.

2. The Muscle: UnitSpeedController.cs

Each movable unit needs its own component to manage its speed. This script has two jobs: remember its original speed and allow a central controller to temporarily override it for formation moves.

// In file: UnitSpeedController.cs
using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class UnitSpeedController : MonoBehaviour
{
    private NavMeshAgent agent;
    private float baseSpeed;
    private bool isMovingInFormation = false;

    void Awake() {
        agent = GetComponent<NavMeshAgent>();
        baseSpeed = agent.speed;
    }

    void Update() {
        // When we arrive at our destination, reset our speed.
        if (isMovingInFormation) {
            if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) {
                ResetSpeed();
            }
        }
    }

    public void SetFormationSpeed(float newSpeed) {
        agent.speed = newSpeed;
        isMovingInFormation = true;
    }

    public void ResetSpeed() {
        agent.speed = baseSpeed;
        isMovingInFormation = false;
    }
}

The key here is that the unit itself is responsible for resetting its own speed upon arrival. This keeps our main controller from having to micro-manage every unit for the entire duration of the move. The !agent.pathPending check is crucial to avoid resetting speed prematurely before a path has even been calculated.


3. The Brain: FormationController.cs

This is where the magic happens. This singleton controller takes a list of units and a destination and orchestrates the entire maneuver.

Let’s look at the core method, MoveGroupInFormation.

// In file: FormationController.cs

// ... (variables and other methods) ...

public void MoveGroupInFormation(List<Movable> units, Vector3 destination) {
    if (units.Count <= 1 || currentFormation == null) {
        // Handle simple cases first
        MoveGroupToSinglePoint(units, destination);
        return;
    }

    // Determine the formation's orientation
    Vector3 groupCenter = GetGroupCenter(units);
    Vector3 direction = (destination - groupCenter).normalized;
    Quaternion formationRotation = Quaternion.LookRotation(direction);

    // --- The Core Logic ---

    // Step 1: Calculate the path for every unit to its formation slot
    var pathData = new List<(UnitSpeedController controller, Vector3 targetPos, float pathLength)>();
    float maxPathLength = 0f;

    for (int i = 0; i < units.Count; i++) {
        // ... (error checking) ...

        int formationIndex = i % currentFormation.points.Length;
        Vector3 offset = currentFormation.points[formationIndex] * currentFormation.spacing;
        Vector3 rotatedOffset = formationRotation * offset;
        Vector3 targetPos = destination + rotatedOffset;

        // CRITICAL: Calculate the ACTUAL path length using the NavMesh
        var path = new NavMeshPath();
        if (NavMesh.CalculatePath(agent.transform.position, targetPos, NavMesh.AllAreas, path)) {
            float length = GetPathLength(path); // A helper method to sum path.corners
            pathData.Add((speedController, targetPos, length));
            if (length > maxPathLength) {
                maxPathLength = length;
            }
        }
    }

    // Step 2: Calculate the time everyone needs to arrive together.
    // Based on the longest path at a consistent speed.
    float baseSpeedForCalc = 5f; // A reasonable default
    float timeToArrive = maxPathLength / baseSpeedForCalc;

    // Step 3: Set each unit's destination and calculated speed.
    foreach (var data in pathData) {
        float requiredSpeed = (timeToArrive > 0) ? data.pathLength / timeToArrive : 0;

        // Clamp the speed to keep movement looking natural
        float agentBaseSpeed = data.controller.GetComponent<NavMeshAgent>().speed;
        float clampedSpeed = Mathf.Clamp(requiredSpeed,
                                          agentBaseSpeed * minFormationSpeedMultiplier,
                                          agentBaseSpeed * maxCatchUpSpeedMultiplier);

        data.controller.SetFormationSpeed(clampedSpeed);
        data.controller.GetComponent<NavMeshAgent>().SetDestination(data.targetPos);
    }
}

Why This Works

The most important line in that logic is NavMesh.CalculatePath(). We are not using a simple Vector3.Distance() check. We are asking the NavMesh system, “How far does this specific unit actually have to travel to get around walls and obstacles to its target slot?”

By finding the longest of these real paths (maxPathLength), we determine our timeToArrive. Then, for every unit, we calculate the requiredSpeed by dividing its individual path length by that universal arrival time.

The result? The unit with the longest path moves at the baseline speed. Every other unit moves at a proportionally slower speed so that they all complete their journey in the exact same amount of time. They arrive in formation, looking like a trained and coordinated fleet.

Final Touches

  • Clamping: Notice the Mathf.Clamp call. This is an important touch for game feel. It prevents units at the front from crawling at an absurdly slow pace or units at the back from rocketing forward unnaturally fast. It keeps the entire group’s speed within a believable range.
  • Marker Component: The List<Movable> is a simple marker component (public class Movable : MonoBehaviour {}) that also uses [RequireComponent(typeof(UnitSpeedController))] to ensure any “Movable” object automatically gets the scripts it needs.

This system has been a game-changer for Antara. It elevates unit control from a simple “go here” command to a genuine tactical maneuver. Feel free to adapt this logic for your own projects!

Final result

From a CHAOTIC mob to a disciplined squadron! Working on tactical formations for your airship fleets in #Antara. No more blobs – COMMAND your airships in Line, Wedge, and Inverted Wedge formations.#gamedev #indiedev #RTS #strategygame #citybuilder #madewithunity #airship pic.twitter.com/ipBMhiTSaZ

— Sotiris (@Tsartsaris) July 4, 2025

Tags:

AIRSHIPS GAME-DEV INDIE-DEV POST-APOCALYPTIC RTS TACTICAL

Share:

Previus Post
A Deep
Next Post
Learn how

Comments are closed

Recent Posts

  • Learn how to build a Real-Time AI Phone Assistant that sounds Stunningly Human.
  • Beyond A-Move: Creating Tactical Formation Movement in Unity
  • A Deep Dive into a Dynamic Unity City Builder Grid System
  • Designing Survival: The Shack System in a post-apocalyptic city builder
  • Building a Smarter Neo4j AI Agent with Google’s GenAI Toolbox

Categories

  • AI
  • Blog
  • Game Dev
Linkedin-in

Copyright 2025 Tsartsaris. All Rights Reserved