Manipulating the environment/non enemy gameobjects

Hi,

I want to create a multiplayer game in which the players can manipulate the environment. Let’s say there is a machine consisting of 1000 parts, and the players are supposed to dismantle the machine part by part. Each part has a different mesh, colliders, etc. The player script detects colliders with Physics.Raycast(…) and on Input.GetMouseButtonDown(0), a part is grabbed. So the player can carry it around. On Input.GetMouseButtonUp(0), a part is released, and drops using a rigidbody. The other players should be able to see the parts being carried around and dropping.

How would I go about that? Each part has to have a NetworkIdentity and a NetworkTransform component, I get that. But does each part of the machine have to be a prefab that I add to the NetworkManager?

I read through everything I could find on Unity Networking, but most examples are about Players shooting Enemies that are spawned at runtime. The machine is a single prefab, and the parts are children of the transform. Do I have to turn each part into its own prefab? Are the individual machine parts considered “enemies”?

What if I wanted to have actual “enemies”, meaning competing AI players that dismantle the machine?

Is what I want even possible with the basic UNET classes, or do I have to implement it from scratch?

Any help greatly appreciated!

Thanks,

Wavy

Depends on your authority model. The easiest solution I can think of is for each player to have a item that they are currently grabbing. If that item is not equal to null. Then you submit the position to the server who then forward it to the clients. (At a interval)

Thanks for the reply.

I tried a simple thing: I have a Player, and I have a Receiver, and a ReceiverSpawner. The Player is supposed to change a text on the Receiver. If that works, I thought it would be possible to do something like Engine.UpdatePart(string partName, Vector3 position, Quaternion rotation);

But setting the text doesn’t work. I get this error:

Here’s the code:

using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    public string text = "Hello Networked World!";

    public Receiver receiver;

    void Start () {
        receiver = FindObjectOfType<Receiver>();
        gameObject.name = (isLocalPlayer ? "Local" : "Remote") + " " + "Player" + " | " + "Network Id: " + netId;
    }

    void Update () {
        if(!isLocalPlayer)
        {
            return;
        }

        if(Input.GetMouseButtonUp(0) && receiver != null)
        {
            receiver.CmdSetText(text + " | " + gameObject.name);
        }
    }
}
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class Receiver : NetworkBehaviour {

    public Text text;

    [Command]
    public void CmdSetText(string text)
    {
        this.text.text = text;
    }
}
using UnityEngine;
using UnityEngine.Networking;

public class ReceiverSpawner : NetworkBehaviour
{
    public GameObject receiverPrefab;

    public override void OnStartServer()
    {
        base.OnStartServer();

        var receiver = Instantiate(receiverPrefab);
        NetworkServer.Spawn(receiver);
    }
}

Here’s the setup in the editor:

Well, you don’t have authority over the reciever. So instead, invoke the command on the object you have authority over, which then invokes a method on the server that can sit on the Reciever.

Ah ok. So instead of saying receiver.CmdSetText(…), I am using a Command inside the Player. By using [Command], the local Player component basically talks to its “mirror” on the server. That one has authority over the other objects residing on the server.

using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    public string text = "Hello Networked World!";

    void Start () {
        receiver = FindObjectOfType<Receiver>();
        gameObject.name = (isLocalPlayer ? "Local" : "Remote") + " " + "Player" + " | " + "Network Id: " + netId;
    }

    // Update is called once per frame
    void Update () {
        if(!isLocalPlayer)
        {
            return;
        }

        if(Input.GetMouseButtonUp(0) && receiver != null)
        {
            //receiver.CmdSetText(text + " | " + gameObject.name);
            CmdSetText();
        }
    }

    [Command]
    void CmdSetText()
    {
        receiver.SetText(text + " | " + gameObject.name);
    }
}
using UnityEngine.Networking;
using UnityEngine.UI;

public class Receiver : NetworkBehaviour {

    public Text text;

    public void SetText(string text)
    {
        this.text.text = text;
    }
}

This now successfully updates the text on the Host application. BUT I do not see the text changing on the clients.
How do I distribute the update to the clients?

You are only changing it on the server. Make the server tell the clients to update the text?

Aaaah! Thanks!
The local player talks to its quantum entangled copy on the server. That server player calls the server receiver to update the text on all its quantum entangled local copies on the clients. I’m not sure if that’s pretty, but it works.

using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    public string text = "Hello Networked World!";

    void Start () {
        receiver = FindObjectOfType<Receiver>();
        gameObject.name = (isLocalPlayer ? "Local" : "Remote") + " " + "Player" + " | " + "Network Id: " + netId;
    }

    // Update is called once per frame
    void Update () {
        if(!isLocalPlayer)
        {
            return;
        }

        if(Input.GetMouseButtonUp(0) && receiver != null)
        {
            //receiver.CmdSetText(text + " | " + gameObject.name);
            CmdSetText();
        }
    }

    [Command]
    void CmdSetText()
    {
        receiver.RpcSetText(text + " | " + gameObject.name);
    }
}
using UnityEngine.Networking;
using UnityEngine.UI;

public class Receiver : NetworkBehaviour {

    public Text text;

    [ClientRpc]
    public void RpcSetText(string text)
    {
        this.text.text = text;
    }
}

When a new client logs on, it doesn’t update the text. Should the string be stored in a [SyncVar] field, so that clients can update the text when they connect?

edit
Works with the sync var.

Either that or you can send a RPC to update it when they connect.

I managed to have some success. I created a Prefab of the machine, which has a Machine : NetworkBehaviour component. The parts have a Part : MonoBehaviour component, and they call Machine.UpdatePart(…). That in turn calls Machine.CmdUpdatePart(…) on the server side, which then in turn calls Machine.RpcUpdatePart(…) to distribute the updates to the clients. It even works with Physics. But I have to disable and enable the behaviours that report the rigidbody movement, otherwise the game lags.

I would just like to add that this is not necessarily a “hard” requirement. For example, you could as well implement some unique identifiers for all the parts and then sync those identifiers across the network with your commands and rpcs to know which part is being manipulated. You would use some kind of singleton part manager to fetch up specific parts by the given identifiers. Also, you would need to pass the position data instead of using NetworkTransform. So, with such an implementation, the parts could be usual MonoBehaviors and it woulod not be necessary that each part is a networked object based on NetworkBehavior with network identity. However, technically, I’m not quite sure if it is a bad thing to have quite high numbers of networked objects or not. The UNET docs give very little information on best practices how to implement specific things. In some use cases such as item management, for example, personally I found it to be overkill if each item would be a networked object. Anyways, I guess your current approach is fine for the use case - just wanted to point that you could also implement this in a different way.

Eventually I came to that same conclusion. The Machine is my Singleton and a NetworkBehaviour. The Part objects are NOT NetworkBehaviours. On Client A, they call Machine.UpdatePart(Part part), which in turn calls Machine.CmdUpdatePart(string id, Vector3 position, Quaternion rotation) on the server side, which in turn calls Machine.RpcUpdatePart(string id, Vector3 position, Quaternion rotation) on the client side.

public class Machine : NetworkBehaviour
{

    bool hasBeenUpdated;

     ...

     // initial call on client side, since the part has been moved there, we check that bool
     public void UpdatePart(Part part)
     {
        hasBeenUpdated = true;

        CmdUpdatePart(part.id, part.position, part.rotation);
     }

    // call from a client to server
    [Command]
    void CmdUpdatePart(string id, Vector3 position, Quaternion rotation)
    {
        RpcUpdatePart(id, position, rotation);
    }

    // call from server to all clients. Check if client has already been updated.
    [ClientRpc]
    void RpcUpdatePart(string id, Vector3 position, Quaternion rotation)
    {
        if(hasBeenUpdated)
        {
            return;
        }
    
        var part = GetPart(id);
        part.position = position;
        part.rotation = rotation;
    }

    ...

}

why do you pass the string id when you could pass the netId and then find the object like NetworkServer.FindLocalObject(netId) or ClientScene.FindLocalObject(netId);

1 Like

The parts have no NetId, because they don’t have a NetworkIdentity component. See, the machine has 1000 parts, and there can only be one NetworkIdentity component per networked prefab, and I don’t want to have 1000 individual part prefabs.

That’s why the “Machine” is a NetworkIdentity, and all the “Parts” are children of the Machine GameObject.

Pass the parent netId and the child index. Passing string as id is nonsense.

The child index is not reliable, as I am also reparenting the children when they are moved (e.g. when they are removed from the machine and lie on the ground, they need to be removed from the hierarchy). But I can use an integer Id, if thats better.

Use a unsigned short