// ================================= // Namespaces. // ================================= using UnityEngine; using System.Collections.Generic; // ================================= // Define namespace. // ================================= namespace MirzaBeig { namespace Scripting { namespace Effects { // ================================= // Classes. // ================================= public abstract class ParticleForceField : MonoBehaviour { // ================================= // Nested classes and structures. // ================================= // ... protected struct GetForceParameters { public float distanceToForceFieldCenterSqr; public Vector3 scaledDirectionToForceFieldCenter; public Vector3 particlePosition; } // ================================= // Variables. // ================================= // ... [Header("Common Controls")] [Tooltip("Force field spherical range.")] public float radius = Mathf.Infinity; [Tooltip("Maximum baseline force.")] public float force = 5.0f; [Tooltip("Internal force field position offset.")] public Vector3 center = Vector3.zero; public float scaledRadius { get { return radius * transform.lossyScale.x; } } float _radius; float radiusSqr; float forceDeltaTime; Vector3 transformPosition; float[] particleSystemExternalForcesMultipliers; [Tooltip("Force scale as determined by distance to individual particles.")] public AnimationCurve forceOverDistance = new AnimationCurve( new Keyframe(0.0f, 1.0f), new Keyframe(1.0f, 1.0f) ); // If (attached to a particle system): forces will be LOCAL. // Else if (attached to a particle system): forces will be SELECTIVE. // Else: forces will be GLOBAL. new ParticleSystem particleSystem; [Tooltip( "If nothing no particle systems are assigned, this force field will operate globally on ALL particle systems in the scene (NOT recommended).\n\n" + "If attached to a particle system, the force field will operate only on that system.\n\n" + "If specific particle systems are assigned, then the force field will operate on those systems only, even if attached to a particle system.")] public List _particleSystems; int particleSystemsCount; // All required particle systems that will actually be used. List particleSystems = new List(); // Particles in each system. // Second dimension initialized to max particle count // and not modified unless max particle count for that system changes. // Prevents allocations each frame. ParticleSystem.Particle[][] particleSystemParticles; ParticleSystem.MainModule[] particleSystemMainModules; Renderer[] particleSystemRenderers; // ^I could also just put the system and the module in a struct and make a 2D array // instead of having a seperate array for the modules. // Current iteration of the particle systems when looping through all of them. // Useful to derived classes (like for the vortex particle affector). protected ParticleSystem currentParticleSystem; // Parameters used by derived force classes. protected GetForceParameters parameters; // Update even when entire particle system is invisible? // Default: FALSE -> if all particles are invisible/offscreen, update will not execute. [Tooltip( "If TRUE, update even if target particle system(s) are invisible/offscreen.\n\n" + "If FALSE, update only if particles of the target system(s) are visible/onscreen.")] public bool alwaysUpdate = false; // ================================= // Functions. // ================================= // ... protected virtual void Awake() { } // ... protected virtual void Start() { particleSystem = GetComponent(); } // Called once per particle system, before entering second loop for its particles. // Used for setting up based on particle system-specific data. protected virtual void PerParticleSystemSetup() { } // Direction is NOT normalized. protected virtual Vector3 GetForce() { return Vector3.zero; } // ... protected virtual void Update() { } // Add/remove from public list of particles being managed. // Remember that adding specific systems will override all // other contexts (global and pure local). // Duplicates are not checked (intentionally). public void AddParticleSystem(ParticleSystem particleSystem) { _particleSystems.Add(particleSystem); } public void RemoveParticleSystem(ParticleSystem particleSystem) { _particleSystems.Remove(particleSystem); } // ... protected virtual void LateUpdate() { _radius = scaledRadius; radiusSqr = _radius * _radius; forceDeltaTime = force * Time.deltaTime; transformPosition = transform.position + center; // SELECTIVE. // If manually assigned a set of systems, use those no matter what. if (_particleSystems.Count != 0) { // If editor array size changed, clear and add again. if (particleSystems.Count != _particleSystems.Count) { particleSystems.Clear(); particleSystems.AddRange(_particleSystems); } // Else if array size is the same, then re-assign from // the editor array. I do this in case the elements are different // even though the size is the same. else { for (int i = 0; i < _particleSystems.Count; i++) { particleSystems[i] = _particleSystems[i]; } } } // LOCAL. // Else if attached to particle system, use only that. // Obviously, this will only happen if there are no systems specified in the array. else if (particleSystem) { // If just one element, assign as local PS component. if (particleSystems.Count == 1) { particleSystems[0] = particleSystem; } // Else, clear entire array and add only the one. else { particleSystems.Clear(); particleSystems.Add(particleSystem); } } // GLOBAL. // Else, take all the ones from the entire scene. // This is the most expensive since it searches the entire scene // and also requires an allocation for every frame due to not knowing // if the particle systems are all the same from the last frame unless // I had a list to compare to from last frame. In that case, I'm not sure // if the performance would be better or worse. Do a test later? else { particleSystems.Clear(); particleSystems.AddRange(FindObjectsOfType()); } parameters = new GetForceParameters(); particleSystemsCount = particleSystems.Count; // If first frame (array is null) or length is less than the number of systems, initialize size of array. // I never shrink the array. Not sure if that's potentially super bad? I could always throw in a public // bool as an option to allow shrinking since there's a performance benefit for each, but depends on the // implementation case. if (particleSystemParticles == null || particleSystemParticles.Length < particleSystemsCount) { particleSystemParticles = new ParticleSystem.Particle[particleSystemsCount][]; particleSystemMainModules = new ParticleSystem.MainModule[particleSystemsCount]; particleSystemRenderers = new Renderer[particleSystemsCount]; particleSystemExternalForcesMultipliers = new float[particleSystemsCount]; for (int i = 0; i < particleSystemsCount; i++) { particleSystemMainModules[i] = particleSystems[i].main; particleSystemRenderers[i] = particleSystems[i].GetComponent(); particleSystemExternalForcesMultipliers[i] = particleSystems[i].externalForces.multiplier; } } for (int i = 0; i < particleSystemsCount; i++) { if (!particleSystemRenderers[i].isVisible && !alwaysUpdate) { continue; } int maxParticles = particleSystemMainModules[i].maxParticles; if (particleSystemParticles[i] == null || particleSystemParticles[i].Length < maxParticles) { particleSystemParticles[i] = new ParticleSystem.Particle[maxParticles]; } currentParticleSystem = particleSystems[i]; PerParticleSystemSetup(); int particleCount = currentParticleSystem.GetParticles(particleSystemParticles[i]); ParticleSystemSimulationSpace simulationSpace = particleSystemMainModules[i].simulationSpace; ParticleSystemScalingMode scalingMode = particleSystemMainModules[i].scalingMode; // I could also store the transforms in an array similar to what I do with modules. // Or, put all of those together into a struct and make an array out of that since // they'll always be assigned/updated at the same time. Transform currentParticleSystemTransform = currentParticleSystem.transform; Transform customSimulationSpaceTransform = particleSystemMainModules[i].customSimulationSpace; // If in world space, there's no need to do any of the extra calculations... simplify the loop! if (simulationSpace == ParticleSystemSimulationSpace.World) { for (int j = 0; j < particleCount; j++) { parameters.particlePosition = particleSystemParticles[i][j].position; parameters.scaledDirectionToForceFieldCenter.x = transformPosition.x - parameters.particlePosition.x; parameters.scaledDirectionToForceFieldCenter.y = transformPosition.y - parameters.particlePosition.y; parameters.scaledDirectionToForceFieldCenter.z = transformPosition.z - parameters.particlePosition.z; parameters.distanceToForceFieldCenterSqr = parameters.scaledDirectionToForceFieldCenter.sqrMagnitude; if (parameters.distanceToForceFieldCenterSqr < radiusSqr) { float distanceToCenterNormalized = parameters.distanceToForceFieldCenterSqr / radiusSqr; float distanceScale = forceOverDistance.Evaluate(distanceToCenterNormalized); Vector3 force = GetForce(); float forceScale = (forceDeltaTime * distanceScale) * particleSystemExternalForcesMultipliers[i]; force.x *= forceScale; force.y *= forceScale; force.z *= forceScale; Vector3 particleVelocity = particleSystemParticles[i][j].velocity; particleVelocity.x += force.x; particleVelocity.y += force.y; particleVelocity.z += force.z; particleSystemParticles[i][j].velocity = particleVelocity; } } } else { Vector3 particleSystemPosition = Vector3.zero; Quaternion particleSystemRotation = Quaternion.identity; Vector3 particleSystemLocalScale = Vector3.one; Transform simulationSpaceTransform = currentParticleSystemTransform; switch (simulationSpace) { case ParticleSystemSimulationSpace.Local: { particleSystemPosition = simulationSpaceTransform.position; particleSystemRotation = simulationSpaceTransform.rotation; particleSystemLocalScale = simulationSpaceTransform.localScale; break; } case ParticleSystemSimulationSpace.Custom: { simulationSpaceTransform = customSimulationSpaceTransform; particleSystemPosition = simulationSpaceTransform.position; particleSystemRotation = simulationSpaceTransform.rotation; particleSystemLocalScale = simulationSpaceTransform.localScale; break; } default: { throw new System.NotSupportedException( string.Format("Unsupported scaling mode '{0}'.", simulationSpace)); } } for (int j = 0; j < particleCount; j++) { parameters.particlePosition = particleSystemParticles[i][j].position; switch (simulationSpace) { case ParticleSystemSimulationSpace.Local: case ParticleSystemSimulationSpace.Custom: { switch (scalingMode) { case ParticleSystemScalingMode.Hierarchy: { parameters.particlePosition = simulationSpaceTransform.TransformPoint(particleSystemParticles[i][j].position); break; } case ParticleSystemScalingMode.Local: { // Order is important. parameters.particlePosition = Vector3.Scale(parameters.particlePosition, particleSystemLocalScale); parameters.particlePosition = particleSystemRotation * parameters.particlePosition; parameters.particlePosition = parameters.particlePosition + particleSystemPosition; break; } case ParticleSystemScalingMode.Shape: { parameters.particlePosition = particleSystemRotation * parameters.particlePosition; parameters.particlePosition = parameters.particlePosition + particleSystemPosition; break; } default: { throw new System.NotSupportedException( string.Format("Unsupported scaling mode '{0}'.", scalingMode)); } } break; } } parameters.scaledDirectionToForceFieldCenter.x = transformPosition.x - parameters.particlePosition.x; parameters.scaledDirectionToForceFieldCenter.y = transformPosition.y - parameters.particlePosition.y; parameters.scaledDirectionToForceFieldCenter.z = transformPosition.z - parameters.particlePosition.z; parameters.distanceToForceFieldCenterSqr = parameters.scaledDirectionToForceFieldCenter.sqrMagnitude; //particleSystemParticles[i][j].velocity += forceDeltaTime * Vector3.Normalize(parameters.scaledDirectionToForceFieldCenter); if (parameters.distanceToForceFieldCenterSqr < radiusSqr) { // 0.0f -> 0.99...f; float distanceToCenterNormalized = parameters.distanceToForceFieldCenterSqr / radiusSqr; // Evaluating a curve within a loop which is very likely to exceed a few thousand // iterations produces a noticeable FPS drop (around minus 2 - 5). Might be a worthwhile // optimization to check outside all loops if the curve is constant (all keyframes same value), // and then run a different block of code if true that uses that value as a stored float without // having to call Evaluate(t). float distanceScale = forceOverDistance.Evaluate(distanceToCenterNormalized); // Expanded vector operations for optimization. I think this is already done by // the compiler, but it's nice to have for the editor anyway. Vector3 force = GetForce(); float forceScale = (forceDeltaTime * distanceScale) * particleSystemExternalForcesMultipliers[i]; force.x *= forceScale; force.y *= forceScale; force.z *= forceScale; switch (simulationSpace) { case ParticleSystemSimulationSpace.Local: case ParticleSystemSimulationSpace.Custom: { switch (scalingMode) { case ParticleSystemScalingMode.Hierarchy: { force = simulationSpaceTransform.InverseTransformVector(force); break; } case ParticleSystemScalingMode.Local: { // Order is important. // Notice how rotation and scale orders are reversed. force = Quaternion.Inverse(particleSystemRotation) * force; force = Vector3.Scale(force, new Vector3( 1.0f / particleSystemLocalScale.x, 1.0f / particleSystemLocalScale.y, 1.0f / particleSystemLocalScale.z)); break; } case ParticleSystemScalingMode.Shape: { force = Quaternion.Inverse(particleSystemRotation) * force; break; } // This would technically never execute since it's checked earlier (above). default: { throw new System.NotSupportedException( string.Format("Unsupported scaling mode '{0}'.", scalingMode)); } } break; } } Vector3 particleVelocity = particleSystemParticles[i][j].velocity; particleVelocity.x += force.x; particleVelocity.y += force.y; particleVelocity.z += force.z; particleSystemParticles[i][j].velocity = particleVelocity; } } } currentParticleSystem.SetParticles(particleSystemParticles[i], particleCount); } } // ... void OnApplicationQuit() { } // ... protected virtual void OnDrawGizmosSelected() { Gizmos.color = Color.green; Gizmos.DrawWireSphere(transform.position + center, scaledRadius); } // ================================= // End functions. // ================================= } // ================================= // End namespace. // ================================= } } } // ================================= // --END-- // // =================================