In Unity3D networking, how to have “more than one

2019-03-01 20:00发布

问题:

I have a simple LAN topology with a server and say three clients,

          -client
server    -client
          -client

(they are not literally "players")

The three clients communicate to the server various information. Of course just using [Command] calls.

However they are "all the same". The same few calls are made.

(The server knows which one has sent the info, from the ID.)

At first, I simply spawned a "player" object, with a script on it with the various [Command] calls.


each client has an abstract player object


it is spawned by the NetworkManager in the usual way

However.

It turns out, the server only "believes" in the last one which connected.

It seems there can only be one "player" in Unity networking.

Really, how do you do that in Unity networking?

回答1:

Answering my own question, for anyone struggling with this:

  • Regarding the automatic spawning of a "player" prefab, which the network does.

In fact, you will get one for each player. (Seems obvious once you say it.) There will be a whole pile of these all over the place.

So here:

          -client
server    -client
          -client

in fact you will see three of the spawned prefabs.

(Aside: in my example I'm using the server as a "server", not a "host" ..

if you were using the "host" concept, there would be yet another spawned player prefab in there.)

So when you launch the app, only one PC, you make it the server, no connections yet ..

then you launch a second PC, the first client, they connect ... it now looks like this

then you launch a third PC, the second client, they connect ... it now looks like this

.. and so on.

So you have one of the cloned player items for each client. (Again, if you use the "host" concept for the server, that just means there will be another client there on the same physical box as the server.)

Each and every "player" has its own auto-spawned player object - and that is on ALL the devices. Five clients, there will be five auto-spawned player objects on EVERY device (on all six devices).

So what caused the specific bug mentioned?

The hidden surprising difficulty in Unet is the .isLocalPlayer concept.

Say you do have a script like Comms in the example which is on the player prefab, which seems a natural thing to do. Say some other part of the game has to reach out to Comms (to say send a message to the server).

In fact, you must find your 'own' player prefab ....

You must find your 'own' player prefab ....

You may invisage having code something like this ..

// laser blows up
// tell Comms to send a message to the server about that
// find Comms ..
Comms co = FindObjectOfType<Comms>();
// tell it to send the message
co.TellServerLaserBlewUp();

However this is wrong and debugging will be incredibly erratic.

In fact your code would look like this:

// laser blows up
// tell Comms to send a message to the server about that
// find >> OUR << Comms ..
foreach (Comms co in FindObjectsOfType<Comms>() ) {

    string ilp = co.isLocalPlayer ? "YY" : "NN";
    Debug.Log("I tried one of the many Comms! .. " + ilp);
    if (co.isLocalPlayer) {

         Debug.Log("Found it!");
         co.TellServerLaserBlewUp();
    }
}

(Note - obviously, you wouldn't Find something like this every time in production, you'd use a singleton or whatever your cup of tea is.)

Consider your script that is "on" the player prefab. In our example it is "Comms.cs".

In most unity examples they envisage you doing stuff "in that script".

In that case it is more intuitive: you just break away if you are not .isLocalPlayer.

However this is very naive, in practice your zillion scripts around the scene will want to / need to contact your "Comms.cs" and have it do things (since it is the only one that connects to the networking.

In fact,

you do have to find "your" .isLocalPlayer at all times.

Tricky!

To repeat - Unity doco examples tend to emphasize doing things "on that script". In practice on any large real world project it's not like that, you are "contacting" that script as a central communications resource.



回答2:

Not a complete answer since you already figured it out by yourself.

(You don't have a minimal code example so to be honest I din't completely follow what you are doing... but)
Anyway I would like to share how I usually do it.

On the Player prefab (that is spawned on connect) I put a special component like e.g.

using UnityEngine;
using UnityEngine.Networking;

[DisallowMultipleComponent]
[RequireComponent(typeof(NetworkIdentity))]
public class PlayerInformation : NetworkBehaviour
{
    // simply a public accessor for isLocalPlayer
    public bool IsLocalPlayer
    {
        get {return isLocalPlayer;}
    }
}

and if needed the same for the other NetworkBehaviour bools like isServer etc.

Than whenever I have to call a method from another component I also hand it the PlayerInformation
-> I directly can check if it is the local player e.g.

public void DoSomething(PlayerInformation player)
{
    // don't even go any further if not local player
    if(!player.IsLocalPlayer) return;

    // ... do your stuff
}

Now for the part of communicating something back to only the player how called something:

Unfortunately you can not pass a Component to commands and Rpc .. but you can pass GameObjects (which have a NetworkIdentity). The NetworkIdentity makes sure the GameObject can be identified over all connected instances.

// This time this is called by the client
[Client]
private void DoSomething()
{
    if(!isLocalPlayer) return;

    // Here we call the command on the server passing our gameObject
    CmdDoSomething(gameObject);
}

// This is executed on the server
// Since the player has a NetworkIdentity the server can identify the correct gameObject we passed
[Command]
private void CmdDoSomething (GameObject player)
{
    // Now we can send back Information
    // E.g. an int or string again passing the GameObject
    RpcDoSomething(player, "test");
}

// Usually this would be executed by ALL clients
// So we have to check if we are the player who originally called the command
// Again the NetworkIdentity makes sure we can identify the passed GameObject
[ClientRpc]
private void RpcDoSomething (GameObject player, string message)
{
    // If we are not the one who invoked the command we do nothing
    if(player != gameObject) return;

    // I like to put Debug.Log containing "this" in the end so you can click a Log
    // Entry and see exactly where it came from
    Debug.Log("I received: " + message, this);
}

Now only the player who invoked the command should execute the rest of the Rpc method (which usually would be executed by all players)

Doing this keeps things easier and uncluttered - at least for me.

Hope it helps