Question about Quaternion.Slerp

Hello Everyone,
I have a question about Quaternion.Slerp() and Quaternion.Lerp(). I am working on a MOBA game similar to League of Legends and I am trying to get my minions to move as expected. I created a Minion prefab for each type of minion, but not for each team. I am using a waypoint system for the pathing of the minions, however, I am having trouble because the blue side minions are looking the opposite direction of what I expect. so any of my code that uses transform.forward always gives undesired results and I must invert it so I can get it to do what I want. This seems unnecessary and makes it more difficult than I feel that it should be. I dont understand why the red minions face the correct direction while the blue minions always face the opposite direction they should (they face the same direction as the red minions). Each team and each lane has their own set of way points and they all move to the correct positions, but the blue minions dont rotate to face where they are going. I will post my full minion code as well as two pictures of what I am talking about. The black cube shows the transform.forward of the minions. In each picture the minions are moving from the left to the right. heading towards the center of the map. I appreciate any and all help with this, please tell me what I am doing wrong or failing to understand. I believe the relevant code is between lines 152 and 163.

.


Here is my code:

public class ScrMinions : MonoBehaviour
{
    [SerializeField] private ScrWaypoints midRedWaypoints;
    [SerializeField] private ScrWaypoints topRedWaypoints;
    [SerializeField] private ScrWaypoints botRedWaypoints;
    [SerializeField] private ScrWaypoints midBlueWaypoints;
    [SerializeField] private ScrWaypoints topBlueWaypoints;
    [SerializeField] private ScrWaypoints botBlueWaypoints;
    [SerializeField] private Material blueTeamMat;
    [SerializeField] private Material redTeamMat;
    [SerializeField] private MeshRenderer miniMapIcon;
    [SerializeField] private Slider healthBar;
    [SerializeField] private SphereCollider targetingRange;
    [SerializeField] private float rotationSpeed = 2f;
    [Range(0.1f, 2f)]
    [SerializeField] private float distanceThreshold = 0.1f;
    [Range(0.1f, 1f)]
    [SerializeField] private float destroyTimerDefault = 0.5f;
    [Range(0.1f, 1f)]
    [SerializeField] private float targetTimerDefault = 0.5f;

    private int health { get; set; }
    private int maxHealth { get; set; }
    private int damage { get; set; }
    private int attackRange { get; set; }
    private int visionRange { get; set; }
    private int armor { get; set; }
    private int magicResist { get; set; }
    private int id { get; }
    private float attackSpeed { get; set; }
    private float attackTimer { get; set; }
    private float minionMoveSpeed { get; set; }

    private float destroyTimer;
    private float targetCooldownTimer;

    private bool fighting = false;
    private bool avoidingObsticle = false;
    private bool toBeDestroyed = false;
    private bool targetCooldownElapsed = true;

    private GameObject target;

    private Transform targetWaypoint;

    private enum Priorities { MostArmor, LestArmor, HighHealth, LowHealth, MostDamage, MostRange };

    private ScrMinionSpawner.Teams team;
    private ScrMinionSpawner.Types minionType;
    private ScrMinionSpawner.Lanes minionLane;
    private Priorities[] minionPriority = new Priorities[3];

    private Vector3 avoidanceTarget;

    private List<GameObject> objectsInRange = new List<GameObject>();

    public bool GetIsDoomed() { return toBeDestroyed; }

    public ScrMinionSpawner.Teams GetMinionTeam() { return team; }

    public void SetAvoidingObsticle(bool avoiding) { avoidingObsticle = avoiding; }
    public bool GetAvoidingObsticle() { return avoidingObsticle; }

    public GameObject GetCurrentTarget() { return target; }

    public void SetAvoidanceTarget( Vector3 targetDestination) { avoidanceTarget = targetDestination; }


    public void Initialize(ScrMinionSpawner.Teams teamToJoin, ScrMinionSpawner.Types typeToAdd, ScrMinionSpawner.Lanes laneToAssign)
    {
        destroyTimer = destroyTimerDefault;
        targetCooldownTimer = targetTimerDefault;
        minionMoveSpeed = 5f;
        team = teamToJoin;
        minionType = typeToAdd;
        minionLane = laneToAssign;
        GetComponent<MeshRenderer>().material = (team == ScrMinionSpawner.Teams.Blue) ? blueTeamMat : redTeamMat;
        /*if (team == ScrMinionSpawner.Teams.Blue)
            transform.RotateAround(transform.position, transform.up, 180f);*/
        miniMapIcon.material = (team == ScrMinionSpawner.Teams.Blue) ? blueTeamMat : redTeamMat;
        this.tag = (team == ScrMinionSpawner.Teams.Blue) ? "BlueTeam" : "RedTeam";
        switch(minionType)
        {
            case ScrMinionSpawner.Types.melee:
                SetMeleeMinionStats();
                break;
            case ScrMinionSpawner.Types.range:
                SetRangeMinionStats();
                break;
            case ScrMinionSpawner.Types.segie:
                SetSegieMinionStats();
                break;
            default:
                Debug.LogError("Incorrect minion type error");
                break;
        }
        maxHealth = health;
        healthBar.maxValue = maxHealth;
        healthBar.value = health;
        targetingRange.radius = visionRange;
        GetNextNode();
        transform.position = targetWaypoint.position;
    }

    // Update is called once per frame
    void Update()
    {
        if (objectsInRange.Count != 0)
        {
            ClearTargetListOfDommedTargets();
            if (fighting == false && targetCooldownElapsed == true)
            {
                target = SelectTarget();
                fighting = true;
            }

            if (target != null && target.GetComponent<ScrMinions>().toBeDestroyed != true && avoidingObsticle == false)
            {
                if (Vector3.Distance(transform.position, target.transform.position) >= attackRange)
                {
                    transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(target.transform.position), Time.deltaTime * rotationSpeed);
                    transform.position = Vector3.MoveTowards(transform.position, target.transform.position, minionMoveSpeed * Time.deltaTime);
                }
                else
                {
                    if (attackTimer > 0)
                        attackTimer -= Time.deltaTime;
                    else
                    {
                        target.GetComponent<ScrMinions>().health -= CalculateDamage();
                        healthBar.value = health;
                        attackTimer += attackSpeed;
                        if (target.GetComponent<ScrMinions>().health <= 0)
                        {
                            objectsInRange.Remove(target);
                            this.toBeDestroyed = true;
                            target = null;
                            targetCooldownElapsed = false;
                        }
                    }
                }
            }
            else if (target != null && target.GetComponent<ScrMinions>().toBeDestroyed == true)
            {
                objectsInRange.Remove(target);
                targetCooldownElapsed = false;
                target = null;
                Debug.Log("target rest");
            }
        }

        if (!fighting && !avoidingObsticle)
        {
            if (Vector3.Distance(transform.position, targetWaypoint.position) < distanceThreshold)
                //sets next waypoint target
                GetNextNode();

            if (targetWaypoint != null)
            {
                //transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(targetWaypoint.position), Time.deltaTime * rotationSpeed);
                transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(targetWaypoint.position), Time.deltaTime * rotationSpeed);
                transform.position = Vector3.MoveTowards(transform.position, targetWaypoint.position, minionMoveSpeed * Time.deltaTime);
            }
            else
                Destroy(this.gameObject);
        }

        if (health <= 0)
            this.toBeDestroyed = true;

        if(this.toBeDestroyed)
        {
            if (destroyTimer > 0)
                destroyTimer -= Time.deltaTime;
            else
            {
                destroyTimer += destroyTimerDefault;
                Destroy(this.gameObject);
            }
        }

        if (target == null && fighting == true && targetCooldownElapsed)
            fighting = false;

        if (target == null && fighting == true && targetCooldownTimer > 0 && targetCooldownElapsed == false)
            targetCooldownTimer -= Time.deltaTime;
        else if(target == null && fighting == true && targetCooldownTimer <= 0 && targetCooldownElapsed == false)
        {
            targetCooldownElapsed = true;
            targetCooldownTimer += targetTimerDefault;
        }
    }

    private void ClearTargetListOfDommedTargets()
    {
        GameObject[] doomedTargets = new GameObject[objectsInRange.Count];
        int numberOfDoomedTargets = 0;
        foreach (GameObject enemy in objectsInRange)
        {
            if (enemy.GetComponent<ScrMinions>().toBeDestroyed)
            {
                doomedTargets[numberOfDoomedTargets] = enemy;
                numberOfDoomedTargets++;
            }
        }

        for(int i = 0; i < numberOfDoomedTargets; i++)
        {
            if (doomedTargets[i] != null)
                objectsInRange.Remove(doomedTargets[i]);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        switch(other.tag)
        {
            case "Untagged":
                break;
            case "BlueTeam":
                if (team == ScrMinionSpawner.Teams.Red && !other.gameObject.GetComponent<ScrMinions>().toBeDestroyed)
                    objectsInRange.Add(other.gameObject);
                break;
            case "RedTeam":
                if (team == ScrMinionSpawner.Teams.Blue && !other.gameObject.GetComponent<ScrMinions>().toBeDestroyed)
                    objectsInRange.Add(other.gameObject);
                break;
            case "BlueTeamPlayer":
                if (team == ScrMinionSpawner.Teams.Red)
                    Debug.Log($"Enemy player in range, Storing game object data. id: {other.GetInstanceID()}");
                break;
            case "RedTeamPlayer":
                if (team == ScrMinionSpawner.Teams.Blue)
                    Debug.Log($"Enemy player in range, Storing game object data. id: {other.GetInstanceID()}");
                break;
            default:
                Debug.LogError("Unhandled collision tag error.");
                break;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (objectsInRange.Contains(other.gameObject))
            objectsInRange.Remove(other.gameObject);
    }

    private GameObject SelectTarget()
    {
        int targetPrio = Random.Range(0, 3);
        if (objectsInRange.Count > 1)
        {
            GameObject currentTarget = objectsInRange[0];
            for (int i = 0; i < objectsInRange.Count - 1; i++)
            {
                if (minionPriority[targetPrio] == Priorities.LowHealth)
                {
                    if (objectsInRange[i + 1].GetComponent<ScrMinions>().health < currentTarget.GetComponent<ScrMinions>().health)
                        currentTarget = objectsInRange[i + 1];
                }
                if (minionPriority[targetPrio] == Priorities.HighHealth)
                {
                    if (objectsInRange[i + 1].GetComponent<ScrMinions>().health > currentTarget.GetComponent<ScrMinions>().health)
                        currentTarget = objectsInRange[i + 1];
                }
                if (minionPriority[targetPrio] == Priorities.MostDamage)
                {
                    if (objectsInRange[i + 1].GetComponent<ScrMinions>().damage > currentTarget.GetComponent<ScrMinions>().damage)
                        currentTarget = objectsInRange[i + 1];
                }
                if (minionPriority[targetPrio] == Priorities.MostRange)
                {
                    if (objectsInRange[i + 1].GetComponent<ScrMinions>().attackRange > currentTarget.GetComponent<ScrMinions>().attackRange)
                        currentTarget = objectsInRange[i + 1];
                }
                if (minionPriority[targetPrio] == Priorities.MostArmor)
                {
                    if (objectsInRange[i + 1].GetComponent<ScrMinions>().armor > currentTarget.GetComponent<ScrMinions>().armor)
                        currentTarget = objectsInRange[i + 1];
                }
                if (minionPriority[targetPrio] == Priorities.LestArmor)
                {
                    if (objectsInRange[i + 1].GetComponent<ScrMinions>().armor < currentTarget.GetComponent<ScrMinions>().armor)
                        currentTarget = objectsInRange[i + 1];
                }
            }
            if (!currentTarget.GetComponent<ScrMinions>().toBeDestroyed)
                return currentTarget;
            else
                return null;
        }
        else if (objectsInRange.Count > 0)
        {
            if (!objectsInRange[0].GetComponent<ScrMinions>().toBeDestroyed)
                return objectsInRange[0];
            else
                return null;
        }
        else
            return null;
    }

    private int CalculateDamage()
    {
        int damageDone;
        damageDone = Mathf.RoundToInt(damage - (damage * (target.GetComponent<ScrMinions>().armor)/100));
        return damageDone;
    }

    private void OnDrawGizmos()
    {
        
    }

    private void SetMeleeMinionStats()
    {
        health = 250;
        damage = 15;
        visionRange = 6;
        attackRange = 2;
        armor = 8;
        magicResist = 5;
        attackSpeed = 1f;
        attackTimer = attackSpeed;
        minionPriority[0] = Priorities.LowHealth;
        minionPriority[1] = Priorities.MostDamage;
        minionPriority[2] = Priorities.MostRange;
    }

    private void SetRangeMinionStats()
    {
        health = 135;
        damage = 12;
        visionRange = 18;
        attackRange = 8;
        armor = 4;
        magicResist = 8;
        attackSpeed = 1f;
        attackTimer = attackSpeed;
        minionPriority[0] = Priorities.HighHealth;
        minionPriority[1] = Priorities.MostDamage;
        minionPriority[2] = Priorities.LestArmor;
    }

    private void SetSegieMinionStats()
    {
        health = 600;
        damage = 25;
        visionRange = 12;
        attackRange = 6;
        armor = 20;
        magicResist = 12;
        attackSpeed = 1.5f;
        attackTimer = attackSpeed;
        minionPriority[0] = Priorities.MostArmor;
        minionPriority[1] = Priorities.LowHealth;
        minionPriority[2] = Priorities.MostDamage;
    }

    private void GetNextNode()
    {
        if (team == ScrMinionSpawner.Teams.Red)
        {
            switch (minionLane)
            {
                case ScrMinionSpawner.Lanes.Bot:
                    targetWaypoint = botRedWaypoints.GetNextWaypoint(targetWaypoint);
                    break;
                case ScrMinionSpawner.Lanes.Top:
                    targetWaypoint = topRedWaypoints.GetNextWaypoint(targetWaypoint);
                    break;
                case ScrMinionSpawner.Lanes.Mid:
                    targetWaypoint = midRedWaypoints.GetNextWaypoint(targetWaypoint);
                    break;
            }
        }
        else
        {
            switch (minionLane)
            {
                case ScrMinionSpawner.Lanes.Bot:
                    targetWaypoint = botBlueWaypoints.GetNextWaypoint(targetWaypoint);
                    break;
                case ScrMinionSpawner.Lanes.Top:
                    targetWaypoint = topBlueWaypoints.GetNextWaypoint(targetWaypoint);
                    break;
                case ScrMinionSpawner.Lanes.Mid:
                    targetWaypoint = midBlueWaypoints.GetNextWaypoint(targetWaypoint);
                    break;
            }
        }
    }
}

LookRotation wants a direction but you’re giving it a position.
Change line 161 to:

transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(targetWaypoint.position-transform.position), Time.deltaTime * rotationSpeed);

Yup that was it, thank you so much. I dont understand why that worked though. I understand what you mean when you say it was expecting a direction but I gave it a position. However, I dont understand why the difference between the current location and the target location is a direction and not a position. If you have the time, would you mind explaining that please.

Yeah the difference could be viewed as a relative position and not a direction.

I guess Unity could update the LookRotation documentation and state that LookRotation wants a direction or a relative position to look towards. It’s common to see scripts that normalize the relative position before passing it to LookRotation because it’s often assumed that it will only work with a direction and not a relative position.

Definitely compulsory reading: Unity - Manual: Vectors

The Vector3 struct and others can play multiple roles. Such as a position, a direction, and even euler angles, sometimes multiple at once, depending on context and how it’s interpreted.