Unity Electric Crackle Effect
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.
csharpusing 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.
Sure, you can add some more logic to your coroutine for this. Here's how:
csharpusing 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.
Sure, you can achieve this by storing the points in a List and perturbing them in the Update method. Here's an example:
csharpusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ElectricCr
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:
csharpusing 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.
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:
csharpusing 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.
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
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
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:
csharpusing 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.
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:
csharpIEnumerator 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.
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:
csharpIEnumerator 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.
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.
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:
csharpIEnumerator 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.
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:
csharpvoid 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.
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:
csharpvoid 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.
Your plan sounds reasonable and is actually a clever use of coroutines. Unity's coroutines are versatile tools and
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!
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:
csharpobject 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.