How Do I Sync a NavMeshAgent?

Hello. I am new to these forums and Photon Unity Networking. I am trying to create a multiplayer game where several players can shoot and kill a bunch of spawning mobs all in the same room. I have the players spawning into the room correctly and I even have firing and other components working. What i am struggling with is enemies spawning and using a navmesh to locate and attack players.

The behavior that is happening is that the Master client's enemies look fine, move smoothly and spawn in the correct location. The behavior on the other clients however is very jittery movement, sometimes not even appearing at all, or its jittering so much that the object looks transparent. Movement does not follow calculated paths at all even though the Master Client is telling them what path they should be taking. They also do not spawn where the spawner is located despite the fact that I have the master client specifically telling the objects where to spawn at which is at the spawn object). the spawning itself is actually based on a pooling class that uses PhotonNetwork.Instantiate. I have tried manually syncing the scripts with OnPhotonSerializeView, tried using PhotonViews, rearranged where RPCs are being called and so on and so far I am totally stumped because nothing is working. The only thing that works correctly are the players and their weapons when those spawn from their triggers.

I have yet to find any tutorials on this specific topic online and Google searches have yielded nothing. Is it even possible to sync a navmeshagent? any assistance would be highly appreciated.

The code I am using: (in next post since there is a character limit apparently)

Answers

  • 
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.AI;
    
    public class EnemyTargeting_MP : Photon.PunBehaviour {
    
        private List<GameObject> playerList;
    
        private Transform target;
        private NavMeshAgent agent;
        private Vector3 destination;
    
        private GameObject PlayerObject
        {
            get
            {
                return playerList[Random.Range(0, GameObject.FindGameObjectsWithTag("Player").Length)];
            }
        }
    
    	// Use this for initialization
    	void Start () {
            if (PhotonNetwork.connected)
            {
                if (PhotonNetwork.isMasterClient)
                {
                    Initialize();
                }
                else
                {
                    GetComponent<NavMeshAgent>().enabled = false;
                    GetComponent<EnemyTargeting_MP>().enabled = false;
                }
            }        
    	}
    
        private void OnEnable()
        {
            if (PhotonNetwork.isMasterClient)
            {
                InvokeRepeating("Refresh", 0.1f, 0.1f);
            }        
        }
    
        private void OnDisable()
        {
            CancelInvoke("Refresh");
        }
    
        private void Initialize()
        {
            playerList = new List<GameObject>();
            for (int i = 0; i < GameObject.FindGameObjectsWithTag("Player").Length; i++)
            {
                playerList.Add(GameObject.FindGameObjectsWithTag("Player")[i]);
            }
    
            target = PlayerObject.transform;
            agent = GetComponent<NavMeshAgent>();
    
            if (gameObject.activeSelf)
            {
                InvokeRepeating("Refresh", 0.1f, 0.1f);
            }
        }
    
        private void Refresh()
        {
            FindPlayer();        
        }
    
        private void FindPlayer()
        {
            if (PlayerObject != null)
            {
                if (PlayerObject.activeSelf)
                {
                    MoveToTarget();
                }
                else
                {
                    if (GameObject.FindGameObjectWithTag("Player"))
                    {
                        target = PlayerObject.transform;
                    }
                }
            }
            else
            {
                if (GameObject.FindGameObjectWithTag("Player"))
                {
                    target = PlayerObject.transform;
                }
            }
        }
    
        private void MoveToTarget()
        {
            if (target.GetComponent<NavMeshLocation>())
            {
                agent.SetDestination(target.GetComponent<NavMeshLocation>().GetNavMeshPosition());
                agent.isStopped = false;
            }
    
            RaycastHit hit;
            Ray ray = new Ray(transform.position, -transform.up);
            if (Physics.Raycast(ray, out hit))
            {
                Vector3 incomingVector = hit.point - transform.position;
                transform.rotation = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation;
            }
    
            Quaternion rotation = (agent.desiredVelocity).normalized != Vector3.zero ? Quaternion.LookRotation((agent.desiredVelocity).normalized) : transform.rotation;
            transform.rotation = rotation;
        }
    }
    The code for enemy spawning:

    using System.Collections.Generic;
    using UnityEngine;

    public class Spawner_MP : Photon.PunBehaviour {

    [SerializeField] private float initialSpawnTime; // The amount of time required before the first spawn is activated.
    [SerializeField] private float repeatSpawnTime; // The amount of time before the next spawn is activated.
    [SerializeField] private List enemies; // The list of objects that can be spawned.

    private GameObject obj;

    // Use this for initialization
    void Start () {
    if (PhotonNetwork.isMasterClient)
    {
    InvokeRepeating("InitiateSpawner", initialSpawnTime, repeatSpawnTime);
    }
    }

    private void InitiateSpawner()
    {
    SpawnEnemy();
    }

    // Spawns enemies at the spawner's location.
    private void SpawnEnemy()
    {
    int i = Random.Range(0, enemies.Count); // Randomly selects an enemy to spawn from the enemies list.
    obj = ObjectPooling_MP.currentPooling.GetPooledObject(enemies[i].tag); // Retrieves prefab from pool.
    obj.transform.position = transform.position; // The location the enemy is to spawn at.
    obj.transform.rotation = transform.rotation; // The direction the enemy is to be facing when spawned.
    obj.SetActive(true); // Activates the prefab and make it visible in the level.
    }
    }
    The class that the spawner inherits from.
    
    
    using System.Collections.Generic;
    using UnityEngine;
    
    // This is the object class that determines a poolable object's base properties.
    [System.Serializable]
    public class PoolItem
    {
        public string name;
        public GameObject pooledObject;
        public int pooledAmount;
        public bool canGrow;
    }
    
    // The actual class that pools objects.
    public class ObjectPooling_MP : Photon.PunBehaviour
    {
        [SerializeField] private List<PoolItem> itemsToPool;
        public static ObjectPooling_MP currentPooling;  // Class reference is declared here.
        private List<GameObject> pooledObjects;
    
        private void Awake()
        {
            currentPooling = this;  // Creates a public reference to itself.
        }
    
        // Initializes the pooled object list.
        private void Start()
        {
            pooledObjects = new List<GameObject>();
    
            foreach (PoolItem item in itemsToPool)
            {
                for (int i = 0; i < item.pooledAmount; i++)
                {
                    GameObject obj = PhotonNetwork.Instantiate(item.pooledObject.name, new Vector3(0, 0, 0), Quaternion.identity, 0) as GameObject;
                    obj.SetActive(false);
                    pooledObjects.Add(obj);
                }
            }
        }
    
        // Returns a pooled object from the pooled objects list.
        public GameObject GetPooledObject(string tag)
        {
            // This returns the object to be pooled.
            for (int i = 0; i < pooledObjects.Count; i++)
            {
                if (!pooledObjects[i].activeInHierarchy && pooledObjects[i].tag == tag)
                {
                    return pooledObjects[i];
                }
            }
    
            // This adds more objects to the pool if Can Grow is checked on.
            foreach (PoolItem item in itemsToPool)
            {
                if (item.pooledObject.tag == tag)
                {
                    if (item.canGrow)
                    {
                        GameObject obj = PhotonNetwork.Instantiate(item.pooledObject.name, new Vector3(0, 0, 0), Quaternion.identity, 0) as GameObject;
                        pooledObjects.Add(obj);
                        return obj;
                    }
                }
            }
    
            return null;
        }
    }
  • The Enemy spawner.
    /*==========================================================================================================
    * Spawner_MP script
    ==========================================================================================================*/
    
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Spawner_MP : Photon.PunBehaviour {
    
        [SerializeField] private float initialSpawnTime;          
    // The amount of time required before the first spawn is activated.
    
        [SerializeField] private float repeatSpawnTime;        
    // The amount of time before the next spawn is activated.
    
        [SerializeField] private List<GameObject> enemies; 
    // The list of objects that can be spawned.
    
        private GameObject obj;
    
    	// Use this for initialization
    	void Start () {
            if (PhotonNetwork.isMasterClient)
            {
                InvokeRepeating("InitiateSpawner", initialSpawnTime, repeatSpawnTime);
            }        
        }
    
        private void InitiateSpawner()
        {
            SpawnEnemy();
        }
    
        // Spawns enemies at the spawner's location.    
        private void SpawnEnemy()
        {
            int i = Random.Range(0, enemies.Count);         
    // Randomly selects an enemy to spawn from the enemies list.
    
            obj = ObjectPooling_MP.currentPooling.GetPooledObject(enemies[i].tag);      
    // Retrieves prefab from pool.
    
            obj.transform.position = transform.position;            
    // The location the enemy is to spawn at.
    
            obj.transform.rotation = transform.rotation;            
    // The direction the enemy is to be facing when spawned.
    
            obj.SetActive(true);         
    // Activates the prefab and make it visible in the level.
        }
    }
    

    The pooling class:
    /*
     * OBJECT POOLING MP
     * This is a generic pooling class for online multiplayer that can pool any object. Attach this to an empty game object in the scene. In the inspector,
     * plug in the object to be pooled, how many are to be pooled at one time, and determine if the pool can be expanded at runtime.
    */
    
    using System.Collections.Generic;
    using UnityEngine;
    
    // This is the object class that determines a poolable object's base properties.
    [System.Serializable]
    public class PoolItem
    {
        public string name;
        public GameObject pooledObject;
        public int pooledAmount;
        public bool canGrow;
    }
    
    // The actual class that pools objects.
    public class ObjectPooling_MP : Photon.PunBehaviour
    {
        [SerializeField] private List<PoolItem> itemsToPool;
        public static ObjectPooling_MP currentPooling;  // Class reference is declared here.
        private List<GameObject> pooledObjects;
    
        private void Awake()
        {
            currentPooling = this;  // Creates a public reference to itself.
        }
    
        // Initializes the pooled object list.
        private void Start()
        {
            pooledObjects = new List<GameObject>();
    
            foreach (PoolItem item in itemsToPool)
            {
                for (int i = 0; i < item.pooledAmount; i++)
                {
                    GameObject obj = PhotonNetwork.Instantiate(item.pooledObject.name, new Vector3(0, 0, 0), Quaternion.identity, 0) as GameObject;
                    obj.SetActive(false);
                    pooledObjects.Add(obj);
                }
            }
        }
    
        // Returns a pooled object from the pooled objects list.
        public GameObject GetPooledObject(string tag)
        {
            // This returns the object to be pooled.
            for (int i = 0; i < pooledObjects.Count; i++)
            {
                if (!pooledObjects[i].activeInHierarchy && pooledObjects[i].tag == tag)
                {
                    return pooledObjects[i];
                }
            }
    
            // This adds more objects to the pool if Can Grow is checked on.
            foreach (PoolItem item in itemsToPool)
            {
                if (item.pooledObject.tag == tag)
                {
                    if (item.canGrow)
                    {
                        GameObject obj = PhotonNetwork.Instantiate(item.pooledObject.name, new Vector3(0, 0, 0), Quaternion.identity, 0) as GameObject;
                        pooledObjects.Add(obj);
                        return obj;
                    }
                }
            }
    
            return null;
        }
    }
    The targeting/movement.
    /*==========================================================================================================
    * EnemyTargeting_MP script
    ==========================================================================================================*/
    
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.AI;
    
    public class EnemyTargeting_MP : Photon.PunBehaviour {
    
        private List<GameObject> playerList;
    
        private Transform target;
        private NavMeshAgent agent;
        private Vector3 destination;
    
        private GameObject PlayerObject
        {
            get
            {
                return playerList[Random.Range(0, GameObject.FindGameObjectsWithTag("Player").Length)];
            }
        }
    
    	// Use this for initialization
    	void Start () {
            if (PhotonNetwork.connected)
            {
                if (PhotonNetwork.isMasterClient)
                {
                    Initialize();
                }
                else
                {
                    GetComponent<NavMeshAgent>().enabled = false;
                    GetComponent<EnemyTargeting_MP>().enabled = false;
                }
            }        
    	}
    
        private void OnEnable()
        {
            if (PhotonNetwork.isMasterClient)
            {
                InvokeRepeating("Refresh", 0.1f, 0.1f);
            }        
        }
    
        private void OnDisable()
        {
            CancelInvoke("Refresh");
        }
    
        private void Initialize()
        {
            playerList = new List<GameObject>();
            for (int i = 0; i < GameObject.FindGameObjectsWithTag("Player").Length; i++)
            {
                playerList.Add(GameObject.FindGameObjectsWithTag("Player")[i]);
            }
    
            target = PlayerObject.transform;
            agent = GetComponent<NavMeshAgent>();
    
            if (gameObject.activeSelf)
            {
                InvokeRepeating("Refresh", 0.1f, 0.1f);
            }
        }
    
        private void Refresh()
        {
            FindPlayer();        
        }
    
        private void FindPlayer()
        {
            if (PlayerObject != null)
            {
                if (PlayerObject.activeSelf)
                {
                    MoveToTarget();
                }
                else
                {
                    if (GameObject.FindGameObjectWithTag("Player"))
                    {
                        target = PlayerObject.transform;
                    }
                }
            }
            else
            {
                if (GameObject.FindGameObjectWithTag("Player"))
                {
                    target = PlayerObject.transform;
                }
            }
        }
    
        private void MoveToTarget()
        {
            if (target.GetComponent<NavMeshLocation>())
            {
                agent.SetDestination(target.GetComponent<NavMeshLocation>().GetNavMeshPosition());
                agent.isStopped = false;
            }
    
            RaycastHit hit;
            Ray ray = new Ray(transform.position, -transform.up);
            if (Physics.Raycast(ray, out hit))
            {
                Vector3 incomingVector = hit.point - transform.position;
                transform.rotation = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation;
            }
    
            Quaternion rotation = (agent.desiredVelocity).normalized != Vector3.zero ? Quaternion.LookRotation((agent.desiredVelocity).normalized) : transform.rotation;
            transform.rotation = rotation;
        }
    }
  • The Enemy spawner.
    /*==========================================================================================================
    * Spawner_MP script
    ==========================================================================================================*/
    
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Spawner_MP : Photon.PunBehaviour {
    
        [SerializeField] private float initialSpawnTime;          
    // The amount of time required before the first spawn is activated.
    
        [SerializeField] private float repeatSpawnTime;        
    // The amount of time before the next spawn is activated.
    
        [SerializeField] private List<GameObject> enemies; 
    // The list of objects that can be spawned.
    
        private GameObject obj;
    
    	// Use this for initialization
    	void Start () {
            if (PhotonNetwork.isMasterClient)
            {
                InvokeRepeating("InitiateSpawner", initialSpawnTime, repeatSpawnTime);
            }        
        }
    
        private void InitiateSpawner()
        {
            SpawnEnemy();
        }
    
        // Spawns enemies at the spawner's location.    
        private void SpawnEnemy()
        {
            int i = Random.Range(0, enemies.Count);         
    // Randomly selects an enemy to spawn from the enemies list.
    
            obj = ObjectPooling_MP.currentPooling.GetPooledObject(enemies[i].tag);      
    // Retrieves prefab from pool.
    
            obj.transform.position = transform.position;            
    // The location the enemy is to spawn at.
    
            obj.transform.rotation = transform.rotation;            
    // The direction the enemy is to be facing when spawned.
    
            obj.SetActive(true);         
    // Activates the prefab and make it visible in the level.
        }
    }
    

    The pooling class:
    /*
     * OBJECT POOLING MP
     * This is a generic pooling class for online multiplayer that can pool any object. Attach this to an empty game object in the scene. In the inspector,
     * plug in the object to be pooled, how many are to be pooled at one time, and determine if the pool can be expanded at runtime.
    */
    
    using System.Collections.Generic;
    using UnityEngine;
    
    // This is the object class that determines a poolable object's base properties.
    [System.Serializable]
    public class PoolItem
    {
        public string name;
        public GameObject pooledObject;
        public int pooledAmount;
        public bool canGrow;
    }
    
    // The actual class that pools objects.
    public class ObjectPooling_MP : Photon.PunBehaviour
    {
        [SerializeField] private List<PoolItem> itemsToPool;
        public static ObjectPooling_MP currentPooling;  // Class reference is declared here.
        private List<GameObject> pooledObjects;
    
        private void Awake()
        {
            currentPooling = this;  // Creates a public reference to itself.
        }
    
        // Initializes the pooled object list.
        private void Start()
        {
            pooledObjects = new List<GameObject>();
    
            foreach (PoolItem item in itemsToPool)
            {
                for (int i = 0; i < item.pooledAmount; i++)
                {
                    GameObject obj = PhotonNetwork.Instantiate(item.pooledObject.name, new Vector3(0, 0, 0), Quaternion.identity, 0) as GameObject;
                    obj.SetActive(false);
                    pooledObjects.Add(obj);
                }
            }
        }
    
        // Returns a pooled object from the pooled objects list.
        public GameObject GetPooledObject(string tag)
        {
            // This returns the object to be pooled.
            for (int i = 0; i < pooledObjects.Count; i++)
            {
                if (!pooledObjects[i].activeInHierarchy && pooledObjects[i].tag == tag)
                {
                    return pooledObjects[i];
                }
            }
    
            // This adds more objects to the pool if Can Grow is checked on.
            foreach (PoolItem item in itemsToPool)
            {
                if (item.pooledObject.tag == tag)
                {
                    if (item.canGrow)
                    {
                        GameObject obj = PhotonNetwork.Instantiate(item.pooledObject.name, new Vector3(0, 0, 0), Quaternion.identity, 0) as GameObject;
                        pooledObjects.Add(obj);
                        return obj;
                    }
                }
            }
    
            return null;
        }
    }
    The targeting/movement.
    /*==========================================================================================================
    * EnemyTargeting_MP script
    ==========================================================================================================*/
    
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.AI;
    
    public class EnemyTargeting_MP : Photon.PunBehaviour {
    
        private List<GameObject> playerList;
    
        private Transform target;
        private NavMeshAgent agent;
        private Vector3 destination;
    
        private GameObject PlayerObject
        {
            get
            {
                return playerList[Random.Range(0, GameObject.FindGameObjectsWithTag("Player").Length)];
            }
        }
    
    	// Use this for initialization
    	void Start () {
            if (PhotonNetwork.connected)
            {
                if (PhotonNetwork.isMasterClient)
                {
                    Initialize();
                }
                else
                {
                    GetComponent<NavMeshAgent>().enabled = false;
                    GetComponent<EnemyTargeting_MP>().enabled = false;
                }
            }        
    	}
    
        private void OnEnable()
        {
            if (PhotonNetwork.isMasterClient)
            {
                InvokeRepeating("Refresh", 0.1f, 0.1f);
            }        
        }
    
        private void OnDisable()
        {
            CancelInvoke("Refresh");
        }
    
        private void Initialize()
        {
            playerList = new List<GameObject>();
            for (int i = 0; i < GameObject.FindGameObjectsWithTag("Player").Length; i++)
            {
                playerList.Add(GameObject.FindGameObjectsWithTag("Player")[i]);
            }
    
            target = PlayerObject.transform;
            agent = GetComponent<NavMeshAgent>();
    
            if (gameObject.activeSelf)
            {
                InvokeRepeating("Refresh", 0.1f, 0.1f);
            }
        }
    
        private void Refresh()
        {
            FindPlayer();        
        }
    
        private void FindPlayer()
        {
            if (PlayerObject != null)
            {
                if (PlayerObject.activeSelf)
                {
                    MoveToTarget();
                }
                else
                {
                    if (GameObject.FindGameObjectWithTag("Player"))
                    {
                        target = PlayerObject.transform;
                    }
                }
            }
            else
            {
                if (GameObject.FindGameObjectWithTag("Player"))
                {
                    target = PlayerObject.transform;
                }
            }
        }
    
        private void MoveToTarget()
        {
            if (target.GetComponent<NavMeshLocation>())
            {
                agent.SetDestination(target.GetComponent<NavMeshLocation>().GetNavMeshPosition());
                agent.isStopped = false;
            }
    
            RaycastHit hit;
            Ray ray = new Ray(transform.position, -transform.up);
            if (Physics.Raycast(ray, out hit))
            {
                Vector3 incomingVector = hit.point - transform.position;
                transform.rotation = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation;
            }
    
            Quaternion rotation = (agent.desiredVelocity).normalized != Vector3.zero ? Quaternion.LookRotation((agent.desiredVelocity).normalized) : transform.rotation;
            transform.rotation = rotation;
        }
    }
  • Did you figure this out? I'm just starting with Photon and my game is based on a lot of navmeshagents, and I get all stuttery motion on everyone except the client issuing the navmesh commands..
  • add the photoniew, transform view and animatorview components to your enemies.
    regards
  • I'm facing a similar problem right now, please keep us updated if you figure it out, @S_Oliver I see a message about "Maintenance Mode" on that link
  • Sorry for necroing this but are you performing the navigation on all clients separately?

    I've discovered that for my project, the cause of the jittery for NavMeshAgents was because I was setting the destination for the enemy NavMeshAgent on all clients each instead of the master client only.

    Because of this, the destination was constantly being calculated multiple times on each client, so that transform of each NavMeshAgent was constantly being changed (which causes the jittery in their PhotonTransformNetwork)

    To solve this, I only set the destination for each NavMeshAgent on the master client by checking if the player was a master client just before I set the destination.

    I then sync the NavMeshAgent's transform by using the PhotonTransformView component and made the PhotonView observe this.

    I hope this helps someone.

  • IWillMakeNameLater
    edited June 2022

    In my case (I'm using Fusion) NavMeshAgent was setting position (default behaviour) and for interpolation (smooth movement) to work you must set position yourself so NavMeshAgent doesn't try to overwrite it

    so at Start() you disable setting position by navmesh agent (I also turn off rigidbody). I share my code

    NavmeshAgent.updatePosition = false;

    NetworkRigidbody2d.Rigidbody.bodyType = RigidbodyType2D.Kinematic;


    and at simulation's update (FixedUpdateNetwork()) you set the position (I also set destination to player's position)

    NetworkRigidbody2d.Rigidbody.position = NavmeshAgent.nextPosition;

    NavmeshAgent.destination = Player.transform.position;