1
\$\begingroup\$

I would like to ask you what is the best way to accomplish my problem. I have a player who can detect ennemy with raycast in a zone, or in a certain angle, like in this picture: enter image description here

As you see I want to get only green one.

I'm in topDown view. I was thinking about using Physics.OverlapSphere first, and then try Angle between player and target. But: In the picture I don't want to get the Purple one.

Also: what is the best way to do that with the best performance ?

\$\endgroup\$
3
  • 1
    \$\begingroup\$ What performance problem have you identified with your current solution? What you've described so far sounds like a reasonable and conventional way to handle this type of detection, so I'd be wary of complicating it unless there's an aspect that's not working for you. \$\endgroup\$
    – DMGregory
    Feb 13, 2017 at 1:46
  • \$\begingroup\$ I'm used to C++, In Unity I don't know how Unity perform, so i was asking which way is the best performance for my problem... I will Edit soon by posting my solution \$\endgroup\$
    – Ugo Hed
    Feb 13, 2017 at 9:53
  • 1
    \$\begingroup\$ In game development we often lack clear-cut universal "best" approaches, and instead contend with trade-offs depending on each game's particulars. The bigger challenge is making something fun, so our usual advice is to build your prototype in whatever simple way makes sense to you, so you can get something playable & testable as soon as possible. No sense spending tons of time engineering a system we might later discard because it doesn't serve the gameplay. ;) Once you have something working, then it's a good time to profile it and look for slow spots to speed up, in the full game context. \$\endgroup\$
    – DMGregory
    Feb 13, 2017 at 13:22

2 Answers 2

2
\$\begingroup\$

There are 3 approaches I know about, how to do this.

1) Like in your picture. First one and maybe one of the most efficient and accurate ways to do it.

  1. You check for Physics.OverlapSphere and get all the objects that could potentially be in field of view.
  2. Then you iterate through them in for loop (no need for while as length of array doesn't change during loop(clean code)). In the loop you check if if for example enemies enemies[i] is in your field of view. We do that first because we don't need to Physics.Raycast him if he is not in the field of view of, course to save some performance. After checking if the object is in your field of view (I would checking the field of view as different method where you pass direction(Vector3) to enemy). Remember if he is not in the field of view - just ignore him and continue the loop, if he is then you Physics.Raycast from transform.position(players position) in this direction enemies[i].transform.position - (player.)transform.position.

    @@@@@ Also when you check if the enemies[i] is in the field of view, you also check if the radius squared (save this squared radius in some variable) is bigger than distance square magnitude(no need to take a square root - it's heavy operation). This distance Vector3 is the same vector as direction.@@@@@)

    You don't need to do @@@@@ step in case Physics.OverlapSphere because the radius is already specified in Physics.OverlapSphere. Also in this case you already have the distance to specify in your rayscasts - it's the radius.

    Remember to use Layers, you don't need to raycast objects that don't affect your field of view. You should have LayerMask variable to save layers raycasts are going to hit. Physics.OverlapSphere in this case should only use layer of "MyEnemy" script, so you could use it as a parent class of your different type of enemies (OOP: Inheritance + Polymorphism in this case) so you won't have objects like walls in your array After raycasting you should check if object that was hit by raycast is really the one you need: enemy. It's not efficient to compare tags even if you use CompareTag(string otherTag) method. The fastest way to check this in Unity for now is hit.collider.GetComponent<MyEnemy>() "MyEnemy" - name of your script(class type (generics)).

2) Second would be to do many raycasts in field of view and check if any hits the object you need.

Not very efficient as you see, for every raycast check if there is an object you need. Also the angle between raycasts should be small enough not to miss the object it's trying to hit.

In this case you would loop through resolution of your field of view (number of raycasts) and translate angle into direction, raycast and check for the object you need on every iteration.

3) Would be to create a collision mesh like your field of view.

Then you would use method OnCollsionEnter(Collision other) or OnTriggerEnter(Collider other), but let's be real it's bad way to do this, because you would check for every object that enters field of view and for every object it would constantly check for collisions with this field of view. In physics engine of course, so we would see it only in the profiler. Also then every object that should have rigidbody and we would raycast them anyway to check if nothing blocks path to them.

To sum up - the best practice I know about for now is the (1) method.

\$\endgroup\$
5
  • \$\begingroup\$ I also love to use hit.collider.GetComponent<MyEnemy>() style checking - I prefer to avoid tags and layers for these kinds of things, it gets messy very quickly. Unfortunately it seems to generate garbage. \$\endgroup\$
    – Sir
    Mar 13, 2018 at 8:30
  • \$\begingroup\$ @Ubity layers can be useful if you use them for Physics methods to detect collisions only on that layer, I will guess it even improves the performance. You can cache the components in some kind of ComponentDataCollection in Dictionary<> and later use it without getting any garbage. \$\endgroup\$ Mar 13, 2018 at 9:22
  • \$\begingroup\$ How would you personally retrieve the component from cache with raycasting? Only thing that comes to my mind is to set unique string key for each component in dictionary and rename enemy to that string, so that you could get hit.collider.gameObject.name and retrieve component by that. \$\endgroup\$
    – Sir
    Mar 13, 2018 at 22:57
  • \$\begingroup\$ @Ubity unique string key is too error prone, simple mistake from designer and you have an error. You can see an example of how unity does it in their new 2d platformer kit in script PhysicsHelper. Basically, they do this - Dictionary<Collider2D, MovingPlatform> m_MovingPlatformCache. I thought of creating my own system, more generic one, but I haven't gotten to it yet. I would probably find a way for key to be int. \$\endgroup\$ Mar 14, 2018 at 1:28
  • \$\begingroup\$ @Ubity though, even the garbage it creates is insignificant. It is still way faster than string comparison. I am not even sure if it creates any garbage at all. It is probably casting the return value to required type, but only thing I can think of it's using type comparison or something else that, maybe, could create garbage. \$\endgroup\$ Mar 14, 2018 at 1:44
0
\$\begingroup\$

I solve my problem, with pretty good performance, here is the code: (assuming I'm in top-down view, with transform.up the actual "forward" of the player. The script is attached to the player gameObject; and my function is called every Update())

EDIT: Thanks to Candid Moon, I change i little mit my function, also I change of procedure, by testing the difference of angle between 2 vector, unlike before (I was testing if B is beteen A & C)

private int layerMask = 1 << 8; //select layer 8 (target and walls)
[Range(0, 360)]         public float fovAngle = 30.0f;

float SignedAngleBetween(Vector3 a, Vector3 b, Vector3 n)
    {
        float angle = Vector3.Angle(a, b);                                  // angle in [0,180]
        float sign = Mathf.Sign(Vector3.Dot(n, Vector3.Cross(a, b)));       //Cross for testing -1, 0, 1
        float signed_angle = angle * sign;                                  // angle in [-179,180]
        float angle360 =  (signed_angle + 360) % 360;                       // angle in [0,360]
        return (angle360);
    }

void Force()
{
    Collider[] hitColliders = Physics.OverlapSphere(gameObject.transform.position, fovRange, layerMask);

    for (int i = 0; i < hitColliders.Length; i++)
    {
        if (hitColliders[i].tag == "Metal")
        {
            Vector3 B = hitColliders[i].transform.position - gameObject.transform.position;
            Vector3 C = Quaternion.AngleAxis(90 + fovAngle / 2, -transform.forward) * -transform.right;
            if (SignedAngleBetween(transform.up, B, transform.up) <= SignedAngleBetween(transform.up, C, transform.up) || fovAngle == 360)
            {
                RaycastHit hit;
                if (Physics.Raycast(transform.position, B, out hit, layerMask))
                {
                   Debug.DrawRay(transform.position, B, Color.red, 0.5f);
                }
            }
        }
    }
}

Old method:

void Force()
    {
      // get every object in a sphere, from this.position, with fovRange = Radius, and layerMask:
      // I have target, and wall, both ones are in that layerMask, for testing if my target are behind wall or not
        Collider[] hitColliders = Physics.OverlapSphere(gameObject.transform.position, fovRange, layerMask);
        int i = 0;
        while (i < hitColliders.Length)
        {
          //test the tag
            if (hitColliders[i].tag == "Metal")
            {
              //create raycast
                RaycastHit hit;
              //create vector director from player to target
                Vector3 vectorDir = hitColliders[i].transform.position - gameObject.transform.position;

              //if Raycast touch something...
                if (Physics.Raycast(transform.position, vectorDir, out hit, layerMask))
                {
                  //if the collider is my target (else, nothing happend)
                    if (hit.collider.tag == "Metal")
                    {
                        //For my purpose, i'm in top-down view, I want to test if vectorDir is between
                        //vector A & vector C
                        Vector3 A = Quaternion.AngleAxis(90 - fovAngle / 2, -transform.forward) * -transform.right; 
                        Vector3 B = vectorDir;
                        Vector3 C = Quaternion.AngleAxis(90 + fovAngle / 2, -transform.forward) * -transform.right;

                        //draw vector
                        Debug.DrawRay(transform.position, A, Color.blue);
                        Debug.DrawRay(transform.position, B, Color.blue);
                        Debug.DrawRay(transform.position, C, Color.blue);

                        //test if B is in middle of A & B
                        Vector3 AB = new Vector3(B.x, B.y, B.z) - new Vector3(A.x, A.y, A.z);
                        Vector3 AC = new Vector3(C.x, C.y, C.z) - new Vector3(A.x, A.y, A.z);

                        Vector3 CB = new Vector3(B.x, B.y, B.z) - new Vector3(C.x, C.y, C.z);
                        Vector3 CA = new Vector3(A.x, A.y, A.z) - new Vector3(C.x, C.y, C.z);

                        float ab = Vector3.Dot(A, B);       //test inverse
                        float abc = Vector3.Dot(AB, AC);
                        float cba = Vector3.Dot(CB, CA);

                      //if vectorDir is in the middle, OR if my fovAngle is 360, the algo work !
                        if ((abc >= 0 && cba >= 0 && ab >= 0) || fovAngle == 360)
                        {
                            Debug.DrawRay(transform.position, vectorDir, Color.green, 0.5f);
                            Debug.Log("OK  !");
                        }
                    }
                }
            }
            i++;
        }
    }
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .