Search Unity

Copy a sprite file to all clients and replace it on character (SOLVED)

Discussion in 'Multiplayer' started by g-hoot, Aug 29, 2016.

  1. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    Hello All,
    Networking Newb here using UNET. What I'm doing is making a multiplayer FPS and want to make it so that you can take a png file and place it on a flag using a sprite image that will always face the main camera when not the local player. I have that working where everyone sees the same image facing the camera.

    The user will create the png image of whatever they want their flag to look like and save it to a certain location on their PC. Then when setting up their character, they will select the pic that they want to use on their flag. This new flag sprite needs to display on all clients, and be buffered so that new clients joining will see that characters flag as well.

    So, what would be the proper way to copy that file to the other clients. For the user I would store the image I suppose in a folder in the games "install" location. Would it be necessary to copy this file to other machines when they join the game? Thanks for any feedback!
     
  2. thegreatzebadiah

    thegreatzebadiah

    Joined:
    Nov 22, 2012
    Posts:
    836
    I'm assuming you're using UNet.

    The general idea is something like:

    Client reads image from disk into a Texture2D
    Client converts Texture2D to byte[]
    Client sends byte[] via NetworkMessage or Command
    Server receives byte[] and stores it in some way associated with the client that sent it (Dictionary<NetworkConnection, byte[]> or something like that)
    New client connects
    Server receives the connection in OnServerConnect
    Server sends all existing byte[] arrays to the new player via NetworkMessage or ClientRPC
    New client receives the byte[] arrays and converts back to Texture2D.

    If you send it as part of the spawn message it will be automatically "buffered" for you (buffered rpc's aren't really a thing anymore but spawn messages get sent every time a player connects. You can also do the same thing manually though by just sending the message in OnServerConnect.

    Unless you want the images to persist there's probably no reason to actually copy the file to other clients' hard drives. Just keep it in ram as a Texture2D.
     
    g-hoot likes this.
  3. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    That's correct, UNET is what's being used. Thanks a bunch for this outline. Gives me a lot to chew on for a while!
     
  4. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    OH, also, at least for now this will be LAN only. Not sure if that would make a difference or not on how to handle it?
     
  5. thegreatzebadiah

    thegreatzebadiah

    Joined:
    Nov 22, 2012
    Posts:
    836
    I don't think LAN will make any difference (other than that it will be fast). Really there are a million ways you could do this, I think sending via RPC or NetworkMessage is the most Unity-centric way though. Another alternative would be to have all the players upload their images to a remotely hosted database and then each client could download all the other client's images. Totally just depends on what your ultimate goal is. For example if you want a player to be able to see other player's flags when they aren't actually in a game with that player (like on a high score list or something) then you would want the flags in a database or something since the player won't be around to rpc it.
     
    g-hoot likes this.
  6. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    Got it close I think. With this code when someone joins everyone sees their new flag, but the new client doesn't see theirs. This is the part missing....

    "New client connects
    Server receives the connection in OnServerConnect
    Server sends all existing byte[] arrays to the new player via NetworkMessage or ClientRPC
    New client receives the byte[] arrays and converts back to Texture2D."

    .....but shouldn't the network manager take care of this since the player prefab is in the spawn info field of the network manager?




    using UnityEngine;
    using System.IO;
    using UnityEngine.UI;
    using UnityEngine.Networking;

    public class LoadFlag : NetworkBehaviour
    {
    byte[] data1;
    Sprite Flag;
    public GameObject OldFlag;
    float scale;
    string path;
    public Text NewFlag;
    Transform trans;

    public override void OnStartLocalPlayer()
    {
    if (isLocalPlayer)
    {
    path = NewFlag.text;
    data1 = File.ReadAllBytes(path);
    CmdChangeFlag(data1);
    }
    }

    [Command]
    public void CmdChangeFlag(byte[] data)
    {
    Texture2D texture = new Texture2D(64, 64, TextureFormat.ARGB32, false);
    texture.LoadImage(data);
    texture.name = Path.GetFileNameWithoutExtension(path);
    Rect rect = new Rect(20, 0, 40, 40);
    Vector2 vect2 = new Vector2(1, 1);
    Flag = Sprite.Create(texture, rect, vect2);
    OldFlag.GetComponent<SpriteRenderer>().sprite = Flag;
    }

    }
     
  7. thegreatzebadiah

    thegreatzebadiah

    Joined:
    Nov 22, 2012
    Posts:
    836
    The network manager will take care of spawning the object but it won't do anything about the commands. They were sent before the new client connected and there's no buffering so they won't be automatically re-sent, you'll have to do it manually.

    Also I think this part was a lie:
    You're just going to have to do it in OnServerConnnect. Whenever a new client connects, send the byte[]'s for all existing clients. Or maybe you could make the byte[]'s syncvars on the player and then they would automatically sync from server to all clients? Really not sure, I don't work with syncvars too often, I tend to prefer the explicitness of rpcs and commands.
     
    g-hoot likes this.
  8. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    "'NetworkBehaviour' does not contain a definition for 'OnServerConnect'"

    That is strange? I even see "OnServerConnect" in the manual.
     
  9. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    OK, I see that OnServerConnect is part of NetworkManager, but when I change NetworkBehaviou to that, then "isLocalPlayer" don't work. Guess I need to split it into two scripts?
     
  10. CarterG81

    CarterG81

    Joined:
    Jul 25, 2013
    Posts:
    1,773
    Posting my reply to a PM here. Sorry I couldn't be of any real help. The OP's problem seems a bit different than mine.

    No problem. Multiplayer is a PITA & consumes an enormous amount of gamedev time. More than anything else, by a longshot. At least for me, anyway.

    I gave up on UNET after wasting almost 40 hours of work on it. I ended up going with Forge. So my experience with UNET is very limited.

    However, my problem wasn't sending a sprite over the network. I was just using predefined sprites/prefabs, and having trouble with the load order. I just sent a string (the name of the sprite) over the network, I believe. My issues with UNET & Forge both seemed to be messages coming in or out of sync. Like trying to setup a character before the network was finished setting up a connection. Those kind of problems. I think Forge fixed those issues, not sure about UNET.

    I see you got help in that thread after this message, and that's how I send data over the network in Forge (convert from/to the object type, to/from Byte[ ]). Not much else I can help with, I don't think? Looks like you got it?

    If you ever have problems with the load order, let me know. I know a few networking tricks to try & work around networked messages coming out of order.

    Delayed Startup
    (ex. a 10 second delay, which was plenty of time for all the automatic network start code to finish)
    Code (csharp):
    1.  
    2. void Update()
    3.     {
    4.         if (!IsOwner && NetworkingManager.IsOnline)
    5.             return;
    6.  
    7.         StartTime += Time.time;
    8.         if (StartTime < 1000) //After 10 seconds, start everything up.
    9.         {
    10.             return;
    11.         }
    12.  
    13.         if (!doneOnce)
    14.         {
    15.             //RequestServerToSetupMyCharacter();    //After 10 seconds, start everything up.
    16.             doneOnce = true;
    17.         }*/
    18.     }
    19.  

    And here are two very useful methods. These are universal. They can convert any (serializable) object back/forth from Byte[] to Object.

    Methods to convert ANY object type to Byte[], and back.
    (ex. )
    Code (csharp):
    1.  
    2. using System.Runtime.Serialization.Formatters.Binary;
    3. using System.IO;
    4.  
    5.     // Convert an object to a byte array
    6.     public static byte[] ObjectToByteArray(System.Object obj)
    7.     {
    8.         BinaryFormatter bf = new BinaryFormatter();
    9.         using (var ms = new MemoryStream())
    10.         {
    11.             bf.Serialize(ms, obj);
    12.             return ms.ToArray();
    13.         }
    14.     }
    15.     // Convert a byte array to an Object
    16.     public static System.Object ByteArrayToObject(byte[] arrBytes)
    17.     {
    18.         using (var memStream = new MemoryStream())
    19.         {
    20.             var binForm = new BinaryFormatter();
    21.             memStream.Write(arrBytes, 0, arrBytes.Length);
    22.             memStream.Seek(0, SeekOrigin.Begin);
    23.             var obj = binForm.Deserialize(memStream);
    24.             return obj;
    25.         }
    26.     }
    27.  
    28.  
    29.  

    And here is an example of how I use those methods to send Byte[ ] data back/forth from Authoritative Server to Client.

    But this example is good for sending/receiving any Byte[ ], postponing what happens afterward.

    Sorry I don't know the UNET way. This is using Forge. But the logic should be very similar, I assume.

    Forge Example ( [BRPC] is just a normal RPC. )
    This has a custom class that holds my entire world's data (just more custom classes; lots of strings & ints), called "World".
    The client requests the server to load the game world (Get the "World" data)
    The server sends the data over the network, using a Byte[ ].
    The client receives the Byte[ ], converts it to a World object, then sets a bool to load the world on the next frame.
    LateUpdate constantly checks if the world should be loaded. When "loadWorld" = true, then it does stuff with that data. It then does some its thing until it's finished. When finished loading (these are large game worlds) it eventually sets "WorldFinishedLoading" to true, and the player does whatever it needs to do AFTER the world is loaded.)
    Code (csharp):
    1.  
    2. [Serializable] //Must be Serializable?
    3. public class World
    4. {
    5.     public Dictionary<myVector2, TileData> WorldTiles = new Dictionary<myVector2, TileData>(); //Collection of all tiles.
    6.     public Dictionary<string, ObjectData> allObjectsData = new Dictionary<string, ObjectData>(); //Collection of all objects.
    7.     public Dictionary<string, PlayerCharacterData> allPlayerCharactersData = new Dictionary<string, PlayerCharacterData>(); //Collection of all players.
    8. }
    9.  
    10. /* LOAD WORLD DATA */
    11.     //1. Send request to Server
    12.     public void RequestWorldData()
    13.     {
    14.         Debug.Log("Client: STEP1 - Requesting WorldData from Server");
    15.         RPC("RequestServerTo_SendWorld", NetworkReceivers.Server); //1. Send request to Server
    16.     }
    17.     //2. Server receives request. Sends World Data to requesting client.
    18.     [BRPC]
    19.     void RequestServerTo_SendWorld()
    20.     {
    21.         Debug.Log("Server: STEP2");
    22.         Debug.Log("Server: Request to SendWorld received. Sending Data to Client.");
    23.     AuthoritativeRPC("Client_ReceiveWorld", CurrentRPCSender, MasterWorld.ObjectToByteArray(MasterWorld.theWorldData)); //2. Send Server's WorldData back to the Client who sent the request.
    24.     }
    25.     //3. Client Receives World Data from Server.
    26.     [BRPC]
    27.     void Client_ReceiveWorld(byte[] worldBytes)
    28.     {
    29.         Debug.Log("Client: Step3: Received World Data from Server! Deserializing Data...");
    30.         World world = (World)MasterWorld.ByteArrayToObject(worldBytes);
    31.         MasterWorld.theWorldData = world;
    32.         loadWorld = true;
    33.     }
    34.  
    35.     void LateUpdate()
    36.     {
    37.         if (!Networking.PrimarySocket.IsServer) //If NOT the Server
    38.         {
    39.             if (loadWorld) //Load World was set to true, so load the world.
    40.             {
    41.                 Debug.Log("Client: Loading World...");
    42.                 ClientGlobals.theClient.LoadWorld(MasterWorld.theWorldData);
    43.                 loadWorld = false;
    44.             }
    45.  
    46.             if (ClientGlobals.theClient.WorldFinishedLoading) //World is finished loading, so do some more stuff.
    47.             {
    48.                 ResetDetectPlayerProximity();
    49.                 ClientGlobals.theClient.WorldFinishedLoading = false;
    50.             }
    51.         }
    52.     }
    53.  
    So that's part of my netcode to load a massive open world. It syncs the world (think- survival genre, open world type world.). Everything that happens AFTER the player connects, is receive via Buffered RPC's. This allows me to sync massive worlds, without having to actually net-sync anything other than characters (things that move - Player, NPCs, etc.) All static objects are a part of "World".

    But this example is good for sending/receiving any Byte[ ], postponing what happens.

    If you want I can post the code to iteratively load large amounts of data (ClientGlobals.theClient.LoadWorld, which leads to ClientGlobals.theClient.WorldFinishedLoading becoming true). But that's not necessary for this thread. None of this really is, besides those few functions.
     
    g-hoot likes this.
  11. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    CarterG81, Thanks a bunch for this. I have Forge and was using UFPS and did't really know what all to do to get it networked. Then I saw that the Opsive 3rd Person Controller had networking built in with UNET, so decided to give that a shot. They've made networking the player easy. I'll take you're byte info and the info from thegreatzebadiah above and see if I can get there in UNET since the player part is already done. Thanks again!
     
  12. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    SOOOO close. With this code, when say player1 presses "L", it updates all clients to player1's new flag. My main issue now is that if player3 jumps in and changes their flag, it will change for all 3 players, but player3 doesn't update with player 1 and 2's flags. player 1 and 2 have to press"L" again to update player3. So, what i need is when player3 joins and presses "L", the server needs to tell the other players to "press L" as well. Having trouble with that part.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.IO;
    3. using UnityEngine.UI;
    4. using UnityEngine.Networking;
    5.  
    6.  
    7. public class LoadFlag : NetworkBehaviour
    8. {
    9.   byte[] data1;
    10.   Sprite Flag;
    11.   public GameObject OldFlag;
    12.   public string path;
    13.  
    14.   //Create text field for path to png file
    15.   void OnGUI()
    16.   {
    17.   if (isLocalPlayer)
    18.   {
    19.   path = GUI.TextField(new Rect(200, Screen.height - 40, 400, 30), path);
    20.   }
    21.   }
    22.  
    23.   //if "L" key is pressed, covert png file to byte array and send it to the CmdChangeFlag Command
    24.   void Update()
    25.   {
    26.   if (Input.GetKeyDown(KeyCode.L))
    27.   {
    28.   if (isLocalPlayer)
    29.   {
    30.   data1 = File.ReadAllBytes(path);
    31.   CmdChangeFlag(data1);
    32.   }
    33.   }
    34.   }
    35.  
    36.   //Send Byte[] to server
    37.   [Command]
    38.   public void CmdChangeFlag(byte[] data)
    39.   {
    40.   data1 = data;
    41.   RpcChangeFlagOnClients(data1);
    42.   }
    43.  
    44.   //Send Byte[] to clients from server and display flag on all clients
    45.   [ClientRpc]
    46.   void RpcChangeFlagOnClients(byte[] data2)
    47.   {
    48.   data1 = data2;
    49.   Texture2D texture = new Texture2D(10, 10, TextureFormat.ARGB32, false);
    50.   texture.LoadImage(data1);
    51.   texture.name = Path.GetFileNameWithoutExtension(path);
    52.   Rect rect = new Rect(20, 0, 40, 40);
    53.   Vector2 vect2 = new Vector2(1, 1);
    54.   Flag = Sprite.Create(texture, rect, vect2);
    55.   this.OldFlag.GetComponent<SpriteRenderer>().sprite = Flag;
    56.   }
    57. }
     
  13. g-hoot

    g-hoot

    Joined:
    Apr 18, 2015
    Posts:
    69
    GOT IT WORKING!!!! Thanks to everyone that helped me on this. @thegreatzebadiah
    Thanks for all of your input. You are da MAN! I have learned a lot with all of your suggestions!

    Code (CSharp):
    1. NumberOfPlayers = NetworkManager.singleton.numPlayers;
    2.  
    3.         if(NumberOfPlayersChanged!= NumberOfPlayers)
    4.  
    5.         {
    6.  
    7.             NumberOfPlayersChanged = NumberOfPlayers;
    8.  
    9.             if (isLocalPlayer)
    10.  
    11.             {
    12. //What I want all clients to do when new player joins
    13.                 data1 = File.ReadAllBytes(path);
    14.  
    15.                 CmdChangeFlag(data1);
    16.  
    17.             }
    18.  
    19.         }
     
    CarterG81 likes this.