// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value. using System.Collections.Generic; using UnityEngine; namespace Animancer { /// /// Replaces the with a copy of it that uses a different /// during every . /// /// /// This script is not specific to Animancer and will work with any animation system. /// https://kybernetik.com.au/animancer/api/Animancer/SpriteRendererTextureSwap /// [AddComponentMenu("Animancer/Sprite Renderer Texture Swap")] [HelpURL("https://kybernetik.com.au/animancer/api/Animancer/" + nameof(SpriteRendererTextureSwap))] [DefaultExecutionOrder(DefaultExecutionOrder)] public class SpriteRendererTextureSwap : MonoBehaviour { /************************************************************************************************************************/ /// Execute very late (32000 is last). public const int DefaultExecutionOrder = 30000; /************************************************************************************************************************/ [SerializeField] [Tooltip("The SpriteRenderer that will have its Sprite modified")] private SpriteRenderer _Renderer; /// The that will have its modified. public ref SpriteRenderer Renderer => ref _Renderer; /************************************************************************************************************************/ [SerializeField] [Tooltip("The replacement for the original Sprite texture")] private Texture2D _Texture; /// The replacement for the original . /// /// If this texture has any s set up in its import settings, they will be completely /// ignored because this system creates new s at runtime. The texture doesn't even need to /// be set to mode. /// /// Call before setting this if you want to destroy any sprites created for the /// previous texture. /// public Texture2D Texture { get => _Texture; set { _Texture = value; RefreshSpriteMap(); } } /************************************************************************************************************************/ [SerializeField] [Tooltip("Should the secondary textures be swapped as well?")] private bool _SwapSecondaryTextures = true; /// Should the secondary textures be swapped as well? public bool SwapSecondaryTextures { get => _SwapSecondaryTextures; set { _SwapSecondaryTextures = value; RefreshSpriteMap(); } } /************************************************************************************************************************/ [SerializeField] [Tooltip("The replacement secondary textures for the Sprite to use")] private SecondarySpriteTexture[] _SecondaryTextures; /// The replacement for the original . /// /// Swapped sprites are cached statically so they can be shared between instances. Unfortunately, the cache /// isn't tied to secondary textures so if multiple instances replace TextureA with TextureB and have different /// secondary textures then they would interfere with each other. That's unlikely to be a real issue because /// TextureB should always have TextureBNormals as a secondary texture. It would be possible to avoid this /// if necessary, but doing so would cost more performance and increase the complexity of this system. /// public SecondarySpriteTexture[] SecondaryTextures { get => _SecondaryTextures; set { _SecondaryTextures = value; RefreshSpriteMap(); } } /************************************************************************************************************************/ private Dictionary _SpriteMap; private void RefreshSpriteMap() => _SpriteMap = GetSpriteMap(_Texture); /************************************************************************************************************************/ protected virtual void OnValidate() { if (_Renderer == null) TryGetComponent(out _Renderer); if (_SwapSecondaryTextures && (_SecondaryTextures == null || _SecondaryTextures.Length == 0) && _Renderer != null && _Renderer.sprite != null) { var sprite = _Renderer.sprite; var count = sprite.GetSecondaryTextureCount(); if (count > 0) { _SecondaryTextures = new SecondarySpriteTexture[count]; sprite.GetSecondaryTextures(_SecondaryTextures); } } if (_SpriteMap != null) DestroySprites(_SpriteMap); } /************************************************************************************************************************/ protected virtual void Awake() => RefreshSpriteMap(); /************************************************************************************************************************/ protected virtual void LateUpdate() { if (_Renderer == null) return; var sprite = _Renderer.sprite; var secondaryTextures = _SwapSecondaryTextures ? _SecondaryTextures : null; if (TrySwapTexture(_SpriteMap, _Texture, secondaryTextures, ref sprite)) _Renderer.sprite = sprite; } /************************************************************************************************************************/ /// Destroys all sprites created for the current . public void ClearCache() { DestroySprites(_SpriteMap); } /************************************************************************************************************************/ private static readonly Dictionary> TextureToSpriteMap = new(); /************************************************************************************************************************/ /// Returns a cached dictionary mapping original sprites to duplicates using the specified `texture`. public static Dictionary GetSpriteMap(Texture2D texture) { if (texture == null) return null; if (!TextureToSpriteMap.TryGetValue(texture, out var map)) TextureToSpriteMap.Add(texture, map = new()); return map; } /************************************************************************************************************************/ /// /// If the is not already using the specified `texture`, this method replaces the /// `sprite` with a cached duplicate which uses that `texture` instead. /// public static bool TrySwapTexture( Dictionary spriteMap, Texture2D texture, SecondarySpriteTexture[] secondaryTextures, ref Sprite sprite) { if (spriteMap == null || sprite == null || texture == null || sprite.texture == texture) return false; if (!spriteMap.TryGetValue(sprite, out var otherSprite)) { var pivot = sprite.pivot; pivot.x /= sprite.rect.width; pivot.y /= sprite.rect.height; secondaryTextures ??= GetSecondaryTexturesCached(sprite); otherSprite = Sprite.Create(texture, sprite.rect, pivot, sprite.pixelsPerUnit, 0, SpriteMeshType.FullRect, sprite.border, false, secondaryTextures); #if UNITY_ASSERTIONS var name = sprite.name; var originalTextureName = sprite.texture.name; var index = name.IndexOf(originalTextureName); if (index >= 0) { var newName = texture.name + name[(index + originalTextureName.Length)..]; if (index > 0) newName = name[..index] + newName; name = newName; } otherSprite.name = name; #endif spriteMap.Add(sprite, otherSprite); } sprite = otherSprite; return true; } /************************************************************************************************************************/ private static List _SecondaryTextureCache; /// A wrapper around which reuses arrays of the same size. public static SecondarySpriteTexture[] GetSecondaryTexturesCached(Sprite sprite) { var count = sprite.GetSecondaryTextureCount(); if (count == 0) return System.Array.Empty(); _SecondaryTextureCache ??= new(); while (_SecondaryTextureCache.Count < count) _SecondaryTextureCache.Add(null); var textures = _SecondaryTextureCache[count - 1]; if (textures == null) { textures = new SecondarySpriteTexture[count]; _SecondaryTextureCache[count - 1] = textures; } sprite.GetSecondaryTextures(textures); return textures; } /// A wrapper around . public static SecondarySpriteTexture[] GetSecondaryTextures(Sprite sprite) { var count = sprite.GetSecondaryTextureCount(); if (count == 0) return System.Array.Empty(); var textures = new SecondarySpriteTexture[count]; sprite.GetSecondaryTextures(textures); return textures; } /************************************************************************************************************************/ /// Destroys all the . public static void DestroySprites(Dictionary spriteMap) { if (spriteMap == null) return; foreach (var sprite in spriteMap.Values) Destroy(sprite); spriteMap.Clear(); } /************************************************************************************************************************/ /// Destroys all sprites created for the `texture`. public static void DestroySprites(Texture2D texture) { if (TextureToSpriteMap.TryGetValue(texture, out var spriteMap)) { TextureToSpriteMap.Remove(texture); DestroySprites(spriteMap); } } /************************************************************************************************************************/ } }