SpriteRendererTextureSwap.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. namespace Animancer
  6. {
  7. /// <summary>
  8. /// Replaces the <see cref="SpriteRenderer.sprite"/> with a copy of it that uses a different <see cref="Texture"/>
  9. /// during every <see cref="LateUpdate"/>.
  10. /// </summary>
  11. ///
  12. /// <remarks>This script is not specific to Animancer and will work with any animation system.</remarks>
  13. /// https://kybernetik.com.au/animancer/api/Animancer/SpriteRendererTextureSwap
  14. ///
  15. [AddComponentMenu("Animancer/Sprite Renderer Texture Swap")]
  16. [HelpURL("https://kybernetik.com.au/animancer/api/Animancer/" + nameof(SpriteRendererTextureSwap))]
  17. [DefaultExecutionOrder(DefaultExecutionOrder)]
  18. public class SpriteRendererTextureSwap : MonoBehaviour
  19. {
  20. /************************************************************************************************************************/
  21. /// <summary>Execute very late (32000 is last).</summary>
  22. public const int DefaultExecutionOrder = 30000;
  23. /************************************************************************************************************************/
  24. [SerializeField]
  25. [Tooltip("The SpriteRenderer that will have its Sprite modified")]
  26. private SpriteRenderer _Renderer;
  27. /// <summary>The <see cref="SpriteRenderer"/> that will have its <see cref="Sprite"/> modified.</summary>
  28. public ref SpriteRenderer Renderer => ref _Renderer;
  29. /************************************************************************************************************************/
  30. [SerializeField]
  31. [Tooltip("The replacement for the original Sprite texture")]
  32. private Texture2D _Texture;
  33. /// <summary>The replacement for the original <see cref="Sprite.texture"/>.</summary>
  34. /// <remarks>
  35. /// If this texture has any <see cref="Sprite"/>s set up in its import settings, they will be completely
  36. /// ignored because this system creates new <see cref="Sprite"/>s at runtime. The texture doesn't even need to
  37. /// be set to <see cref="Sprite"/> mode.
  38. /// <para></para>
  39. /// Call <see cref="ClearCache"/> before setting this if you want to destroy any sprites created for the
  40. /// previous texture.
  41. /// </remarks>
  42. public Texture2D Texture
  43. {
  44. get => _Texture;
  45. set
  46. {
  47. _Texture = value;
  48. RefreshSpriteMap();
  49. }
  50. }
  51. /************************************************************************************************************************/
  52. [SerializeField]
  53. [Tooltip("Should the secondary textures be swapped as well?")]
  54. private bool _SwapSecondaryTextures = true;
  55. /// <summary>Should the secondary textures be swapped as well?</summary>
  56. public bool SwapSecondaryTextures
  57. {
  58. get => _SwapSecondaryTextures;
  59. set
  60. {
  61. _SwapSecondaryTextures = value;
  62. RefreshSpriteMap();
  63. }
  64. }
  65. /************************************************************************************************************************/
  66. [SerializeField]
  67. [Tooltip("The replacement secondary textures for the Sprite to use")]
  68. private SecondarySpriteTexture[] _SecondaryTextures;
  69. /// <summary>The replacement for the original <see cref="Sprite.GetSecondaryTextures"/>.</summary>
  70. /// <remarks>
  71. /// Swapped sprites are cached statically so they can be shared between instances. Unfortunately, the cache
  72. /// isn't tied to secondary textures so if multiple instances replace TextureA with TextureB and have different
  73. /// secondary textures then they would interfere with each other. That's unlikely to be a real issue because
  74. /// TextureB should always have TextureBNormals as a secondary texture. It would be possible to avoid this
  75. /// if necessary, but doing so would cost more performance and increase the complexity of this system.
  76. /// </remarks>
  77. public SecondarySpriteTexture[] SecondaryTextures
  78. {
  79. get => _SecondaryTextures;
  80. set
  81. {
  82. _SecondaryTextures = value;
  83. RefreshSpriteMap();
  84. }
  85. }
  86. /************************************************************************************************************************/
  87. private Dictionary<Sprite, Sprite> _SpriteMap;
  88. private void RefreshSpriteMap() => _SpriteMap = GetSpriteMap(_Texture);
  89. /************************************************************************************************************************/
  90. protected virtual void OnValidate()
  91. {
  92. if (_Renderer == null)
  93. TryGetComponent(out _Renderer);
  94. if (_SwapSecondaryTextures &&
  95. (_SecondaryTextures == null || _SecondaryTextures.Length == 0) &&
  96. _Renderer != null &&
  97. _Renderer.sprite != null)
  98. {
  99. var sprite = _Renderer.sprite;
  100. var count = sprite.GetSecondaryTextureCount();
  101. if (count > 0)
  102. {
  103. _SecondaryTextures = new SecondarySpriteTexture[count];
  104. sprite.GetSecondaryTextures(_SecondaryTextures);
  105. }
  106. }
  107. if (_SpriteMap != null)
  108. DestroySprites(_SpriteMap);
  109. }
  110. /************************************************************************************************************************/
  111. protected virtual void Awake() => RefreshSpriteMap();
  112. /************************************************************************************************************************/
  113. protected virtual void LateUpdate()
  114. {
  115. if (_Renderer == null)
  116. return;
  117. var sprite = _Renderer.sprite;
  118. var secondaryTextures = _SwapSecondaryTextures ? _SecondaryTextures : null;
  119. if (TrySwapTexture(_SpriteMap, _Texture, secondaryTextures, ref sprite))
  120. _Renderer.sprite = sprite;
  121. }
  122. /************************************************************************************************************************/
  123. /// <summary>Destroys all sprites created for the current <see cref="Texture"/>.</summary>
  124. public void ClearCache()
  125. {
  126. DestroySprites(_SpriteMap);
  127. }
  128. /************************************************************************************************************************/
  129. private static readonly Dictionary<Texture2D, Dictionary<Sprite, Sprite>>
  130. TextureToSpriteMap = new();
  131. /************************************************************************************************************************/
  132. /// <summary>Returns a cached dictionary mapping original sprites to duplicates using the specified `texture`.</summary>
  133. public static Dictionary<Sprite, Sprite> GetSpriteMap(Texture2D texture)
  134. {
  135. if (texture == null)
  136. return null;
  137. if (!TextureToSpriteMap.TryGetValue(texture, out var map))
  138. TextureToSpriteMap.Add(texture, map = new());
  139. return map;
  140. }
  141. /************************************************************************************************************************/
  142. /// <summary>
  143. /// If the <see cref="Sprite.texture"/> is not already using the specified `texture`, this method replaces the
  144. /// `sprite` with a cached duplicate which uses that `texture` instead.
  145. /// </summary>
  146. public static bool TrySwapTexture(
  147. Dictionary<Sprite, Sprite> spriteMap,
  148. Texture2D texture,
  149. SecondarySpriteTexture[] secondaryTextures,
  150. ref Sprite sprite)
  151. {
  152. if (spriteMap == null ||
  153. sprite == null ||
  154. texture == null ||
  155. sprite.texture == texture)
  156. return false;
  157. if (!spriteMap.TryGetValue(sprite, out var otherSprite))
  158. {
  159. var pivot = sprite.pivot;
  160. pivot.x /= sprite.rect.width;
  161. pivot.y /= sprite.rect.height;
  162. secondaryTextures ??= GetSecondaryTexturesCached(sprite);
  163. otherSprite = Sprite.Create(texture,
  164. sprite.rect, pivot, sprite.pixelsPerUnit,
  165. 0, SpriteMeshType.FullRect, sprite.border, false, secondaryTextures);
  166. #if UNITY_ASSERTIONS
  167. var name = sprite.name;
  168. var originalTextureName = sprite.texture.name;
  169. var index = name.IndexOf(originalTextureName);
  170. if (index >= 0)
  171. {
  172. var newName =
  173. texture.name +
  174. name[(index + originalTextureName.Length)..];
  175. if (index > 0)
  176. newName = name[..index] + newName;
  177. name = newName;
  178. }
  179. otherSprite.name = name;
  180. #endif
  181. spriteMap.Add(sprite, otherSprite);
  182. }
  183. sprite = otherSprite;
  184. return true;
  185. }
  186. /************************************************************************************************************************/
  187. private static List<SecondarySpriteTexture[]> _SecondaryTextureCache;
  188. /// <summary>A wrapper around <see cref="Sprite.GetSecondaryTextures"/> which reuses arrays of the same size.</summary>
  189. public static SecondarySpriteTexture[] GetSecondaryTexturesCached(Sprite sprite)
  190. {
  191. var count = sprite.GetSecondaryTextureCount();
  192. if (count == 0)
  193. return System.Array.Empty<SecondarySpriteTexture>();
  194. _SecondaryTextureCache ??= new();
  195. while (_SecondaryTextureCache.Count < count)
  196. _SecondaryTextureCache.Add(null);
  197. var textures = _SecondaryTextureCache[count - 1];
  198. if (textures == null)
  199. {
  200. textures = new SecondarySpriteTexture[count];
  201. _SecondaryTextureCache[count - 1] = textures;
  202. }
  203. sprite.GetSecondaryTextures(textures);
  204. return textures;
  205. }
  206. /// <summary>A wrapper around <see cref="Sprite.GetSecondaryTextures"/>.</summary>
  207. public static SecondarySpriteTexture[] GetSecondaryTextures(Sprite sprite)
  208. {
  209. var count = sprite.GetSecondaryTextureCount();
  210. if (count == 0)
  211. return System.Array.Empty<SecondarySpriteTexture>();
  212. var textures = new SecondarySpriteTexture[count];
  213. sprite.GetSecondaryTextures(textures);
  214. return textures;
  215. }
  216. /************************************************************************************************************************/
  217. /// <summary>Destroys all the <see cref="Dictionary{TKey, TValue}.Values"/>.</summary>
  218. public static void DestroySprites(Dictionary<Sprite, Sprite> spriteMap)
  219. {
  220. if (spriteMap == null)
  221. return;
  222. foreach (var sprite in spriteMap.Values)
  223. Destroy(sprite);
  224. spriteMap.Clear();
  225. }
  226. /************************************************************************************************************************/
  227. /// <summary>Destroys all sprites created for the `texture`.</summary>
  228. public static void DestroySprites(Texture2D texture)
  229. {
  230. if (TextureToSpriteMap.TryGetValue(texture, out var spriteMap))
  231. {
  232. TextureToSpriteMap.Remove(texture);
  233. DestroySprites(spriteMap);
  234. }
  235. }
  236. /************************************************************************************************************************/
  237. }
  238. }