303 lines
12 KiB
C#
303 lines
12 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// Replaces the <see cref="SpriteRenderer.sprite"/> with a copy of it that uses a different <see cref="Texture"/>
|
|
/// during every <see cref="LateUpdate"/>.
|
|
/// </summary>
|
|
///
|
|
/// <remarks>This script is not specific to Animancer and will work with any animation system.</remarks>
|
|
/// 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
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Execute very late (32000 is last).</summary>
|
|
public const int DefaultExecutionOrder = 30000;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField]
|
|
[Tooltip("The SpriteRenderer that will have its Sprite modified")]
|
|
private SpriteRenderer _Renderer;
|
|
|
|
/// <summary>The <see cref="SpriteRenderer"/> that will have its <see cref="Sprite"/> modified.</summary>
|
|
public ref SpriteRenderer Renderer => ref _Renderer;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField]
|
|
[Tooltip("The replacement for the original Sprite texture")]
|
|
private Texture2D _Texture;
|
|
|
|
/// <summary>The replacement for the original <see cref="Sprite.texture"/>.</summary>
|
|
/// <remarks>
|
|
/// If this texture has any <see cref="Sprite"/>s set up in its import settings, they will be completely
|
|
/// ignored because this system creates new <see cref="Sprite"/>s at runtime. The texture doesn't even need to
|
|
/// be set to <see cref="Sprite"/> mode.
|
|
/// <para></para>
|
|
/// Call <see cref="ClearCache"/> before setting this if you want to destroy any sprites created for the
|
|
/// previous texture.
|
|
/// </remarks>
|
|
public Texture2D Texture
|
|
{
|
|
get => _Texture;
|
|
set
|
|
{
|
|
_Texture = value;
|
|
RefreshSpriteMap();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField]
|
|
[Tooltip("Should the secondary textures be swapped as well?")]
|
|
private bool _SwapSecondaryTextures = true;
|
|
|
|
/// <summary>Should the secondary textures be swapped as well?</summary>
|
|
public bool SwapSecondaryTextures
|
|
{
|
|
get => _SwapSecondaryTextures;
|
|
set
|
|
{
|
|
_SwapSecondaryTextures = value;
|
|
RefreshSpriteMap();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField]
|
|
[Tooltip("The replacement secondary textures for the Sprite to use")]
|
|
private SecondarySpriteTexture[] _SecondaryTextures;
|
|
|
|
/// <summary>The replacement for the original <see cref="Sprite.GetSecondaryTextures"/>.</summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
public SecondarySpriteTexture[] SecondaryTextures
|
|
{
|
|
get => _SecondaryTextures;
|
|
set
|
|
{
|
|
_SecondaryTextures = value;
|
|
RefreshSpriteMap();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private Dictionary<Sprite, Sprite> _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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Destroys all sprites created for the current <see cref="Texture"/>.</summary>
|
|
public void ClearCache()
|
|
{
|
|
DestroySprites(_SpriteMap);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static readonly Dictionary<Texture2D, Dictionary<Sprite, Sprite>>
|
|
TextureToSpriteMap = new();
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns a cached dictionary mapping original sprites to duplicates using the specified `texture`.</summary>
|
|
public static Dictionary<Sprite, Sprite> GetSpriteMap(Texture2D texture)
|
|
{
|
|
if (texture == null)
|
|
return null;
|
|
|
|
if (!TextureToSpriteMap.TryGetValue(texture, out var map))
|
|
TextureToSpriteMap.Add(texture, map = new());
|
|
|
|
return map;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// If the <see cref="Sprite.texture"/> is not already using the specified `texture`, this method replaces the
|
|
/// `sprite` with a cached duplicate which uses that `texture` instead.
|
|
/// </summary>
|
|
public static bool TrySwapTexture(
|
|
Dictionary<Sprite, Sprite> 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<SecondarySpriteTexture[]> _SecondaryTextureCache;
|
|
|
|
/// <summary>A wrapper around <see cref="Sprite.GetSecondaryTextures"/> which reuses arrays of the same size.</summary>
|
|
public static SecondarySpriteTexture[] GetSecondaryTexturesCached(Sprite sprite)
|
|
{
|
|
var count = sprite.GetSecondaryTextureCount();
|
|
if (count == 0)
|
|
return System.Array.Empty<SecondarySpriteTexture>();
|
|
|
|
_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;
|
|
}
|
|
|
|
/// <summary>A wrapper around <see cref="Sprite.GetSecondaryTextures"/>.</summary>
|
|
public static SecondarySpriteTexture[] GetSecondaryTextures(Sprite sprite)
|
|
{
|
|
var count = sprite.GetSecondaryTextureCount();
|
|
if (count == 0)
|
|
return System.Array.Empty<SecondarySpriteTexture>();
|
|
|
|
var textures = new SecondarySpriteTexture[count];
|
|
sprite.GetSecondaryTextures(textures);
|
|
return textures;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Destroys all the <see cref="Dictionary{TKey, TValue}.Values"/>.</summary>
|
|
public static void DestroySprites(Dictionary<Sprite, Sprite> spriteMap)
|
|
{
|
|
if (spriteMap == null)
|
|
return;
|
|
|
|
foreach (var sprite in spriteMap.Values)
|
|
Destroy(sprite);
|
|
|
|
spriteMap.Clear();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Destroys all sprites created for the `texture`.</summary>
|
|
public static void DestroySprites(Texture2D texture)
|
|
{
|
|
if (TextureToSpriteMap.TryGetValue(texture, out var spriteMap))
|
|
{
|
|
TextureToSpriteMap.Remove(texture);
|
|
DestroySprites(spriteMap);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|