Difficulty with rotating agent formations

,

I am currently speaking from a place of study in the Unity.3D.RPG series but this inquiry transcends such banal stereotyping. We got some RTS inspirations tossing their gremlins into the gears. I factored the movement logic to support parties of player characters and formation movement logic.

The code examples below correctly navigates agents to precalculated positions inside a List<T> . The agents are fed a target Vector3 based on their rank in another list dedicated to currently selected units. The failure of my logic is that the entire formation will not rotate as I have it so far. The individuals all rotate and path independently, but the alignment of positions remains fixed in the same direction with every call to these functions.

TL;DR : this moves units into a tactical arrangement but does not rotate the formation as a whole. I want to rotate the whole formation.

namespace E04.Movement
{
    public class ReadyUnit
    {
        public PlayerControl player { get; set; }
        public int rank { get; set; }
    }

    public class Formation : MonoBehaviour
    {
        public Formation Instance { get; private set; }

        enum order
        {
            loose,
            column,
            circle,
            vanguard,
            firingLine
        }

        [Header("All player characters share the same SINGLE PathOrganizer in the scene.")]
        [SerializeField] order arrangement = order.loose;
        [Range(1, 15)][SerializeField] int rank;
        PathOrganizer organizer;
        ReadyUnit unit;
        public static List<ReadyUnit> readyUnitList = new List<ReadyUnit>();
        
        private void Awake()
        {
            unit = new ReadyUnit { player = gameObject.GetComponent<PlayerControl>(), rank = rank };
        }

        private void Start()
        {
            if (IsLeader())
            {
                unit.rank = 0;
            }
            else
            {
                unit.rank = rank;
            }

            /* There should only be ONE instance of PathOrganizer in the scene */
            organizer = FindFirstObjectByType<PathOrganizer>();
            Selector.Instance.UnitSelectedEvent += OnUnitSelected;
        }

        void OnUnitSelected(object sender, EventArgs e)
        {
            if (Selector.Instance.GetSelectedPlayersList().Contains(gameObject.GetComponent<PlayerControl>()) && 
                readyUnitList.Contains(unit) == false)
            {
                readyUnitList.Add(unit);
            }

            if (Selector.Instance.GetSelectedPlayersList().Contains(gameObject.GetComponent<PlayerControl>()) == false &&
                readyUnitList.Contains(unit))
            {
                readyUnitList.Remove(unit);
            }

            if (Selector.Instance.GetSelectedUnit() == gameObject.GetComponent<PlayerControl>())
            {
                unit.rank = 0;
            }
            else
            {
                unit.rank = rank;
            }

            SortUnitsByRank();
        }

        void SortUnitsByRank()
        {
            readyUnitList.Sort((leftHand, rightHand) => leftHand.rank.CompareTo(rightHand.rank));
        }

        public void FallInStep(Vector3 point)
        {
            if (point == null)
            {
                return;
            }

            if (readyUnitList == null)
            {
                return;
            }

            SortUnitsByRank();
            MoveToPosition(point);
        }

        void MoveToPosition(Vector3 point)
        {
            switch (arrangement)
            {
                case order.loose:
                    LooseArrangement(point);
                    break;
                case order.column:
                    ColumnArrangement(point);
                    break;
                case order.circle:
                    CircleArrangement(point);
                    break;
                case order.vanguard:
                    VanguardArrangement(point);
                    break;
                case order.firingLine:
                    FiringLine(point); 
                    break;
            }
        }

        bool IsLeader()
        {
            Debug.Log("F - IsLeader() " + Selector.Instance.GetSelectedUnit().name);

            return Selector.Instance.GetSelectedUnit() == gameObject.GetComponent<PlayerControl>();
        }

        void LooseArrangement(Vector3 point)
        { 
            float offset = Random.Range(-2f, 2f);

            List<Vector3> values = new List<Vector3>
            {
                point,
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset),
                point + new Vector3(offset, 0, offset)
            };

            for (int i = 0; i < readyUnitList.Count; i++)
            {
                PlayerControl player = readyUnitList[i].player;
                organizer.transform.position = point;
                organizer.AssignPosition(values[i]);

                player.GetComponent<MovementManager>().MoveToDestination(organizer.ConfirmedPosition());
            }
        }

        void ColumnArrangement(Vector3 point)
        {
            List<Vector3> values = new List<Vector3>()
            {
                point,
                point + new Vector3(2f, 0, 0),
                point + new Vector3(0, 0, -2f),
                point + new Vector3(2f, 0, -2f),
                point + new Vector3(0, 0, -4f),
                point + new Vector3(2f, 0, -4f),
                point + new Vector3(0, 0, -6f),
                point + new Vector3(2f, 0, -6f),
                point + new Vector3(0, 0, -8f),
                point + new Vector3(2f, 0, -8f),
                point + new Vector3(0, 0, -10f),
                point + new Vector3(2f, 0, -10f),
            };

            for (int i = 0; i < readyUnitList.Count; i++)
            {
                PlayerControl player = readyUnitList[i].player;
                organizer.transform.position = point;
                organizer.AssignPosition(values[i]);
                
                player.GetComponent<MovementManager>().MoveToDestination(organizer.ConfirmedPosition());
            }
        }

        void CircleArrangement(Vector3 point) 
        {
            float diameter = 2.5f;

            for (int i = 0; i < readyUnitList.Count; i++)
            {
                float angle = (float)(i * (2f * Math.PI / readyUnitList.Count));
                float cos = (float)Math.Cos(angle) * diameter;
                float sin = (float)Math.Sin(angle) * diameter;

                point = new Vector3(point.x + cos, point.y, point.z + sin);
                PlayerControl player = readyUnitList[i].player;
                player.GetComponent<MovementManager>().MoveToDestination(point);
            }
        }

        void VanguardArrangement(Vector3 point)
        {
            List<Vector3> values = new List<Vector3>()
            {
                point,
                point + new Vector3(1f, 0, -1f),
                point + new Vector3(-1f, 0, -1f),
                point + new Vector3(2f, 0, -2f),
                point + new Vector3(-2f, 0, -2f),
                point + new Vector3(3f, 0, -3f),
                point + new Vector3(-3f, 0, -3f),
                point + new Vector3(4f, 0, -4f),
                point + new Vector3(-4f, 0, -4f),
                point + new Vector3(5f, 0, -5f),
                point + new Vector3(-5f, 0, -5f),
                point + new Vector3(0, 0, -1f),
            };

            for (int i = 0; i < readyUnitList.Count; i++)
            {
                PlayerControl player = readyUnitList[i].player;
                organizer.transform.position = point;
                organizer.AssignPosition(values[i]);

                player.GetComponent<MovementManager>().MoveToDestination(organizer.ConfirmedPosition());
            }
        }

        void FiringLine(Vector3 point)
        {
            List<Vector3> values = new List<Vector3>()
            {
                point,
                point + new Vector3(1f, 0, 0),
                point + new Vector3(-1f, 0, 0),
                point + new Vector3(2f, 0, 0),
                point + new Vector3(-2f, 0, 0),
                point + new Vector3(3f, 0, 0),
                point + new Vector3(-3f, 0, 0),
                point + new Vector3(4f, 0, 0),
                point + new Vector3(-4f, 0, 0),
                point + new Vector3(5f, 0, 0),
                point + new Vector3(-5f, 0, 0),
                point + new Vector3(6f, 0, 0),
            };

            for (int i = 0; i < readyUnitList.Count; i++)
            {
                PlayerControl player = readyUnitList[i].player;
                organizer.transform.position = point;
                organizer.AssignPosition(values[i]);

                player.GetComponent<MovementManager>().MoveToDestination(organizer.ConfirmedPosition());
            }
        }
    }
}

This reaches across the aisle to:

{
    public class PathOrganizer : MonoBehaviour
    {
        [SerializeField] GameObject destinationMarker = null;
        NavMeshAgent agent;
        Vector3 callback = Vector3.zero;

        private void Awake()
        {
            agent = GetComponent<NavMeshAgent>();
        }

        public void ClearDestinationMarkers()
        {
            foreach(Transform child in gameObject.transform)
            {
                Destroy(child.gameObject);
            }
        }

        public void AssignPosition(Vector3 point)
        {
            SteerFormation();
            GameObject waypoint = Instantiate(destinationMarker, transform.position, Quaternion.identity);
            waypoint.transform.SetParent(gameObject.transform, true);
            waypoint.transform.localPosition = point;
            waypoint.transform.eulerAngles = new Vector3(90, 0, 0);
            callback = point;
        }

        public Vector3 ConfirmedPosition()
        {
            return callback;
        }

       public void SteerFormation()
      {
         Vector3 heading = Input.mousePosition - transform.position;
         float distance = heading.magnitude;
         Vector3 direction = heading / distance;

         agent.destination = Input.mousePosition;
        transform.LookAt(direction);
     }
}

That works just dandy. That last method (SteerFormation) is where I snag. What you see there is just a place holder command until I can finally score a “W” against this challenge. I have tried more complicated rotation logic outside of finding headings and magnitudes, at one point I even aligned my waypoint container with the camera’s field of view. The previous failures do indeed turn the container GameObject (PathOrganizer) but the positions within are never altered.

The problem as I see it is that these positions that are instantiated (via destinationMarker) reflect global positioning and are really no more than stamps on the mesh where they popped into existance. I want these markers to ride inside of the PathOrganizer like it is a bus and the agents are dogs chasing the bus. In theory, if I steer the bus the whole flock follows in kind. I have investigated similar queries but haven’t found (or perhaps did not understand) a satisfying solution.

If you have seen a question worded exactly like this on Stack Overflow then I would say, “Nuh-Uh,! I Didn’t write that post. Pants on Fire! Pants on Fire!”

But I won’t… Because I am more mature than you.

                          **---- *bump *----**

I am not bumping this topic up the stack because tactical combat maneuvers are the hook of my game and getting this rotation conundrum corrected is the keystone to my combat structure. Or, because the past three and a half days losing against this challenge has beaten my brain like a Pangalactic Gargleblaster.

I am bumping this topic because I am histrionic and need the attention.

Current iteration of unsuccess. This is the state of PathOrganizer.cs now;

 public class PathOrganizer : MonoBehaviour
 {
     [SerializeField] GameObject[] destinationMarkers = null;
     [SerializeField] float timeToOrganize = 0.8f;
     List<ReadyUnit> readyUnitList = new();
     NavMeshAgent agent;
     Vector3 callback = Vector3.zero;
     InputControl control;
     int index = 0;

     private void Awake()
     {
         agent = GetComponent<NavMeshAgent>();
         control = new();
         control.Enable();

         if (destinationMarkers != null)
         {
             for (int i = 0; i < destinationMarkers.Length; i++)
             {
                 GameObject marker = destinationMarkers[i];
                 marker.transform.eulerAngles = new Vector3(90, 0, 0);
                 marker.SetActive(false);
             }
         }
     }

     public List<ReadyUnit> PassList(List<ReadyUnit> passedList)
     {
         readyUnitList = new();
         readyUnitList = passedList;
         return passedList; 
     }

     public void DeactivateMarkers()
     {
         foreach (Transform child in gameObject.transform)
         {
             child.gameObject.SetActive(false);
         }
     }

     public void AssignPosition(Vector3 point)
     {
         StartCoroutine(AssembleInRanks(point));
         SteerFormation();
     }

     IEnumerator AssembleInRanks(Vector3 point)
     {
         for (int i = 0; i < readyUnitList.Count; i++)
         {
             GameObject marker = destinationMarkers[i];
             marker.SetActive(true);
             marker.transform.localPosition = point;
             callback = marker.transform.position;
             if (index > readyUnitList.Count || index < 0)
             {
                 index = 0;
             }
             index++;
             while (index < readyUnitList.Count) 
             {
                 yield return null;
             }
         }

         index = 0;
   
     }

     public Vector3 ConfirmedPosition()
     {
         return callback;
     }

     public void SteerFormation()
     {
         Vector3 heading = Input.mousePosition - transform.position;
         float distance = heading.magnitude;
         Vector3 direction = heading / distance;

         agent.destination = Input.mousePosition;
         transform.LookAt(direction);
     }
 }

The only change in Formation.cs is to pass the readyUnitList over to PathOrganizer. Not better, just different. I am trying. Anyone got anything?

This isn’t something I’ve tried to work with before… simply hasn’t occurred to me…

I’m going to describe the process I would go through to make a group try to stay in a formation. I think in general, almost anything is going to break down at a certain point because of the difference in the required move speed when a formation turns.

First, I would make a formation GameObject. This item will have a NavMeshAgent, and will be what follows the path set by the character.

Then each point in the formation will be a child object of this Formation GameObject. These will be assigned to each individual within the formation accordingly. Now if you put a simply script on each of these GameObjects

void OnGizmos()
{
    Gizmos.DrawSphere(transform.position, .5f);
    Gizmos.DrawLine(transform.position, transform.position+(transform.forward));
}

You’ll see that each of the child objects is maintaining formation as the master formation moves.

Now set each individual’s NavMeshAgent to follow one of these child objects. The agent will have to be regularly refreshed with the new position of it’s individual target.

I think I am trying to achieve just what you suggest. In this case the Leader was supposed to act as the destination setter for the following agents and the PathOrganizer.gameObject was the destination setter for the Leader. I did a little shift on where the destinationMarkers are placed inside of PathOrganizer. It did not go well but it did show and tell.

IEnumerator AssembleInRanks()
{
    Vector3 point = transform.position;
    for (int i = 0; i < readyUnitList.Count; i++)
    {
        GameObject marker = destinationMarkers[i];
        marker.SetActive(true);
        marker.transform.localPosition = point;
        callback = marker.transform.position;
        if (index > readyUnitList.Count || index < 0)
        {
            index = 0;
        }
        index++;
        while (index < readyUnitList.Count)
        {
            yield return null;
        }
    }
    yield return new WaitForSeconds(1f);
    index = 0;
}

Keep in mind that the destinationMarkers are “physically” present at all times. AssembleInRanks just turns them on and off and pushes them around. I emphasized the assembly time with the WaitForSeconds. The commands execute in order (no surprise) but in view in scene view all of the destinationMarkers collapse atop each other as the agents begin to trek.

I’ve still got nothing but the question, “Why?” However, I do know about a new, “Why not.”

I’ll update if I find something else relevant.

In my model, the destination markers don’t move at all through scripting… only however they would move as child objects of the master follow object. If they’re collapsing on top of one another, something other than the master transform is moving them.

I am not intending to turn this into a fireside discussion. I am still stumped but I found my Akum’s razor. If turning the container has not worked, no matter the method applied. and trying to space the units relative to the Leader unit does work, but only in a single, consistent pattern. I think maybe I am blaming the children for the faults of their parent.

If I can somehow orient the input Vector3 point, as if it were a GameObject itself, there may be a fix. To illustrate my hypothosis without opening Gimp and making a whole presentation: Take this V. The point of intersection of the vertices is where Leader stands point. That point is marked in World Space with no reference to rotation. It is just the position. So even if I want the Leader’s position > over here or < over there (where presumably the other positions will be calculated relative to the Leader but in Local Space) the formation will always be shaped V. Spin the die but the pips stay in place.

That is where my head is at. How can I INPUT the position and rotation to this formula in some sensible way? I am thinking by adjustment in the Formation class or back at the root in MovementManager. Don’t got it yet though. Just don’t.

Here’s a look at my sample formation… in this case, it’s pre made, like a prefab, though making the TargertPoint Gameobjects their own prefabs would allow you to create the points like you have in your original script.

So right now, these have gizmos on them, the main FormationLeader is the blue triangle (represented through DrawGizmos), and each sphere with a line pointing forward is a Formation Point.

You’ll assign each member of the formation one formation point. Their Mover will be instructed to follow the individual formation point, not the Formation Leader.

The Formation Leader will be assigned the ultimate destination of where you want the formation to go.

using UnityEngine;
using UnityEngine.AI;

public class FormationLeader : MonoBehaviour
{
    private Vector3 target;
    private NavMeshAgent agent;

    private void Awake()
    {
        agent = GetComponent<NavMeshAgent>();
    }

    void Update()
    {
        if (Vector3.Distance(transform.position, target) < 2f)
        {
            agent.isStopped = true;
        }
    }

    public void SetTarget(Vector3 targetPoint)
    {
        target = targetPoint;
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.blue;
        Gizmos.DrawLine(transform.position - transform.right*3 - transform.forward*3, transform.position + transform.forward*3);
        Gizmos.DrawLine(transform.position + transform.right*3 - transform.forward*3, transform.position + transform.forward*3);
        Gizmos.DrawLine(transform.position - transform.right*3 - transform.forward*3, transform.position + transform.right*3 - transform.forward*3);
        Gizmos.DrawLine(transform.position, transform.position + transform.forward*6f);
    }
}

Now… as the FormationLeader moves, the FormationPoints move automatically because they are child objects to the FormationLeader.

In the picture below, all I have changed is the FormationLeader’s position and rotation. The child objects simply followed the leader (including rotation) with no additional input.

The individual members of the formation should each get assigned a spot in the formation, with a component that seeks to move the character to the spot in the formation, not anything to do with the formation leader. When the character has gotten within say… 1 unit, snap the character to the position of the transform point and set his forward vector (transform.forward = target.transform.forward).

Ok, I tried to be terse, then I proofed my post and came back up to warn you that I wasn’t. Despite the maudlin tone in what follows, I am on the brink with this one… God, I hope so…

I did end up following the plan outlined above. It was sensible to build the shape of the formation ahead of time save it as a prefab. It could, it should, and someday when parkas are vogue in Hell, it WILL work.

I have ONE and only ONE prefab for these experiments in torture porn. It is a neat little column that stays in shape and is willingly lead by an agent dangling from a stick. I give a little quasi-callback to every unit listening like so:

List<GameObject> markerList = new List<GameObject>();
markerList = column.GetComponent<FormationOrganizer>().GetDestinationMarkers();
for (int i = 0; i < readyList.Count; i++)
{
    GameObject marker = markerList[i];
    marker.SetActive(true);
    readyList[i].player.GetComponent<MovementManager>().AssignPosition(marker.transform.position);
}

Back in MovementManager (read Mover)

public void AssignPosition(Vector3 target)
{
    mark = target;
}

In MovementManager this also toggles a Boolean that lets Update know if the agent can or cannot follow the mark. This exchange touches only what it should and it almost works. Here is the rub; we are back to local VS. world space. The destination markers cruise around all over the map wherever I tell them. They ARE going where they should and rotating as a whole structure. But, let us say a marker is in shared space at (50, 0, 50), since it is a child of a game object, its own transform reads (0, 0, -2). Guess which way my dimwits amble. So, I got busy (spoiler alert, none of the following saves the day), tried referencing everybody’s local and global positions, tried to convert using transform.TransformPoint, tried every hairbrained parenting and reparenting scheme guaranteed not to work, starting coding on tilt and just got up to no good at all. No matter what, it always comes back to this one mocking, soul eating, Pazuzu.

Who would have thought that something so simple, so, “No-duh” would become a lecturer’s whiteboard graffitied by Will Hunting. I am not using any hyperbole in describing these past several days as damaging to my self-esteem. I had some quit thoughts. It has taken time to give in and admit that I am outside of my capabilities. No self-deprecating jests this time. I am sincerely humble.

I know I can’t see the forest because of all of these damned trees. if any of ya’ll know that something I don’t; may I, pretty please, know it too.

EDIT -

Am I even using this right?

readyList[i].player.GetComponent<MovementManager>().AssignPosition(marker.transform.TransformPoint(marker.transform.localPosition));
         
Debug.Log("Marker[" + i + "].position = " + marker.transform.position + ", localPosition" +
            marker.transform.localPosition + ", TransformPoint = " + marker.transform.TransformPoint(marker.transform.localPosition));

Doesn’t work.

The positions I want to see are: Marker[0] => (48.05, 0, 45.76), Marker[1] => (45.23, 0, 47.15), etcetera…

DUDE!!! Dude! Dude! Dude! Dude… dudedudeDude! Before you ask; Yes! This really is me and I really am that cool. Touches are 3 crypto, selfies: 10.

There is so much to unpack that I am going to spare you most all of it. You (Brian) were right to offer your alternative solution but there was far more refactoring than anticipated. Bottom line is that during my far-reaching extracurricular investigation I struggled with some concepts. With so many moving parts, some of my issues were just plain oversight that turned into my personal worst bug hunt. Other issues were purely in the House of Coding Why-Nots. In the end It does what I want it to do, the result isn’t quite below par yet. I think, next time, I should get the coloring book with instructions.

The quasi-victory actually did come in the form of Transform.TransformPoint. That little beauty got me across the finish line. Then all I had to do was find out where the race was being held, of course.

I am going to mark this as solved. It is a bloated topic. I am simply exhausted. I have a full beard for the first time in sixteen years. Though that may seem unrelated, it most certainly is not. Such a simple little thing; so much genuine mental strife. There are several anomalies that need to be understood, plus I have loads of clean up and polish to do. I will be coming back, sooner than later. Put the dog outside.

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

If you’ve ever met @bahaa, these two posts are terse.

This post has me in stitches!

What’s a Pazuzu?

Almost anybody whose ever been a Junior Developer with a list of outrageous demands from a Senior Developer who just went out for a three martini lunch.

Not sure why position wasn’t working, except to say that my first reading of the debugs led me to believe that the master object was at 0,0,0 based on the debugs.

And if Unity ever finishes those instructions I’ll be out of a job. :stuck_out_tongue:

Good job getting it sorted, and enjoy your squad formations!

1 Like

Privacy & Terms