Search code examples
c#unity-game-enginespriteunity3d-unet

Can't change the Sprite of a spawned game object (Client only)


I am attempting to instantiate, spawn, then assign a sprite to a custom GameObject in Unity3D. The objects are a generic CardContainer that call a SetCard method to give it its custom stats. Calling SetCard also assigns the CardContainer its Sprite.

My problem is that, whenever I change the SpriteRenderer.sprite of a spawned GameObject, the sprite change does not take affect on the client instance.

It also seems to not reflect if I've made any changes to the Sprite before I spawn the object. Is this possible to change the sprite, and how can I do it?

I have set up a few small poc tests, but nothing has worked so far. Here they are:

//cardContainerTesting
Vector3 testingContainerCoords= new 
Vector3(0, 1, -1);
GameObject testingCardObjectInstance = Instantiate(testingCardCOntainerGameObject, testingContainerCoords, Quaternion.identity); 
NetworkServer.Spawn(testingCardObjectInstance);
SpriteRenderer objectSprite = testingCardObjectInstance.GetComponent<SpriteRenderer>();
objectSprite.sprite = testingSprite1;


//GenericGameObjectExampleTesting
Vector3 origin = new Vector3(0, 0, -1);
GameObject instantiatedPrefab = Instantiate(myPrefabExample, origin, Quaternion.identity);
NetworkServer.Spawn(instantiatedPrefab);
SpriteRenderer exampleSpriteRenderer = instantiatedPrefab.GetComponent<SpriteRenderer>();
exampleSpriteRenderer.sprite = testingSprite2;

Solution

  • Actually it is quite tricky and depends on your case.

    The best-case would be that beforehand you know which Sprites are available and store them e.g. in a List<Sprite> .. then you can simply tell the clients which sprite to use by setting e.g. a [SyncVar] on the spawned object. Something like

    // on the spawned object
    public class SpriteController : NetworkBehaviour
    {
        // Also good if you reference this already in the Inspector
        [SerializeField] private SpriteRenderer spriteRenderer;
    
        // Configured via the Inspector befrorehand
        public List<Sprite> Sprites;
    
        // Whenever this is changed on the Server
        // the change is automatically submitted to all clients
        // by using the "hook" it calls the OnSpriteIndexChanged and passes
        // the new value in as parameter
        [SyncVar(hook = nameof(OnSpriteIndexChanged))] public int SpriteIndex;
    
        // Will be called everytime the index is changed on the server
        [ClientCallback]
        private void OnSpriteIndexChanged(int newIndex)
        {
            // First when using a hook you have to explicitly apply the changed value at some point
            SpriteIndex = newIndex;
    
            if (!spriteRenderer) spriteRenderer = GetComponent<SpriteRenderer>();
            spriteRenderer.sprite = Sprites[SpriteIndex];
        }
    }
    

    and then do e.g.

    // If you make this of type SpriteController the Inspector automatically
    // references the correct component and you can get rid of the GetComponent call later
    public SpriteController testingCardCOntainerGameObject;
    
    var testingCardObjectInstance = Instantiate(testingCardCOntainerGameObject, testingContainerCoords, Quaternion.identity); 
    
    // for testing use 1 since 0 is the default for int ;)
    testingCardObjectInstance.SpriteIndex = 1;
    
    NetworkServer.Spawn(testingCardObjectInstance);
    

    Now the target sprite object initializes itself with the correct sprite.

    Additionally using the hook now it is actually changed every time the index is changed on the server. So now you can even switch the Sprite at runtime dynamically by simply assigning a new index:

    private void Update()
    {
        if(!isServer || !Input.GetKeyDown(KeyCode.ArrowUp)) return;
    
        SpriteIndex = (SpriteIndex + 1) % Sprites.Count;
    }
    

    enter image description here


    An alternative would maybe consist in transmitting the actual Texture2D data. This is a bit tricky since the allowed parameters/data types passed via UNet are very limited

    // the sprite we will transfer
    public Sprite targetSprite;
    
    // the prefab to spawn
    // directly use the component type here so we get rid of one GetComponent call
    public SpriteRenderer examplePRefab;
    
    [Command]
    public void Cmd_Spawn()
    {
        // ON SERVER
    
        var obj = Instantiate(examplePRefab);
        // on the server set the sprite right away
        obj.sprite = targetSprite;
    
        // spawn (sprite will not be set yet)
        NetworkServer.Spawn(obj.gameObject);
    
        // tell clients to set the sprite and pass required data
        Rpc_AfterSpawn(obj.gameObject, targetSprite.texture.EncodeToPNG(), new SpriteInfo(targetSprite));
    }
    
    [Serializable]
    private struct SpriteInfo
    {
        public Rect rect;
        public Vector2 pivot;
    
        public SpriteInfo(Sprite sprite)
        {
            rect = sprite.rect;
            pivot = sprite.pivot;
        }
    }
    
    [ClientRpc]
    private void Rpc_AfterSpawn(GameObject targetObject, byte[] textureData, SpriteInfo spriteInfo)
    {
        // ON CLIENTS
    
        // the initial width and height don't matter
        // they will be overwritten by load
        // also the texture format will automatically be RGB24 for jpg data
        // and RGBA32 for png
        var texture = new Texture2D(1, 1);
        //  load the byte[] into the texture
        texture.LoadImage(textureData);
        var newSprite = Sprite.Create(texture, spriteInfo.rect, spriteInfo.pivot);
    
        // finally set the sprite on all clients
        targetObject.GetComponent<SpriteRenderer>().sprite = newSprite;
    }
    

    enter image description here

    Note however:

    • This is also very limited since UNet only allows a NetworkBuffer of afaik 64kb so any bigger image/texture (+ the rest of data!) will not be possible to transmit on this way and it would get more complex.

      Also note in this regard that EncideToXY often results in a bigger data size than the original image.

    • I'm also not sure right now if the execution order of Spawn and Rpc_AfterSpawn will be reliable over the network. It might happen that Rpc_AfterSpawn reaches the clients before the Spawn is actually done.