Could Missing Local Identfier In File be stopping NavMeshAgent from working?

After completing the RPG course, I’ve added a follow-player feature to my Mover script. This is all working fine on characters that I place in the scene, however, if the same prefabbed character is instantiated from a spawner that references a Scriptable Object that has a reference to the character prefab… (the same as how the Pickup items in the world are spawned)… although it spawns that character correctly, and they are playing their idle animations and are interactable, they don’t seem to be able to move.

The only difference I could see was that in the inspector if I enable Debug mode, any Instantiated game object has ‘0’ for its ‘LocalIdentfierInFile’ field, whereas Items and characters that I place in the scene have a unique string of numbers. Could this missing field be the reason the NavmeshAgent can’t move the character? I have made other checks that the surface is On the Navmesh, and I am getting no other console errors.

Thanks

I don’t think that’s the issue… and I’ve used spawners extensively with this project…

I’m not entirely sure what the cause is, however… so we’re going to have to start at the beginning…

Let’s see your modified Mover.cs for a start, so I can get an idea what direction you’ve taken to get followers.

Sure, thanks for this. Here is the total Mover.cs attached. This is all working fine if have the character placed in the scene, just not if they’re spawned in :slight_smile:

using RPG.Core;
using UnityEngine;
using UnityEngine.AI;
using GameDevTV.Saving;
using RPG.Attributes;
using System.Collections;
using UnityEngine.Events;
using RootMotion.FinalIK;
using System;

namespace RPG.Movement
{
    public class Mover : MonoBehaviour, IAction, ISaveable
    {
        [SerializeField] float maxSpeed = 6f;
        [SerializeField] float injuredMinSpeed = 2f;
        [SerializeField] float maxNavPathLength = 40f;
        [SerializeField] float accelleration = 2f;
        [SerializeField] AnimationCurve stoppingDistance;
        NavMeshAgent navMeshAgent;
        Health health;
        Animator animator;
        float desiredSpeed;
        float layerWeightVelocity;
        float currentHealthPoints;
        float healthPercentage;
        private GameObject followTarget;
        private bool isFollowing = false;
        //Knockback
        bool knockback;
        Vector3 direction;
        public float knockbackForce = 1f;
        //Debug
        public Vector3 debugDestinationPosition;


        public GameObject waypointClickPrefab = null;

        private void Awake()
        {
            navMeshAgent = GetComponent<NavMeshAgent>();
            health = GetComponent<Health>();
            animator = GetComponent<Animator>();
            knockback = false;
        }
        void Update()
        {
            navMeshAgent.enabled = !health.IsDead();
            UpdateAnimator();
            SetMaxSpeed();
            SetInjuredAnimLayerWeight();
            UpdatePlayerStoppingDistance();
            FollowTarget(followTarget);
        }

        private void UpdatePlayerStoppingDistance()
        {
            var velocity = navMeshAgent.velocity.magnitude;
            navMeshAgent.stoppingDistance = stoppingDistance.Evaluate(velocity);
            //Debug.Log(velocity);
        }

        void FixedUpdate()
        {
            if (knockback)
            {
                navMeshAgent.velocity = direction * knockbackForce;
            }
        }
        public void StartMoveAction(Vector3 destination, float speedFraction)
        {
            GetComponent<ActionScheduler>().StartAction(this);
            //disabled look towards clickpoint as it was stopping block direction rotation if it was interupted
            //if(waypointClickPrefab != null && GetDistanceToMovePoint(destination) > 1.5f) LookAtIK(navMeshAgent.gameObject, destination);
            
            MoveTo(destination, speedFraction);
        }
        private float GetDistanceToMovePoint(Vector3 destination)
        {
            float distance = Vector3.Distance(transform.position, destination);
            return distance;
        }

        private void LookAtIK(GameObject callingController, Vector3 destination)
        {
            LookAtController lookAt = callingController.GetComponent<LookAtController>();
            if (lookAt != null && waypointClickPrefab != null)
            {
                waypointClickPrefab.transform.position = destination;
                lookAt.target = waypointClickPrefab.transform;
                lookAt.weight = 1f;
                lookAt.offset = new Vector3(0,.75f,0);
                StartCoroutine(RemoveLookAtTarget(1f, lookAt));
            }
        }

        IEnumerator RemoveLookAtTarget(float duration, LookAtController lookAtController)
        {
            yield return new WaitForSeconds(duration);
            lookAtController.target = null;
            lookAtController.weight = 0f;

        }

        public bool CanMoveTo(Vector3 destination)
        {
            //Prevent walkable cursor from showing if nav meshes do not connect
            NavMeshPath path = new NavMeshPath();
            bool hasPath = NavMesh.CalculatePath(transform.position, destination, NavMesh.AllAreas, path);
            if (!hasPath) return false;
            if (path.status != NavMeshPathStatus.PathComplete) return false;

            //Prevent walkable if Path is too long
            if (GetPathLength(path) > maxNavPathLength) return false;

            return true;
        }
        private float GetPathLength(NavMeshPath path)
        {
            float total = 0;
            if (path.corners.Length < 2) return total;
            for (int i = 0; i < path.corners.Length - 1; i++)
            {
                total += Vector3.Distance(path.corners[i], path.corners[i + 1]);
            }
            return total;
        }

        public bool HasCompletedPath(NavMeshPath path)
        {
            float distance = GetPathLength(path);
            if (distance != Mathf.Infinity && navMeshAgent.pathStatus == NavMeshPathStatus.PathComplete && navMeshAgent.remainingDistance == 0)
            {
                return true;
            }
            else return false;
        }

        public void MoveTo(Vector3 destination, float speedFraction)
        {
            if(navMeshAgent.enabled)
            {
                navMeshAgent.destination = destination;
                //SetAccelleration();
                navMeshAgent.speed = desiredSpeed * Mathf.Clamp01(speedFraction);
                navMeshAgent.isStopped = false;

                debugDestinationPosition = destination;
            }
        }

        public void FollowTarget(GameObject target)
        {
            if(isFollowing && followTarget!= null)
            {
                Debug.Log("Following");
                float followRange = 2f;
                float closeEnoughToTarget = 2f;
                float distanceToTarget = Vector3.Distance(this.transform.position, target.transform.position);
                if (distanceToTarget > followRange)
                {
                    MoveTo(target.transform.position, 1f);
                }
            }
        }

        public void SetFollowTarget(GameObject target)
        {
            followTarget = target;
            isFollowing = true;
            Debug.Log("following " + target);
        }

        public void SetIsFollowing(bool value)
        {
            isFollowing = value;
        }

        private void SetAccelleration()
        {
            if (animator.GetBool("Angry")) navMeshAgent.acceleration = accelleration * 2;
            else navMeshAgent.acceleration = accelleration;
        }

        private void SetMaxSpeed()
        {
            currentHealthPoints = health.GetHealthPoints();
            healthPercentage = currentHealthPoints / 100;
            if(currentHealthPoints <= 100)
            {
                float newSpeed = maxSpeed * healthPercentage;
                if(newSpeed <= injuredMinSpeed)
                {
                    newSpeed = injuredMinSpeed;
                }
                desiredSpeed = newSpeed;
            }
            else
            {
                desiredSpeed = maxSpeed;
            }
        }

        private void SetInjuredAnimLayerWeight()
        {
            int injuredAnimLayerIndex = animator.GetLayerIndex("Injured");
            float currentInjuredLayerWeight = animator.GetLayerWeight(injuredAnimLayerIndex);
            float targetLayerWeight = 1 - healthPercentage;

            animator.SetLayerWeight(injuredAnimLayerIndex, Mathf.SmoothDamp(currentInjuredLayerWeight, targetLayerWeight, ref layerWeightVelocity, 0.2f));
        }

        public void Cancel()
        {
            if (!knockback)
            {
                StopAllCoroutines();
                navMeshAgent.isStopped = true;
            }
        }

        private void UpdateAnimator()
        {
            Vector3 velocity = navMeshAgent.velocity;
            Vector3 localVelocity = transform.InverseTransformDirection(velocity);
            float speed = localVelocity.z;
            GetComponent<Animator>().SetFloat("forwardSpeed", speed);
        }

        [System.Serializable]
        struct MoverSaveData
        {
            public SerializableVector3 position;
            public SerializableVector3 rotation;
        }
        public void Knockback()
        {
            direction =  -transform.forward;
            knockbackForce = Mathf.Min(health.damageLastTaken * .25f, 3f);
            StartCoroutine(Impact());
        }
        IEnumerator Impact()
        {
            knockback = true;
            navMeshAgent.speed = 100f;
            navMeshAgent.angularSpeed = 0; //Keeps the enemy facing forward ranter than spinning
            navMeshAgent.acceleration = 100f;

            yield return new WaitForSeconds(0.1f); //Only knock the enemy back for a short time
            //Reset to default values
            navMeshAgent.speed = maxSpeed;
            navMeshAgent.angularSpeed = 2000f;
            navMeshAgent.acceleration = 5f;
            knockback = false;
        }

        public object CaptureState()
        {
            MoverSaveData data = new MoverSaveData();
            data.position = new SerializableVector3(transform.position);
            data.rotation = new SerializableVector3(transform.eulerAngles);
            return data;
        }

        public void RestoreState(object state)
        {
            MoverSaveData data = (MoverSaveData)state;
            NavMeshAgent navMeshAgent = GetComponent<NavMeshAgent>();

            if (IsNewPositionOnNavMesh(data.position.ToVector()))
            {
                navMeshAgent.enabled = false;
                transform.position = data.position.ToVector();
                transform.eulerAngles = data.rotation.ToVector();
                navMeshAgent.enabled = true;
            }
        }

        private bool IsNewPositionOnNavMesh(Vector3 targetDestination)
        {
            NavMeshHit hit;
            if(NavMesh.SamplePosition(targetDestination, out hit, 1f, NavMesh.AllAreas))
            {
                return true;
            }
            return false;
        }
    }
}

The target for the FollowTarget() method is passed in from this ‘CommandNPC.cs’ that is attached to the character… and this is are triggered via Game Events from on the Dialogue system

using GameDevTV.Saving;
using RPG.Dialogue;
using RPG.Movement;
using UnityEngine;

public class CommandNPC : MonoBehaviour, ISaveable
{
    // STATE
    PartyMember partyMember;

    public Dialogue commandDialogue = null;
    public Dialogue[] stopTakingCommands = null;

    public bool isBeingCommanded = false;
    private GameObject player;
    private Mover mover;
    public Vector3 homeDestination;
    private PartyManager partyManager;

    private void Start()
    {
        mover = GetComponent<Mover>();
        player = GameObject.FindGameObjectWithTag("Player");
        homeDestination = transform.position;
        partyManager = player.GetComponent<PartyManager>();
    }

    public void Setup(PartyMember partyMember)
    {
        this.partyMember = partyMember;
    }

    public bool GetIsBeingCommanded()
    {
        return isBeingCommanded;
    }

    private void AddToParty()
    {
        partyManager.AddMemberToList(partyMember);
    }

    private void RemoveFromParty()
    {
        partyManager.RemoveMemberFromList(partyMember);
    }

    public void SetIsBeingCommanded(bool value)
    {
        isBeingCommanded = value;
        if (isBeingCommanded == true)
        { 
            AddToParty(); 
        }

        else 
        {
            RemoveFromParty();
        } 
            
    }

    public Dialogue GetStopFollowingCommandsDialogue()
    {
        SetIsBeingCommanded(false);
        int random = Random.Range(0, stopTakingCommands.Length);
        return stopTakingCommands[random];
    }

    public void FollowPlayer()
    {
        mover.SetIsFollowing(true);
        mover.SetFollowTarget(player);
    }

    public void WaitHere()
    {
        mover.SetIsFollowing(false);
        mover.SetFollowTarget(null);
        //RemoveFromParty();
    }

    public void ReturnHome()
    {
        mover.SetIsFollowing(false);
        mover.MoveTo(homeDestination, 1f);
        //RemoveFromParty();
    }

    public object CaptureState()
    {
        return isBeingCommanded;
    }

    public void RestoreState(object state)
    {
        isBeingCommanded = (bool)state;
    }
}

Ok, I went through this enough to get a good idea of what you’re doing… One area I’m not sure of the specfics is your PartyManager/PartyMember…

  1. What is a PartyMember? (Maybe show the PartyMember.cs)
  2. Who is responsible for creating the party member, it looks like that’s something assigned at runtime in the Setup

Next homework assy… Add this to your Setup in CommandNPC.cs:

if(partyMember==null)
{
    Debug.Log($"{name} is being assigned a null Party Member.");
}

Thanks so much! Party Manager sits on the Player and is a list of PartyMembers that are following them. I need to keep track of this for spawning them between levels etc. Party Member is a ScriptableObject that has a reference to the character prefab. I needed this to be able to have an ID to save to and I thought it would be better to make the NPC’s more like how the items in the game work. Here are both scripts attached. (I added in your debug code, and it was not called).

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

[CreateAssetMenu(menuName = ("RPG/NPC/New Party Member"))]
[System.Serializable]
public class PartyMember : ScriptableObject, ISerializationCallbackReceiver
{
    // CONFIG DATA
    [Tooltip("Auto-generated UUID for saving/loading. Clear this field if you want to generate a new one.")]
    [SerializeField] string memberID = null;
    public CommandNPC characterPrefab;

    // STATE
    static Dictionary<string, PartyMember> partyMemberLookupCache;

    // PUBLIC

    /// <summary>
    /// Get the partyMember instance from its UUID.
    /// </summary>
    /// <param name="memberID">
    /// String UUID that persists between game instances.
    /// </param>
    /// <returns>
    /// PartyMember instance corresponding to the ID.
    /// </returns>
    public static PartyMember GetFromID(string memberID)
    {
        if (partyMemberLookupCache == null)
        {
            partyMemberLookupCache = new Dictionary<string, PartyMember>();
            var memberList = Resources.LoadAll<PartyMember>("");
            foreach (var member in memberList)
            {
                if (partyMemberLookupCache.ContainsKey(member.memberID))
                {
                    Debug.LogError(string.Format("Looks like there's a duplicate GameDevTV.UI.InventorySystem ID for objects: {0} and {1}", partyMemberLookupCache[member.memberID], member));
                    continue;
                }

                partyMemberLookupCache[member.memberID] = member;
            }
        }

        if (memberID == null || !partyMemberLookupCache.ContainsKey(memberID)) return null;
        return partyMemberLookupCache[memberID];
    }

    /// <summary>
    /// Spawn the character gameobject into the world.
    /// </summary>
    /// <param name="position">Where to spawn the character.</param>
    /// <returns>Reference to the character spawned.</returns>
    public CommandNPC SpawnCharacter(Vector3 position, Vector3 rotation)
    {
        var partyMember = Instantiate(this.characterPrefab);
        partyMember.GetComponent<NavMeshAgent>().Warp(position);
        if (partyMember.GetComponent<NavMeshAgent>().isOnNavMesh)
        {
            Debug.Log("IM ON A NAV MESH");
        }

        //partyMember.transform.position = position;
        partyMember.transform.eulerAngles = rotation;
        partyMember.Setup(this);
        return partyMember;
    }
    public string GetMemberID()
    {
        return memberID;
    }
    

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {
        // Generate and save a new UUID if this is blank.
        if (string.IsNullOrWhiteSpace(memberID))
        {
            memberID = System.Guid.NewGuid().ToString();
        }
    }

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
        // Require by the ISerializationCallbackReceiver but we don't need
        // to do anything with it.
    }
}


using GameDevTV.Inventories;
using GameDevTV.Saving;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PartyManager : MonoBehaviour, ISaveable
{
    public List<PartyMember> partyMembers;
    private float scatterDistance = 3f;
    //CONSTANTS
    const int ATTEMPTS = 60;

    private void Start()
    {
        SpawnPartyMembers();
    }

    public void AddMemberToList(PartyMember member)
    {
        partyMembers.Add(member);
    }

    public void RemoveMemberFromList(PartyMember member)
    {
        partyMembers.Remove(member);
    }

    public void SpawnPartyMembers()
    {
        if(partyMembers.Count > 0 && partyMembers != null)
        {
            foreach (PartyMember member in partyMembers)
            {
                Instantiate(member.characterPrefab, GetRandomPosition(), Quaternion.identity);
            }
        }
    }

    private bool TooCloseToPlayer(NavMeshHit hit)
    {
        float distanceToPlayer = Vector3.Distance(hit.position, transform.position);
        if (distanceToPlayer < 1f)
        {
            return true;
        }
        return false;
    }
    private Vector3 GetRandomPosition()
    {
        //We might need to try more than once to get the NavMesh
        for (int i = 0; i < ATTEMPTS; i++)
        {
            Vector3 randomPoint = transform.position + Random.insideUnitSphere * scatterDistance;
            NavMeshHit hit;
            if (NavMesh.SamplePosition(randomPoint, out hit, 1f, NavMesh.AllAreas))
            {
                if (hit.position == transform.position)continue;
                return hit.position;
            }
        }
        return transform.position;
    }

    internal bool FindPartyMember(CommandNPC character)
    {
        foreach (var member in partyMembers)
        {
            if (member.name == character.name)
            {
                return true;
            }
        }
        return false;
    }

    [System.Serializable]
    private struct PartyMemberRecord
    {
        public string memberID;
    }

    public object CaptureState()
    {
        var partyMemberRecord = new PartyMemberRecord[partyMembers.Count];
        for (int i = 0; i < partyMembers.Count; i++)
        {
            partyMemberRecord[i].memberID = partyMembers[i].GetMemberID();
        }
        return partyMemberRecord;
    }

    public void RestoreState(object state)
    {
        partyMembers.Clear();
        var partyMemberRecord = (PartyMemberRecord[])state;
        for (int i = 0; i < partyMemberRecord.Length; i++)
        {
            partyMembers.Add(PartyMember.GetFromID(partyMemberRecord[i].memberID));
        }
    }
}

The fact that it wasn’t called is likely the reason that the characters aren’t responding as wanted…

Ok, it looks like PartyManager is spawning in new members in Start(), which would be either the Party Members assigned in the inspector before the game started playing or the restored list of party members… Are these characters doing their thing? If not, how are you spawning in new party members to add to the party? How are they set up?

Yes that is correct, If I disable the NPCSpawner, and add the PartryMember to the PartyManager List manually in the Inspector, I get the same result, the character spawns in next to the player as wanted, but can not move when set to follow.

PartyMember

PartyMembers are spawned in the PartyManager’s Start Method. It goes through the List of Members if it’s not null and Instantiates the prefab that is attached to the PartyMember ScriptableObject

NPCSpawner? You know the drill. :slight_smile:

Sorry, thought I’d already posted that one, here it is:

using UnityEngine;
using GameDevTV.Saving;
using UnityEngine.AI;

namespace GameDevTV.Inventories
{
    /// <summary>
    /// Spawns potential Party members.
    /// </summary>
    [ExecuteInEditMode]
    public class NPCSpawner : MonoBehaviour, ISaveable
    {
        // CONFIG DATA
        [SerializeField] PartyMember partyMember = null;
        private PartyManager playerPartyList;

        // LIFECYCLE METHODS
        private void Awake()
        {
            
            //triangulation = NavMesh.CalculateTriangulation();
            foreach (Transform child in transform)
            {
                GameObject.DestroyImmediate(child.gameObject);
            }
            playerPartyList = GameObject.FindGameObjectWithTag("Player").GetComponent<PartyManager>();

            // Spawn in Awake so can be destroyed by save system after.
            SpawnCharacter();
        }
        // PUBLIC

        /// <summary>
        /// Returns the commandable partyMember spawned by this class if it exists.
        /// </summary>
        /// <returns>Returns null if the pickup has been collected.</returns>
        public CommandNPC GetCharacterPrefab()
        {
            return GetComponentInChildren<CommandNPC>();
        }

        /// <summary>
        /// True if member is in players Party.
        /// </summary>
        public bool inParty()
        {
            return playerPartyList.FindPartyMember(GetCharacterPrefab());
        }

        //PRIVATE
        private void SpawnCharacter()
        {
            if (partyMember != null && transform.childCount <= 1)
            {
                var spawnedCharacter = partyMember.SpawnCharacter(transform.position, transform.eulerAngles);
                Debug.Log("NPC spawned: " + partyMember.name);
                spawnedCharacter.transform.SetParent(transform);
            }
        }

        private void DestroyNPC()
        {
            if (GetCharacterPrefab())
            {
                Destroy(GetCharacterPrefab().gameObject);
            }
        }

        object ISaveable.CaptureState()
        {
            return inParty();
        }

        void ISaveable.RestoreState(object state)
        {
            bool shouldBeInParty = (bool)state;
            if (Application.isPlaying)
            {
                if (shouldBeInParty && !inParty())
                {
                    DestroyNPC();
                }

                if (!shouldBeInParty && inParty())
                {
                    SpawnCharacter();
                }

            }

        }
    }
}

I added some debug to the Mover.cs on my Instantiated Characters,

public void FollowTarget(GameObject target)
        {
            if(isFollowing && followTarget!= null)
            {
                Debug.Log("Following");
                float followRange = 2f;
                float closeEnoughToTarget = 2f;
                float distanceToTarget = Vector3.Distance(this.transform.position, target.transform.position);
                if (distanceToTarget > followRange)
                {
                    Debug.Log("My Target is: " + target.name);
                    MoveTo(target.transform.position, 1f);
                }
            }
        }

Both of these are getting called and I can confirm that the character is following and has a target of ‘Player’. So Then in the MoveTo() method I added another Debug.Log to see if the NavMesh was not enabled:

public void MoveTo(Vector3 destination, float speedFraction)
        {
            if(navMeshAgent.enabled)
            {
                if (!IsNewPositionOnNavMesh(destination))
                {
                    Debug.Log("Destination is NOT on a NavMesh");
                }
                navMeshAgent.destination = destination;
                //SetAccelleration();
                navMeshAgent.speed = desiredSpeed * Mathf.Clamp01(speedFraction);
                navMeshAgent.isStopped = false;

                debugDestinationPosition = destination;
            }
            else
            {
                Debug.Log("NavMesh on " + name + " not enabled!");
            }
        }

But those did not fire, so it would seem that its not the case that the spawned in character is not on the NavMesh, or that the destination is inaccessible.

I think I’m going to need to take a closer look at the project. Zip up your project and upload it to https://gdev.tv/projectupload. Be sure to remove the Library folder to conserve space.

Thanks, Zipped up and without the library files it comes to 5.8gig, and exceeds the limit allowed to upload sorry. Is there anything else I could do or remove?

Try copying the project to a new directory, and then removing any assets/models/textures/audio files that aren’t being used. If you’ve built the game, be sure to remove any of those builds as well… I just need the assets needed for the scene (and the scripts, of course).

If you cant’ get it small enough, we can keep doing 20 questions, but I’m feeling like there’s something obvious I’m missing.

He he , yep sure, I’m culling it all out now, I think can shave off a few gig pretty easy :slight_smile:

I did my best to get the size down, but seems that 1.8gb is as small as I can make it. I’ve submitted a link to DL it, I hope that’s ok. I too feel like it is something obvious that I’m missing :confused:

Our system didn’t put in an entry in with the link… possibly it’s only saving data when there’s an actual file. DM me the link.

Privacy & Terms