- Home /
Manually Placing Particles
Hi all,
I have some questions around the proper methodology to manually place particles at specific locations in world space. The intention is that the particles are generated once and persist.
In my particle system I have everything disabled except for "Renderer" and "Emission". Part of my questions below relate to the "Shape" module.
Here's some code. I've renamed/removed unrelated code:
public class BuildPositions : MonoBehaviour {
public bool showDebugMessages;
public ParticleSystem theParticleSystem;
public Transform objectPrefab;
public float fDistanceModifier = 1f;
private ParticleSystem.Particle[] arrParticles;
private List<systems> listObjects;
private float farDistance = 0f;
public bool isPositioned;
// Use this for initialization
void Start () {
// Generate the empty game objects and place them
GenerateObjects ();
}
void Update () {
}
private void LateUpdate()
{
// If not positioned
if (!isPositioned)
{
// Start positioning
isPositioned = true;
StartCoroutine(setPositions());
}
}
IEnumerator setPositions()
{
// Get the # particles
int numParticlesAlive = theParticleSystem.GetParticles (arrParticles);
// Set the scale of the system
theParticleSystem.gameObject.transform.localScale = new Vector3 (farDistance, farDistance, farDistance);
Debug.Log (numParticlesAlive.ToString ());
// Change only the particles that are alive
for (int i = 0; i < numParticlesAlive; i++) {
arrParticles [i].position = new Vector3 (listObjects [i].x, listObjects [i].y, listObjects [i].z);
yield return null;
}
// Apply the particle changes to the particle system
theParticleSystem.SetParticles (arrParticles, numParticlesAlive);
// Enable the particle system - play on awake is checked
theParticleSystem.gameObject.SetActive (true);
}
void GenerateObjects() {
//listObjects = <A LIST GENERATED FROM SQL QUERY UP TO 100,000 rows)
// Each of these objects has an x,y,z coordinate - see myObj class below
if(showDebugMessages)
Debug.Log (listObjects.Count.ToString ());
int i = 0;
// Clear anything existing
theParticleSystem.Clear ();
// Set the count
theParticleSystem.maxParticles = listObjects.Count;
// Create the array to hold the particle instances
arrParticles = new ParticleSystem.Particle[listObjects.Count];
Vector3 home = new Vector3 (0f, 0f, 0f);
foreach (myObj obj in listObjects)
{
float x = obj.x * fDistanceModifier;
float y = obj.y * fDistanceModifier;
float z = obj.z * fDistanceModifier;
Vector3 pos = new Vector3 (x,y,z);
//Debug.Log(pos.ToString());
if(Vector3.Distance(pos, home) > farDistance)
{
farDistance = Vector3.Distance(pos, home);
}
// Creating an empty game object here and placing it so I can easily jump to where I expect a particle to be
Transform clone = (Transform)Instantiate(objectPrefab, pos, Quaternion.identity);
clone.name = obj.name;
clone.SetParent(gameObject.transform);
i++;
}
// Multiply by 2 to get a full radius
farDistance *= 2f;
}
}
// Instance class
class myObj {
public float x;
public float y;
public float z;
public string name;
}
Here are my questions:
In my particle system am I REQUIRED to set a "Shape" in order to place particles? If I do not the particles all smoosh together around world origin. I have code above (farDistance) which should get me the full radius of a sphere where all these coordinates should live.
Am I getting/setting the particles correctly? I've seen a number of posts and followed the basic methodology but I am just not getting the particles to place. They still "randomize" locations each time I start the game in the editor. I've even tried setting just 1 particle to 0,0,0 and no matter what that particle seems to go where it wants.
I know I'm missing something here... Can anyone help me out with some insight?
Ok so changing my setPositions function to this DOES place the particles correctly... just one at a time. It takes about 10 $$anonymous$$utes to render them all out. Brutal.
Like so:
for (int i = 0; i < listObjects.Count; i++) {
Vector3 pos = new Vector3(listObjects [i].x * fDistance$$anonymous$$odifier, listObjects [i].y * fDistance$$anonymous$$odifier, listObjects [i].z * fDistance$$anonymous$$odifier);
theParticleSystem.Emit(pos,Vector3.zero,10f,900f,new Color32(255,255,0,255));
yield return null;
}
I also tried it like this thinking that if I set each particle manually and then fired the emit it would work. This did not work.
IEnumerator setPositions()
{
theParticleSystem.gameObject.transform.localScale = new Vector3 (farDistance, farDistance, farDistance);
// Change only the particles that are alive
for (int i = 0; i < listSystems.Count; i++) {
arrParticles [i].position = new Vector3 (listObjects[i].x * fDistance$$anonymous$$odifier, listObjects[i].y * fDistance$$anonymous$$odifier, listObjects[i].z * fDistance$$anonymous$$odifier);
arrParticles [i].velocity = Vector3.zero;
arrParticles [i].size = 10f;
arrParticles [i].lifetime = 900f;
arrParticles [i].color = new Color32(255,255,0,255);
yield return null;
}
theParticleSystem.SetParticles (arrParticles, arrParticles.Length);
theParticleSystem.Emit (arrParticles.Length);
}
Is there no other way to do this other than emit them one at a time?
Answer by r-dotfunky · Aug 26, 2015 at 08:22 AM
Ok so I do believe I finally got this nailed down. I'm running around ~36k particles right now and it starts in less than 2 seconds.
To answer my own questions:
No - shape is NOT required. In my example below I have disabled everything in the particle system except for the renderer. Getting a possible radius of the entire space is cool but not required.
I was missing some critical steps which I've marked with "Critical Step!" below. a. After telling the code how many particles to create... you need to "GetParticles" and pass in the array to be populated with the base generic length particles. b. When modifying a particle it is not enough to just set the values on the particle. One must set the particle values and then ASSIGN that particle back into the array in its proper location. You'll see that where I have 'arrParticles [i] = par;'. c. It seems that any manipulation of particles should be done in LateUpdate. I tried Update and just didn't work. d. Coroutines do not work here. I tried one in LateUpdate and it did not work. e. Unity Editor will fool you. You may try to navigate to where you expect a particle to be and see nothing. Mess around with the position of the camera in the area of the particle and eventually it (and the rest) will show up. Not sure if this is a Unity bug or not. Main Camera seems unaffected.
Here's the code.
public class testParticlePlacer : MonoBehaviour {
public bool showDebugMessages;
public ParticleSystem theParticleSystem;
public Transform objPrefab;
public float fDistanceModifier = 1f;
private Vector3 mainCamPos;
private ParticleSystem.Particle[] arrParticles;
private List<myObj> listObjects;
private float farDistance = 0f;
public bool isPositioned;
/*
USAGE NOTES
1. In the particle system disable EVERYTHING except for Renderer.
*/
// Use this for initialization
void Start () {
// Setup the database connection
_dbManager = EDExplorationCompanion.instance.dbManagerObject;
GenerateObjects ();
}
// Update is called once per frame
void Update () {
// Keep the position of the main camera
mainCamPos = Camera.main.transform.position;
}
private void LateUpdate()
{
if (!isPositioned)
{
// This whole block should only fire once when isPositioned is false. Set to true to prevent firing on the next frame
isPositioned = true;
// Create a new array to hold the particles we expect to display from the particle system. In this case use the listObjects count
// to tell how many particles are needed
arrParticles = new ParticleSystem.Particle[listObjects.Count];
// Critical step! You MUST fire GetParticles and pass the array you just created into GetParticles
// This gets a generic set of particles to modify and set BACK to the system once completed
theParticleSystem.GetParticles(arrParticles);
// Run a loop through the number of particles.. i is our counter value
for (int i = 0; i < arrParticles.Length; i++) {
// For ease of use assign a ParticleSystem.particle for the current object
ParticleSystem.Particle par = arrParticles [i];
// Now assign the values you wish to change via 'par'.
par.position = new Vector3 (listObjects [i].x * fDistanceModifier, listObjects [i].y * fDistanceModifier, listObjects [i].z * fDistanceModifier);
par.velocity = Vector3.zero;
par.size = 10f;
par.lifetime = 900f;
par.color = new Color32(255,255,0,255);
// Critical step! You MUST assign the modified particle BACK into the current position of the particles
// This seems to be how the data is saved to the particle.
arrParticles [i] = par;
}
// Apply the particle changes to the particle system
// Note there is no need for Emit or Play here. Execute SetParticles and it works.
theParticleSystem.SetParticles (arrParticles, arrParticles.Length);
}
}
void GenerateObjects() {
//listObjects should be populated by whatever means you need - DB, array, text file et
listObjects = SOMETHING YOU SPECIFY
int i = 0;
theParticleSystem.Clear ();
theParticleSystem.maxParticles = listObjects.Count;
foreach (myObj obj in listObjects)
{
// Get the X Y and Z coordinates for the object
float x = obj.x * fDistanceModifier;
float y = obj.y * fDistanceModifier;
float z = obj.z * fDistanceModifier;
// Set the position to a vector3
Vector3 objPos = new Vector3 (x,y,z);
if(showDebugMessages)
Debug.Log("Position of obj: " + objPos.ToString());
// Create the game objects that sit at the positions in space where objs exist
// Set the position and parent the object accordingly
Transform clone = (Transform)Instantiate(objPrefab, objPos, Quaternion.identity);
clone.name = obj.name;
clone.SetParent(gameObject.transform);
i++;
}
if(showDebugMessages)
Debug.Log("Generate GenerateObjects has finished.");
}
} // end class
Answer by Artifact-Jesse · Nov 21, 2018 at 07:59 AM
@r-dotfunky thanks for your answer, it helped me arrive at this one.
For those stumbling across this while using Shuriken in Unity 2018.x, the solution I finally hunted down was to use the overloaded ParticleSystem.Emit(EmitParams emitParams, int count)
particles, rather than trying to create them from data like above. This seems much more convenient, compact, and has the added benefit of not completely breaking the normal operations of particles in the system, which for some reason happened between @r-dotfunky's answer and mine; so now you can, like, change particle velocities in LateUpdate() and stuff again (not shown in this example, but I can whip one up if needed).
Disclaimer: I haven't tested to see how performant this solution is since I'm only emitting one particle every few frames, but I expect it's pretty good. There's no frame-by-frame overhead beyond that of a normal particle system and you don't have to regenerate the entire array of particles every frame, plus lots of stuff is cached.
Here's my complete, functional example of a fairly generic particle pool:
using UnityEngine;
[RequireComponent(typeof(ParticleSystem))]
public class ParticlePool : MonoBehaviour
{
private int lastParticleIndex = 0; // keeps track of our oldest particle (for deletion)
// these will all be inited in Initialize() on Start()
private ParticleSystem particleSys; // our object's particle system
private ParticleSystem.Particle[] particles; // our reusable array of particles
private ParticleSystem.EmitParams emitParams; // reusable emitparams
private int maxParticles = 0; // total number of particles in our scene before re-using
private void Awake()
{
Initialize(); // initialize all of our member variables
}
public void CreateParticle(Vector3 position, float size, Vector3 velocity, float angularVelocity)
{
// if we're at our particle count limit, kill our oldest particle.
int activeParticles = particleSys.GetParticles(particles);
/// this thing sucks. Read the Unity docs VERY carefully to understand...
/// basically the parameter (particles) is an out parameter which will
/// write out the existing particles in the particle system to our
/// reusable array. After that, we can directly modify the particles
/// and then when we're done, write the particles back into the
/// particle system with ParticleSystem.SetParticles( particles, count )
if (activeParticles >= maxParticles)
{
// set lifetime to -1 to kill the particle
particles[lastParticleIndex].remainingLifetime = -1;
// we need to reset start lifetime to a normal value, too or the particle will still have infinite lifetime
particles[lastParticleIndex].startLifetime = 1;
lastParticleIndex++; // keep track of oldest particle
if (lastParticleIndex >= maxParticles) lastParticleIndex = 0;
// have to re-write
particleSys.SetParticles(particles, particles.Length); // write those pesky particles back into our ParticleSystem
}
// set up params for this particle, you can use whatever you want (see unity docs for EmitParams for what's available)
emitParams.angularVelocity = angularVelocity;
emitParams.position = position;
emitParams.startSize = size;
emitParams.velocity = velocity;
emitParams.startLifetime = float.MaxValue; // float.MaxValue makes the particle's lifetime infinite
particleSys.Emit(emitParams, 1);
particleSys.Play();
}
void Initialize()
{
if (particleSys == null)
particleSys = GetComponent<ParticleSystem>();
maxParticles = particleSys.main.maxParticles;
if (particles == null || particles.Length < particleSys.main.maxParticles)
particles = new ParticleSystem.Particle[particleSys.main.maxParticles];
}
}
Edit: I should mention, the particle system in this example is just bare mininum with a renderer. Not looping, no start on awake, simulation space set to World. MaxParticles is respected by the above script.
Hope that helps someone.
Your answer
Follow this Question
Related Questions
PS Custom Vertex Streams to Particle Trail 0 Answers
How can I get the specific particle of an OnParticleCollision? 0 Answers
Set Particle lifetime by distance, not time? 2 Answers
Shuriken: Make Particle System Completely Scale 2 Answers
ParticleAnimator doesn't change color of particles in ParticleSystem(Shuriken) 0 Answers