Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

Advanced search

1402806 Posts in 68129 Topics- by 61756 Members - Latest Member: TeamBavovna

September 30, 2022, 05:06:08 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityDevLogsYear In The Trees
Pages: 1 ... 8 9 [10]
Author Topic: Year In The Trees  (Read 37482 times)
Level 1

now comes good sailing

View Profile WWW
« Reply #180 on: June 26, 2020, 12:48:22 PM »

Only done a brief skim through the devlog but looks really polished and nicely put together. Coffee Posting to follow and to remind myself to look through more thoroughly in the future Smiley

Thank you Ishi! Gomez

Level 1

now comes good sailing

View Profile WWW
« Reply #181 on: June 27, 2020, 12:47:34 PM »

Addendum to my last post on after-image effects: Someone on reddit ended up asking for a beginner-friendly code example in Unity for an effect like this. Since I went through the trouble of writing it up, I figured it'd be good to paste here.


// 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/

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AfterImages : MonoBehaviour {

// 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 :)
public class Request {
public Transform targetTransform;
public SpriteRenderer targetSpriteRenderer;
public float elapsed;
public float duration;
public float imageFrequency;
public float imageFrequencyElapsed;
public float imageDuration;

public class Image {
public Transform transform;
public SpriteRenderer spriteRenderer;
public float elapsed;
public float duration;

public Transform targetTransform;
public SpriteRenderer targetSpriteRenderer;
public float effectDuration = 10f;
public float imageFrequency = 0.1f;
public float imageDuration = 2f;

public bool pressThisButtonToTest;

// Here's an easy way to test your code! OnValidate is run every time you change something on this script in the inspector.
// I wish someone had shown me this when I was starting out in Unity. I used to bind everything to a key press :)
void OnValidate() {
if (pressThisButtonToTest) {
pressThisButtonToTest = false;
PlayEffect(targetTransform, targetSpriteRenderer, effectDuration, imageFrequency, imageDuration);

// Create a prefab that is just a single GameObject with a SpriteRenderer attached. Assign that prefab to this variable in the inspector.
// 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.
public GameObject imagePrefab;

List<Request> requests = new List<Request>(32);
List<Image> images = new List<Image>(128);

public void PlayEffect(Transform targetTransform, SpriteRenderer targetSpriteRenderer, float duration, float imageFrequency, float imageDuration) {
Request request = new Request();
request.targetTransform = targetTransform;
request.targetSpriteRenderer = targetSpriteRenderer;
request.duration = duration;
request.imageFrequency = imageFrequency;
request.imageDuration = imageDuration;


void Update() {

// Notice that we're going through the active requests in reverse here since we might be removing some along the way.
for (int i = requests.Count - 1; i >= 0; i--) {
requests[i].elapsed += Time.deltaTime;

if (requests[i].elapsed > requests[i].duration) { // We're done with a request when its duration elapses.

if (requests[i].imageFrequencyElapsed == 0f) {
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.
Image image = new Image();
image.transform = imageObject.GetComponent<Transform>();
image.spriteRenderer = imageObject.GetComponent<SpriteRenderer>();
image.duration = requests[i].imageDuration;

// 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.
image.transform.localPosition = requests[i].targetTransform.position;
image.transform.localScale = requests[i].targetTransform.lossyScale;

image.spriteRenderer.sprite = requests[i].targetSpriteRenderer.sprite;
image.spriteRenderer.sortingLayerID = requests[i].targetSpriteRenderer.sortingLayerID;
image.spriteRenderer.sortingOrder = requests[i].targetSpriteRenderer.sortingOrder - 1;

Color color = requests[i].targetSpriteRenderer.color;
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.
image.spriteRenderer.color = color;


// Every imageFrequency seconds of game time that passes, we'll reset imageFrequencyElapsed to 0f and create a new image next frame.
// 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.
// 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!
// So, you can see why object pooling to re-use images would be desirable here.
requests[i].imageFrequencyElapsed += Time.deltaTime;
if (requests[i].imageFrequencyElapsed >= requests[i].imageFrequency) {
requests[i].imageFrequencyElapsed = 0f;

// After we're done going through the active requests, we'll update all the active images in a similar fashion.
for (int i = images.Count - 1; i >= 0; i--) {
images[i].elapsed += Time.deltaTime;

if (images[i].elapsed > images[i].duration) { // This image's lifetime has expired.
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.

// Here is where you can update your images over their lifetime. Maybe you want your images to fade out over their duration, like so:
Color color = images[i].spriteRenderer.color;
color.a = 1f - (images[i].elapsed / images[i].duration);
images[i].spriteRenderer.color = color;


A few of the preset effects from my after-images system.

I used a pool of sprite renderers for the after-images and a list of effects requests (these are both just implemented as arrays of structs).
Every frame you go through and update any active requests or images.
For each request, while the effect is active you emit an image every however many seconds according to the effect. The image is alive for some duration determined by the effect and then returns to the pool when it expires.
"Emitting an image" here just means getting a sprite renderer from the pool, copying the target of the effect's current sprite and position to the renderer, and then making any other adjustments you want (e.g. changing the renderer's color, setting some shader properties on its material, etc.)
For each active image, you will update it according to the effect that emitted it, i.e. maybe it needs to fade out over its duration, flicker, change color...whatever you can think of really.

For "shadow clones" style effect, it's a bit more complex. You actually emit all the images at once at the start of the effect, and then have them copy the target but with each image using an increasingly longer delay, e.g. the first image is 0.1 seconds behind the target, the second image is 0.2 seconds behind the target, and so on.
In order to get your images to copy what the target was doing however many seconds ago, you need to sample the target and store enough data for the clone with the longest delay, e.g. if you sample the target 30 times a second and you've got 4 clones and each clone is 0.1 seconds behind the previous clone, then the last clone is 0.4 seconds behind the target so you need to be able to store at least 12 samples (30 * 0.4 = 12)
How much data you need to copy from the target depends on your game, but in my case a "sample" was just struct with a sprite, a position, an x scale (for flipping), and a sorting order.
My sample "buffer" was just a fixed size array that I filled circularly, i.e. the next sample gets written to (lastSampleIndex + 1) % sampleArrayLength
Once the effect begins I start filling this sample buffer with the target's data. Each image that is emitted updates to whatever is in that buffer with an offset relative to how far behind that image is from the target. The images do not begin updating until their delay has elapsed.
e.g. If we wrote the target's latest sample to index 11, the first image which is 0.1 seconds behind the target will update to the sample at index 8 (30 * 0.1 = 3 samples, 11 - 3 = 8 ), the second image which is 0.2 seconds behind the target will update to sample 5, etc. An image doesn't update until the current sample index is >= its sample offset.

Islet Sound
Level 0

View Profile WWW
« Reply #182 on: July 10, 2020, 01:24:12 PM »

Wow, beautiful stuff in all aspects!

Anders Hedenholm - Composer | Soundcloud | Twitter
Pages: 1 ... 8 9 [10]
Jump to:  

Theme orange-lt created by panic