Search Unity

How to create ghost or after image on an animated sprite?

Discussion in '2D' started by ScratchModed, Jun 17, 2015.

  1. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Hey there.
    I am having an impossible time recreating this effect for my 2D platformer:


    I'd like to create a sprite trail just like in the example but I can't seem to figure it out; I can almost create the effect for just one still sprite with particles, but to have it trail after the sprite animations seems way out of my grasp. I've tried trail and line renderer but both those don't create a trail from the sprite sheet, only their own little separate effect.
     
    Last edited: Jun 17, 2015
  2. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Last edited: Jun 17, 2015
  3. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Ok, previous solution works for me fine, but not as good as I want, because it updates every trail part texture continuously.
    Here's another small example I've wrote for couple of minutes:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4.     void Start()
    5.     {
    6.         InvokeRepeating("SpawnTrail", 0, 0.2f); // replace 0.2f with needed repeatRate
    7.     }
    8.  
    9.     void SpawnTrail()
    10.     {
    11.         GameObject trailPart = new GameObject();
    12.         SpriteRenderer trailPartRenderer = trailPart.AddComponent<SpriteRenderer>();
    13.         trailPartRenderer.sprite = GetComponent<SpriteRenderer>().sprite;
    14.         trailPart.transform.position = transform.position;
    15.         Destroy(trailPart, 0.5f); // replace 0.5f with needed lifeTime
    16.  
    17.         StartCoroutine("FadeTrailPart", trailPartRenderer);
    18.     }
    19.  
    20.     IEnumerator FadeTrailPart(SpriteRenderer trailPartRenderer)
    21.     {
    22.         Color color = trailPartRenderer.color;
    23.         color.a -= 0.5f; // replace 0.5f with needed alpha decrement
    24.         trailPartRenderer.color = color;
    25.  
    26.         yield return new WaitForEndOfFrame();
    27.     }
    Add this code to your character's script on GameObject with Animator component.
    The code is pretty simple and works perfect for me. I wrote it in hurry, so it can be refactored and optimized:) For example you could reuse trailPart gameobjects instead of instantiating and destroying it everytime (object pool technique), etc., depends on you;)
     
    ScratchModed and BusyCat like this.
  4. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Well this is some pretty magnificent script you wrote for me; so pardon my while I show some cringe-worthy noobness.

    The ghost doesn't appear to be be affected by my Flip () function. so while it looks great heading in the correct direction. if i turn around my sprite the ghost doesn't turn as well :oops:
    Code (CSharp):
    1. void Flip ()
    2.     {
    3.         facingRight = !facingRight;
    4.         Vector3 theScale = transform.localScale;
    5.         theScale.x *= -1;
    6.         transform.localScale = theScale;
    7.     }
    Think you can help iron out this last wrinkle?
     
  5. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Ok, try this:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3.  
    4. List<GameObject> trailParts = new List<GameObject>();
    5.  
    6. void Start()
    7. {
    8.     InvokeRepeating("SpawnTrailPart", 0, 0.2f); // replace 0.2f with needed repeatRate
    9. }
    10.  
    11. void SpawnTrailPart()
    12. {
    13.     GameObject trailPart = new GameObject();
    14.     SpriteRenderer trailPartRenderer = trailPart.AddComponent<SpriteRenderer>();
    15.     trailPartRenderer.sprite = GetComponent<SpriteRenderer>().sprite;
    16.     trailPart.transform.position = transform.position;
    17.     trailParts.Add(trailPart);
    18.  
    19.     StartCoroutine(FadeTrailPart(trailPartRenderer));
    20.     StartCoroutine(DestroyTrailPart(trailPart, 0.5f)); // replace 0.5f with needed lifeTime
    21. }
    22.  
    23. IEnumerator FadeTrailPart(SpriteRenderer trailPartRenderer)
    24. {
    25.     Color color = trailPartRenderer.color;
    26.     color.a -= 0.5f; // replace 0.5f with needed alpha decrement
    27.     trailPartRenderer.color = color;
    28.    
    29.     yield return new WaitForEndOfFrame();
    30. }
    31.  
    32. IEnumerator DestroyTrailPart(GameObject trailPart, float delay)
    33. {
    34.     yield return new WaitForSeconds(delay);
    35.  
    36.     trailParts.Remove(trailPart);
    37.     Destroy(trailPart);
    38. }
    39.  
    40. void Flip()
    41. {
    42.     facingRight = !facingRight;
    43.     Vector3 theScale = transform.localScale;
    44.     theScale.x *= -1;
    45.     transform.localScale = theScale;
    46.      
    47.     FlipTrail();
    48. }
    49.  
    50. void FlipTrail()
    51. {
    52.     foreach (GameObject trailPart in trailParts)
    53.     {
    54.         Vector3 trailPartLocalScale = trailPart.transform.localScale;
    55.         trailPartLocalScale.x *= -1;
    56.         trailPart.transform.localScale = trailPartLocalScale;
    57.     }
    58. }
    But I highly recommend you to use some kind of object pool system instead of new/Instantiate/Destroy calls:)
     
    BusyCat likes this.
  6. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Alright , updated the code to our new attempt but no luck. Seems like these ghosts just don't want to flip! Script still runs but acts unchanged.

    I will also haphazard this guess(which might be completely wrong since I know very little);
    The Hierarchy is instantiating the ghosts as "New Game Object", and I feel like maybe it's supposed to create them as trailPart for the "foreach" function to work.
     
    Last edited: Jun 20, 2015
  7. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Weird, it works perfect for my test project. Can you share your project?
     
  8. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    I'd be happy to share with you, if I knew how.
     
  9. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Hmm, upload it on dropbox or something and put the public link here
     
  10. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    [Link Disabled]

    Project called "character move tutorial without rigidbody" inside unity fails direcotry . I should probably rename it haha
     
    Last edited: Jun 20, 2015
  11. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Which scene should I open to test it?:)
     
  12. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    only have 1 scene called Level 1 haha
     
  13. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Weird, Level 1 scene is empty, did you save it?

    There are several scenes inside "ExampleScenes" folder:)
     
  14. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Unity Fails2\Character Move Tutorial without rigidbody\Assets\Levels
     
  15. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Yep, but there's nothing on this scene, there's no any object in the hierarchy
     
  16. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Wow that's wild- those examples are all part of A* pathfinding i think.

    Let me run a "save as" and see if I can send that over
     
  17. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    I promise these scenes aren't empty. They work on my second machine if I go into unity File>Open Project>Open Other>Character Move Tutorial Without Rigidbody>Select folder.

    Guess the whole project is required for the scenes inside it to work
     
  18. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    Oh, such a silly mistake!!!:)

    That's all you need:
    Code (CSharp):
    1. void Start()
    2. {
    3.     InvokeRepeating("SpawnTrailPart", 0, 0.2f); // replace 0.2f with needed repeatRate
    4. }
    5.  
    6. void SpawnTrailPart()
    7. {
    8.     GameObject trailPart = new GameObject();
    9.     SpriteRenderer trailPartRenderer = trailPart.AddComponent<SpriteRenderer>();
    10.     trailPartRenderer.sprite = GetComponent<SpriteRenderer>().sprite;
    11.     trailPart.transform.position = transform.position;
    12.     trailPart.transform.localScale = transform.localScale; // We forgot about this line!!!
    13.     trailParts.Add(trailPart);
    14.  
    15.     StartCoroutine(FadeTrailPart(trailPartRenderer));
    16.     Destroy(trailPart, 0.5f); // replace 0.5f with needed lifeTime
    17. }
    18.  
    19. IEnumerator FadeTrailPart(SpriteRenderer trailPartRenderer)
    20. {
    21.     Color color = trailPartRenderer.color;
    22.     color.a -= 0.5f; // replace 0.5f with needed alpha decrement
    23.     trailPartRenderer.color = color;
    24.  
    25.     yield return new WaitForEndOfFrame();
    26. }
    I completely forgot about trailPart.transform.localScale
    You don't need DestroyTrailPart and FlipTrail methods and you don't need List<GameObject> trailParts:)
     
    kishica likes this.
  19. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Oh that's much better, friend!
    I don't need this either, right?
    Code (CSharp):
    1.  
    2.  
    3. using System.Collections.Generic;
    4.  
    5. trailParts.Add(trailPart);
    Thank you so much!
    if you got another two seconds I'd be delighted to know how to change it's color as well?
     
    Last edited: Jun 21, 2015
  20. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    right.

    Code (CSharp):
    1. trailPartRenderer.color = Color.blue; // or new Color(0, 0, 255) or whatever you want
     
  21. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Fantastic!
    I'll watch that Object pool technique later today and see if I can optimize this to your recommendation.
    I really appreciate it!
    Thanks so much.
     
  22. ANTARES_XXI

    ANTARES_XXI

    Joined:
    Dec 23, 2014
    Posts:
    141
    No problem!
     
  23. kishica

    kishica

    Joined:
    Jul 6, 2015
    Posts:
    2
    Good solution, but there is a way to make the ghost's scale "fading out" ?
     
    Last edited: Jul 6, 2015
  24. TheGabeHD

    TheGabeHD

    Joined:
    Sep 23, 2020
    Posts:
    1
    Apologies for necroposting but this thread was exactly what I was looking for and I wanted to share some modifications I made to the script in case anyone else comes looking.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. // Based on script by ANTARES_XXI on Unity Forums:
    6. // https://forum.unity.com/threads/how-to-create-ghost-or-after-image-on-an-animated-sprite.334079/
    7. // Modified by TheGabeHD
    8.  
    9. /// <summary>
    10. /// Creates fading copies of the sprite displayed by the sprite renderer on this object.
    11. /// </summary>
    12. [RequireComponent(typeof(SpriteRenderer))]
    13. public class AfterImage : MonoBehaviour
    14. {
    15.     [Tooltip("Number of sprites per distance unit.")]
    16.     [SerializeField] private float rate = 2f;
    17.     [SerializeField] private float lifeTime = 0.2f;
    18.  
    19.     private SpriteRenderer baseRenderer;
    20.     private bool isActive = false;
    21.     private float interval;
    22.     private Vector3 previousPos;
    23.  
    24.     private void Start()
    25.     {
    26.         baseRenderer = GetComponent<SpriteRenderer>();
    27.         interval = 1f / rate;
    28.     }
    29.  
    30.     private void Update()
    31.     {
    32.         if (isActive && Vector3.Distance(previousPos, transform.position) > interval)
    33.         {
    34.             SpawnTrailPart();
    35.             previousPos = transform.position;
    36.         }
    37.     }
    38.  
    39.     /// <summary>
    40.     /// Call this function to start/stop the trail.
    41.     /// </summary>
    42.     public void Activate(bool shouldActivate)
    43.     {
    44.         isActive = shouldActivate;
    45.         if (isActive)
    46.             previousPos = transform.position;
    47.     }
    48.  
    49.     private void SpawnTrailPart()
    50.     {
    51.         GameObject trailPart = new GameObject(gameObject.name + " trail part");
    52.  
    53.         // Sprite renderer
    54.         SpriteRenderer trailPartRenderer = trailPart.AddComponent<SpriteRenderer>();
    55.         CopySpriteRenderer(trailPartRenderer, baseRenderer);
    56.  
    57.         // Transform
    58.         trailPart.transform.position = transform.position;
    59.         trailPart.transform.rotation = transform.rotation;
    60.         trailPart.transform.localScale = transform.lossyScale;
    61.  
    62.         // Sprite rotation
    63.         //trailPart.AddComponent<CameraSpriteRotater>();
    64.  
    65.         // Fade & Destroy
    66.         StartCoroutine(FadeTrailPart(trailPartRenderer));
    67.     }
    68.  
    69.     private IEnumerator FadeTrailPart(SpriteRenderer trailPartRenderer)
    70.     {
    71.         float fadeSpeed = 1 / lifeTime;
    72.  
    73.         while(trailPartRenderer.color.a > 0)
    74.         {
    75.             Color color = trailPartRenderer.color;
    76.             color.a -= fadeSpeed * Time.deltaTime;
    77.             trailPartRenderer.color = color;
    78.  
    79.             yield return new WaitForEndOfFrame();
    80.         }
    81.  
    82.         Destroy(trailPartRenderer.gameObject);
    83.     }
    84.  
    85.     private static void CopySpriteRenderer(SpriteRenderer copy, SpriteRenderer original)
    86.     {
    87.         // Can modify to only copy what you need!
    88.         copy.sprite = original.sprite;
    89.         copy.flipX = original.flipX;
    90.         copy.flipY = original.flipY;
    91.         copy.sortingLayerID = original.sortingLayerID;
    92.         copy.sortingLayerName = original.sortingLayerName;
    93.         copy.sortingOrder = original.sortingOrder;
    94.     }
    95. }
    Finally here is what it looks like in a test project:


    EDIT: Changed to rate over distance instead of time.

     
    Last edited: Jan 25, 2022