Unity Tutorial: Particle Plexus (Part 2)
Use a pool of LineRenderers to robustly visualize connections between particles.
Intro
In the last part of this tutorial series, we setup the basis of our Plexus system. Lines were correctly rendered between particles, and we sampled the colour of each pair.
In this part, we’ll replace our debug lines with LineRenderers.
Using LineRenderers offers us plenty of flexibility, but most importantly, they actually render in game builds (unlike Debug.Draw methods).
LineRenderer
Let’s first create our LineRenderer “template” (prefab) game object. This is the object from which all our lines will be instantiated.
Create a new LineRenderer game object:
Effects → Line
You don’t have to change much.
Ensure the transform is reset.
Position/Rotation: (0, 0, 0), Scale: (1, 1, 1)
Enable Use World Space. ✅
You may wish to change the material.
The settings below are the default for the URP Unlit Particle shader.
You can then save this game object as a prefab, or simply disable it in the editor and leave it leave it alone for now. Either way will work for our code modifications.
Without rendering any lines, we might have something like this.
Code Modifications
We need an exposed property/field to reference the template/prefab LineRenderer.
// Template for lines connecting particles.
public LineRenderer lineRendererPrefab;
We’ll be storing instantiated lines in a private array.
// Line renderer pool.
LineRenderer[] lineRenderers;
We’ll also need to limit the number of lineRenderers.
// Maximum number of lines to render.
public int maxLines = 1000;
And lastly, we’ll be introducing controls for line width.
[Range(0.0f, 1.0f)]
public float lineWidth = 0.2f;
We can create our LineRenderer pool on start.
void Start()
{
// Create line renderer pool.
if (Application.isPlaying)
{
// If line renderers already exist, destroy them.
DestroyAllLineRenderersIfNotNull();
// Create new line renderers.
lineRenderers = new LineRenderer[maxLines];
// Instantiate line renderers from prefab.
// > this transform as parent.
for (int i = 0; i < lineRenderers.Length; i++)
{
lineRenderers[i] =
Instantiate(lineRendererPrefab, transform);
}
}
}
This is self-explanatory: DestroyAllLineRenderersIfNotNull();
If lineRenderers exist, destroy them.
void DestroyAllLineRenderersIfNotNull()
{
if (lineRenderers != null)
{
for (int i = 0; i < lineRenderers.Length; i++)
{
if (lineRenderers[i] != null)
{
DestroyImmediate(lineRenderers[i].gameObject);
}
}
}
}
And we also place this in OnDestroy() to ensure that if this component is destroyed, our instance pool is appropriately destroyed with it.
void OnDestroy()
{
DestroyAllLineRenderersIfNotNull();
}
Finally, we make the following adjustments to our loop:
Below
DebugDrawLineGradient
, we setup and enable lineRenderers from our pool if in Play Mode.Unlike debug lines, we can now make use of GetCurrentSize to scale the ends of the line to match the size of the particles being connected. This is further scaled by the public lineWidth value.
Outside our double-loop, we can disable any remaining lineRenderers.
I use Application.isPlaying to only execute code in Play Mode for line renderers as it’s risky/tricky to manage a pool of instantiated objects in edit mode.
// Compare each particle to every other particle.
int lineRendererCount = 0;
for (int i = 0; i < n; i++)
{
ParticleSystem.Particle particleA = particles[i];
for (int j = i + 1; j < n; j++)
{
ParticleSystem.Particle particleB = particles[j];
// If distance between particles < threshold, draw line.
if (Vector3.Distance(
particleA.position, particleB.position) < minLineDistance)
{
Color colourA = particleA.GetCurrentColor(particleSystem);
Color colourB = particleB.GetCurrentColor(particleSystem);
DebugDrawLineGradient(
particleA.position, particleB.position, colourA, colourB, 10);
if (Application.isPlaying)
{
if (lineRendererCount < lineRenderers.Length)
{
LineRenderer lineRenderer =
lineRenderers[lineRendererCount];
lineRenderer.SetPosition(0, particleA.position);
lineRenderer.SetPosition(1, particleB.position);
lineRenderer.startColor = colourA;
lineRenderer.endColor = colourB;
float sizeA = particleA.GetCurrentSize(particleSystem);
float sizeB = particleB.GetCurrentSize(particleSystem);
lineRenderer.startWidth = sizeA * lineWidth;
lineRenderer.endWidth = sizeB * lineWidth;
lineRenderer.gameObject.SetActive(true);
lineRendererCount++;
}
}
}
}
}
// Disable any remaining line renderers.
if (Application.isPlaying)
{
for (int i = lineRendererCount; i < lineRenderers.Length; i++)
{
lineRenderers[i].gameObject.SetActive(false);
}
}
And- we’re all done with the code!
Full Script
Here’s the full script, dispersed with comments.
I’ve expanded and clarified the names of several elements since Part 1.
using UnityEngine;
[ExecuteAlways]
public class ParticlePlexus : MonoBehaviour
{
// Particle system and particles array/list.
ParticleSystem particleSystem;
ParticleSystem.Particle[] particles;
// Line distance threshold.
public float minLineDistance = 1.0f;
[Space]
// Maximum number of lines to render.
public int maxLines = 1000;
[Range(0.0f, 1.0f)]
public float lineWidth = 0.2f;
[Space]
// Template for lines connecting particles.
public LineRenderer lineRendererPrefab;
// Line renderer pool.
LineRenderer[] lineRenderers;
// Render a line between pA and pB,
// with start/end colours cA and cB.
// n = number of segments to render.
static void DebugDrawLineGradient(
Vector3 pA, Vector3 pB, Color cA, Color cB, uint n)
{
for (uint i = 0; i < n; i++)
{
float t = i / (float)n;
float tNext = (i + 1.0f) / n;
Vector3 a = Vector3.Lerp(pA, pB, t);
Vector3 b = Vector3.Lerp(pA, pB, tNext);
Color c = Color.Lerp(cA, cB, t);
Debug.DrawLine(a, b, c);
}
}
void DestroyAllLineRenderersIfNotNull()
{
if (lineRenderers != null)
{
for (int i = 0; i < lineRenderers.Length; i++)
{
if (lineRenderers[i] != null)
{
DestroyImmediate(lineRenderers[i].gameObject);
}
}
}
}
void OnDestroy()
{
DestroyAllLineRenderersIfNotNull();
}
void LateUpdate()
{
// Get particle system component if null.
if (particleSystem == null)
{
particleSystem = GetComponent<ParticleSystem>();
}
// Initialize particles array if null or size mismatch to max.
int maxParticleCount = particleSystem.main.maxParticles;
if (particles == null || particles.Length != maxParticleCount)
{
particles = new ParticleSystem.Particle[maxParticleCount];
}
// Initialize line renderers if null or size mismatch to maxLines.
if (Application.isPlaying)
{
if (lineRenderers == null || lineRenderers.Length != maxLines)
{
// If line renderers already exist, destroy them.
DestroyAllLineRenderersIfNotNull();
// Create new line renderers.
lineRenderers = new LineRenderer[maxLines];
// Instantiate line renderers from prefab.
// > this transform as parent.
for (int i = 0; i < lineRenderers.Length; i++)
{
lineRenderers[i] =
Instantiate(lineRendererPrefab, transform);
}
}
}
// Load particles from system into our array.
int particleCount = particleSystem.GetParticles(particles);
// Compare each particle to every other particle.
int lineRendererCount = 0;
// Create line renderer pool.
// Leaving this in Update allows for resizing maxLines at runtime.
if (Application.isPlaying)
{
if (lineRenderers == null || lineRenderers.Length != maxLines)
{
// If line renderers already exist, destroy them.
DestroyAllLineRenderersIfNotNull();
// Create new line renderers.
lineRenderers = new LineRenderer[maxLines];
// Instantiate line renderers from prefab.
// > this transform as parent.
for (int i = 0; i < lineRenderers.Length; i++)
{
lineRenderers[i] =
Instantiate(lineRendererPrefab, transform);
}
}
}
// Search particles.
for (int i = 0; i < particleCount; i++)
{
ParticleSystem.Particle particleA = particles[i];
for (int j = i + 1; j < particleCount; j++)
{
ParticleSystem.Particle particleB = particles[j];
// If distance between particles < threshold, draw line.
if (Vector3.Distance(
particleA.position, particleB.position) < minLineDistance)
{
Color colourA = particleA.GetCurrentColor(particleSystem);
Color colourB = particleB.GetCurrentColor(particleSystem);
DebugDrawLineGradient(
particleA.position, particleB.position,
colourA, colourB, 10);
if (Application.isPlaying)
{
if (lineRendererCount < lineRenderers.Length)
{
LineRenderer lineRenderer =
lineRenderers[lineRendererCount];
lineRenderer.SetPosition(0, particleA.position);
lineRenderer.SetPosition(1, particleB.position);
lineRenderer.startColor = colourA;
lineRenderer.endColor = colourB;
float sizeA =
particleA.GetCurrentSize(particleSystem);
float sizeB =
particleB.GetCurrentSize(particleSystem);
lineRenderer.startWidth = sizeA * lineWidth;
lineRenderer.endWidth = sizeB * lineWidth;
lineRenderer.gameObject.SetActive(true);
lineRendererCount++;
}
}
}
}
}
// Disable any remaining line renderers.
if (Application.isPlaying)
{
for (int i = lineRendererCount; i < lineRenderers.Length; i++)
{
lineRenderers[i].gameObject.SetActive(false);
}
}
}
}
Remember to drag in your template/prefab object.
This does not need to be a prefab. You can leave it (deactivated) in the scene and drag it directly into the
lineRendererPrefab
slot without saving it as a prefab.
Hit Play, and that’s it.
You can play around with the particle colours, and have multiple systems.
You can follow me on Twitter/X for more (@TheMirzaBeig)!