[MLAPI ] need help to sync spawned objects

Hi,
First i used to use Mirror for a long time ago project and for this new one , i’m trying to use the new MLAPI .
I’m sorry for my non native English . I 'm a beginner too in multiplayer coding.

What i’m trying to do : make a simple app in 2D , i have a UI canvas with 2 Panels : Entry and Exit ; Entry contains two scrollviews : one for player waiting and one for players playing .

Player is a 2d UI Button which contains network object component and network transform .

I want to create a kind of lobby, very basically when a player click on his name on the first panel : the button mooves to the second panel and vice versa .

When i spawn a player with networkObject.Spawn() the player appears in client but has not the wanted transform , i don’t know how to implement the good transform and the name of button to be synced with the one on server …

I tried to use [rpc] and networkvariable but did’nt work .

Can someone help me ( showing simple example of lobby room code …) ?

Thanks in advance !

In the Docs, it is stated that Spawn method should only be done on the server as the object will automatically replicate on the other clients. You must Instantiate and then call spawn, all in the server.

To sync the position you can use NetworkVariableVector3 to indicate object position. You can assign that in server or owner of that object and read the value if not. Read more in here

For example my simple spawn method

public class GameClient : NetworkBehaviour
{
    public override void NetworkStart()
    {
        base.NetworkStart();
        SpawnServerRPC(NetworkManager.Singleton.LocalClientId);
    }

    [ServerRpc]
    private void SpawnServerRPC(ulong clientId)
    {
        // get yourGameObject here
        NetworkObject obj = Instantiate(yourGameObject).GetComponent<NetworkObject>();
        obj.SpawnWithOwnership(clientId);
        InitializeClientRPC(clientId, obj.NetworkObjectId);
    }

    [ClientRpc]
    private void InitializeClientRPC(ulong clientId, ulong objectId)
    {
        if(clientId == NetworkManager.Singleton.LocalClientId)
        {
            GameObject spawnedObject = NetworkSpawnManager.SpawnedObjects[objectId].gameObject;
        }
    }
}
2 Likes

Thank you for your answer firaui ;

I get an null referenceException now and i don’t know why :

Steps to reproduce i made a simple scene with 4 network buttons Starthost start client and start Spawning ( fig 1 )

Start host calls a public method and invoke NetworkManager.singleton.Starthost ; the same for startServer …

I added a GameClientSpawner in scene with your script ( i just SerializeField the gameobject )

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityGoogleDrive;
using UnityGoogleDrive.Data;
using MLAPI.Messaging;
using MLAPI.NetworkVariable;
using MLAPI;
using MLAPI.Spawning;
public class GameClient : NetworkBehaviour
{

    [SerializeField] GameObject gamePrefab;

    public override void NetworkStart()
    {
        base.NetworkStart();
        SpawnServerRPC(NetworkManager.Singleton.LocalClientId);
    }

    [ServerRpc]
    private void SpawnServerRPC(ulong clientId)
    {
        // get yourGameObject here
        NetworkObject obj = Instantiate(gamePrefab).GetComponent<NetworkObject>();
        obj.SpawnWithOwnership(clientId);
        InitializeClientRPC(clientId, obj.NetworkObjectId);
    }

    [ClientRpc]
    private void InitializeClientRPC(ulong clientId, ulong objectId)
    {
        if (clientId == NetworkManager.Singleton.LocalClientId)
        {
            GameObject spawnedObject = NetworkSpawnManager.SpawnedObjects[objectId].gameObject;
        }
    }
}

And i call the networkstart function with the button “StartSpawning”

my gameObject is a simple diamond with a script attached to moove it following the mouse cursor ; it has the networkObject component and a network prefab
i added this gameobject in networkManager (in prefabs)

Then i build my game, i start the game in editor and then in the built version ;

i start server in the editor playing version and start client on the built one , and when i start Sapwning in editor i get this nullreferenceexception ( see in picture)

I don’t know what i m doing wrong here ? plz help me and thanks a lot in advance !!!


So ; i’m here again and facing the same problem , after googling a lot to find a solution , i’m coming back to try to explain much better what’s going wrong :

i’m making a simple app for the place where i work , basically we have to face with covid constraint as we receive people inside a place where we need to stop people coming if there are too many …

For this i have to create an app working on two tablet /phone :one will be at the entry and the other at the exit

in the first tablet , after selecting the entry panel the admin enters a googlesheet id to get a list of users( only firstname and name ) and they have to appear in the scrollview content " player list" , when the person arrives and watches his name he clicks on the button and then it mooves to “present list” scrollview

It has to be synced with the exit tablet as the person will have to click again at the exit and then his name disappear from the present list to return at the playerlist ( i hope i am clearer … ) and once again sorry for my English :wink: .

Without networking , it works like a charm !

I copied the unity folder and opened with unityhub both project (original and the copy) when i run as a server the first one and client the second one i can and i import the sheet i can see playerbuttons appear in hierarchy , with the same position, but with no names and not under the parent transform ( the content of the scrollview)

I’m trying to figure out why it doesn’t work but i’m new to networking …

that’s my googledriveimportand spawning script :

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityGoogleDrive;
using UnityGoogleDrive.Data;
using MLAPI.Messaging;
using MLAPI.NetworkVariable;
using MLAPI;
using MLAPI.Spawning;

public class DriveManager : NetworkBehaviour
{

    // the id of the googlesheet in google drive
    [SerializeField] private string fileId ;
    [SerializeField] InputField inputField;
    // Prefab of playerbutton
    [SerializeField] GameObject playerPrefab;

    private GoogleDriveFiles.ExportRequest request;


    GameObject playerGo;
    [SerializeField] Transform playerList_Scroll_Transform;

    //When we click on the validate button for the googlesheet
    public void OnClick()
    {
        SetGoogleSheetId();
        GetDataFromDrive();
    }

    // Methode pour aller chercher une liste sur google drive ( file id indiqué dans les paramètres
    private void GetDataFromDrive()
    {
        Debug.Log("Get Data from Google Drive...");
        if (fileId == "") { return; }
            request = GoogleDriveFiles.Export(fileId, "text/csv");
            request.Send().OnDone += SetResult;  
    }
    // indique une id de google drive  et entre l id comme parametre pour aller chercher le bon fichier
    public void SetGoogleSheetId()
    {
        fileId = inputField.text;

    }

    //Make the list  of button with google sheet params and spawn all objects in scene
    private void SetResult(File file)
    {

        if(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost) {
        Debug.Log("GetResult from google Drive , waiting to encode and make prefab ");

           // decode et transform  le fichier téléchargé en une string
           string eleveData = Encoding.UTF8.GetString(file.Content);

           // transforme la string copntenant les données en caracteres UTF8 pour séparer les mots
            string[] data = eleveData.Split(new char[] { '\n' });

            // reads the first name from the first word and name from the second
            for (int i = 1; i < data.Length - 1; i++)
            {
                string[] row = data[i].Split(new char[] { ',' });
                //instantiate button on server
                playerGo = Instantiate(playerPrefab, playerList_Scroll_Transform);

                // set the player variables in player component
                Player player = playerGo.GetComponent<Player>();
                if (row[0] != "")
                {
                    player.player_FirstName = row[0];
                    player.player_Name = row[1];
                    player.heureEntree = System.DateTime.MinValue;
                    player.heureSortie = System.DateTime.MinValue;
                    player.playerState = PlayerState.absent;
                    playerGo.name = "" + player.player_FirstName + player.player_Name;
                    GameManager.singleton.AddPlayerToList(player);
                }

                Debug.Log("Getting parentId ");
                // Get the parent id of the player object
                var transformParentId = playerList_Scroll_Transform.GetComponent<NetworkObject>().NetworkObjectId;

                Debug.Log("adding  parentId to playerScript ");
                //put the parentid to the player component
                player.parentNetId.Value = (int) transformParentId;
                //Add the text to the button
                playerGo.GetComponentInChildren<Text>().text = player.player_FirstName + " " + player.player_Name;
                Debug.Log("Spawn network player object ");
                //Spawn player network object
                playerGo.GetComponent<NetworkObject>().Spawn();

            }
        }
    }

}

And that’s my player’s script :

using MLAPI;
using MLAPI.NetworkVariable;
using UnityEngine;


public enum PlayerState { absent, present, enAttente }

[RequireComponent(typeof(NetworkObject))]
public class Player : NetworkBehaviour
{
    //Player state ( presnt or missing or waiting )
    public PlayerState playerState = PlayerState.absent;
    // players name and surname and hours (at what time does he enter and leave
    public string player_FirstName;
    public string player_Name;
    public System.DateTime heureEntree;
    public System.DateTime heureSortie;

    // the id of the parent object( the scroll view content )
    public NetworkVariableInt parentNetId;

    // Method to change playerbutton's list ( from playerlist to presentlist )
    public void OnClick()
    {
        GameManager.singleton.ToggleList(this);
    }

    private void Start()
    {
        if (NetworkManager.Singleton.IsClient)
        {
            // When we are spawned on the client,
            // find the parent object using its ID,
            // and set it to be our transform's parent.
            NetworkObject[] parentObjectArray = FindObjectsOfType<NetworkObject>();
            for (int i = 0; i < parentObjectArray.Length; i++)
            {
                if ((int)parentObjectArray[i].NetworkObjectId == parentNetId.Value)
                {
                    var parentObject = parentObjectArray[i];
                    transform.SetParent(parentObject.transform);
                }
            }
        }
    }


    // TO DO  implement the player variables over the network  ( NOT sure if it can work like this )
    //public void SetPlayerStat()
    //{
    //    if (NetworkManager.Singleton.IsServer)
    //    {
    //        playerStats.Value = this;
    //    }
    //    playerState = playerStats.Value.playerState;
    //    player_FirstName = playerStats.Value.player_FirstName;
    //    player_Name = playerStats.Value.player_Name;
    //    heureEntree = playerStats.Value.heureEntree;
    //    heureSortie = playerStats.Value.heureSortie;
    //}

    //public NetworkVariable<Text> playerText = new NetworkVariable<Text>(new NetworkVariableSettings
    //{
    //    WritePermission = NetworkVariablePermission.ServerOnly,
    //    ReadPermission = NetworkVariablePermission.Everyone
    //});
}

in the two pictures you can see from the original that the server doesnt spawn any prefab on the screen

But in the client playerbutton spawn but not within the good parent tranqform

the error in the console say :[MLAPI] Cannot find parent. Parent objects always have to be spawned and replicated BEFORE the child

i don’t know if i have to spawn parent gameobject in the script ( actually i tried and i got an other error ) ; also i put the parent gameObject in the player prefab tab within the networkmanager but it doesn t change …

  1. Do i have to put all possible parents transform ( my 2 scrollview contents) in this tab within Networkmanager component ?

  2. Is that a simpler way to sync the prefab of a gameobject ?

  3. is the fact that my player gameobject is a UIcompoonent (button ) a bad thing ?

Sorry for the long thread but i 'm learning how all these thing work at the same time and i feel i’m almost succeeding…


You must apply NetworkObject component to GameObject that you want to spawn for it to work

something like this

Ok so i fixed it , i used the rpc command to tell where to find the parent transform id and it worked ( that makes sense , also first time i tried i was not enabling the entry panel on the client side so the transform id couldn 't be found …) But now it works .

Firaui , thanks for the help ; i already put the network object on playerbutton prefab and on all scroollviews content object .
I could see that there is a networkTransform component , , i tried to add it but it doesn’t seem to sync automatically transform parent …

So i put the transform parent id to networkvar , as my code shows belong :

my player script

using MLAPI;
using MLAPI.NetworkVariable;
using UnityEngine;
using UnityEngine.UI;


public enum PlayerState { absent, present, enAttente }

[RequireComponent(typeof(NetworkObject))]
public class Player : NetworkBehaviour
{
    [SerializeField] Text buttonText;

    //Player state ( presnt or missing or waiting )
    public PlayerState playerState = PlayerState.absent;
    //players name and surname and hours(at what time does he enter and leave
    public string player_FirstName;
    public string player_Name;
    public System.DateTime heureEntree;
    public System.DateTime heureSortie;


    public NetworkVariableString n_player_FirstName;
    public NetworkVariableString n_player_Name;

    public NetworkVariable<PlayerState> n_playerstate;

    public NetworkVariableULong parentNetId;

    // the id of the parent object( the scroll view content )
    public NetworkVariable<System.DateTime> n_player_heureEntree;
    public NetworkVariable<System.DateTime> n_player_heureSortie;

    // Method to change playerbutton's list ( from playerlist to presentlist )
    public void OnClick()
    {
        GameManager.singleton.ToggleList(this);
    }

    private void Start()
    {
        if (NetworkManager.Singleton.IsClient)
        {
            SetPlayerTransform();
            SetPlayer();
        }
    }

    private void SetPlayerTransform()
    {
        // When we are spawned on the client,
        // find the parent object using its ID,
        // and set it to be our transform's parent.
        NetworkObject[] parentObjectArray = FindObjectsOfType<NetworkObject>();
        for (int i = 0; i < parentObjectArray.Length; i++)
        {
            if (parentObjectArray[i].NetworkObjectId == parentNetId.Value)
            {
                var parentObject = parentObjectArray[i];
                transform.SetParent(parentObject.transform);
            }
        }
    }

    private void Update()
    {
        //buttonText.text = player_FirstName + "" + player_Name;
        //gameObject.name = buttonText.text;
    }


    public void SetPlayer()
    {
        if (NetworkManager.Singleton.IsClient)
        {
            player_FirstName = n_player_FirstName.Value;
            player_Name = n_player_Name.Value;
            heureEntree = n_player_heureEntree.Value;
            heureSortie = n_player_heureSortie.Value;
            playerState = n_playerstate.Value;
        }
        buttonText.text = player_FirstName + "" + player_Name;
        gameObject.name = buttonText.text;
    }
}

and my importer script

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityGoogleDrive;
using UnityGoogleDrive.Data;
using MLAPI.Messaging;
using MLAPI.NetworkVariable;
using MLAPI;
using MLAPI.Spawning;

public class DriveManager : MonoBehaviour
{

    // the id of the googlesheet in google drive
    [SerializeField] private string fileId ;
    [SerializeField] InputField inputField;
    // Prefab of playerbutton
    [SerializeField] GameObject playerPrefab;

    private GoogleDriveFiles.ExportRequest request;


    GameObject playerGo;
    [SerializeField] Transform playerList_Scroll_Transform;

    //When we click on the validate button for the googlesheet
    public void OnClick()
    {
        SetGoogleSheetId();
        GetDataFromDrive();
    }

    // Methode pour aller chercher une liste sur google drive ( file id indiqué dans les paramètres
    private void GetDataFromDrive()
    {
        Debug.Log("Get Data from Google Drive...");
        if (fileId == "") { return; }
            request = GoogleDriveFiles.Export(fileId, "text/csv");
            request.Send().OnDone += SetResult;  
    }
    // indique une id de google drive  et entre l id comme parametre pour aller chercher le bon fichier
    public void SetGoogleSheetId()
    {
        fileId = inputField.text;

    }

    //Make the list  of button with google sheet params and spawn all objects in scene
    private void SetResult(File file)
    {

        if(NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsHost) {
        Debug.Log("GetResult from google Drive , waiting to encode and make prefab ");

           // decode et transform  le fichier téléchargé en une string
           string eleveData = Encoding.UTF8.GetString(file.Content);

           // transforme la string copntenant les données en caracteres UTF8 pour séparer les mots
            string[] data = eleveData.Split(new char[] { '\n' });

            // reads the first name from the first word and name from the second
            for (int i = 1; i < data.Length - 1; i++)
            {
                string[] row = data[i].Split(new char[] { ',' });
                //instantiate button on server
                playerGo = Instantiate(playerPrefab, playerList_Scroll_Transform);

                // set the player variables in player component
                Player player = playerGo.GetComponent<Player>();
                if (row[0] != "")
                {
                    player.player_FirstName = row[0];
                    player.player_Name = row[1];
                    player.heureEntree = System.DateTime.MinValue;
                    player.heureSortie = System.DateTime.MinValue;
                    player.playerState = PlayerState.absent;
                    //playerGo.name = "" + player.player_FirstName + player.player_Name;
                    GameManager.singleton.AddPlayerToList(player);
                }


                SetNetworkingValues(player);
                SetTransformetworkingValues(player);
                //Add the text to the button
                // playerGo.GetComponentInChildren<Text>().text = player.player_FirstName + " " + player.player_Name;
                Debug.Log("Spawn network player object ");
                //Spawn player network object
                playerGo.GetComponent<NetworkObject>().Spawn();


            }
        }
    }
    [ServerRpc]
    private void SetNetworkingValues(Player player)
    {
        player.n_player_Name.Value = player.player_Name;
        player.n_player_FirstName.Value = player.player_FirstName;
        player.n_player_heureEntree.Value = player.heureEntree;
        player.n_player_heureSortie.Value = player.heureSortie;
        player.n_playerstate.Value = player.playerState;
    }
    [ServerRpc]
    private void SetTransformetworkingValues(Player player)
    {
        Debug.Log("Getting parentId ");
        // Get the parent id of the player object
        var transformParentId = playerList_Scroll_Transform.GetComponent<NetworkObject>().NetworkObjectId;
        //And spawn it
        //playerList_Scroll_Transform.GetComponent<NetworkObject>().SpawnAsPlayerObject(transformParentId);
        Debug.Log("adding  parentId to playerScript ");
        //put the parentid to the player component
        player.parentNetId.Value = transformParentId;
    }
}

LAst question as i’m a bit confused even if it works , isn’t it a simpler way to sync component inside playerprefab, i mean have we always to script a rpc command to set all network var and then set the var in the player prefab as i did?

I find it redundant and not code efficient so i guess i missed an important concept in syncing ?