Search Unity

Anyway to instantly spawn objects on the client?

Discussion in 'Multiplayer' started by mlukeyb, Sep 16, 2015.

  1. mlukeyb

    mlukeyb

    Joined:
    Nov 8, 2014
    Posts:
    3
    I am trying to create a projectile on the client side, then have the server create the same projectile on all other clients. I did this easily, but realized that the client had to wait for the server to create his own projectile. This makes my game feel horrible and laggy. I've tried something new which partially works, but I get the "Object reference not set to an instance of an object" error. Everything works when creating the object on the host client with no errors and no lag, but not on any other. Here is my code:

    Code (CSharp):
    1.  
    2. void Update() {
    3.     if (isLocalPlayer) {
    4.         if (Input.GetKeyDown (KeyCode.E)) {
    5.             GameObject tempProjectile = Instantiate(fireballPrefab, fireballStartObject.transform.position, Quaternion.identity) as GameObject;
    6.             CmdShoot (tempProjectile);
    7.        }
    8.     }
    9. }
    10.  
    11. [Command]
    12. void CmdShoot(GameObject projectile){
    13.     NetworkServer.Spawn(projectile);
    14. }
    Any idea what could be going wrong here? The fireballPrefab exists in the Registered Spawnable Prefabs of the network manager.

    NullReferenceException: Object reference not set to an instance of an object
    UnityEngine.Networking.NetworkServer.GetNetworkIdentity (UnityEngine.GameObject go, UnityEngine.Networking.NetworkIdentity& view) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkServer.cs:1096)
    UnityEngine.Networking.NetworkServer.SpawnObject (UnityEngine.GameObject obj) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkServer.cs:1302)
    UnityEngine.Networking.NetworkServer.Spawn (UnityEngine.GameObject obj) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkServer.cs:1531)
    Player.CmdShoot (UnityEngine.GameObject projectile) (at Assets/Scripts/Player.cs:133)
    Player.InvokeCmdCmdShoot (UnityEngine.Networking.NetworkBehaviour obj, UnityEngine.Networking.NetworkReader reader)
    UnityEngine.Networking.NetworkBehaviour.InvokeCommandDelegate (Int32 cmdHash, UnityEngine.Networking.NetworkReader reader) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkBehaviour.cs:303)
    UnityEngine.Networking.NetworkBehaviour.InvokeCommand (Int32 cmdHash, UnityEngine.Networking.NetworkReader reader) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkBehaviour.cs:81)
    UnityEngine.Networking.NetworkIdentity.HandleCommand (Int32 cmdHash, UnityEngine.Networking.NetworkReader reader) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkIdentity.cs:492)
    UnityEngine.Networking.NetworkServer.OnCommandMessage (UnityEngine.Networking.NetworkMessage netMsg) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkServer.cs:1290)
    UnityEngine.Networking.NetworkConnection.HandleReader (UnityEngine.Networking.NetworkReader reader, Int32 receivedSize, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:416)
    UnityEngine.Networking.NetworkConnection.HandleBytes (System.Byte[] buffer, Int32 receivedSize, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:372)
    UnityEngine.Networking.NetworkConnection.TransportRecieve (System.Byte[] bytes, Int32 numBytes, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:522)
    UnityEngine.Networking.NetworkServer.InternalUpdate () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkServer.cs:684)
    UnityEngine.Networking.NetworkServer.Update () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkServer.cs:546)
    UnityEngine.Networking.NetworkIdentity.UNetStaticUpdate () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkIdentity.cs:887)
     
  2. SpeakUpGames

    SpeakUpGames

    Joined:
    Nov 25, 2014
    Posts:
    56
    I recently had to deal with this issue and this is how I solved it; someone else can chime in if they have a better way (I'd love to hear it).

    Passing a gameobject from client to server won't work because it doesn't exist on the server. But you can pass other useful information:

    Code (CSharp):
    1. void Update() {
    2.     if (isLocalPlayer) {
    3.         if (Input.GetKeyDown (KeyCode.E)) {
    4.             GameObject tempProjectile = Instantiate(fireballPrefab, fireballStartObject.transform.position, Quaternion.identity) as GameObject;
    5.             CmdShoot (fireballStartObject.transform.position);
    6.        }
    7.     }
    8. }
    9. [Command]
    10. void CmdShoot(Vector3 pos){
    11.    GameObject g = Instantiate(fireballPrefab, pos, Quaternion.identity) as GameObject;
    12.     NetworkServer.Spawn(g);
    13. }
    That way you'll get immediate response locally and it will spawn on other clients. The only other issue is that this will spawn a second projectile on the initial client; I solved it by adding some logic for a projectile destroy itself immediately if it was spawned from the server to the initial client.
     
  3. darthbator

    darthbator

    Joined:
    Jan 21, 2012
    Posts:
    169
    Do you then destroy that object in tempProjectile on the client? When NetworkServer.Spawn(g); runs you'll end up with 2 instances of a bullet on the local client that made a local bullet instantiation.
     
  4. mlukeyb

    mlukeyb

    Joined:
    Nov 8, 2014
    Posts:
    3
    SpeakUpGames's solution is working nearly perfectly for me. However, I think I'd rather have the server projectile override the local projectile. So that way when a network projectile is spawned, the local projectile is destroyed. I'm not sure how to do that though. If I come up with a solution I'll post it.

    Darthbator, if you look at the last paragraph of SpeakUpGames's solution, your question is explained.
     
  5. moco2k

    moco2k

    Joined:
    Apr 29, 2015
    Posts:
    294
    My current approach is similar to the idea of SpeakUpGames, but just a little different in some aspects. It is as follows:

    There is a single function to spawn the projectile. This function is called either from a local client (shooting player), from the server (by command) or from an RPC (all clients). This function has a bool flag passed in to check if it has been fired on the server or not. When fired on the server, the flag is set true and the function knows that full hit detection shall be performed. Depending on the code architecture, I think it can have advantages to have the functions in a script directly attached to the players. Notably, NetworkServer.Spawn() is not used at all. Instead, each projectile is a local, non-networked object. To handle projectile spawns on the clients, for example, the function is just called from an RPC.

    It works like this: When the projectile is initially fired on the shooting client, the projectile spawn function is locally called and the projectile instantly moves away, without delay (this is what we want). I also do some predictive hit detection on the local client already so that blood and hit animations can be triggered without delay for the shooting player as well. Anyway, full hit detection including actual damage calculation is still exclusively performed on the sever who has final authority.
    When shooting the projectile, the client also sends a command to the server. The server then knows who fired the projectile and spawns his own projectile, this time with full hit detection and damage calculation (and ideally also with lag compensation). Note that when we are a Host (Client + Server at same time), we also have 2 projectiles at the same time. An easy solution is to just disable the renderers of the server projectile so that it is invisible but still used for actual hit detection.
    Now, the main difference in this approach is that the server does not call NetworkServer.Spawn(), but instead sends out an RPC to inform the other clients about the projectile spawn. This RPC is needed for all clients but the one who actually fired the projectile, because the shooting player already has his own projectile on the way and does not want to spawn another one. Thus, some logic is needed to enable the shooting player to detect if the RPC affects his own projectile (in this case it shall be ignored). To do so, for example, some kind of player id could be passed in the RPC, or, if the respective scrips are attached to the player objects, I guess the detection is even possible without the need for any additional arguments.

    While this approach is just a little different, it also fulfills the goal of the initial thread question because it allows player-owned projectiles to be instantly spawned for the shooting player.
    In addition, I hope that it is a bit more efficient in terms of network bandwidth consumption because it does not require each projectile to be a networked object. However, I haven't done any further research and comparisons on this yet so I do not really know if there are any significant bandwidth differences so far.

    Anyway, mlukeyb, if you go with your approach of destroying / overriding local projectile with server ones, it would be nice if you post about the results.
     
    Last edited: Sep 18, 2015
    jjbish likes this.
  6. SpeakUpGames

    SpeakUpGames

    Joined:
    Nov 25, 2014
    Posts:
    56
    Interesting approach. Though I don't understand the point of the client RPC calls, because it sounds like your RPC is almost exactly the same as a NetworkServer.Spawn() call.

    Thinking about the problem more, another potential solution I'll probably look into would be to simply make the client's bullet only matter until the server one exists. Same as my original solution, but as soon as the client detects the server's bullet [second one], the client's bullet is destroyed [not the server's], the server's bullet is sent info about where it should be (via the client local bullet), and then you should have a properly updated bullet on the server which is also correct on the client.
    So the client's bullet exists for maybe a split second, enough for it to seem to be reactive, then the position is sent to the server bullet.

    The only issue I see here is some potential for "jumping bullets" on other clients. And also possible problems with bullets having to know a fair bit of info about each other.

    Edit: also I just realized that the OP mentioned this solution already. Sorry about that.
     
  7. vinsolo

    vinsolo

    Joined:
    Mar 1, 2016
    Posts:
    1
    Hi SpeakUpGames,
    was just wondering if you could give details on how you destroyed the client's projectile?
    I haven't see how the initiating client(i.e. when it sends the Cmd to server to spawn), can/is notified that the broadcasted server spawn is/has occurred(causing the 2nd duplicated object)...
    Can you please give simple sample code of how to detect this spawn on the initiating client(and how to correlate that Networked Object instance with its original client-side "temp" placeholder object(to delete it).
    Appreciate any details you can give on this, haven't seen anything on the web/googled/etc describing what notification we can snoop on clients to detect/listen for a server Spawn completion/etc.
    Thanks!
     
  8. jjbish

    jjbish

    Joined:
    Jun 16, 2016
    Posts:
    5
    This is an old post so it may not help anyone in this thread, but maybe it will help anyone who stumbles upon this in the future. I solved this problem similar to how moco2k talks about above.

    For me, I instantiated the projectile on all clients using a Command call to the server from the player wishing to fire the projectile and then in that Command function called a ClientRPC which spawned the projectile. Thus, all projectiles were spawned and owned locally for all client's in that ClientRPC function. (No NetworkServer.Spawn() needed).

    In order to then detect collision and destroy the projectile's across all client's, as vinsolo asks above, I created a variable in my Game Manager object called int projectileCounter. Every time a projectile spawned (i.e. a ClientRPC was called from a command from a player to fire a projectile) the counter would update, and in the start method on that projectile it would rename its transform.name to projectileCounter.ToString() so all projectile's had the same name after instantiation across all clients. Note: I could have also renamed them in the ClientRPC call itself, instead of the projectile's start function. Just after instantiation of "go" (or whatever you name the GameObject you are instantiating) do go.transform.name = projectileCounter.ToString().

    Then when they needed to be destroyed, In the OnCollisionEnter function on the projectile, I got a reference to the local player, and if (isLocalPlayer) had them call a Command on the Server and passed in the transform.name of that projectile (which we know is the same on all Clients), and then called a ClientRPC from that command, passing in the transform.name of the projectile. Then in the ClientRPC just did Destory(_name), where _name is the name of the projectile we passed in to be destroyed. Since all projectiles get set the same name on all clients upon being instantiated, we know this sufficiently also kills all the projectiles on all clients as well when we want to Destroy them.

    It fulfills most of the performance requirements as mentioned above and largely bypasses the networking of the projectile Rigidbodies that can display lag, delay, etc. by having each client run their own simulation on their own machine. Then If the projectile is Destroyed on any one of the Client's local machines, it sends a command across for all other clients to destroy that projectile essentially keeping everything in sync that way.

    Hopefully this helps anyone above or more likely anyone in the future who comes across this post.
     
    Thimoca and Tathorpe89 like this.
  9. Tathorpe89

    Tathorpe89

    Joined:
    Jul 7, 2014
    Posts:
    2

    I've had this bookmarked for a while waiting for this response.. haha... thank you!
     
  10. Tathorpe89

    Tathorpe89

    Joined:
    Jul 7, 2014
    Posts:
    2
    Actually, since you are still instantiating the projectile in a Command, isn't there still going to be a delay in between when you click Fire and when you actually see the projectile?
     
    james_hoegerl and Sungold like this.
  11. zni3h

    zni3h

    Joined:
    Apr 16, 2017
    Posts:
    1
    //Try this

    void Shoot()
    {
    if (shootTimer < shootRate)
    shootTimer += Time.deltaTime;
    else
    {
    if(!isServer)
    {
    CmdShoot(gameObject.GetComponent<PlayerStats>().ID);
    }
    else
    {
    GameObject GO = Instantiate(bulletPrefab, bulletOrigin.position, bulletOrigin.rotation) as GameObject;
    GO.GetComponent<BulletScript>().ID = gameObject.GetComponent<PlayerStats>().ID;
    Destroy(GO, 0.6f);
    RpcShoot(gameObject.GetComponent<PlayerStats>().ID);
    }
    shootTimer = 0;
    }
    }

    [ClientRpc]
    void RpcShoot(NetworkInstanceId id)
    {
    GameObject GO = Instantiate(bulletPrefab, bulletOrigin.position, bulletOrigin.rotation) as GameObject;
    GO.GetComponent<BulletScript>().ID = id;
    Destroy(GO, 0.6f);
    }

    [Command]
    void CmdShoot(NetworkInstanceId id)
    {
    GameObject GO = Instantiate(bulletPrefab, bulletOrigin.position, bulletOrigin.rotation) as GameObject;
    GO.GetComponent<BulletScript>().ID = id;
    Destroy(GO, 0.6f);
    RpcShoot(id);
    }