无法更改生成的游戏对象的 Sprite(仅限客户端)

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

我正在尝试在 Unity3D 中实例化、生成精灵,然后将精灵分配给自定义 GameObject。这些对象是一个通用的 CardContainer,它调用 SetCard 方法为其提供自定义统计信息。调用 SetCard 还会为 CardContainer 分配其 Sprite。

我的问题是,每当我更改生成的 GameObjectSpriteRenderer.sprite 时,精灵更改不会影响客户端实例。

它似乎也没有反映我是否在 Sprite 生成对象之前对 进行了任何更改。这是否可以更改 sprite,我该怎么做?

我已经设置了一些小的 poc 测试,但到目前为止没有任何效果。他们在这里:

//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;

实际上这很棘手,取决于你的情况。

最好的情况是事先知道哪些 Sprites 可用并存储它们,例如在 List<Sprite> .. 然后你可以简单地告诉客户通过设置例如使用哪个精灵a [SyncVar] 在生成的对象上。像

// 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];
    }
}

然后做例如

// 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);

现在目标 sprite 对象使用正确的 sprite 初始化自身。

另外使用 hook 现在它实际上是 每次 在服务器 上更改索引 。所以现在你甚至可以通过简单地分配一个新的索引在运行时动态地切换 Sprite:

private void Update()
{
    if(!isServer || !Input.GetKeyDown(KeyCode.ArrowUp)) return;

    SpriteIndex = (SpriteIndex + 1) % Sprites.Count;
}


另一种方法可能是传输实际的 Texture2D 数据。这有点棘手,因为允许通过 UNet 传递的 parameters/data 类型非常有限

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

但请注意

  • 这也非常有限,因为 UNet 只允许 afaik 64kb 的网络缓冲区,所以任何更大的 image/texture(+ 其余数据!)将无法通过这种方式传输并且它会变得更复杂。

    另请注意,在这方面 EncideToXY 通常会导致比原始图像大 的数据大小。

  • 我现在也不确定 SpawnRpc_AfterSpawn 的执行顺序在网络上是否可靠。 Rpc_AfterSpawn 可能会在 Spawn 实际完成之前到达客户。