There must be a better way to do this

Hello!

I’m doing something relatively simple: rotating a battalion of soldiers towards a point at 90-degree intervals (so everything is perfectly aligned). Basically, the script determines if the point is to the left or right of the battalion of soldiers. Then, it looks what directions the soldiers are facing at the moment. Based on that, it decides how to rotate. However, the method I’m using seems very inefficient. Here’s the script I wrote:

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class BatalionController:MonoBehaviour {

    private bool AllowMoveToPoint;
    public GameObject Arrows;
    public bool IsMovingToPoint;
    public bool IsRotating;
    public List<Vector3> WaypointList;
    public List<GameObject> Soldiers;
    [Header("Movement settings")]
    public int RotationSpeed;
    private bool TargetIsAbove;
    private bool TargetIsBelow;
    private bool TargetIsLeft;
    private bool TargetIsRight;
    //facing bools show what direction the soldiers are facing BEFORE starting to rotate
    public bool FacingUp;
    public bool FacingDown;
    public bool FacingLeft;
    public bool FacingRight;
    public float DegreesTurned;
    public float OrigYRotation;
    public int TurnAmount;
    // Use this for initialization
    void Start ()
    {
        Soldiers = GetComponent<BatalionGenerator>().SpawnedObjects;
    }
 
    // Update is called once per frame
    void FixedUpdate ()
    {
        if (IsMovingToPoint == true)
        {
            //now get direction of movement - up/down/left/right
            if (IsRotating)
            {
                if (TargetIsRight)
                {
                    RotateTargetIsRight();
                }

                if (TargetIsLeft)
                {
                    RotateTargetIsLeft();
                }
                    if (DegreesTurned >= TurnAmount)
                        IsRotating = false; Debug.Log("Finished rotating");
                    Debug.Log("Rotation Y: " + Soldiers.FirstOrDefault().transform.localEulerAngles.y);
                }
            }

        }

    void RotateTargetIsRight()
    {
        if (FacingUp)
        {
            TurnAmount = 90;
            foreach (GameObject soldier in Soldiers)
            {
                //turn right
                soldier.transform.Rotate(Vector3.up * RotationSpeed * Time.deltaTime);
                DegreesTurned = soldier.transform.localEulerAngles.y;
            }
        }

        if (FacingDown)
        {
            TurnAmount = 90;
            foreach (GameObject soldier in Soldiers)
            {
                //turn right
                soldier.transform.Rotate(Vector3.up * RotationSpeed * Time.deltaTime);
                DegreesTurned = 180 - soldier.transform.localEulerAngles.y;
            }
        }

        if (FacingRight)
        {
            foreach (GameObject soldier in Soldiers)
            {
                //already facing needed direction
                IsRotating = false;
            }
        }

        if (FacingLeft)
        {
            TurnAmount = 180;
            foreach (GameObject soldier in Soldiers)
            {
                soldier.transform.Rotate(Vector3.up * -RotationSpeed * Time.deltaTime);
                DegreesTurned = 270 - soldier.transform.localEulerAngles.y;
            }
        }
    }

    void RotateTargetIsLeft()
    {
        if (FacingUp)
        {
            TurnAmount = 90;
            foreach (GameObject soldier in Soldiers)
            {
                //turn left
                soldier.transform.Rotate(Vector3.up * -RotationSpeed * Time.deltaTime);
                DegreesTurned = 360 - soldier.transform.localEulerAngles.y;
            }
        }

        if (FacingDown)
        {
            TurnAmount = 90;
            foreach (GameObject soldier in Soldiers)
            {
                //turn right
                soldier.transform.Rotate(Vector3.up * RotationSpeed * Time.deltaTime);
                DegreesTurned = soldier.transform.localEulerAngles.y - 180;
            }
        }

        if (FacingRight)
        {
            TurnAmount = 180;
            foreach (GameObject soldier in Soldiers)
            {
                soldier.transform.Rotate(Vector3.up * RotationSpeed * Time.deltaTime);
                DegreesTurned = -(90 - soldier.transform.localEulerAngles.y);
            }
        }

        if (FacingLeft)
        {
            foreach (GameObject soldier in Soldiers)
            {
                //already facing needed direction
                IsRotating = false;
            }
        }
    }

    void GetDirection()
    {
        DegreesTurned = 0;
        TargetIsAbove = false;
        TargetIsBelow = false;
        TargetIsLeft = false;
        TargetIsRight = false;
        FacingUp = false;
        FacingDown = false;
        FacingLeft = false;
        FacingRight = false;
        GameObject OneSoldier = Soldiers.FirstOrDefault();
        OrigYRotation = OneSoldier.transform.localEulerAngles.y;
        if (OrigYRotation < 1 && OrigYRotation > -1)
        {
            FacingUp = true;
        }

        if (OrigYRotation < 271 && OrigYRotation > 269)
        {
            FacingLeft = true;
        }

        if (OrigYRotation < 181 && OrigYRotation > 179)
        {
            FacingDown = true;
        }

        if (OrigYRotation < 91 && OrigYRotation > 90)
        {
            FacingRight = true;
        }
        //SpriteLine.transform.position = SpawnPositions[SpawnPositions.Count - 1];
        float DiffX = WaypointList[WaypointList.Count - 1].x - transform.position.x;
        //make sure it's positive
        if (DiffX < 0)
        {
            DiffX *= -1;
            TargetIsLeft = true;
        }

        else
        {
            TargetIsLeft = false;
            TargetIsRight = true;
        }

        float DiffZ = transform.position.z - WaypointList[WaypointList.Count - 1].z;
        //make sure it's positive
        if (DiffZ < 0)
        {
            DiffZ *= -1;
            TargetIsBelow = true;
        }

        else
        {
            TargetIsBelow = false;
            TargetIsAbove = true;
        }
        IsRotating = true;
    }

    public void StartMoveAlongPoints()
    {
        Arrows.SetActive(false);
        GetDirection();
        IsMovingToPoint = true;
        Debug.Log("Batalion movement started");
    }
}

I have an obscene amount of if statements, and all they do is decide what direction to turn my battalion of soldiers. I’ve only done the script so the soldiers can point towards targets that are to the left or right only as I know I’m wasting my time doing this wrong. I just don’t think having 16 if statements is the proper way to do this (4 target locations (down, up, left, right) * 4 battalion start look rotation states (down, up, left right)). This will end up to about 200 lines of code of nothing but if statements.

The script works perfectly, but is there a way to shorten it? Note that I do NOT want to use Quaternion.Lerp as it has an acceleration/deceleration of rotation speed at the start and end of rotating. I need the soldiers to rotate at a fixed speed.

Thank you for any insight!

I’m nearly positive this isn’t true.

One problem in your code is your using Time.deltaTime in FixedUpdate. This will not be valid. It will be close… but off. That is because Time.deltaTime is updated each UPDATE, which run slower or faster than FixedUpdate. You should be using Time.fixedDeltaTime.
Apparently Time.deltaTime and Time.fixedDeltaTime are the same in a FixedUpdate

I created a Unity Project and added a Cube with a small cylinder sticking out the front. I attached the script below to it.
I created a Unity SPhere, and dragged it onto the Transform spot for target in the inspector on the script.

Moving the Unity Sphere around the cube always rotates and points to the sphere along 90 degrees bounds. I wasn’t sure how you handled a target that might 122 degrees away from my cube’s forward face, so I round all angles to 90 degrees. (In this case the cube will turn 90 degrees and stop. If the target was 153 degrees away, it would turn a full 180).

You can uncomment the Debug.Log in the FixedUpdate and see its clearly moving linearly the entire time. Without any speedup or slowdowns. I think using deltaTime in a fixedUpdate caused you to see this speedups/slowdowns. I’m not sure what made you think it has a slowdown or speed up. Sometimes when you hit play in the Unity editor, it misses a few frames for some reason, so the object seems to “jump” in the rotation. This is just an artifact of the Editor playing the game.

Turning on 90 Degrees Code

using System;using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GenericTest : MonoBehaviour {

    public Transform target;
    public float turnSpeed = 45;
    float angle;
    float direction;
    bool isMoving = true;
    float currentTime;
    float totalTime;
    Quaternion startRotation;
    Quaternion endRotation;

    void Start () {

        SetUpTurn();
   
    }

    private void FixedUpdate()
    {
        if (isMoving)
        {
            if (currentTime < totalTime)
            {
                currentTime += Time.fixedDeltaTime;
                transform.rotation = Quaternion.Slerp(startRotation, endRotation, currentTime / totalTime);
                //Debug.Log("Time = " + currentTime + ", " + transform.rotation.eulerAngles.y);
            }
            else
                isMoving = false;
        }
    }

    public void SetUpTurn()
    {
        Vector3 tarDir = target.position - transform.position;
        angle = Vector3.Angle(transform.forward, tarDir);
 
        // Round the angle to the nearest 90
        angle = angle / 90f;
        angle = Mathf.Round(angle);
        angle *= 90;

        // We are already facing the target
        if (angle == 0)
            return;

        // This just determines left/right of us.  negative is left, positive is right
        direction = transform.forward.z * tarDir.x - transform.forward.x * tarDir.z;

        if (direction != 0)
            direction = Mathf.Sign(direction);

        // Set up our Lerp initialization
        currentTime = 0;
        totalTime = angle / turnSpeed;
   
        startRotation = transform.rotation;
        Vector3 eulerAngles = startRotation.eulerAngles;
        // if direction = 0 it must be 180 degrees behind us.
        if (direction != 0)
            eulerAngles.y += angle * direction;
        else
            eulerAngles.y += angle;
        endRotation = Quaternion.Euler(eulerAngles);

        isMoving = true;
    }

}

It avoids all the if statements by determing the angle between us and the target. And using the formula for determining if a point is to the left/right of a line. The full formula assumes you have a line segment AB and some point X,Y then its:
(By-Ay)(X-Ax) - (Bx-Ax)(Y-Ay)

But in our case we are treating A as the 0,0 point. and B is our local forward vector. X,Y is the direction Vector from us to the target.

Edited my response to reflect @Suddoha response below.

1 Like

It returns the correct value when in FixedUpdate. Unity handles that internally.

2 Likes

@Suddoha Heh I never know that. Just changed my code to Time.deltaTime and it does indeed give fixedDeltaTime. I guess that got added in sometime after they had the two different variables and never bothered to get rid of fixedDeltaTime.

Regardless Quaternion.Slerp does not have any speedup/slowdowns at the beginning or ends :slight_smile:

Well that was much more detailed than I expected, huge thanks! I added it to my script, fixed the variable names and it works great :smile:

You’re right, I didn’t clarify enough. I forgot to mention that I already have a system in place to plot the points. The points will ALWAYS be directly in front/back/left/right of the soldiers. That’s why my code didn’t worry about non-perfectly-straight angles. Also, I made an assumption when I said “I do NOT want to use Quaternion.Lerp as it has an acceleration/deceleration of rotation speed at the start and end of rotating”. Turns out that I used Vector3.Lerp before and that’s the behavior I got, and I assumed Quaternion.Lerp would be the same.

Since this mostly works as I want I’ll probably just dump most of the code I wrote and adapt it to what I need. Huge thanks again!

Ill try to write up an example tomorrow if needed, but the general idea is as follows:

1 Get the direction that target is closest to. Just get the targets angle using Vector3.angle using Vector3.up and the targets direction from the squad then round this to 90 degrees.

  1. Using Vector3.RotateTowards rotate the squad to the desired heading. An example can be found here:
    Unity - Scripting API: Vector3.RotateTowards

That should drastically drop the amount of if statements and if you want different degrees as long as you did the for loop correctly it should be an easy change.

1 Like

I think Unity has a built in function for this

angle = Vector3.Angle(transform.forward, tarDir);

I posted some code a few responses back (hidden in a spoiler) that uses your idea but implements it with the builtIn unity function. I use a quick point is left/right of a line determinant to figure out the angle we need. (since Vector3.Angle only returns an absolute shortest angle, but no direction of which way to turn).

I saw your eadlier response after writing mine and edited it once I realised I over thought that part. I came to the same conclusion once I read “Round the angle” :stuck_out_tongue: