Hey, guys. I’ve been trying to find a work-around for this issue for a couple of weeks now, and keep coming up short. The orbital velocity of a particle is always effected by the rotation of the particle transform, even when the space is set to world. So for example, let’s say you want to make a gun for your character using the particle system that shoots around the sphere of a planet. This would be super easy using the particle system orbital velocity, except for the fact that as the character rotates, the particles rotate their velocity accordingly, even with space set to world.
I dove pretty deep working on scripts that manually move particles based on position, but the issues you run into with this is that much of the calculations that the particle system uses for it’s modules revolve around velocity, not placement over time, so many things such as trails and stretched billboard effects do not work properly if you are updating position directly.
Does anyone have a clever hack that gets around this? Obviously the goal is to have the particles continue on their initial orbital velocity even as the particle system transform rotates. Any help appreciated. Hoping devs will chime in on this one since I’ve found other posts about this same issue with no resolution. Video showing effect of particle transform rotation below. @richardkettlewell .
Thanks for the response, Richard! I did spend some time on your second suggestion and wasn’t able to get anywhere with it. I did look into option 3 as well, but it seems a bit beyond my capabilities, so I ended up creating a bug report. I appreciate that you guys have a lot on your plate, and this may not be high up on the list, but if you are able to find a way to accomplish this, I really feel like it would add a lot of versatility to the particle system. Thanks for all you do!
Hey, I’m stoked you’re working on this! I thought it was just going to go in what I’m sure is an already overwhelming pile of suggestions. I appreciate you taking the time.
I tried it out, but unfortunately it’s not exactly what I was getting at. I probably wasn’t doing a very good job of explaining it. It looks like what you’ve got going now it that when the orbital velocity is set to world space, the initial velocity of all the particles is always in the same direction, even when the transform rotates, but the velocity of the already enabled particles changes as the transform rotates. What I was more looking for is that it would be great if the particles already enabled continue on their original orbital velocity, even if the transform rotation is changed. Hard to describe, but I’ve got a couple of videos below that will hopefully shed some light.
This is what you have at the moment. As you can see, as the transform rotates, the particles update their orbital velocity to line up with the transform, even though there initial velocity is always in the same direction. This is with the ParticleSystem.main set to local. When set to world, no rotation is allowed at all.
I haven’t fully digested this yet, i will do that next week but, a random idea - could rotating the shape transform in the shape module help here? instead of rotating the game object.
So unfortunately the shape doesn’t really apply here since I’m not using the it to create the spherical travel, just the starting point of the particles, so the velocity direction isn’t effected by the particle spawn shape. Good thought though. I tested it out just to make sure, but no dice. If only it were that easy sigh…
Ok, I’ve read your reply in more detail this morning, and I think it’s perhaps a bit specific for us to implement built in to the UI. (because each particle would need to track its original rotation direction, in order to preserve it when the system’s rotation changes)
However, I think you could do this with the job system support for the particle system. I’ll see if I can put something together.
The problems lie with particle trails and the render modes in the renderer module. The only way I was able to code what I wanted was to manually change the position of the particles over time, which it seems that a lot of the modules in the particle system use velocity, not position in their math. This is especially obvious when you try to use Stretched Billboard in the Render Mode, since it doesn’t stretch with the particle movement at all. Particle trails kind of work, but it’s buggy as you can see in this vid.
The script is just kind of a clumsy strong-arm way to get what I wanted. I found the most reliable method was to enable all the particles right off the bat so their id is set in the array, then disable them. Then I create a separate array with Pose so that I can set and reference the initial position and rotation of the transform when the particle is enabled. A good bit of this doesn’t apply, but starting at line 181 is where I set the position of the particles.
EDIT: To clarify, the particle system emission and velocity are set to zero. The code is solely responsible for enabling the particles and moving them.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
[System.Serializable]
public class ParticleClass
{
public bool isOccupied;
public Pose pose;
public float angle;
public float startTime;
public float size;
}
public class ParticleDamage : MonoBehaviour
{
public bool isEnabled, isPersistent, isLaser;
public float damage, fireRate, speed, stagger, timeAlive, subTimeAlive, radius, damageRate;
public float collisionRad, damageRad;
public ParticleSystem ps, subps, muzzleFlash;
ParticleSystem.Particle[] particles, subParticles;
public LayerMask colliderMask, damageMask;
public List<ParticleClass> parts;
private void Awake()
{
clock = 999;
//transform.localPosition = new Vector3(0, -radius, 0);
CreatePartsList();
SetParticles();
}
private void CreatePartsList()
{
if (isLaser) { return; }
parts = new List<ParticleClass>();
int i = 0;
while(i < ps.main.maxParticles)
{
i++;
parts.Add(new ParticleClass());
}
}
private void FixedUpdate()
{
AddParticle();
Position();
Damage();
RemoveOld();
AgeSubs();
Lighting();
}
private void SetParticles()
{
if (isLaser) { return; }
//Primary Particles
particles = new ParticleSystem.Particle[ps.main.maxParticles];
ps.Emit(ps.main.maxParticles);
ps.GetParticles(particles);
for (int i = 0; i < particles.Length; i++)
{
particles[i].position = Vector3.zero;
particles[i].remainingLifetime = -1;
parts[i].size = particles[i].size;
}
ps.SetParticles(particles, particles.Length);
//subParticles
if (subps != null)
{
subParticles = new ParticleSystem.Particle[subps.main.maxParticles];
subps.Emit(subps.main.maxParticles);
subps.GetParticles(subParticles);
for (int i = 0; i < subParticles.Length; i++)
{
subParticles[i].position = Vector3.zero;
subParticles[i].remainingLifetime = -1;
}
subps.SetParticles(subParticles, subParticles.Length);
}
}
float clock;
public int numParticles, subNumParticles;
public bool emit;
int count;
void AddParticle()
{
clock += Time.deltaTime;
if (clock < fireRate || !isEnabled) { return; }
clock = 0;
for (int i = 0; i < parts.Count; i++)
{
if (!parts[i].isOccupied)
{
count = i;
break;
}
}
parts[count].isOccupied = true;
parts[count].startTime = Time.time;
//set position and rotation for new particle
parts[count].pose.rotation.eulerAngles = transform.rotation.eulerAngles;
transform.localEulerAngles = new Vector3(0, 90 + Random.Range(-stagger, stagger), 0);
parts[count].pose.position = transform.localPosition;
particles[count].position = parts[count].pose.position;
ps.SetParticles(particles, count); //Needed to keep trail from skipping back to zero mark
particles[count].remainingLifetime = timeAlive;
if (muzzleFlash != null) { muzzleFlash.Emit(1); }
}
float damageClock;
void Damage()
{
damageClock += Time.deltaTime;
if (damageClock < damageRate || particles.Length == 0) { return; }
damageClock = 0;
// iterate through the alive particles for damage
for (int i = 0; i < particles.Length; i++)
{
if (parts[i].isOccupied)
{
var position = particles[i].position;
Collider[] hitColliders = Physics.OverlapSphere(position, collisionRad, colliderMask);
if (hitColliders.Length > 0)
{
if (subps != null)
{
subParticles[i].remainingLifetime = subTimeAlive;
subParticles[i].startLifetime = subTimeAlive;
subParticles[i].position = position;
subps.SetParticles(subParticles, subParticles.Length);
}
hitColliders = Physics.OverlapSphere(position, damageRad, damageMask);
foreach (Collider enemy in hitColliders)
{
enemy.GetComponent<EnemyHealth>().incomingPulseDamage += damage;
}
if (!isPersistent && !isLaser)
{
particles[i].remainingLifetime = -1;
}
}
}
}
}
void AgeSubs()
{
if (isLaser) { return; }
if (subps != null)
{
for (int i = 0; i < subParticles.Length; i++)
{
if (subParticles[i].remainingLifetime >= 0)
{
subParticles[i].remainingLifetime -= Time.deltaTime;
}
}
subps.SetParticles(subParticles, subParticles.Length);
}
}
void Position() //Moves the particles over a sphere of radius based off initial rotation
{
if (isLaser) { return; }
for (int i = 0; i < parts.Count; i++)
{
if (parts[i].isOccupied)
{
particles[i].remainingLifetime -= Time.deltaTime;
parts[i].angle += Time.deltaTime * speed; // update angle
Vector3 direction = Quaternion.AngleAxis(parts[i].angle, parts[i].pose.forward) * parts[i].pose.up; // calculate direction from center - rotate the up vector Angle degrees clockwise
particles[i].position = (direction * radius) + parts[i].pose.position; // update position based on the direction and the radius
if (partSize.keys.Length > 0) { particles[i].size = parts[i].size * partSize.Evaluate(0.01f / particles[i].remainingLifetime); }
}
}
ps.SetParticles(particles, parts.Count); //This must be included to apply changes to particles
}
void RemoveOld()
{
if (isLaser) { return; }
for (int i = 0; i < particles.Length; i++)
{
if (particles[i].remainingLifetime <= 0)
{
ResetPart(i);
//particles[i].position = Vector3.zero;
}
}
}
void ResetPart(int index)
{
parts[index].isOccupied = false;
parts[index].pose = new Pose();
parts[index].angle = 0;
parts[index].startTime = 0;
}
public List<LightFlicker> lights;
void Lighting()
{
if (lights.Count == 0) { return; }
if (isEnabled)
{
foreach (LightFlicker light in lights)
{
light.on = true;
}
}
else
{
foreach (LightFlicker light in lights)
{
light.on = false;
}
}
}
public AnimationCurve partSize;
float variablesClock;
void Variables()
{
variablesClock += Time.deltaTime;
if (variablesClock < 0.25f) { return; }
variablesClock = 0;
var emission = ps.emission;
emission.rateOverTime = fireRate;
}
}
I had a very similar thought, but couldn’t get the math to work out. It’s not exactly my strong point. This seemed closer than my own attempts, but obviously still has some issues. Here’s the new codes for position.
void Position() //Moves the particles over a sphere of radius based off initial rotation
{
if (isLaser) { return; }
float invDt = 1.0f / Time.deltaTime;
for (int i = 0; i < parts.Count; i++)
{
if (parts[i].isOccupied)
{
var oldPosition = particles[i].position; //Added
particles[i].remainingLifetime -= Time.deltaTime;
parts[i].angle += Time.deltaTime * speed; // update angle
Vector3 direction = Quaternion.AngleAxis(parts[i].angle, parts[i].pose.forward) * parts[i].pose.up; // calculate direction from center - rotate the up vector Angle degrees clockwise
var newPosition = (direction * radius) + parts[i].pose.position; //Added
particles[i].velocity = (newPosition - oldPosition) * invDt; //Added
if (partSize.keys.Length > 0) { particles[i].size = parts[i].size * partSize.Evaluate(0.01f / particles[i].remainingLifetime); }
}
}
ps.SetParticles(particles, parts.Count); //This must be included to apply changes to particles
}
Here’s the result. It’s almost as if the radius value is fluctuating, but it’s not. Not sure what’s causing that jitter.
My last ideas: as well as using this new code, also change the Maximum Particle Timestep in the Time Project Settings, to be something very large. To force the internal update to do the whole thing in 1 slice.
Or, take over the particle simulation too, and call ps.Simulate, with false for the fixedTimeStep parameter, so you can control the simulation yourself too.
And finally, make sure the velocity isn’t left applied for more than 1 frame. with this approach, you’ll need to set it every frame. For example when isOccupied==false, do you need to set the velocity to 0?
Thanks for the ideas, Richard. Unfortunately, none of these seemed to have any effect at all. I’m stumped. I’ll keep fiddling with it, but not I’m not sure what’s going on. Also, I noticed when turning on the trails that not all the particles seem to be producing a trail, which leads me to wonder if there isn’t something more nefarious wrong with my code. Perhaps the way I’m referencing the particles. Just not sure.
@richardkettlewell I feel that velocity over lifetime relies on the transform values of the game object the particle system is attached to rather than utilizing the particle’s own velocity. This is a bigger headache when trying to use a subemitter, as it’s basing it’s orbit on the game object regardless if you’re using local or world space.