Hide N Seek: Here is my solution to Spreading the Load !Spoilers!

Here is my solution to Spreading the Load:

Summary

I look for a random point to hide using various nested coroutines.
These points are created in a list of hiding places from the ever increasing loop Sam gave us.
In creating this list I wait a frame before checking the next one.

Code

This text will be hidden

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class HideAI : MonoBehaviour
{
    [SerializeField] float timeBetweenChecks = 1;
    [SerializeField] float minRemainingDistance = 1;

    List<Vector3> filteredPoints = new List<Vector3>();
    Vector3 newPoint;

    FirstPersonMover player;
    NavMeshAgent agent;

    void Awake()
    {
        player = FindObjectOfType<FirstPersonMover>();
    }

    IEnumerator Start()
    {
        agent = GetComponent<NavMeshAgent>();

        while (true)
        {
            yield return GetRandomFilteredPoint(1, 100);

            agent.SetDestination(newPoint);

            yield return new WaitUntil(() => !NavMeshAgentIsMoving());
            yield return new WaitUntil(() => CanBeSeenByPlayer(transform.position));
        }
    }

    IEnumerator GetRandomFilteredPoint(float interval, float maxDistance)
    {
        yield return FindFilteredPoints(interval, maxDistance);

        newPoint = filteredPoints[UnityEngine.Random.Range(0, filteredPoints.Count)];
    }

    IEnumerator FindFilteredPoints(float interval, float maxDistance)
    {
        filteredPoints.Clear();

        Vector3 location = transform.position;
        for (float distance = interval; distance < maxDistance; distance += interval)
        {
            Vector2 randomCircle = Random.insideUnitCircle * distance;
            Vector3 randomOffset = new Vector3(randomCircle.x, 0, randomCircle.y);
            var point = location + randomOffset;

            if (NavMesh.SamplePosition(point, out NavMeshHit hit, 10, NavMesh.AllAreas))
            {                
                if (!CanBeSeenByPlayer(hit.position)) filteredPoints.Add(location + randomOffset);//TODO add point
            }
            yield return null;
        }

        if (filteredPoints.Count == 0)
        {
            filteredPoints.Add(Vector3.zero);
        }
    }

    bool NavMeshAgentIsMoving()
    {
        //print($"{agent.pathPending} {agent.pathStatus} {agent.remainingDistance > 0} {agent.remainingDistance} ");
        if (agent.pathPending) return true;
        if (agent.pathStatus != NavMeshPathStatus.PathComplete) return false;
        return agent.remainingDistance > minRemainingDistance;
    }

    bool CanBeSeenByPlayer(Vector3 point)
    {
        Vector3 direction = point - player.transform.position;
        float distance = direction.magnitude;
        if (Physics.Raycast(player.transform.position, direction, distance))
        {
            return false;
        }
        return true;
    }
}

This is mine. Seeing yours makes me feel better about using While(true) at certain points of my “moment”. I got rid of them but only because my game then had a game over state so it then just because while(!gameOver)
I stuck with the IEnumberble even though I wasn’t happy with it. It limits your enemy to 100 moves but that’s what the original code does so I kept it.

If I’m reading your correctly though, each iteration you have 100 frames of populating the filteredpoints list before your AI can move again? (I do like that it moves to a random point on the list rather than moving 1 space the first time then 2 then 3 etc (assuming those points are not visible to the player). With mine it only waits until its found a valid spot but those spots are increasing distances away and then when its got to 100, then its over, like the original.

I really want to see Sam’s solution now!

Summary

`
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class HideAI : MonoBehaviour
{
[SerializeField] float maxNavMeshProjectionDistance;
Vector3 destination;

IEnumerator Start()
{
    NavMeshAgent agent = GetComponent<NavMeshAgent>();

    foreach(int iteration in StartDistance(1,100))
    {
        yield return StartCoroutine(FindHidingPlace(1, 100, iteration));
        agent.SetDestination(destination);

        while (NavMeshAgentIsMoving())
        {
            yield return new WaitForSeconds(0.5f);
        }
        yield return new WaitUntil(() => CanBeSeenByPlayer(transform.position));
    }
    
}

IEnumerable<float> StartDistance(float interval,float max)
{
    int interation = 0;
    int totalIterations = Mathf.CeilToInt(max / interval);
    while(interval< totalIterations)
    {
        interation ++;
        yield return interation;
    }
     
}

IEnumerator FindHidingPlace(float interval, float maxDistance, int iteration)
{
    Vector3 location = transform.position;
    for (float distance = (interval*iteration); distance < maxDistance; distance += interval)
    {
        Vector2 randomCircle = Random.insideUnitCircle * distance;
        Vector3 randomOffset = new Vector3(randomCircle.x, 0, randomCircle.y);
        var point = location + randomOffset;
        //check if point is even valid
        NavMeshHit navMeshHit;
        bool hasCastToNavMesh = NavMesh.SamplePosition(point, out navMeshHit, maxNavMeshProjectionDistance, NavMesh.AllAreas);

        if (hasCastToNavMesh && !CanBeSeenByPlayer(navMeshHit.position))
        {
            destination = navMeshHit.position;
            yield break;
        }
        yield return null;
    }
    
}

bool NavMeshAgentIsMoving()
{
    NavMeshAgent agent = GetComponent<NavMeshAgent>();
    return agent.pathPending || agent.remainingDistance > 0;
}

bool CanBeSeenByPlayer(Vector3 point)
{
    Vector3 direction = point - Camera.main.transform.position;
    float distance = direction.magnitude;
    if (Physics.Raycast(Camera.main.transform.position, direction, distance))
    {
        return false;
    }
    return true;
}

}
`

(also can someone explain to me why the first few lines of my code are always unformatted on here?)

I see putting things in Update as being a very similar to a while(true) and if its going to loop forever…dont see an issue.

Yes - I wanted them to go a bit further and bit more random and it made the spreading the load a bit more obvious as the side effect also makes them look like they are thinking plus some appear a little less clever than others imo!!

You know you are just writing a ‘for’ loop here in StartDistance :wink: and that would be easier to understand!!

I know, I was trying to do what we were told without like ditching the other things we were told to do, so that meant keeping the IEnumerable. This is why I really want to see the “correct” answer because mine never felt “right”. It works but, I would never write code like that normally.

Agreed - that was my problem with is Quest.
I felt I should be leaving things there that I didnt want.
So it took longer to try and squeeze an answer in.
Till I said this isnt working for me and ditched.
Lesson learnt for next time - ditch first and put back in later if needed!
I did try to portray my frustration in the feedback I left for Rick but how much gets through in these long posts I’m not sure:

I understand the purpose of it in the original scenario - it only calculates points as needed - which will be good for performance. In your example, you calculate 1 position per frame, but you do calculate all 100 points per iteration - (and by the time you get to frame 100, the player could have moved considerably so the original positions could be invalid). (flip side is a more random/natural movement, instead of the increasing range) In mine I’m using yield break to get out of the coroutine when we’ve found a valid position, so its keeping calculating points to the minimum and all the IEnumerable really does is keep track of the start distance so it can keep the ever increasing search distance. And that could really have been done in a much simpler way.

Oh yes mine are definitely slow thinkers but it was easier to see that the Spreading The Load was working looping all over 100 frames as it took a second or two for the enemy to take off again. For me finding a nearby place was too quick to notice a change in the code. In reality if I found doing all 100 in a frame was a performance issue and 1 a frame too slow - I’m sure there would be a compromise in between.

Ah, I output the frame number to tell it was working.

I feel like part of the point of this challenge was getting us to consider the balance between gameplay and performance.

I think for yours its fairly easily solved, you don’t need 100 points for them to choose from, even 20 would give a good degree of randomness and you could increase the interval so you’re still getting points from within that 100 unit radius and especially with a navmesh maxDistance of 10, you’re going to find a hit on a good number of your tests. The way you’ve written it, its very easy play around with the figures until you find a good balance.

Here is my code I used update as its called once per frame anyway

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class HideAI : MonoBehaviour
{

    PlacesToHide letsHide;
    bool spotted = false;

    IEnumerator Start()
    {
        NavMeshAgent agent = GetComponent<NavMeshAgent>();
        letsHide = GameObject.Find("PlacesToHide").GetComponent<PlacesToHide>();


        foreach (Vector3 point in HidingPlaces(1,100))
        {
            yield return new WaitForEndOfFrame();
            Debug.Log("Moving to new location");
            agent.SetDestination(point);

            yield return new WaitUntil(()=>!NavMeshAgentIsMoving());
            spotted = false;
            yield return new WaitWhile(() => spotted==false);
        }
    }

    void Update()
    {

        if (CanBeSeenByPlayer(transform.position))
        {
            spotted = true;
        }

    }

    bool CanBeSeenByPlayer(Vector3 point)
    {
        Vector3 direction = point - Camera.main.transform.position;
        float distance = direction.magnitude;
        //Debug.Log("Ray ....");
        if (Physics.Raycast(Camera.main.transform.position, direction, distance))
        {
            return false;
        }
        return true;
    }



    IEnumerable<Vector3> RandomPoints(float interval, float maxDistance)
    {
        NavMeshAgent agent = GetComponent<NavMeshAgent>();
        Vector3 location = transform.position;
        for (float distance = interval; distance < maxDistance; distance += interval)
        {
            Vector2 randomCircle = Random.insideUnitCircle * distance;

            Vector3 randomOffset = new Vector3(randomCircle.x, 0, randomCircle.y);

            NavMeshHit hit;
            if (NavMesh.SamplePosition(location + randomOffset, out hit, 1.0f, NavMesh.AllAreas))

                if (!CanBeSeenByPlayer(randomOffset))
                {
                    Debug.Log("Yes");
                    yield return location + randomOffset;
                }
                else
                {
                    Debug.Log("Noooooo");
                }
        }
    }

    bool NavMeshAgentIsMoving()
    {
        NavMeshAgent agent = GetComponent<NavMeshAgent>();
        return agent.pathPending || agent.remainingDistance > 0;
    }


    IEnumerable<Vector3> HidingPlaces(float interval, float maxDistance)
    {
        Vector3 location = transform.position;
        for (float distance = interval; distance < maxDistance; distance += interval)
        {

            Vector3 randomOffset = letsHide.Where().position;
            if (!CanBeSeenByPlayer(randomOffset))
            {
                yield return randomOffset;
            }

        }
    }
}

That’s fair enough but my view is that I write good clean code and worry about performance when it actually becomes an issue. There was certainly no issue for 3 enemies and I had 50 odd not being too noticable either. You can worry too much about performance when there isnt going to be any.

Honestly I didn’t really consider the numbers - left them as Sam had them.

But my code is only attempting to find up to 100 points - any and many may get rejected if outside or if visible to player so the choice may be alot less and, if too few it may not find a place to go.

As i say I’ve not tweaked the numbers - time vs gain.

[Edit] Oops - Just re-read then end of your comment and yes definitely room for improvement - not sure I even know what SamplePosition is actually doing :wink:

Two things from a quick look Kurt:

A: using Update for spotted

The trouble with Update is that it is called every frame so is not very efficient. The check for
setting spotted only needs to be done when the enemy has stopped moving.

B: Spreading The Load

I think what Sam is asking is that the loop in your RandomPoints doesnt get all done at the same time in one frame.
We look for one and if no good wait a frame and then try again.
That’s at least how I translated it.
It is a bit of a brain strain and involves a bit of reworking/changing the code

Hi, what exactly is the logic behind this check?
Are you checking for a collider between the player and the enemy?
When is it supposed to return true, and when is it supposed to return false?

Thanks

It’s Sam’s code - but yes it checks for something between the player and a position provided.

If it hits something then the player cannot see that position and returns false otherwise nothing is hit, the player can see the position so return true.

It is all a bit double negative looking at it (im confusing myself as I write it) and may be better done as IsHidden…

As far as I can tell, this method doesn’t work.
It will check for the first thing the raycast hits… If it hits a wall OR a player, it will return false. And if it hits nothing, it will return true.

I wondered that about hitting the player when I was trying to answer your question earlier (even checking to see if my player had a collider) but I knew it worked as I successfully did the Quest.
Then I noticed in the docs -
Notes: Raycasts will not detect Colliders for which the Raycast origin is inside the Collider.
And realised the RayCast in the code is coming from the player back to the point.
Though it works, it is very confusing and not how I’d have coded it…

1 Like

How exactly do you mean when you say that “it worked”? I ran your code (I used it as the starting point)
It seems to me that the enemies would eventually gravitate towards the edges of map, because of the way the matrix was set up to get filteredPoints (as you called them).

However, the method for checking if the player was looking at the enemy was definitely broken so that part of the logic did not work.

Something like this would work, I think!

bool CanBeSeenByPlayer(Vector3 point)
{
    Vector3 direction = transform.position - Player.transform.position;

    RaycastHit otherObject;

    if (Physics.Raycast(transform.position, - direction, out otherObject, Mathf.Infinity))
    {
        if (otherObject.collider.tag == "Environment")
        {
            return canBeSeenByPlayer = false;
        }

        if (otherObject.collider.tag == "Player")
        {
            return canBeSeenByPlayer = true;
        }
    }

    return canBeSeenByPlayer = false;
    Debug.LogError("CanBeSeenByPlayer in HideAI.cs");
}

But it wont hit a player because the raycast originates with the player and there is only 1 player. It shoots a ray from the player to the position being checked, if the raycast hits something - then the player cannot have direct line of sight from their current position to that position. While the script is on the enemy, the raycast it produces originates at the player not at the enemy.

1 Like

My code did work and there is a video here

I did have to do things to get the enemies moving properly but that was not an issue with
CanBeSeenByPlayer

And why my NavMeshAgentIsMoving() is different to Sam’s and I had to play around with the blocks, Nav Static and NavMesh rebakes too especially on the original probuilder version

I’ll try and have look at your code bits tomorrow as busy tonight.