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. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question How can I (Efficiently) prevent flocking behaviour of NPCs

Discussion in 'Scripting' started by BFranse_, Nov 16, 2022.

  1. BFranse_

    BFranse_

    Joined:
    Mar 7, 2021
    Posts:
    4
    Hi there,

    I'm pretty new to Untiy and while I have some programming experience in other languages, c# is still quite new to me too. For a while I've been trying to figure out how to prevent flocking behaviour in my game, as these NPCs are chasing the same destination.

    The game so far is fairly simple: You buy fish, they get hungry. You feed them (The food slowly sinks to the ocean floor) and when they're well fed, they drop coins so you can buy more fish. You can see a couple of fish and coins in the screenshot below.

    The challenge I am facing is that the fish stack and group together chasing the same food. Which is more obvious the more fish there are.

    I am struggling with this in code but also conceptually, as they are supposed to chase the food if they're hungry enough, but I would love for the fish to not fully stack (Overlapping a bit is fine).
    I've tried working with Rigidbody2D and 2D Collider, but ran into two issues:
    1. They fish always ended up slightly rotated (Even when freeze rotation was ticked)
    2. It becomes laggy if there are many fish

    The fish are currently on their own layer, they have Rigidbody2D's and CapsuleCollider2D's attached. I did tick the box for 'Is Trigger' and the following NpcCollider script attached:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. /// <summary>
    6. /// Handles the collision of NPCs.
    7. /// </summary>
    8. public abstract class NpcCollider : MonoBehaviour
    9. {
    10.     // Misc
    11.     protected Collider2D npcCollider;
    12.     protected NpcState npcState;
    13.  
    14.     /// <summary>
    15.     /// Checks if the <paramref name="collision"/> has the given <paramref name="tag"/>.
    16.     /// </summary>
    17.     protected virtual bool IsCollisionOfTag(Collider2D collision, string tag)
    18.     {
    19.         return collision.gameObject.CompareTag(tag);
    20.     }
    21.  
    22.     /// <summary>
    23.     /// Ignores collision <see cref="GameObject"/> with the same tags.
    24.     /// </summary>
    25.     protected virtual void OnCollisionEnter2D(Collision2D collision)
    26.     {
    27.         if (IsCollisionOfTag(collision.collider, gameObject.tag))
    28.         {
    29.             Physics2D.IgnoreCollision(collision.collider, npcCollider);
    30.         }
    31.     }
    32. }
    I have a feeling my current setup (The 'Is Trigger' and the IgnoreCollision part) is preventing me from solving this. How would I conceptually, and perhaps with pseudo code examples solve this? Are there any resources (Videos, articles, etc.) that come to mind to help me out?

    Thanks in advance. If you have any additional questions, or need more information, please let me know.
     
  2. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,127
    1) Welcome to the UnityForum :)
    2) That's a lovely looking game you have there.
    3) I think people will need a little more information to help you out. For example how the fish movement is working at the moment.
    If you have set your colliders to IsTrigger then they will no longer collide (in the sense of bumping into each other). Instead they will just intersect and trigger a collision callback. I am also not sure what the purpose of the ignoreCollision code is in your setup. Maybe you can elaborate on that.
     
    BFranse_ likes this.
  3. TzuriTeshuba

    TzuriTeshuba

    Joined:
    Aug 6, 2019
    Posts:
    185
  4. BFranse_

    BFranse_

    Joined:
    Mar 7, 2021
    Posts:
    4
    1) Thank you!
    2) Thank you again!
    3) Fair feedback. I did change the colliders to prevent collision between the fish as that was giving me issues with the sprites rotating while they shouldn't. I understand the changes I made to solve that issue, but maybe I didn't oversee the impact in the long run, which is where I am at now.

    This is my current implementation of NpcMovement:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. /// <summary>
    6. /// Handles the movement of NPCs.
    7. /// </summary>
    8. public abstract class NpcMovement : MonoBehaviour
    9. {
    10.     // Movement
    11.     [SerializeField] protected float movementSpeedMin;
    12.     [SerializeField] protected float movementSpeedMax;
    13.     [SerializeField] protected float movementSpeed;
    14.     protected IEnumerator moveToFixedDestination;
    15.     protected Vector3 destination;
    16.     protected bool moving = false;
    17.     protected bool movingToFixedDestination = false;
    18.  
    19.     // Waiting
    20.     [SerializeField] protected float waitTimeMin;
    21.     [SerializeField] protected float waitTimeMax;
    22.     [SerializeField] protected float waitChance;
    23.  
    24.     // Misc
    25.     protected SpriteRenderer spriteRenderer;
    26.     protected ScreenManager screenManager;
    27.     protected Rigidbody2D npcRigidbody;
    28.  
    29.     /// <summary>
    30.     /// ONLY FOR TESTING PURPOSES. Draws gizmos on screen, if enabled in editor.
    31.     /// </summary>
    32.     private void OnDrawGizmos()
    33.     {
    34.         Gizmos.color = Color.green;
    35.         Gizmos.DrawLine(transform.position, destination);
    36.     }
    37.  
    38.     /// <summary>
    39.     /// Sets the <see cref="destination"/> to <paramref name="destination"/> if given, otherwise randomly.
    40.     /// </summary>
    41.     protected virtual void SetDestination(Vector3? destination = null)
    42.     {
    43.         if (destination != null)
    44.         {
    45.             this.destination = (Vector3)destination;
    46.         }
    47.         else
    48.         {
    49.             this.destination = screenManager.GetRandomPosition(null, 91);
    50.         }
    51.     }
    52.  
    53.     /// <summary>
    54.     /// Sets the <see cref="movementSpeed"/> to <paramref name="speed"/> if given, random if <paramref name="random"/> is true, or does nothing.
    55.     /// </summary>
    56.     protected virtual void SetMovementSpeed(float? speed, bool random = false)
    57.     {
    58.         if (random)
    59.         {
    60.             movementSpeed = Random.Range(movementSpeedMin, movementSpeedMax);
    61.         }
    62.         else if (speed != null)
    63.         {
    64.             movementSpeed = (float)speed;
    65.         }
    66.     }
    67.  
    68.     /// <summary>
    69.     /// Sets a random destination and starts movement coroutine.
    70.     /// </summary>
    71.     protected virtual void MoveRandom()
    72.     {
    73.         SetDestination();
    74.         moving = true;
    75.         moveToFixedDestination = MoveToFixedDestination(destination, movementSpeed);
    76.         StartCoroutine(moveToFixedDestination);
    77.     }
    78.  
    79.     /// <summary>
    80.     /// Returns all the transforms of <see cref="GameObject"/> with the given <paramref name="tag"/>.
    81.     /// </summary>
    82.     protected virtual List<Transform> GetAllGameObjectTransformsWithTag(string tag)
    83.     {
    84.         List<Transform> transforms = new();
    85.         GameObject[] objectsWithTag = GameObject.FindGameObjectsWithTag(tag);
    86.  
    87.         if (objectsWithTag != null)
    88.         {
    89.             foreach (GameObject objectWithTag in objectsWithTag)
    90.             {
    91.                 transforms.Add(objectWithTag.GetComponent<Transform>());
    92.             }
    93.         }
    94.  
    95.         return transforms;
    96.     }
    97.  
    98.     /// <summary>
    99.     /// Returns the closest <see cref="Transform"/> from <paramref name="transforms"/>.
    100.     /// </summary>
    101.     protected virtual Transform GetClosestObject(List<Transform> transforms)
    102.     {
    103.         Transform closestObject = null;
    104.  
    105.         if (transforms.Count == 1)
    106.         {
    107.             closestObject = transforms[0];
    108.         }
    109.         else if (transforms.Count > 1)
    110.         {
    111.             float closestDistanceSqr = Mathf.Infinity;
    112.             Vector3 currentPosition = transform.position;
    113.  
    114.             foreach (Transform objectTransform in transforms)
    115.             {
    116.                 Vector3 directionToTarget = objectTransform.position - currentPosition;
    117.                 float dSqrToTarget = directionToTarget.sqrMagnitude;
    118.  
    119.                 if (dSqrToTarget < closestDistanceSqr)
    120.                 {
    121.                     closestDistanceSqr = dSqrToTarget;
    122.                     closestObject = objectTransform;
    123.                 }
    124.             }
    125.         }
    126.  
    127.         return closestObject;
    128.     }
    129.  
    130.     /// <summary>
    131.     /// Moves towards <see cref="destination"/> with increasing speed the closer it gets until collison or <see cref="destination"/> is no longer set.
    132.     /// </summary>
    133.     protected IEnumerator ChaseDestination(float speed)
    134.     {
    135.         while (Vector3.Distance(transform.position, destination) > speed * Time.deltaTime)
    136.         {
    137.             transform.position = Vector3.MoveTowards(transform.position, destination, speed * Time.deltaTime);
    138.             yield return null;
    139.         }
    140.         transform.position = destination;
    141.         moving = false;
    142.     }
    143.  
    144.     /// <summary>
    145.     /// Moves towards a fixed (Unmoving) destination with a chance to wait at the location.
    146.     /// </summary>
    147.     protected IEnumerator MoveToFixedDestination(Vector3 destination, float speed)
    148.     {
    149.         movingToFixedDestination = true;
    150.         while (Vector3.Distance(this.transform.position, destination) > speed * Time.deltaTime)
    151.         {
    152.             this.transform.position = Vector3.MoveTowards(this.transform.position, destination, speed * Time.deltaTime);
    153.             yield return null;
    154.         }
    155.  
    156.         if (Random.value > waitChance)
    157.         {
    158.             yield return new WaitForSeconds(Random.Range(waitTimeMin, waitTimeMax));
    159.         }
    160.  
    161.         this.transform.position = destination;
    162.         moving = false;
    163.         movingToFixedDestination = false;
    164.     }
    165.  
    166.     /// <summary>
    167.     /// Starts chasing destination coroutine if not already.
    168.     /// </summary>
    169.     protected void StartChasingDestination()
    170.     {
    171.         if (moving) { return; }
    172.  
    173.         moving = true;
    174.         StartCoroutine(ChaseDestination(movementSpeed));
    175.     }
    176.  
    177.     /// <summary>
    178.     /// Stops moving to fixed destination if it currently is.
    179.     /// </summary>
    180.     protected void StopMovingToFixedDestination()
    181.     {
    182.         if (!movingToFixedDestination) { return; }
    183.  
    184.         StopCoroutine(moveToFixedDestination);
    185.         moving = false;
    186.         movingToFixedDestination = false;
    187.     }
    188.  
    189.  
    190.     /// <summary>
    191.     /// Flips the sprite horizontally, opposite to its current direction.
    192.     /// </summary>
    193.     protected void FlipSprite()
    194.     {
    195.         if (this.transform.position.x < destination.x)
    196.         {
    197.             spriteRenderer.flipX = true;
    198.         }
    199.         else if (this.transform.position.x > destination.x)
    200.         {
    201.             spriteRenderer.flipX = false;
    202.         }
    203.     }
    204. }
    TLDR: There's a destination that can be set to which it will move. If there is food within range (Which expands the more hunger the fish get) that would be the destination, otherwise it will be random. So in essence the movement comes down to:
    Vector3.MoveTowards()

    However, the fish have some specific things they need to do (In contrast to for example crawlers like snails and crabs, so there's a FishMovement, that inherits from NpcMovement:
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. /// <summary>
    5. /// Handles the movement of fish.
    6. /// </summary>
    7. public class FishMovement : NpcMovement
    8. {
    9.     private FishHunger fishHunger;
    10.  
    11.     /// <summary>
    12.     /// Called on the frame when a script is enabled just before any of the Update methods are called the first time.
    13.     /// </summary>
    14.     private void Start()
    15.     {
    16.         screenManager = GameObject.Find("GameManager").GetComponent<ScreenManager>();
    17.         fishHunger = GetComponent<FishHunger>();
    18.         npcRigidbody = GetComponent<Rigidbody2D>();
    19.         npcRigidbody.constraints = RigidbodyConstraints2D.FreezeRotation;
    20.         destination = transform.position;
    21.         spriteRenderer = GetComponentInChildren<SpriteRenderer>();
    22.     }
    23.  
    24.     /// <summary>
    25.     /// Called every frame, if the MonoBehaviour is enabled.
    26.     /// </summary>
    27.     private void Update()
    28.     {
    29.         SetMovementSpeed(null, true);
    30.         List<Transform> allFood = GetAllGameObjectTransformsWithTag("Food");
    31.         FlipSprite();
    32.  
    33.         if (allFood.Count > 0)
    34.         {
    35.             Transform closestFood = GetClosestObject(allFood);
    36.             if (closestFood != null)
    37.             {
    38.                 SetDestination(closestFood.position);
    39.                 SetMovementSpeed(movementSpeedMax);
    40.                 StopMovingToFixedDestination();
    41.                 StartChasingDestination();
    42.             }
    43.             else if (!moving)
    44.             {
    45.                 MoveRandom();
    46.             }
    47.         }
    48.         else
    49.         {
    50.             if (!moving)
    51.             {
    52.                 MoveRandom();
    53.             }
    54.        
    55.         }
    56.     }
    57.  
    58.     /// <summary>
    59.     /// Returns the closest <see cref="Transform"/> from <paramref name="objectTransforms"/>.
    60.     /// </summary>
    61.     protected override Transform GetClosestObject(List<Transform> objectTransforms)
    62.     {
    63.         Transform closestObject = null;
    64.         Vector3 currentPosition = transform.position;
    65.         float foodDetectionRange = fishHunger.GetFoodDetectionRange();
    66.  
    67.         if (objectTransforms.Count > 0)
    68.         {
    69.             foreach (Transform objectTransform in objectTransforms)
    70.             {
    71.                 float distanceToTarget = Vector3.Distance(objectTransform.position, currentPosition);
    72.                 if (distanceToTarget < foodDetectionRange)
    73.                 {
    74.                     closestObject = objectTransform;
    75.                 }
    76.             }
    77.         }
    78.  
    79.         return closestObject;
    80.     }
    81. }
    The gif below should show it in action. You can see when I drop food they move towards it, but if it's in range of multiple fish, they all stack and move together towards the food.
    https://s4.gifyu.com/images/Unity_6hLnqCBlmm.gif
     
  5. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    You could have a tracker how many fish are going for the food item. And if more than 3 then don’t allow them to go for it.
     
    BFranse_ likes this.
  6. BFranse_

    BFranse_

    Joined:
    Mar 7, 2021
    Posts:
    4
    That would be a nice workaround, based on level of hunger and distance perhaps. Still curious to hear other suggestions though!
     
  7. RadRedPanda

    RadRedPanda

    Joined:
    May 9, 2018
    Posts:
    1,596
    If you just used small colliders then they wouldn't be able to bunch up so much. It doesn't have to match the size of the fish though, if you used one that was pretty tiny you could prevent them from perfectly aligning like that but still have some overlap.
     
    BFranse_ likes this.
  8. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,127
    You should not mix movement via transform and physics simulation. Either make all your movement based on physics (use forces instead of transform.*) or make all your movement transform based. Currently your movement is transform based (which is fine) but you are trying to get some physics constraints in too. That won't work. The constraints only operate within the physics simulation (which you are not using, except for the triggers). Triggers are fine.

    Now for a suggestion on the fish movement. You could add a radius to the food and the fish that gets inside first will be the only one to chase it. All other will break off. Also add a radius to the fish. If two come too close together then make one of them move away (mirror the movement vector of fish A along the movement vector of fish B for example).

    And a final suggestion on your code. Don't build up your logic with inheritance. Unity is a Component based engine. Have a "Movement" Component and a Fish Component and add those two to your fish game object. Maybe even break the movement and targeting apart into two components (Movement, Targeting). Though your classes are still pretty small so it might be overkill but I promise you will run into trouble if you try to add some other fish with differen movement styles.

    Also, you are encapsulating (hiding) a lot of "utility style" methods in your objects. Extract them into a public static class and make your object use them. There is nothing inherently "NPC" about the "GetAllGameObjectTransformsWithTag" or "GetClosestObject" for example. You may want to use these in many other places too (DRY priciple). Also over time those will give you a handy library to draw from instead of searching in old objects and copy pasting out methods which then (of course) have to be rewritten because all the parameters were part of the old object, ...

    Nice gif :)
     
    BFranse_ likes this.
  9. NicBischoff

    NicBischoff

    Joined:
    Mar 19, 2014
    Posts:
    204
    You could measure the distance between the fish food and the all of the fish. Pick the closest three to grab that food item.
     
  10. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,925
    Here are some cheap game tricks:

    o Fish only check for food every so often and not all at once, so some won't notice right away (this is a great trick for many things). The chance to see the food decreases for fish further away.

    o Some fish cautiously approach, while some dart towards it. This probably requires an extra variable on each fish to remember how fast it decided to go.

    o Fish don't always go exactly for the food -- they might go for an area under it, for example (requires an extra variable on each fish to remember where it's going).

    o Another version of that -- fish just don't swim straight. They use something like waypoints (pick a point sort of towards the food, swim there, pick a closer one when you get there).

    o fish sometimes do a raycast to the food. If another fish is in front and really close, they sideslip for a while (move a little sideways from the food). This may require extra variables (am I sideslipping?, how much slideslip is left? In which direction?) Or else swim randomly if another fish is in the way.
     
    BFranse_ likes this.
  11. BFranse_

    BFranse_

    Joined:
    Mar 7, 2021
    Posts:
    4
    Thank you, that is a lot of valuable information. It seems like I have some reworking to do. First of all I'll make sure to clean up my code and file structure (No inheritance and encapsulation). Then I'll have another go at changing the movement from transform based to physics based, as it seems I'm really going to need the collider to work for what I have in mind.

    Thanks so much for the detailed answer!

    Makes perfect sense! That's something I'll have to try once I have reworked my code a bit, thank you!

    Those are some good tips I'll keep in mind when refactoring my code.

    I think I got what I needed so far. There's still some figuring out for me, but it seems I know how to move forward. Thanks everyone for pitching in! It's much appreciated!
     
    _geo__ likes this.