0

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;
derHugo
  • 83,094
  • 9
  • 75
  • 115
  • Do you know the possible sprites beforehand so you can e.g. store them in a list or dictionary? – derHugo Sep 19 '19 at 03:50
  • I have all my sprites stored in my "SpriteCollection" dictionary and they can be gathered from and CardContainer through my "SetCard" function. It was working great until I tried multiplayer. I ran out of characters on my other comment but I just want to thank you a bunch. Yours post was really thorough and must have taken a while. I really appreciate it! – john_shreds Sep 20 '19 at 01:01

1 Answers1

1

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.

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • Thank you! I've been stuck for an embarrassingly long time on this issue. I've been unsuccessfully trying to apply your instructions today. I have your code pasted into my project verbatim. I am now getting *a* sprite to appear for my client, but not the correct one. The sprite appearing is the 0'th item on my "Sprites" list rather than the first, or any other index I input. Its finding the correct sprite in the host. Any ideas? To me it seems like the SpriteIndex is not being shared properly. I added your start function code to the update function to make sure it runs and still nothing. – john_shreds Sep 20 '19 at 00:59
  • Hey I'm sorry .. not sure anymore since UNet was quite a while ago ... I was pretty sure that it works using `Start` and afaik the `[SyncVar]` should be syncronized after Spawn before `Start` is called. It also worked for me .. but maybe it was a runtime "bug" and only worked for me since I'm testing within the same PC. However, I updated the code to instead use a `hook` and `[ClientCallback]` for the synced index so now you can even change the index afterwards (on the Server) and the sprite will be exchanged dynamically on all clients (and the server ofcourse ;) ) – derHugo Sep 20 '19 at 03:45
  • Hey man, I appreciate all you help here. I've been working on it for a while with absolutely no luck. Even when I manually change values in the inspector I cannot get my SyncVars to actually sync. No idea what the problem is. My host and client appear to be connected. I've even put together a simple poc project with just your code in it and I'm getting the same result. Im gonna reinstall a new version of Unity in case this is some crazy bug. Your answer appears to be correct so Im gonna mark it that way. Thanks a bunch for all you help. – john_shreds Sep 24 '19 at 20:43
  • @john_shreds the **only** working way is by changing the value on the server side **via code**. Changing it via the inspector doesn't trigger the `SyncVar`! Therefore for the fist demo I used `private void Update(){if(Input.GetKeyDown(KeyCode.UpArrow)) { SpriteIndex = (SpriteIndex + 1) % Sprites.Count; } }` in order to increase it by script – derHugo Sep 24 '19 at 20:46
  • Wow I feel really dumb, I thought I had tried that earlier but I just tried it again and it worked. Only in my poc (not my real project) but this should still serve as an example to work from. I swear I tried this method first before using the inspector manually but who knows. I am a little confused by the gif you posted though. I thought I saw you editing values in the host inspector and seeing the client synchvars change as a result. – john_shreds Sep 24 '19 at 20:59
  • No worries, unfortunately this is not well documented and I also always expect it to work using the Inspector for test it – derHugo Sep 24 '19 at 21:02