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;
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;
}
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;
}
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.