Search Unity

Playing tag logic: Running to random positions

Discussion in 'Navigation' started by j111194, Jul 12, 2022.

  1. j111194

    j111194

    Joined:
    Mar 14, 2020
    Posts:
    20
    Hi all!

    For awhile I am struggling to find the best logic to a (seemingly simple) solution.
    Hopefully someone can stir me in the right direction.
    This is the usecase:
    I want to create some kind of simple 'playing tag' logic, in which the player walk around in a circular area. A target NPC is running around and you have to catch it.

    I managed to implement some coding in which the target moves away from the player with an agent. Just a simple flee logic like this, which sets a new destination when the player/target distance is lower than the flee distance.

    But here are the issues:
    1. The target is only running around the edge of the circle, which makes sense, because it is constantly setting a new destination moving away from the player.
    2. I tried shooting raycasts from the target to the edge/walls and set a new destination, but because the flee distance is smaller, it also makes the other logic to set a new destination away from the player.
    3. I tried make the player as a navMeshObstacle, that makes the target nicely walk around it, but it also pushed the target from the player as a kind of wall.

    So the end goal is:
    Have some nice logic in which the target walks randomly around the playarea (triggered when the player comes close, the closer, the further it will move away), without only being stuck on the edges.

    So, are there any ideas how to make this work flawlessly?
    Any ideas or questions are really welcome :)
    Thanks already!

    And in case it helps, two sample codes, but I've also tried many other versions and ideas.

    Code (CSharp):
    1. {
    2.     public TargetProperties targetProperties;
    3.     public Transform player;
    4.     private Transform target;
    5.     private NavMeshAgent _agent;
    6.     private Vector2 posPlayer;
    7.     private Vector2 posTarget;
    8.     public float defaultRadius;
    9.     public Vector3 manualPosition;
    10.  
    11.     void Awake()
    12.     {
    13.         target = GameObject.FindGameObjectWithTag("Target").transform;
    14.         _agent = GetComponent<NavMeshAgent>();
    15.     }
    16.  
    17.     void Update()
    18.     {
    19.         posPlayer = new Vector2(player.position.x, player.position.z);
    20.         posTarget = new Vector2(target.position.x, target.position.z);
    21.  
    22.         float distance = Vector2.Distance(posPlayer, posTarget);
    23.  
    24.         if (distance < targetProperties.fleeDistance)
    25.         {
    26.             Vector3 dirToPlayer = transform.position - player.transform.position;
    27.             Vector3 newPos = transform.position + dirToPlayer;
    28.  
    29.             SetPosition(newPos);
    30.  
    31.         }
    32.  
    33.     }
    34.  
    35.     public void SetPosition(Vector3 position)
    36.     {
    37.         _agent.SetDestination(position);
    38.     }
    39. }
    Code (CSharp):
    1.  
    2.     void Awake()
    3.     {
    4.         player = Camera.main.gameObject.transform;
    5.         target = GameObject.FindGameObjectWithTag("Target").transform;
    6.         _agent = GetComponent<NavMeshAgent>();
    7.  
    8.         walls = GameObject.FindGameObjectsWithTag("Wall");
    9.  
    10.     }
    11.  
    12.     void Update()
    13.     {
    14.         posPlayer = new Vector2(player.position.x, player.position.z);
    15.         posTarget = new Vector2(target.position.x, target.position.z);
    16.  
    17.         //float distance = Vector3.Distance(player.position, target.position);
    18.         float projectedDistance = Vector2.Distance(posPlayer, posTarget);
    19.         //SetPosition();
    20.  
    21.         if (projectedDistance < targetProperties.fleeDistance)
    22.         {
    23.             Vector3 dirToPlayer = transform.position - player.transform.position;
    24.             Vector3 newPos = transform.position + dirToPlayer;
    25.  
    26.             SetPositionOld(newPos);
    27.            
    28.         }
    29.     }
    30.  
    31.     public GameObject FindClosestWall()
    32.     {
    33.         GameObject[] gos;
    34.         gos = GameObject.FindGameObjectsWithTag("Wall");
    35.         GameObject closest = null;
    36.         float distance = Mathf.Infinity;
    37.         Vector3 position = transform.position;
    38.         foreach (GameObject go in gos)
    39.         {
    40.             Vector3 diff = go.transform.position - position;
    41.             float curDistance = diff.sqrMagnitude;
    42.             if (curDistance < distance)
    43.             {
    44.                 closest = go;
    45.                 distance = curDistance;
    46.             }
    47.         }
    48.         return closest;
    49.    
    50.     }
    51.  
    52.  
    53.  
    54.     public void SetPositionOld(Vector3 position)
    55.     {
    56.         //Check if too close to the edge.
    57.         //Find the closest edge.
    58.         GameObject closest = FindClosestWall();
    59.         Vector3 diff = closest.transform.position - transform.position;
    60.         float curDistance = diff.sqrMagnitude;
    61.  
    62.         if (minWallDistance < curDistance)
    63.         {
    64.             Debug.Log("Object too close");
    65.  
    66.             //Give some correction to the position.
    67.  
    68.         }
    69.         _agent.SetDestination(position);
    70.     }
    71.  
    72.     public void SetPosition()
    73.     {
    74.  
    75.         //_agent.SetDestination(position);
    76.  
    77.         //We will check if enemy can flee to the direction opposite from the player, we will check if there are obstacles
    78.         bool isDirSafe = false;
    79.  
    80.         //We will need to rotate the direction away from the player if straight to the opposite of the player is a wall
    81.         float vRotation = 0;
    82.  
    83.         while (!isDirSafe)
    84.         {
    85.             //Calculate the vector pointing from Player to the Enemy
    86.             Vector3 dirToPlayer = transform.position - player.transform.position;
    87.  
    88.             //Calculate the vector from the Enemy to the direction away from the Player the new point
    89.             Vector3 newPos = transform.position + dirToPlayer;
    90.  
    91.             //Rotate the direction of the Enemy to move
    92.             newPos = Quaternion.Euler(0, vRotation, 0) * newPos;
    93.  
    94.             //Shoot a Raycast out to the new direction with 5f length (as example raycast length) and see if it hits an obstacle
    95.             bool isHit = Physics.Raycast(transform.position, newPos, out RaycastHit hit, rayCastlength);
    96.  
    97.             if (hit.transform == null)
    98.             {
    99.                 Debug.Log("Good to go");
    100.                 //If the Raycast to the flee direction doesn't hit a wall then the Enemy is good to go to this direction
    101.                 _agent.SetDestination(newPos);
    102.                 isDirSafe = true;
    103.             }
    104.  
    105.             //Change the direction of fleeing is it hits a wall by 20 degrees
    106.             if (isHit && hit.transform.CompareTag("Wall"))
    107.             {
    108.                 Debug.Log("Will hit wall, adjusting direction now.");
    109.                 vRotation += angleWallCorrection;
    110.                 isDirSafe = false;
    111.             }
    112.             else
    113.             {
    114.                 Debug.Log("Good to go, with a correction");
    115.                 //If the Raycast to the flee direction doesn't hit a wall then the Enemy is good to go to this direction
    116.                 _agent.SetDestination(newPos);
    117.                 isDirSafe = true;
    118.             }
    119.         }
    120.     }
    121. }
     
  2. j111194

    j111194

    Joined:
    Mar 14, 2020
    Posts:
    20
    Here a more (quickly made) graphic representation. Hope it helps, but it is hard to explain the issue.
     

    Attached Files:

  3. Inxentas

    Inxentas

    Joined:
    Jan 15, 2020
    Posts:
    278
    If I'm right you're calculating these new positions by drawing a line between the player and the enemy, and calculating a point further down that line consistently (with some angle variation). So you get pretty consistant behavior where the enemy has no choice but to stick around at the edges, as under no condition it will move closer to the player. This makes it easy to catch when it's also slower then the player, and impossible to catch when it's faster.

    However, when you see two people chase one another, they will try to get rid of a pursuer in more then one way. For example, they could try sharp turns, or even circle around quickly to prevent being boxed in against a wall. So you could for example, introduce more then one behavior and decide on behaviors pseudo-randomly or in deterministic fashion.

    Say that "behavior one" could be the one you already coded.
    Perhaps "behavior two" could be that the enemy circles the player, in order to get behind him. This is a risky move, so perhaps the enemy should speed up when using this behavior.
    Perhaps "behavior three" could be that the enemy moves perpendicularly to the calculated line, so it flees "to the right" or "to the left" instead of "straight ahead".

    Things that flee, are often afraid to be caught and will exhibit behavior as unpredictable as their intelligence allows. For instance, alligators are bad at changing direction. Fleeing from one in a zig-zag pattern is more effective then running in a straight line. Perhaps explore some more variations of how an entity could avoid, and not merely run away, from a threat.
     
  4. Inxentas

    Inxentas

    Joined:
    Jan 15, 2020
    Posts:
    278
    By the way, this question kinda overlaps with a question about an AI that hunts the player, which is the same principle but then in reverse. I linked something about State Machines in there. That's a bit more advanced programming, but I think it might have merit to delve a little deeper in the programming side of things before you desire more complex behavior from your agents.

    I have no idea how proficient you are at Object Oriented Programming and assumed you're kinda fresh because of your example code, and I personally experienced that learning a few common design patterns (which are basically abstract ideas on how to organize code) can really help with getting a handle on AI. Basically it prevents your Update method from becoming this huge monolith of a method.

    Meanwhile, I was intrigued the the question and came up with this. I slightly edited your code to get it working quickly in a new project, so I just used the Player's transform and that of the GameObject carrying the NavMeshAgent to make the calculations. Explanation at the end.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.AI;
    5.  
    6. public class Enemy : MonoBehaviour
    7. {
    8.     public Transform player;
    9.     public float timer = 0;
    10.  
    11.     private NavMeshAgent _agent;
    12.  
    13.     private enum Behaviors { flee, fleeHarder, fleeInAbsoluteTerror }
    14.     private Behaviors behavior;
    15.  
    16.     void Awake()
    17.     {
    18.         _agent = GetComponent<NavMeshAgent>();
    19.     }
    20.  
    21.     void Update()
    22.     {
    23.         // We increase the timer each frame, so our AI at least has an understanding of passing time.
    24.         IncreaseTimer();
    25.  
    26.         if(timer > 2)
    27.         {
    28.             //go to the next behavior every 2 seconds. You can come up with your own conditions,
    29.             //but since our AI doesn't have "feelers" yet we're gonna use Unity's internal clock
    30.             //so the thing gets prodded to change it's behavior from time to time.
    31.             SetBehavior();
    32.         }
    33.     }
    34.  
    35.     public void SetPosition(Vector3 position)
    36.     {
    37.         _agent.SetDestination(position);
    38.     }
    39.  
    40.     public void Flee()
    41.     {
    42.         // this is how you did it in the example code.
    43.         Vector3 dirToPlayer = transform.position - player.transform.position;
    44.         Vector3 newPos = transform.position + dirToPlayer;
    45.         _agent.speed = 3.5f;
    46.         _agent.SetDestination(newPos);
    47.     }
    48.  
    49.     public void FleeHarder()
    50.     {
    51.         // sorry, this does the same for now. Our AI doesn't have any "feelers" yet so all we can work
    52.         // with is the player's position, and our own. Well, we can have it flee TOWARDS the player for now...
    53.         Vector3 dirToPlayer =  player.transform.position - transform.position;
    54.         Vector3 newPos = transform.position + dirToPlayer;
    55.         _agent.speed = 5.0f;
    56.         _agent.SetDestination(newPos);
    57.     }
    58.  
    59.     public void FleeInAbsoluteTerror()
    60.     {
    61.         // You know what? I'm gonna send the enemy to (2,2,0) just because that is a valid position.
    62.         Vector3 newPos = new Vector3(2,2,0);
    63.         _agent.speed = 8.0f;
    64.         _agent.SetDestination(newPos);
    65.     }
    66.  
    67.     // This method increases the timer each frame. Deltatime is the "time since last frame render"
    68.     // and timeScale is the scale at which the game is running. So if the game was paused, this would
    69.     // become zero and thus the result would also become zero: now it's "pause proof"!
    70.     public void IncreaseTimer()
    71.     {
    72.         timer += Time.deltaTime * Time.timeScale;
    73.     }
    74.  
    75.     // This is just a poor man's State Machine that does "a thing" and then sets the next behavior.
    76.     // Not very inspired but it demonstrated nicer then randomness.
    77.     public void SetBehavior()
    78.     {
    79.         // reset the timer so it can be increased again.
    80.         timer = Time.deltaTime;
    81.  
    82.         // check our current behavior. We then run the appropriate method and select the next behavior,
    83.         // which will be triggered in 2 seconds.
    84.         switch (behavior)
    85.         {
    86.             case Behaviors.flee:
    87.                 Flee();
    88.                 behavior = Behaviors.fleeHarder;
    89.                 break;
    90.  
    91.             case Behaviors.fleeHarder:
    92.                 FleeHarder();
    93.                 behavior = Behaviors.fleeInAbsoluteTerror;
    94.                 break;
    95.  
    96.             case Behaviors.fleeInAbsoluteTerror:
    97.                 FleeInAbsoluteTerror();
    98.                 behavior = Behaviors.flee;
    99.                 break;
    100.         }
    101.  
    102.         // log the behavior so we can see it in action easily.
    103.         Debug.Log(behavior);
    104.     }
    105. }
    106.  
    Basically it's a poor man's State Machine that sets a new position every 2 seconds, but using different methods to do it. Within these methods you'll find your own code, as well as changes to the agent's speed for added flavor. Because your AI did not have much in the way of "feelers" it doesn't know much about the world. To give it eyes and ears, you could opt to write some methods, or devise more complex ways of instructing the NPC.

    I like to use a component that calculates things like "what's my distance to the player" but also "can I see the player" or "is the player in front of me". Perhaps it can retrieve a list of hiding spots within a certain range? Perhaps it knows the player can't swim, but it can and will prefer to flee across water? How to combine all these things into an interesting game is the design challenge.
     
  5. j111194

    j111194

    Joined:
    Mar 14, 2020
    Posts:
    20
    Super thanks for your elaborated answer!
    It's all clear and makes a lot of sense. It will be a challenge with a lot of trial and error to get the different behaviours work smoothly, but this direction helps a lot. I'm wondering how the timer will work, because it will maybe make the NPC not flee enough (only once every X seconds), while having it set to 0 will only make the position update infinitely. Maybe checking if the destination is reached could be a way to do this (like a dynamic timer...?).

    I will also look into the State Machine anyways, because by reading half of the tutorial it feels like a more structured way to do this.

    Many thanks. If I have any more questions arising during the process, you might hear of me again.

    Thank you!
     
  6. j111194

    j111194

    Joined:
    Mar 14, 2020
    Posts:
    20
    Ah wait, my bad, within the Behaviour I can, ofcourse, update destination more often. ;)
     
  7. Inxentas

    Inxentas

    Joined:
    Jan 15, 2020
    Posts:
    278
    Yes, the timer is entirely for demonstration purposes, so you could see the whole thing execute every 5 seconds.

    Should you ever decide to use actual State Machines, the States themselves would evaluate variables and conditions and then decide to do something. That way you can make behavior more interesting, for instance by checking the distance to the player every frame. Once it's above or below a certain treshold another behavior could kick in.

    In my own prototype enemies have Sensor components that keeps track of distance to the player, line of sight, and even in which quadrant (front, left, right, back) the player is relatively. All those variables are public, so the States can evaluate them on the fly. The scope chain is something like this:

    State > State Machine > Enemy > Sensor

    In my experience it's best to decide beforehand what the enemies must be able to respond to (for your Sensors) and what kinds actions they can take (to develop the States). Keep it simple at first, with states like Idle, Pursue and Attack for instance. My Idle state evaluates distance to the player, and when close enough AND within line of sight it will start to Pursue, coming closer. When close enough, it moves to the Attack state. When the player moves away and it's attack animation finishes, it evaluates again and chooses between another Attack or a Pursue state.

    Once you get a handle on this the trial and error is more or less tied to how complex your enemies and States become.