Posted January 31, 2023 by ChocolaMint
The "image copies" behind Marisa and the Yin-Yang Orb are what we call afterimages. It's a kind of visual illusion that happens when your eyes are too slow to keep up with a fast-moving object.
(Note: I renamed some properties and added comments to make this more readable)
using System.Collections; using System.Collections.Generic; using UnityEngine; // Place this component on the root / topmost GameObject containing all the SpriteRenderers // you want an afterimage effect for. public class Afterimage : MonoBehaviour { // Component that's put on every SpriteRenderer copy. A "sprite frame". private class Frame : MonoBehaviour { // Note: The public properties are set by the Afterimage class. public float lifetime = 1.0f; // How long the frame lasts. private float startTime = 0; // Record start time for color interpolation. public SpriteRenderer spriteRenderer; // The frame's own SpriteRenderer, added by the Afterimage class. private Color startColor; // The frame's SpriteRenderer's initial color. private void Start() { startTime = Time.time; startColor = spriteRenderer.color; } private void Update() { // Calculate "progress" of the frame. float t = (Time.time - startTime) / lifetime; // If "done" (t >= 1), destroy the frame along with its GameObject. if(t >= 1) Destroy(gameObject); // Otherwise, interpolate from the initial color to transparent black (Color.clear). else spriteRenderer.color = Color.Lerp(startColor, Color.clear, t); } } // We record an array of SpriteRenderers to take care of the case of nested SpriteRenderers. private SpriteRenderer[] spriteRenderers; // How long should we wait before placing the next Afterimage Frame? [SerializeField, Min(0.01f)] private float interval = 0.1f; // How long should each Afterimage Frame last? [SerializeField, Min(0.01f)] private float frameLifetime = 0.25f; // The scheduled time for the next Afterimage Frame. private float nextFrameTime = 0; // The hue of the Afterimage Frame. // We will do multiplicative blending with each SpriteRenderer's original color. [SerializeField] private Color hue = new Color(0.5f, 0.5f, 0.8f); private void Awake() { // Initialize: Get all SpriteRenderers from the current GameObject and its children. spriteRenderers = GetComponentsInChildren<spriterenderer>(); } // Start is called before the first frame update void Start() { // Schedule the first Afterimage Frame. It can be Time.time as well, doesn't really matter. nextFrameTime = Time.time + interval; } // Update is called once per frame protected virtual void Update() { // If it's time for the scheduled Afterimage Frame if(Time.time >= nextFrameTime) { // Create a frame for each SpriteRenderer. foreach(var spriteRenderer in spriteRenderers) CreateFrame(spriteRenderer); // Then schedule the next Afterimage nextFrameTime = Time.time + interval; } } // Method that handles the creation and initialization of each Frame object. private void CreateFrame(SpriteRenderer spriteRenderer) { GameObject frameGO = new(); // Create new GameObject for the frame. frameGO.hideFlags = HideFlags.HideInHierarchy; // Set HideFlags so the scene hierarchy don't get polluted by stray Frames. frameGO.transform.SetPositionAndRotation(spriteRenderer.transform.position, spriteRenderer.transform.rotation); frameGO.transform.localScale = spriteRenderer.transform.lossyScale; // Copy the SpriteRenderer's world scale AKA lossy scale. var frame = frameGO.AddComponent(); // Add the Frame component to the GameObject. frame.spriteRenderer = frameGO.AddComponent<spriterenderer>(); // Give the GameObject a SpriteRenderer, and pass the reference to the Frame component. // Copy SpriteRenderer properties over. frame.spriteRenderer.sprite = spriteRenderer.sprite; frame.spriteRenderer.flipX = spriteRenderer.flipX; frame.spriteRenderer.flipY = spriteRenderer.flipY; frame.spriteRenderer.color = spriteRenderer.color * hue; // Multiplicative blending to make the frame darker. frame.spriteRenderer.sharedMaterial = spriteRenderer.sharedMaterial; frame.spriteRenderer.sortingOrder = spriteRenderer.sortingOrder; // You can subtract 1 from this to make afterimages always appear below the original. frame.lifetime = frameLifetime; // Pass the frameLifetime property to the Frame component. } }
As for why I'm writing this? I just feel like I should try writing more tech posts to lean a bit more on my programmer side :3