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