lunoland

Simple After-Images Effect

Jun 27th, 2020
218
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // Enjoy! For context, this code was created in response to https://www.reddit.com/r/gamedev/comments/hge51j/creating_a_flexible_system_for_afterimage_shadow/fw66qxn/
  2.  
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using UnityEngine;
  6.  
  7. public class AfterImages : MonoBehaviour {
  8.    
  9.         // Something to notice here: The Request and Image classes in this script are just data containers; They don't have any methods/behavior. Most tutorials would probably teach you the object oriented way of solving this problem where you would add an AfterImage component to your image object prefab and scatter your behavior and fields between that class and this one. Stop that! It only ever leads to madness and it's worse design along almost every dimension (extra ceremony, over-generalized, speculative, harder to understand/modify, slower, more memory, worse cache-coherence). In my version, you'll use just one MonoBehavior (instead of one per image or worse), and all the code is in the same place right where it's used, inline. Simple :)
  10.     public class Request {
  11.         public Transform targetTransform;
  12.         public SpriteRenderer targetSpriteRenderer;
  13.         public float elapsed;
  14.         public float duration;
  15.         public float imageFrequency;
  16.         public float imageFrequencyElapsed;
  17.         public float imageDuration;
  18.     }
  19.  
  20.     public class Image {
  21.         public Transform transform;
  22.         public SpriteRenderer spriteRenderer;
  23.         public float elapsed;
  24.         public float duration;
  25.     }
  26.  
  27.  
  28.     #if UNITY_EDITOR
  29.     [Header("Tests")]
  30.     public Transform targetTransform;
  31.     public SpriteRenderer targetSpriteRenderer;
  32.     public float effectDuration = 10f;
  33.     public float imageFrequency = 0.1f;
  34.     public float imageDuration = 2f;
  35.  
  36.     [Space(10)]
  37.     public bool pressThisButtonToTest;
  38.  
  39.     // Here's an easy way to test your code! OnValidate is run every time you change something on this script in the inspector.
  40.     // I wish someone had shown me this when I was starting out in Unity. I used to bind everything to a key press :)
  41.     void OnValidate() {
  42.         if (pressThisButtonToTest) {
  43.             pressThisButtonToTest = false;
  44.             PlayEffect(targetTransform, targetSpriteRenderer, effectDuration, imageFrequency, imageDuration);
  45.         }
  46.     }
  47.     #endif
  48.  
  49.  
  50.     // Create a prefab that is just a single GameObject with a SpriteRenderer attached. Assign that prefab to this variable in the inspector.
  51.     // You could also create this simple object in script, i.e. instantiate a new GameObject and then add the SpriteRenderer component, but it's slower and there's no real advantage.
  52.     [Header("References")]
  53.     public GameObject imagePrefab;
  54.  
  55.  
  56.     List<Request> requests = new List<Request>(32);
  57.     List<Image> images = new List<Image>(128);
  58.  
  59.  
  60.     public void PlayEffect(Transform targetTransform, SpriteRenderer targetSpriteRenderer, float duration, float imageFrequency, float imageDuration) {
  61.         Request request = new Request();
  62.         request.targetTransform = targetTransform;
  63.         request.targetSpriteRenderer = targetSpriteRenderer;
  64.         request.duration = duration;
  65.         request.imageFrequency = imageFrequency;
  66.         request.imageDuration = imageDuration;
  67.  
  68.         requests.Add(request);
  69.     }
  70.  
  71.     void Update() {
  72.        
  73.         // Notice that we're going through the active requests in reverse here since we might be removing some along the way.
  74.         for (int i = requests.Count - 1; i >= 0; i--) {
  75.             requests[i].elapsed += Time.deltaTime;
  76.  
  77.             if (requests[i].elapsed > requests[i].duration) { // We're done with a request when its duration elapses.
  78.                 requests.RemoveAt(i);
  79.                 continue;
  80.             }
  81.                
  82.             if (requests[i].imageFrequencyElapsed == 0f) {
  83.                 GameObject imageObject = Instantiate<GameObject>(imagePrefab, transform); // With object pooling, this is where you'd get an inactive image prefab from your pool. You'd instantiate a bunch of images to re-use in awake.
  84.                 Image image = new Image();
  85.                 image.transform = imageObject.GetComponent<Transform>();
  86.                 image.spriteRenderer = imageObject.GetComponent<SpriteRenderer>();
  87.                 image.duration = requests[i].imageDuration;
  88.  
  89.                 // Use this new image to "clone" the target of the effect. Exactly which fields you copy over or modify will depend on your game and what you want the effect to look like.
  90.                 image.transform.localPosition = requests[i].targetTransform.position;
  91.                 image.transform.localScale = requests[i].targetTransform.lossyScale;
  92.  
  93.                 image.spriteRenderer.sprite = requests[i].targetSpriteRenderer.sprite;
  94.                 image.spriteRenderer.sortingLayerID = requests[i].targetSpriteRenderer.sortingLayerID;
  95.                 image.spriteRenderer.sortingOrder = requests[i].targetSpriteRenderer.sortingOrder - 1;
  96.  
  97.                 Color color = requests[i].targetSpriteRenderer.color;
  98.                 color.r *= 0.66f; color.g *= 0.66f; color.b *= 0.66f;  // You can tweak this later, but let's start out by making the clone's color a bit darker.
  99.                 image.spriteRenderer.color = color;
  100.  
  101.                 images.Add(image);
  102.             }
  103.                
  104.             // Every imageFrequency seconds of game time that passes, we'll reset imageFrequencyElapsed to 0f and create a new image next frame.
  105.             // With an imageFrequency of 0.1f, and a request with a duration of 10f, we will end up creating and destroying 100 objects over the lifetime of the effect.
  106.             // If you had a few enemies using this effect at the same time you could be creating and destroying hundreds of these image objects (which is slow and creates garbage). That sounds needlessly wasteful!
  107.             // So, you can see why object pooling to re-use images would be desirable here.
  108.             requests[i].imageFrequencyElapsed += Time.deltaTime;
  109.             if (requests[i].imageFrequencyElapsed >= requests[i].imageFrequency) {
  110.                 requests[i].imageFrequencyElapsed = 0f;
  111.             }
  112.         }
  113.  
  114.         // After we're done going through the active requests, we'll update all the active images in a similar fashion.
  115.         for (int i = images.Count - 1; i >= 0; i--) {
  116.             images[i].elapsed += Time.deltaTime;
  117.  
  118.             if (images[i].elapsed > images[i].duration) { // This image's lifetime has expired.
  119.                 Destroy(images[i].transform.gameObject); // With an object pool, this is where you'd just deactivate the image's game object and save it for later.
  120.                 images.RemoveAt(i);
  121.                 continue;
  122.             }
  123.  
  124.             // Here is where you can update your images over their lifetime. Maybe you want your images to fade out over their duration, like so:
  125.             Color color = images[i].spriteRenderer.color;
  126.             color.a = 1f - (images[i].elapsed / images[i].duration);
  127.             images[i].spriteRenderer.color = color;
  128.         }
  129.  
  130.     }
  131. }
RAW Paste Data