shadow clones(Castlevania style) 2D

hi i was wondering if anyone can guide me through how i can create shadow clones for my character?? i just don’t know how to achieve just like Castlevania Symphony of the Night, the way richter moves, it’s awesome but i can’t figure out how they made it since the shadow clone reacts to the sprite moves richter does. Here is a reference image of what i’m trying to explain.

If someone could tell me a method or some info to achieve this would be great, i’ll be in touch, thanks

to all.

This has been asked for several times, so I’ll just pass along a script I wrote awhile back.

Put this on anything with a sprite renderer, enable/disable the component to turn it on and off.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
  
public class SpriteAfterImage : MonoBehaviour {
    [Tooltip("The color each after-image will fade to over its lifetime. Alpha of 0 is recommended")]
    public Color finalColor = Color.clear;
    [Tooltip("The amount of time an after-image will take to fade away.")]
    public float trailLifetime = .25f;
    [Tooltip("The distance this object must move to spawn one after-image.")]
    public float distancePerSpawn = .1f;
    [Tooltip("Optimization - number of after-images to create before the effect starts, to reduce the start-up load.")]
    public int spawnOnStart = 0;
    private SpriteRenderer mainSpriteRenderer;    // the sprite renderer to trail after
    private List<SpriteRenderer> readyObjects;    // the list of objects ready to be shown
    private float distanceTraveledSinceLastSpawn; // the distance this object has moved since the last object was shown
    private Vector3 lastSpawnPosition;            // the position the last object was spawned
    private Color initialColor;
    private void Awake() {
        // get the sprite renderer on this object
        mainSpriteRenderer = GetComponent<SpriteRenderer>();
        initialColor = mainSpriteRenderer.color;
        // initialize the empty list
        readyObjects = new List<SpriteRenderer>();
        // optionally populate list beforehand with objects to use
        for(int i = 0; i < spawnOnStart; i++) {
            readyObjects.Add(makeSpriteObject());
        }
    }
    private void OnEnable() {
        StartCoroutine(trailCoroutine());
    }
    // function to create a sprite gameobject ready for use
    private SpriteRenderer makeSpriteObject() {
        // create a gameobject named "TrailSprite" with a SpriteRenderer component
        GameObject spriteObject = new GameObject("TrailSprite", typeof(SpriteRenderer));
        // parent the object to this object so that it follows it
        spriteObject.transform.SetParent(transform);
        // center it on this object
        spriteObject.transform.localPosition = Vector3.zero;
        // hide it
        spriteObject.SetActive(false);
        return spriteObject.GetComponent<SpriteRenderer>();
    }
    private IEnumerator trailCoroutine() {
        // keep running while this component is enabled
        while(enabled) {
            // get the distance between the current position and the last position
            // a trail object was spawned
            distanceTraveledSinceLastSpawn = Vector2.Distance(lastSpawnPosition, transform.position);
            // if that distance is greater than the specified distance per spawn
            if(distanceTraveledSinceLastSpawn > distancePerSpawn) {
                // if there aren't any objects ready to show, spawn a new one
                if(readyObjects.Count == 0) {
                    // add that object's sprite renderer to the trail list
                    readyObjects.Add(makeSpriteObject());
                }
                // get the next object in the ready list
                SpriteRenderer nextObject = readyObjects[0];
                // set this trailSprite to reflect the current player sprite
                nextObject.sprite = mainSpriteRenderer.sprite;
                // this makes it so that the trail will render behind the main sprite
                nextObject.sortingLayerID = mainSpriteRenderer.sortingLayerID;
                nextObject.sortingOrder = mainSpriteRenderer.sortingOrder - 1;
                // set it loose in the world
                nextObject.transform.SetParent(null, true);
                // match the copy's scale to the sprite's world-space scale
                nextObject.transform.localScale = mainSpriteRenderer.transform.lossyScale;
                // show it
                nextObject.gameObject.SetActive(true);
                // start it fading out over time
                StartCoroutine(fadeOut(nextObject));
                // remove it from the list of ready objects
                readyObjects.Remove(nextObject);
                // save this position as the last spawned position
                lastSpawnPosition = transform.position;
                // reset the distance traveled
                distanceTraveledSinceLastSpawn = 0;
            }
            // wait until next frame to continue the loop
            yield return null;
        }
        // reduce number of sprites back to original pool size
        foreach(SpriteRenderer sprite in this.readyObjects) {
            if(this.readyObjects.Count > spawnOnStart) {
                Destroy(sprite.gameObject);
            } else {
                resetObject(sprite);
            }
        }
    }
    private IEnumerator fadeOut(SpriteRenderer sprite) {
        float timeElapsed = 0;
        // while the elapsed time is less than the specified trailLifetime
        while(timeElapsed < trailLifetime) {
            // get a number between 0 and 1 that represents how much time has passed
            // 0 = no time has passed, 1 = trailLifetime seconds has passed
            float progress = Mathf.Clamp01(timeElapsed / trailLifetime);
            // linearly interpolates between the initial color and the final color
            // based on the value of progress (0 to 1)
            sprite.color = Color.Lerp(initialColor, finalColor, progress);
            // track the time passed
            timeElapsed += Time.deltaTime;
            // wait until next frame to continue the loop
            yield return null;
        }
        // reset the object so that it can be reused
        resetObject(sprite);
    }
    // resets the object so that it is ready to use again
    private void resetObject(SpriteRenderer sprite) {
        // hide the sprite
        sprite.gameObject.SetActive(false);
        // reset the tint to default
        sprite.color = initialColor;
        // parent it to this object
        sprite.transform.SetParent(transform);
        // center it on this object
        sprite.transform.localPosition = Vector3.zero;
        // add it to the ready list
        readyObjects.Add(sprite);
    }
}
3 Likes

Dude this is just awesome!!! it works so amazing that i just can’t describe how happy i am!!! you’re really a master!!!
I appreciate so much!!! thank you!!!

1 Like