Shared Chat•
GPT-4

Unity Electric Crackle Effect

July 25, 2023
I'm using Unity3d. I want to make a stylistic looking little electric crackle effect, sort of like a burning bush. Basically I want to use a line renderer and have the bottom point of it anchored to local (0,0,0), then every short amount of time (say randomly from 0.05s to 0.15s) I want the line to grow one segment upwards, but I want it slightly offset from its previous point. I only want the line segment to perhaps go 7-8 segments high, and a total height of 3 units or so. I'm gonna bundle some number of these things together so their effect will be in aggregate quite crackly, I think
ChatGPT

This is a simple enough task for Unity3D and C#. Here's a basic script which creates this effect. It uses Unity's LineRenderer and a Coroutine to add new segments over time.

csharp
using UnityEngine; using System.Collections; public class ElectricCrackle : MonoBehaviour { public int maxSegments = 8; public float totalHeight = 3f; public float minHeightTime = 0.05f; public float maxHeightTime = 0.15f; public float xOffsetMax = 0.1f; public float yOffset = 0.3f; private LineRenderer line; private Vector3 currentPos; void Start() { line = gameObject.AddComponent<LineRenderer>(); line.positionCount = 1; line.startWidth = 0.05f; line.endWidth = 0.05f; currentPos = Vector3.zero; line.SetPosition(0, currentPos); StartCoroutine(GrowLine()); } IEnumerator GrowLine() { for (int i = 1; i < maxSegments; i++) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); line.positionCount++; line.SetPosition(i, currentPos); if (currentPos.y >= totalHeight) { yield break; } } } }

In this script, you can set the maximum number of segments maxSegments, the total height totalHeight, minimum and maximum height time minHeightTime and maxHeightTime, maximum offset for x-axis xOffsetMax and offset for y-axis yOffset.

Attach this script to a GameObject where you want the electric crackle to start. The Start() method initializes the LineRenderer and sets the first position to (0,0,0).

The GrowLine() Coroutine is then called, which gradually adds segments to the LineRenderer. This is done in a for-loop, where in each iteration it waits for a random amount of time between minHeightTime and maxHeightTime, calculates a new position which is slightly offset from the previous position, and adds that to the LineRenderer.

The offset on the x-axis is random between -xOffsetMax and xOffsetMax, and the offset on the y-axis is constant yOffset. This gives the effect of the line crackling upwards with a little side-to-side motion.

If at any point the y-position of the current point exceeds totalHeight, the coroutine stops with yield break.

The overall effect should be a line that grows upwards, crackling side to side slightly as it goes.

That is awesome! How would you amend the above so that each individual line would reach its maximum length, then wait some short amount of time, say 0.2s to 0.4s, and then begin retracing its steps and growing shorter until it is 1 point. After that I want it to endlessly re-loop and restart a fresh new series of random points going up, pause, etc., endlessly
ChatGPT

Sure, you can add some more logic to your coroutine for this. Here's how:

csharp
using UnityEngine; using System.Collections; public class ElectricCrackle : MonoBehaviour { public int maxSegments = 8; public float totalHeight = 3f; public float minHeightTime = 0.05f; public float maxHeightTime = 0.15f; public float xOffsetMax = 0.1f; public float yOffset = 0.3f; public float minPauseTime = 0.2f; public float maxPauseTime = 0.4f; private LineRenderer line; private Vector3 currentPos; void Start() { line = gameObject.AddComponent<LineRenderer>(); line.positionCount = 1; line.startWidth = 0.05f; line.endWidth = 0.05f; currentPos = Vector3.zero; line.SetPosition(0, currentPos); StartCoroutine(GrowLine()); } IEnumerator GrowLine() { while (true) { for (int i = 1; i < maxSegments; i++) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); line.positionCount++; line.SetPosition(i, currentPos); if (currentPos.y >= totalHeight) { yield break; } } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); line.positionCount--; } currentPos = Vector3.zero; } } }

This script has a new outer while-loop in the GrowLine coroutine, which makes the script keep creating new lines indefinitely. After the line grows to its maximum length, it waits for a random time between minPauseTime and maxPauseTime, and then starts removing segments from the line. Once it reaches a single point, the loop starts again.

Please note that since the LineRenderer uses a fixed array of points, removing points from the end will actually make the line shrink from the start. If you want the line to shrink from the end, you would need to manually shift all the points down each time, which would be more computationally expensive. If you just want a stylistic effect and don't care which end the line shrinks from, this simpler method should work fine.

Thank you. I refactored a bit so that it uses GetComponent; I wanted to author the basics of the LineRenderer myself in the prefab. Now I want a little bit more of a change. I would like to record the chosen random points going up, and then every single frame, perturb them slightly while maintaining their original curve chosen during the generation going up part. I want to make them crackle like lightning within a narrow tube of chosen points, not drift around. It seems two coroutines and some shared data might be in order to preserve the step-step-step nature of the add / remove while implementing frame-by-frame jitter.
ChatGPT

Sure, you can achieve this by storing the points in a List and perturbing them in the Update method. Here's an example:

csharp
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ElectricCr
Oop, sorry, everything I said above, plus let me show you my code right now so we can start from there! This is the code you gave me with some slight changes as I outlined above. using UnityEngine; using System.Collections; public class ElectricCrackle : MonoBehaviour { public int maxSegments = 8; public float totalHeight = 3f; public float minHeightTime = 0.05f; public float maxHeightTime = 0.15f; public float xOffsetMax = 0.5f; public float yOffset = 0.3f; public float minPauseTime = 0.2f; public float maxPauseTime = 0.4f; private LineRenderer line; private Vector3 currentPos; void Start() { line = gameObject.GetComponent<LineRenderer>(); line.positionCount = 1; currentPos = Vector3.zero; line.SetPosition(0, currentPos); StartCoroutine(GrowAndShrinkLine()); } IEnumerator GrowAndShrinkLine() { while (true) { for (int i = 1; i < maxSegments; i++) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); line.positionCount++; line.SetPosition(i, currentPos); if (currentPos.y >= totalHeight) { yield break; } } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); line.positionCount--; } currentPos = Vector3.zero; } } }
ChatGPT

Alright, let's continue from there. We'll keep a list of the points and perturb them slightly in each frame using the Update() method. To do this, you'll need to maintain a list of points in your class.

Here's your updated code:

csharp
using UnityEngine; using System.Collections; using System.Collections.Generic; public class ElectricCrackle : MonoBehaviour { public int maxSegments = 8; public float totalHeight = 3f; public float minHeightTime = 0.05f; public float maxHeightTime = 0.15f; public float xOffsetMax = 0.5f; public float yOffset = 0.3f; public float minPauseTime = 0.2f; public float maxPauseTime = 0.4f; public float perturbAmount = 0.01f; private LineRenderer line; private Vector3 currentPos; private List<Vector3> points; void Start() { line = gameObject.GetComponent<LineRenderer>(); line.positionCount = 1; points = new List<Vector3>(); currentPos = Vector3.zero; points.Add(currentPos); StartCoroutine(GrowAndShrinkLine()); } void Update() { for (int i = 0; i < points.Count; i++) { points[i] += new Vector3(Random.Range(-perturbAmount, perturbAmount), 0, 0); line.SetPosition(i, points[i]); } } IEnumerator GrowAndShrinkLine() { while (true) { for (int i = 1; i < maxSegments; i++) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); line.positionCount++; points.Add(currentPos); } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); line.positionCount--; points.RemoveAt(0); } currentPos = Vector3.zero; points.Clear(); points.Add(currentPos); } } }

We added a list points to keep track of all the positions of the line. Every time you add a point in the GrowAndShrinkLine() coroutine, you also add it to points. When you remove a point, you remove it from both the LineRenderer and points.

Then, in Update(), you loop through all the points and add a small random perturbation to the x value, and update the LineRenderer with the new value.

Remember to reset the points list whenever you reset the currentPos, just like you do with the LineRenderer.

The perturbAmount variable controls how much the points are perturbed each frame. You can adjust it to suit your needs. This will create a jittering effect along the x-axis for all points on the line, simulating the electric crackle effect you described.

That's very close... it has a very strange transient line glitch that happens. the glitch seems to happen in the one frame when a fresh point is added. For that one single frame something stands wrong with at least one point for a single frame and it draws an extra streak right down through the rest of the bolt. Can you see how that might be happening? It feels like timing...
ChatGPT

The glitch you're describing might be caused by the fact that we're modifying the points list while simultaneously iterating over it in Update().

What could be happening is that the Update() method begins to run, then partway through a new point is added in the GrowAndShrinkLine() coroutine, causing the Update() method to try to access an index of points that no longer exists, or doesn't have a corresponding index in the LineRenderer yet.

A good way to fix this issue is by introducing a boolean lock that ensures both the coroutine and the Update method are not modifying the points list and the LineRenderer simultaneously.

Here is an example of how you could implement such a lock:

csharp
using UnityEngine; using System.Collections; using System.Collections.Generic; public class ElectricCrackle : MonoBehaviour { public int maxSegments = 8; public float totalHeight = 3f; public float minHeightTime = 0.05f; public float maxHeightTime = 0.15f; public float xOffsetMax = 0.5f; public float yOffset = 0.3f; public float minPauseTime = 0.2f; public float maxPauseTime = 0.4f; public float perturbAmount = 0.01f; private LineRenderer line; private Vector3 currentPos; private List<Vector3> points; private bool isModifyingLine; void Start() { line = gameObject.GetComponent<LineRenderer>(); line.positionCount = 1; points = new List<Vector3>(); currentPos = Vector3.zero; points.Add(currentPos); StartCoroutine(GrowAndShrinkLine()); } void Update() { if (!isModifyingLine) { for (int i = 0; i < points.Count; i++) { points[i] += new Vector3(Random.Range(-perturbAmount, perturbAmount), 0, 0); line.SetPosition(i, points[i]); } } } IEnumerator GrowAndShrinkLine() { while (true) { for (int i = 1; i < maxSegments; i++) { isModifyingLine = true; yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); line.positionCount++; points.Add(currentPos); isModifyingLine = false; } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { isModifyingLine = true; yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); line.positionCount--; points.RemoveAt(0); isModifyingLine = false; } currentPos = Vector3.zero; points.Clear(); points.Add(currentPos); } } }

In this code, I added a boolean isModifyingLine which is set to true before we modify the line or points, and then set to false after. In the Update() method, we first check isModifyingLine before perturbing the points. This ensures that the coroutine and the Update method are not trying to modify the points list or the LineRenderer at the same time.

Hm, I don't think that will help because it makes the crackle stop during the most of the wait frame. Let me post the code I have. I changed the removal to take away the last point in the line when the line is retracting but that's not the problem. Could the problem be that we are adding a new position but not updating the line renderer and for a moment the newly-added point shows up as (0,0,0)?
ChatGPT

Yes, that's a good point. The line renderer's positions array is indeed initialized with zeros, so if you add a new position but don't set its value immediately, it will default to (0,0,0) for one frame. That could explain the sudden streaks you're seeing.

You can resolve this issue by adding the new point to the `points

Oop, here's the code as it stands:
ChatGPT

I apologize for the confusion, but it seems your latest message does not include the code you were intending to share. If you could please send it again, I

HA, my bad... here it is at last. using UnityEngine; using System.Collections; using System.Collections.Generic; public class ElectricCrackle : MonoBehaviour { public int maxSegments = 8; public float totalHeight = 3f; public float minHeightTime = 0.05f; public float maxHeightTime = 0.15f; public float xOffsetMax = 0.5f; public float yOffset = 0.3f; public float minPauseTime = 0.2f; public float maxPauseTime = 0.4f; public float perturbAmount = 0.01f; private LineRenderer line; private Vector3 currentPos; private List<Vector3> points; void Start() { line = gameObject.GetComponent<LineRenderer>(); line.positionCount = 1; points = new List<Vector3>(); currentPos = Vector3.zero; points.Add(currentPos); StartCoroutine(GrowAndShrinkLine()); } void Update() { for (int i = 0; i < points.Count; i++) { points[i] += new Vector3(Random.Range(-perturbAmount, perturbAmount), 0, 0); line.SetPosition(i, points[i]); } } IEnumerator GrowAndShrinkLine() { while (true) { for (int i = 1; i < maxSegments; i++) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); line.positionCount++; points.Add(currentPos); } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); line.positionCount--; points.RemoveAt(line.positionCount); // remove last point } currentPos = Vector3.zero; points.Clear(); points.Add(currentPos); } } }
ChatGPT

Thank you for the code. I see what you mean about the potential for a new point defaulting to (0,0,0) causing a visual glitch. Let's modify the points list first before adjusting the line.positionCount. That way, when a new point is added, we immediately set its position, and this should eliminate any frame of glitch.

Here's your updated code:

csharp
using UnityEngine; using System.Collections; using System.Collections.Generic; public class ElectricCrackle : MonoBehaviour { public int maxSegments = 8; public float totalHeight = 3f; public float minHeightTime = 0.05f; public float maxHeightTime = 0.15f; public float xOffsetMax = 0.5f; public float yOffset = 0.3f; public float minPauseTime = 0.2f; public float maxPauseTime = 0.4f; public float perturbAmount = 0.01f; private LineRenderer line; private Vector3 currentPos; private List<Vector3> points; void Start() { line = gameObject.GetComponent<LineRenderer>(); line.positionCount = 1; points = new List<Vector3>(); currentPos = Vector3.zero; points.Add(currentPos); StartCoroutine(GrowAndShrinkLine()); } void Update() { for (int i = 0; i < points.Count; i++) { points[i] += new Vector3(Random.Range(-perturbAmount, perturbAmount), 0, 0); line.SetPosition(i, points[i]); } } IEnumerator GrowAndShrinkLine() { while (true) { for (int i = 1; i < maxSegments; i++) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); points.Add(currentPos); line.positionCount = points.Count; // Changed this line } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); points.RemoveAt(line.positionCount - 1); // remove last point line.positionCount = points.Count; // Changed this line } currentPos = Vector3.zero; points.Clear(); points.Add(currentPos); } } }

By first adding the new position to points and then updating the line.positionCount, we ensure that when the LineRenderer creates the new point, we immediately provide a position for it. The same is true for the removal process - first remove from points, then adjust line.positionCount.

This should prevent any (0,0,0) defaults from appearing for a frame and causing a visual glitch.

Hm, I think there might still be an issue... I'm looking at the for loop that adds new points and I think the problem is that this loop ALSO must write that new point to the lineRenderer, right? Otherwise it would still be (0,0,0) until Update() got around to writing it...
ChatGPT

Yes, you're correct. For the least possible latency, the position should be set immediately after being added. Here's how the modification of the GrowAndShrinkLine() method would look:

csharp
IEnumerator GrowAndShrinkLine() { while (true) { for (int i = 1; i < maxSegments; i++) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); points.Add(currentPos); line.positionCount = points.Count; line.SetPosition(line.positionCount - 1, currentPos); // set the position of new point immediately } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); points.RemoveAt(line.positionCount - 1); // remove last point line.positionCount = points.Count; } currentPos = Vector3.zero; points.Clear(); points.Add(currentPos); } }

The addition of line.SetPosition(line.positionCount - 1, currentPos); makes sure that we're setting the position of the newly added point right after it's created, thus it won't default to (0,0,0) even for a single frame.

That was it! Now I want to work on this line a bit: currentPos += new Vector3(Random.Range(-xOffsetMax, xOffsetMax), yOffset, 0); I want to change it so that it randomly chooses a new offset, adds and proposes a new position based on currentPos, but then checks this proposed position's distance from the vertical (eg, distance purely on X and Z planes). if the proposed distance is greater than 1.0 unit then instead it subtracts the original random offset and uses that point instead.
ChatGPT

The logic you described can be implemented with a simple if condition that checks the distance of the proposed point from the vertical axis (i.e., the XZ plane). Here's the modified code:

csharp
IEnumerator GrowAndShrinkLine() { while (true) { for (int i = 1; i < maxSegments; i++) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); float randomOffsetX = Random.Range(-xOffsetMax, xOffsetMax); Vector3 proposedPos = currentPos + new Vector3(randomOffsetX, yOffset, 0); if (new Vector2(proposedPos.x, proposedPos.z).magnitude > 1.0f) // check the distance on XZ plane { proposedPos = currentPos - new Vector3(randomOffsetX, 0, 0); // subtract the original random offset } currentPos = proposedPos; points.Add(currentPos); line.positionCount = points.Count; line.SetPosition(line.positionCount - 1, currentPos); } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { yield return new WaitForSeconds(Random.Range(minHeightTime, maxHeightTime)); points.RemoveAt(line.positionCount - 1); // remove last point line.positionCount = points.Count; } currentPos = Vector3.zero; points.Clear(); points.Add(currentPos); } }

In this modification, the line float randomOffsetX = Random.Range(-xOffsetMax, xOffsetMax); generates a random X offset. The proposedPos variable represents the proposed position with this random X offset applied.

We then check if the distance of this proposed position from the vertical axis (ignoring the Y component by using Vector2) exceeds 1.0. If it does, we negate the X offset to ensure it stays within 1.0 unit distance on the XZ plane. If it's already within 1.0 unit distance, we use the proposedPos as it is.

Thanks! I like your style of procedural generation. I changed things around a bit more... I wanted the crackle to be in X and Z axes. Thanks for the help. Here's where my final crackle stands so I'm gonna leave it like this for now: using UnityEngine; using System.Collections; using System.Collections.Generic; public class ElectricCrackle : MonoBehaviour { public int minSegments = 8; public int maxSegments = 12; public float totalHeightLimit = 3f; public float minStepTime = 0.05f; public float maxStepTime = 0.15f; public float lateralOffsetMax = 0.25f; // for both X and Z directions public float minYOffset = 0.2f; public float maxYOffset = 0.3f; public float minPauseTime = 0.2f; public float maxPauseTime = 0.4f; public float perturbAmount = 0.01f; private LineRenderer line; private Vector3 currentPos; private List<Vector3> points; void Start() { line = gameObject.GetComponent<LineRenderer>(); line.positionCount = 1; points = new List<Vector3>(); currentPos = Vector3.zero; points.Add(currentPos); StartCoroutine(GrowAndShrinkLine()); } void Update() { line.positionCount = points.Count; for (int i = 0; i < points.Count; i++) { points[i] += Random.insideUnitSphere * perturbAmount; line.SetPosition(i, points[i]); } } IEnumerator GrowAndShrinkLine() { while (true) { int numSegments = Random.Range( minSegments, maxSegments); for (int i = 1; i < numSegments; i++) { yield return new WaitForSeconds(Random.Range(minStepTime, maxStepTime)); Vector3 randomLateralOffset = new Vector3( Random.Range(-lateralOffsetMax, lateralOffsetMax), 0, // move only flat for now, so we can negate if need be Random.Range(-lateralOffsetMax, lateralOffsetMax)); Vector3 proposedPos = currentPos + randomLateralOffset; if (new Vector2(proposedPos.x, proposedPos.z).magnitude > 1.0f) // check the distance on XZ plane { proposedPos = currentPos - randomLateralOffset; // subtract the original random offset } currentPos = proposedPos; //always go upwards float chosenYOffset = Random.Range( minYOffset, maxYOffset); currentPos.y += chosenYOffset; points.Add(currentPos); } yield return new WaitForSeconds(Random.Range(minPauseTime, maxPauseTime)); while (line.positionCount > 1) { yield return new WaitForSeconds(Random.Range(minStepTime, maxStepTime)); points.RemoveAt(0); // remove first point } currentPos = Vector3.zero; points.Clear(); points.Add(currentPos); } } }
ChatGPT

This code looks great! It seems you've made some thoughtful enhancements. Adding the ability to vary the number of segments is a nice touch, and separating the calculation of the lateral offset from the vertical one should give you more control over the behavior of the effect.

The use of Random.insideUnitSphere to add a little jitter in the Update function is a nice touch. It should give the lightning a more lifelike, random movement, while still keeping it within a controlled range due to the multiplication by perturbAmount.

The changes you've made to the coroutine where you calculate the random lateral offset separately from the y offset also make the code clearer and easier to understand. Negating the lateral offset if the proposed position would be more than 1 unit away from the center is a smart move and should help to keep the lightning within a defined boundary.

Overall, this code should give you a nice, controlled, yet randomized lightning effect. Good job on these enhancements! If you need any more assistance or if you want to enhance this effect further, feel free to ask.

I put a bunch of these items together in the same spot and the effect is indeed like a crackling bush. However, at start they are all synchronized to grow out at once. What would be the dead-simplest way to change the above code "prewarm" one of these items so once actual gameplay starts it is in a nominally random point in one of its cycles?
ChatGPT

Great to hear the combined effect is working out!

To "prewarm" or desynchronize each crackle, you could add a random delay at the start of the GrowAndShrinkLine coroutine. This would give each instance a different starting point in its cycle. Here's how you could implement it:

csharp
IEnumerator GrowAndShrinkLine() { // Random initial delay before starting the cycle. yield return new WaitForSeconds(Random.Range(0f, maxPauseTime)); while (true) { // The rest of your code... } }

This code will cause each instance of the crackle to wait for a random duration (up to maxPauseTime) before it starts its cycle. Because the delay is random, each instance will start at a different time, creating a desynchronized effect.

Hm, that definitely desynchronizes all the instances, but it still has the issue that they haven't truly been "pre-warmed" into some state of their inner loop. Can you think of any other way to prewarm these coroutines before starting them?
ChatGPT

The best way to "pre-warm" a coroutine would be to run some of its cycles immediately on start. In your case, to ensure the crackle is already in a random state of its cycle, you can add a phase on start where you "fast forward" through a random number of its steps.

In Start(), you can call a new function PrewarmCrackle() that performs a random number of iterations through the GrowAndShrinkLine loop immediately, without waiting for any delay. Here's how you might implement this:

csharp
void Start() { // Your initialization code... PrewarmCrackle(); StartCoroutine(GrowAndShrinkLine()); } void PrewarmCrackle() { int numPrewarmCycles = Random.Range(minSegments, maxSegments); for (int i = 0; i < numPrewarmCycles; i++) { // Copy the body of your 'GrowAndShrinkLine' coroutine here, but without the 'yield return new WaitForSeconds' lines. // You may want to factor out the code inside 'GrowAndShrinkLine' into a separate method so you don't have to duplicate code. } }

In PrewarmCrackle, the GrowAndShrinkLine cycle is run immediately a random number of times, which will effectively "fast forward" the crackle to a random point in its cycle. This will give the appearance that the crackle has been running since before the game started, and ensure that each crackle is in a unique state when the game begins.

Rather than copying the code, is there any way I could create the IEnumerator and prewarm it directly before passing it to StartCoroutine() ?
ChatGPT

In theory, you could manually iterate the coroutine to "prewarm" it, but it's worth noting that this approach might not be very straightforward or recommended due to how coroutines and yield return work in Unity.

Coroutines in Unity are essentially state machines. When you use yield return, it halts the execution of the coroutine until the yielded condition is met. This means, if you try to manually iterate the coroutine, you would still need to take care of the yield return statements somehow.

That said, you could still prewarm it with some careful management. Here's an example of how you could do this:

csharp
void Start() { line = gameObject.GetComponent<LineRenderer>(); line.positionCount = 1; points = new List<Vector3>(); currentPos = Vector3.zero; points.Add(currentPos); // Get an instance of the coroutine IEnumerator coroutine = GrowAndShrinkLine(); // Prewarm the coroutine PrewarmCoroutine(coroutine, Random.Range(minSegments, maxSegments)); // Now start the coroutine StartCoroutine(coroutine); } void PrewarmCoroutine(IEnumerator coroutine, int steps) { // "Fast forward" through the coroutine by manually moving its execution forward. for (int i = 0; i < steps; i++) { // This would normally halt the coroutine until the next frame, but since we're manually // iterating it, it just moves the execution forward to the next yield return. if (coroutine.Current is WaitForSeconds) { // Skip waiting } else if (coroutine.Current == null) { // Do nothing } // Move to the next step of the coroutine. if (!coroutine.MoveNext()) { // If MoveNext returns false, the coroutine has completed. Break the loop. break; } } }

This would simulate the execution of a number of steps of the GrowAndShrinkLine coroutine, skipping any waiting periods, and then starts the coroutine as normal. This effectively "prewarms" the coroutine by advancing its state before it starts.

However, I would like to emphasize that this approach might be seen as a misuse of coroutines, as it works against their intended design as a way to easily manage delayed or spread-out execution. The more straightforward, maintainable, and recommended way to solve your problem would still be to refactor the code inside the coroutine into separate methods, which can be called independently as needed for prewarming.

Rather than "misuse of coroutines," I prefer to think of it as liberating coroutines to do even more than they were originally envisioned to do. :) Here was my version of prewarmer code, which of course uses "inside knowledge" that this coroutine never ends and that it also interoperates with the Update() pumps, which in this case act as a co-coroutine. In my next phase I will rename Update() to something like JitterLineRandomly() so that I can call it during the prewarm phase, then reconnect JitterLineRandomly() to the stock Update() function to guard against adding extra code in Update() in the future, code which probably wouldn't want to be prewarmed. Thoughts?
ChatGPT

Your plan sounds reasonable and is actually a clever use of coroutines. Unity's coroutines are versatile tools and

Oop, here is the code: { IEnumerator coro = GrowAndShrinkLine(); // random prewarm via coroutine abuse for (int i = Random.Range( 0, 100); i > 0; i--) { coro.MoveNext(); Update(); } StartCoroutine(coro); }
ChatGPT

Your solution to "prewarm" the coroutine is indeed very creative and it indeed takes advantage of Unity's powerful Coroutine system in a unique way. It's a clear demonstration of your understanding of how coroutines work in Unity, and it's an interesting way to handle the initialization of the effect.

Just remember to be cautious when using IEnumerator.MoveNext() directly. While it works in this scenario, it could lead to unexpected behavior in other contexts. Coroutines in Unity are designed to be used with StartCoroutine() and StopCoroutine(), and Unity handles their execution behind the scenes.

By calling MoveNext(), you're manually advancing the IEnumerator, bypassing the normal flow of control Unity provides with coroutines. This could potentially cause issues in more complex scenarios where you're relying on Unity's coroutine scheduling, especially when dealing with WaitForSeconds, WaitForEndOfFrame, WaitForFixedUpdate, and other timing-based commands.

In your specific case, you seem to be well aware of what you're doing and it's a controlled scenario, so the approach is quite innovative. But it's always good to remember the caveats. Good job on your creativity!

Good point. I realize now that I am mis-simulating the number of jitter calls per main outer "step" of the coroutine. Is there any way for me to introspect the contents of the WaitForSeconds() object returned by coro.Current to pump the jitter extra times before moving onto the main coro.MoveNext() loop?
ChatGPT

If you need to introspect the WaitForSeconds object, you could do so by checking if the current coroutine's object is of that type. If it is, you can cast it and access the properties. In the case of WaitForSeconds, however, the only property is 'keepWaiting' which does not give you the actual time delay.

As of my knowledge cutoff in 2021, Unity does not provide a way to access the duration of a WaitForSeconds object once it has been created. This could be a design decision made by Unity to keep the implementation simple and maintain good performance.

Here is an example of how you might attempt to introspect the coroutine's current return value:

csharp
object currentYieldInstruction = coro.Current; if (currentYieldInstruction is WaitForSeconds) { WaitForSeconds wait = (WaitForSeconds)currentYieldInstruction; if (wait.keepWaiting) { // This won't give you the delay duration, but it indicates that the coroutine is currently in a waiting state. } }

However, you will not be able to extract the delay duration from a WaitForSeconds instance. To properly simulate the number of jitter calls per main outer "step", you would probably need to design your coroutine or pre-warming logic in such a way that you manually keep track of the time delays that you're using with WaitForSeconds.

This could be as simple as storing the last delay in a private field or property before using it in a WaitForSeconds object, but would require you to rework your coroutine and pre-warming logic to take this into account. You could then use this stored delay to calculate how many jitters should occur before the next coroutine step.

Continue this conversation
|Terms of use|Privacy policy