Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Determine how close an object is to an angle

Discussion in 'Scripting' started by SolitaryDeveloper, Feb 8, 2021.

  1. SolitaryDeveloper

    SolitaryDeveloper

    Joined:
    Feb 7, 2021
    Posts:
    8
    I don't see a way to lock posts or change titles, but my problem was solved! Thank you everyone for taking the time of day to answer!

    Hi

    I'm trying to write an alogirthm to determine the angle (angleToTarget) between two 2D objects, and compare the similarity between that angle and another angle (contextAngle) then output a value (interest) between 0 and 1 depending on how similar those two values are.Furthermore, I want to apply this algorithm to a list containing various angles between 0 and 360, and have each one of them get a different interest based on the algorithm.

    I recognize this is a bit confusing so I'll explain in other terms:
    I have two angles
    angleToTarget- This is the angle between an enemy Game Object and a Target Game Object.
    I calculate this in the algorithm.

    contextAngle- This is a predetermined angle between 0 and 360 that I use for AI movement decision making.
    These are stored in a list of MovementContext objects, MovementContexts being a class used to store an angle, and a vector that relates that angle to a vector.

    I want to calculate a float
    interest- This will be a value between 0 and 1 depending on how similar those angles are. If the angles are both 360, I want interest to be 1. If one angle is 0 and the other 360, I want interest to be 0. 0 and 180 would be 0.5, etc.

    I then want to run this calculation on a list of contextAngles, assigning an interest to each contextAngle based on it's relation to angleToTarget.

    So far a shortened version of what I've come up with is this: (Shortened to remove stuff irreleveant to the question to stop information overload, if I forgot anything I'll happily post it.) I put the problem function at the very bottom so feel free to skip past the top codeblocks to save time. I think the top ones are fine, but I included them so it's clear how the system should work.


    Code (CSharp):
    1. /* This is a data container used for AI decision making. These are generated when the AI spawns and have a predetermined angle between 0 and 360. Direction is then generated based on that angle, and is just a vector going 1Unit in the direction of the angle.
    2.  
    3. Interest will be a value between 0 and 1 depending on how relevant this direction is to the AI.
    4. */
    5.  
    6.     public class DirectionContext
    7.     {
    8.         public Vector3 direction;
    9.         public float interest;
    10.         public float angle;
    11.  
    12.         public DirectionContext(float angle)
    13.         {
    14.             this.angle = angle;
    15.             this.direction = new Vector3(Mathf.Sin(angle * Mathf.Deg2Rad), Mathf.Cos(angle * Mathf.Deg2Rad), 0).normalized;
    16.         }
    17.     }
    Code (CSharp):
    1.     public class ContextMap
    2.     {
    3.         [SerializeField] private List<DirectionContext> contextMap = new List<DirectionContext>();
    4.         public List<DirectionContext> GetContext()
    5.         {
    6.             return contextMap;
    7.         }
    8.  
    9.         public void UpdateContext(int index, float interest)
    10.         {
    11.             //CheckInterestAndSet is a simple function that sets the DirectionContext's interest to the specified number only if it's higher than the one already set.
    12.             contextMap[index].CheckInterestAndSet(interest);
    13.         }
    14.  
    15.         public void DebugDrawMap(Vector3 position, Color color)
    16.         {
    17.             foreach (DirectionContext direction in contextMap)
    18.             {
    19.                 Debug.DrawLine(position, (position + direction.direction * direction.interest);
    20.             }
    21.         }
    22.  
    23.         public void GenerateMap(int detail)
    24.         {
    25.             contextMap.Clear();
    26.  
    27.             for (int i = 0; i < detail; i++)
    28.             {
    29.                 float angle = (360 / detail * i) - 180;
    30.                 contextMap.Add(new DirectionContext(angle));
    31.             }
    32.         }
    33.     }
    Code (CSharp):
    1. /*This is a data container class. It's provided with the neccessary details by the EnemyMovement class, then passed off to other movement scripts.
    2. */
    3.     public class SteeringContext
    4.     {
    5.         public EnemyMovement parent; //A reference to the gameObject that wants the information.
    6.         public Vector3 targetDestination; //The target to move to
    7. }
    Code (CSharp):
    1. /* This is the problem function.
    2. It somewhat works as intended but not in a way that makes any sense to me (and not in a way that actually does whats intended.)
    3.  
    4. It doesn't apply interest to all of the directions (I want it to apply a little value to every single direction if possible, from 0 to 1 depending on proximity to the targetDestination.
    5.  
    6. It also doesn't apply interest as expected on the directions it does. It applies interest to the angles on the closest side, but slightly offset from where I want them to.
    7. A ContextMap with 8 Directions (UpDown/LeftRight/Diagonals,) with a targetDestination directly East, will show strongest interest to the NorthEast. This is the part that confuses me most.
    8. */
    9.         public override ContextMap AddContext(ContextMap contextMap, SteeringContext steeringContext)
    10.         {
    11.             float angleToTarget = AngleBetween(Vector3.down, steeringContext.targetDestination - steeringContext.parent.transform.position);
    12.  
    13.             for (int i = 0; i < contextMap.GetContext().Count; i++)
    14.             {
    15.                 //Dividing the lower of the numbers by the higher of the numbers SHOULD give me the number between 0 and 1 I'm looking for, shouldn't it?
    16.                 float interest =
    17.                     Mathf.Min(angleToTarget, contextMap.GetContext()[i].angle) /
    18.                     Mathf.Max(angleToTarget, contextMap.GetContext()[i].angle);
    19.                 contextMap.UpdateContext(i, interest);
    20.             }
    21.  
    22.             return contextMap;
    23.         }
    24.  
    25.         private float AngleBetween(Vector3 vector1, Vector3 vector2)
    26.         {
    27.             float sin = vector1.x * vector2.y - vector2.x * vector1.y;
    28.             float cos = vector1.x * vector2.x + vector1.y * vector2.y;
    29.             return Mathf.Atan2(sin, cos) * (180) / Mathf.PI;
    30.         }
    31.  
    If I forgot anything or didn't explain clearly please let me know. I didn't want to paste hundreds of lines of code for people to root through so I did my best to only take out the relevant parts, but it's entirely possible I forgot parts. If anything looks like it shouldn't compile it's because I made a copying mistake-- Everything compiles in my project.
    I left out the entire EnemyMovement script as it was functioning fine prior to this change so I really don't believe it's the problem. If everything I posted looks like it should work I have no problem posting the relevant parts, I just didn't want this post getting longer than it already is.
     
    Last edited: Feb 10, 2021
  2. arfish

    arfish

    Joined:
    Jan 28, 2017
    Posts:
    777
    SolitaryDeveloper likes this.
  3. SolitaryDeveloper

    SolitaryDeveloper

    Joined:
    Feb 7, 2021
    Posts:
    8
    I don't believe it's part of my problem. I was using the built in ones previously (It's commented out in my project but the codes still there.) I went to using my own because it gave me an angle consistent with the ones I already generated, but later discovered Vector2.unisigned angle achieved what I wanted.
     
  4. seejayjames

    seejayjames

    Joined:
    Jan 28, 2013
    Posts:
    685
    I think it's just the division (line 17). If you divide 0 by 360, you get 0, as you wanted. But if you divide 0 by 90, you'll also get 0. So instead you want the difference, which can be in a range of 0 to 360, then divide that by 360.

    Interesting scripts btw, looks like they could be very useful for AI behavior!
     
    SolitaryDeveloper likes this.
  5. SolitaryDeveloper

    SolitaryDeveloper

    Joined:
    Feb 7, 2021
    Posts:
    8
    I tried doing
    Code (CSharp):
    1.  
    2.                 float interest =
    3.                     Mathf.Max(angleToTarget, contextMap.GetContext()[i].GetAngle()) -
    4.                     Mathf.Min(angleToTarget, contextMap.GetContext()[i].GetAngle());
    5.                 interest /= 360;
    But it achieves the opposite of what I'm looking for. If one angle is 360, and one angle is 0, it returns 1. What I want is for it to return 0, which is why I was doing the division originally.

    I tried adding 0.01 to the Min value if it was 0 and it still didn't fix the problem. On top of this, the code you gave me, I would have suspected it would still populate the entire interest map, but instead it only adds values to about half of the interests at a time, so I'm suspecting maybe there's something bigger going on? I can't find any reason it shouldn't at least apply to the full map.



    On the topic of the scripts being interesting-- I'm trying to implement something I read about called Context Steering. My game is making pretty heavy use of steering behaviors for AI movement, both for performance reasons (The maps are procedurally generated and Destructable,) and for interest reasons (Pathfinding doesn't make for satisfying movement when there's lots of enemies on screen.)

    The issue I'm running into with steering behaviors is when you want to apply multiple at once, they tend to cancel eachother out in oddball situations and lead to broken movement (AI going back and forth between an obstacle and wall, Circling left to right behind an obstacle, etc.) On top of this steering behaviors lead to "flickering" movement, which can be smoothed a good bit, but for the larger enemies I can't get it to not be noticeable.

    Cue context steering: I have two context maps. One for danger, one for interests. The steering behaviors populate those context maps fully with where they want the AI to steer. (I'm doing mine 8 directions, so I want Seek to populate all 8 directions with a value, higher values for the more tempting destinations.)

    You populate the Danger map first, then use it to exclude any directions in the Interest Map that are over a certain threshold. Then you just use a classic Seek steering behavior to steer towards the most interesting direction in the Interest map. (The maps only have 8 directions in my game, but I get fully 360* movement by blending the most interesting direction with the ones to it's left and right, weighed by their interest value.)

    The issue is-- I can't find any source code for context steering to check my work against. I found 3 academic papers, one talk, and two blog posts, but nothing to help me with this specific problem.
     
    seejayjames likes this.
  6. arfish

    arfish

    Joined:
    Jan 28, 2017
    Posts:
    777
    It's not very clear what your problem is. Is calculating the angles the difficult part, or is the problem how to normalize them, or comparing them?

    One problem with angles is that the exact same physical angle can be represented by infinite positive, or negative angles, +360*n, or -360*n. So thats the first to normize, to for example from -180 to +180. And you have to normalize them to the same reference angle, to which you indend to compare them too, like your player forward angle, or each enimy forward angle.
     
    SolitaryDeveloper likes this.
  7. SolitaryDeveloper

    SolitaryDeveloper

    Joined:
    Feb 7, 2021
    Posts:
    8
    I've already fixed the problem and all of the angles are normalized between -180 and +180. I think I said 360 in the original post as I had done that just prior to a change to be able to use Unity's SignedAngle function instead of my own.

    I have a class I call a context map. It divides the world into angles between -180 and +180, depending on the detail level I set. (So if I set it to 8, it generates a list going -180, -135, -90, etc...) It saves these angles as DirectionContext classes, which have a float Angle, and a float Interest.

    Then I have the function at the bottom. What I want it to do is take an input position (The position of the player in my case,) and compare it's angle relative to another position to all of the angles in the context map.

    If the angles are the same, I want it to set the Interest to 1. If the values are as opposite as possible, I want it to set the interest to 0.

    So if the angle between pos1 and pos2 is 180, and the context map angle is 180 , I want the interest to be 1.
    If the angle between pos1 and pos2 is -180, and the context map angle is 180, I want the interest to be 0.

    To even more simplify and take out any phrasing related to my specific project:
    I need a function that can take in two Vector2's and a float that will be between -180 and 180 as Input. I need it to output a float value between 0 and 1.
    I want it to compare the angle between the two Vector2's using the SignedAngle function, and compare that to the float input.
    If the two angles are identical, I want the function to output a value of 1. If the two angles have a difference of 360, I want the output to be 0. Any value in between, I want to return 0.x relative to how different the numbers are.

    I've managed to do the opposite of this, and have it return 0 if there's no difference, and 1 if the difference is 360, but that's not what I'm trying to achieve. I want the value to be higher the more similar the numbers are.

    I apologize for the poor explanations. I've learned through books/youtube videos/trial and error so I'm lost when I need to actually communicate with others when it comes oto this stuff. Any other questions I'll happily answer! Thank you
     
  8. arfish

    arfish

    Joined:
    Jan 28, 2017
    Posts:
    777
    How does all of the context map angles relate to the player? If they represent an angle to a different target, they probably has to be recalculated to the one of interest, like the player.

    If the context map contains some kind of global directions. You may compare them to your player global direction. It will tell if the player is heading in a parallel course.

    If thats the plan, one way to normalize the unsigned differense may be something like:

    Code (CSharp):
    1. float delta = Matfh.Abs(contextMapAngle - playerAngle);
    2. if (delta > 180f) {
    3.   delta = 360f - delta;
    4. }
    5. normalizedValue = 1f - delta/180f;
     
    Last edited: Feb 9, 2021
    SolitaryDeveloper likes this.
  9. SolitaryDeveloper

    SolitaryDeveloper

    Joined:
    Feb 7, 2021
    Posts:
    8

    The angle actually relates to the enemy AI (that's what the system is for.) I convert the angle to a direction vector using:
    Code (CSharp):
    1.  
    2.             direction = new Vector3(Mathf.Sin(angle * Mathf.Deg2Rad), Mathf.Cos(angle * Mathf.Deg2Rad), 0).normalized;
    and steer the enemy in that direction.

    So what the function I'm trying to write does is take the angle between the enemy and it's target, and compare it to the angles in the context map, to determine how interesting each direction is for the enemy to go in.

    Your code works almost perfectly, but for some reason it flips the left and right / diagonal directions. This is easier to explain with pictures:
    The black object is the object doing the calculation (the enemy,) while the Red Object is the position I want to determine the interest for.
    The lines are a debug draw of the context map. The lines are longer the higher the interest in that direction (Based off the angle. The direction is determined by the code I posted higher up this reply.)

    So it works perfectly when the object is directly above or below, but falls apart on the diagonals/sides.

    To hopefully help I'm going to post all relevant code, as I'm really lost on whats going on here (Maths really not my strong point and is the part of AI design I hate.)

    Code (CSharp):
    1.  
    2.     public class DirectionContext
    3.     {
    4.         [SerializeField] private Vector3 direction;
    5.         [SerializeField] private float interest;
    6.         [SerializeField] private float angle;
    7.         [SerializeField] private bool isExcluded = false;
    8.         public Vector3 GetDirection() { return direction; }
    9.         public float GetInterest() { return interest; }
    10.         public void SetInterest(float value) { interest = value; }
    11.         public void CheckInterestAndSet(float newInterest)
    12.         {
    13.             if (newInterest > interest)
    14.                 interest = newInterest;
    15.         }
    16.  
    17.         public float GetAngle() { return angle; }
    18.  
    19.         public DirectionContext(float angle)
    20.         {
    21.             this.angle = angle;
    22.             this.direction = new Vector3(Mathf.Sin(angle * Mathf.Deg2Rad), Mathf.Cos(angle * Mathf.Deg2Rad), 0).normalized;
    23.         }
    24. }
    Code (CSharp):
    1.     public class ContextMap
    2.     {
    3.         [SerializeField] private List<DirectionContext> contextMap = new List<DirectionContext>();
    4.         public List<DirectionContext> GetContext()
    5.         {
    6.             return contextMap;
    7.         }
    8.  
    9.         public void GenerateMap(int detail)
    10.         {
    11.             contextMap.Clear();
    12.  
    13.             for (int i = 0; i < detail; i++)
    14.             {
    15.                 float angle = (360 / detail * i) - 180;
    16.                 contextMap.Add(new DirectionContext(angle));
    17.             }
    18.         }
    19.  
    20.         public void UpdateContext(int index, float interest)
    21.         {
    22.             contextMap[index].CheckInterestAndSet(interest);
    23.         }
    24.  
    25.         public void DebugDrawMap(Vector3 position, Color color)
    26.         {
    27.             foreach (DirectionContext direction in contextMap)
    28.             {
    29.                 Debug.DrawLine(position, (position + direction.GetDirection()).normalized * direction.GetInterest());
    30.             }
    31.         }
    32.  
    33.         public void Clear()
    34.         {
    35.             foreach (DirectionContext dir in contextMap)
    36.                 dir.Clear();
    37.         }
    38.     }
    Code (CSharp):
    1.     public class Seek : SteeringBehavior
    2.     {
    3.         public override ContextMap AddContext(ContextMap contextMap, SteeringContext steeringContext)
    4.         {
    5.             float angleToTarget = AngleBetween(Vector3.up, steeringContext.targetDestination - steeringContext.parent.transform.position);
    6.             //Debug.Log(angleToTarget);
    7.  
    8.             for (int i = 0; i < contextMap.GetContext().Count; i++)
    9.             {
    10.  
    11.                 float delta = Mathf.Abs(Mathf.Max(angleToTarget, contextMap.GetContext()[i].GetAngle()) - Mathf.Min(angleToTarget, contextMap.GetContext()[i].GetAngle()));
    12.                 if (delta > 180)
    13.                 {
    14.                     delta = 360f - delta;
    15.                 }
    16.                 float interest = 1f - delta / 180f;
    17.  
    18.                 contextMap.UpdateContext(i, interest);
    19.             }
    20.  
    21.             return contextMap;
    22.         }
    23.  
    24.         private float AngleBetween(Vector3 vector1, Vector3 vector2)
    25.         {
    26.             float sin = vector1.x * vector2.y - vector2.x * vector1.y;
    27.             float cos = vector1.x * vector2.x + vector1.y * vector2.y;
    28.             return Mathf.Atan2(sin, cos) * (180) / Mathf.PI;
    29.         }
    30.     }
    And I make a call for seek to populate the context map with:
    Code (CSharp):
    1.         private void PopulateInterestMap()
    2.         {
    3.             interestMap.Clear();
    4.  
    5.             foreach (SteeringBehavior behavior in interestBehaviors)
    6.             {
    7.                 interestMap = behavior.AddContext(interestMap, steeringContext);
    8.             }
    9.         }
     
  10. arfish

    arfish

    Joined:
    Jan 28, 2017
    Posts:
    777
    You should put in some Debug.Logs in the code to see whats going on, and how the values differs when everything is right, and when it goes wrong.
     
    Last edited: Feb 9, 2021
    SolitaryDeveloper likes this.
  11. seejayjames

    seejayjames

    Joined:
    Jan 28, 2013
    Posts:
    685
    Can't you simply map the (360, 0) range to (0, 1)? No need to shift by 180 if you're getting the full 360 range to start with. Try this range mapping function.

    Example:
    value to map = 135 (val)
    in1 = 360
    in2 = 0
    out1 = 0
    out2 = 1

    Code (CSharp):
    1. public float remap(float val, float in1, float in2, float out1, float out2)
    2.     {
    3.         return out1 + (val - in1) * (out2 - out1) / (in2 - in1);
    4.     }
    Maybe I'm missing something more complex in the project though.
     
    SolitaryDeveloper likes this.
  12. SolitaryDeveloper

    SolitaryDeveloper

    Joined:
    Feb 7, 2021
    Posts:
    8
    I've been having logs spat out the whole time, the issue I have is trying to decipher the logs and what they mean sometimes, as the script needs to run for all 8 angles (in this case,) and is called every frame. I did manage to figure out what my problem was though, and it wasn't at all what I thought it would be.

    The problem was my script where I was converting those angles to directions, I had flipped the Cos and Sin functions order. The commented out code is the original code, the uncommented is the fix.

    Code (CSharp):
    1.             this.direction = new Vector3(Mathf.Cos(rad), Mathf.Sin(rad), 0);
    2.  
    3.             //this.direction = new Vector3(rad), Mathf.Cos(angle * rad), 0);
    Thank you very much for taking the time out of your day!

    I didn't get a chance to test your method as I managed to fix the problem just now, and it was somewhere else entirely in my code (Go figures.) Thank you very much for taking the time to answer though!
     
    Last edited: Feb 10, 2021
    arfish and seejayjames like this.
  13. seejayjames

    seejayjames

    Joined:
    Jan 28, 2013
    Posts:
    685
    Glad to hear you got it worked out!
    Keep the mapping function around, it's handy for lots of things ;)
    Good luck!