Hello Guest

Author Topic: Recommended method for simple particle effects?  (Read 20877 times)

Tochas

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 15
    • View Profile
Re: Recommended method for simple particle effects?
« Reply #15 on: December 13, 2012, 04:43:04 pm »
Hi unikron,

I already have a Bullet9mm prefab which consists of a tk2dSprite with a Bullet script attached, the script has properties as damage, range, firedById, etc.
I also have a ObjectPoolComponent gameObject with an ObjectPool script attached where I am pre-instantiating 200 Bullet9mm.
On the update method of the bullet script I test if the distance between startPosition and the currentPosition is greater than range, then the object is returned to the pool.
On my Weapon script I get a Bullet gameObject from the pool and apply a force to it +  a random slight change on the startPosition and force direction to generate a spread of bullets instead of a single line.
On my CharacterController script I test for any Trigger Enter event and apply the damage if necesary and return the bullet to the pool.

Could you explain how to build the deterministic path?, especially for parabolic ones (as I plan to have grenades) and I guess I didnt get the chain of projectiles thing, does that mean the spread of bullets? or bullets + trail?
should I still use the trigger event to test for collisions? or should I use some sort of function of the collider to test if it contains a specific point/vector

unikronsoftware

  • Administrator
  • Hero Member
  • *****
  • Posts: 9709
    • View Profile
Re: Recommended method for simple particle effects?
« Reply #16 on: December 14, 2012, 12:28:32 am »
Deterministic paths

What usually happens in Unity, especially when using physics, is positions are evaluated every frame from velocity, and that from acceleration using Euler integration. Another downside is you can't create something "in the past". Lets say you have an enemy who rapid fires 10 bullets in rapid succession, followed by a 0.5s pause. Using Unity physics, you'd be limited to each of this bullets firing per physics step. You'd have to do some pretty funky stuff to get bullets firing at a higher rate.

So with this other system, the position is evaluated every frame from a function. The start time of the bullet is passed in to the bullet when it is fired - so all other derivations are performed with this time. Normally you'd pass current time, but you could just as easily pass in time in the past or in the future.

Code: [Select]
float startTime; // start time of the behaviour
float lifeTime; // how long the bullet lives for
float3 startPosition; // store the start position of the bullet

In Update/FixedUpdate, all you need to do is move the position. Lets start with something really simple.

Code: [Select]
float normalizedTime = (Time.time - startTime) / lifeTime;
float clippedNormalizedTime = Mathf.Clamp01(normalizedTime);
transform.position = startPosition + (endPosition - startPosition) * clippedNormalizedTime;
The bullet moves from start position -> endPosition over its lifetime. I've missed out the bit where you kill the gameObject once normalizedTime > 1, but that is easy to add. You know the exact position of the bullet at any given time, accurately and without error.

Now that you have this information, you can then do some more exciting stuff with it. For example, you can change how time affects the function. You can find some functions here: http://wiki.unity3d.com/index.php/Mathfx

Code: [Select]
transform.position = startPosition + (endPosition - startPosition) * FunkyFunction(clippedNormalizedTime);

Instead of directly going from start -> end position, you could apply some basic physics to it.

Code: [Select]
float t = FunkyFunction(clippedNormalizedTime);
transform.position = startPosition + velocity * t + 0.5f * acceleration * t * t;

Or indeed any other function.


For now set the rigidbody to kinematic and simply use the trigger. At a later date, you could try doing custom collision detection, etc. Again, build up the system in such a way that it is simple to switch bits to custom code - it is easy in Unity to code stuff in such a way that is impossible to break away from normal Unity behaviour. It really is up in the air if you will actually need more complicated collision detection than is offered in Unity, so don't bother writing a complex system until you know you need it.


Tochas

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 15
    • View Profile
Re: Recommended method for simple particle effects?
« Reply #17 on: December 14, 2012, 05:26:34 pm »
Thanks, let me implement those functions on my code instead of the physics. I'll post here my results

Tochas

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 15
    • View Profile
Re: Recommended method for simple particle effects?
« Reply #18 on: December 19, 2012, 03:19:41 am »
Hi

I implemented the proposed solution to my bullet system, it works great! thanks

I also applied some of the gathered knlowdege to build a basic Particle Engine script component. hope this code can help someone.

Code: [Select]
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[ExecuteInEditMode]
public enum EmitterType{
Point
}

public class ParticleEngine : MonoBehaviour {

public class tk2dParticle{
private ParticleEffect effect;
public tk2dSprite Shape;
public float LifeTime;
public float StartTime;
public Vector3 StartPosition;
public Vector3 Velocity;
public Vector3 Acceleration;
public float Speed;
public Vector3 SpinFactor;
public Vector3 Scale;
public float StartScaleFactor;
public float FinalScaleFactor;
public float StartAlpha;
public float FinalAlpha;
public Color Tint;
public Color FinalTint;

public tk2dParticle(ParticleEffect effect, tk2dSprite shape){
this.effect = effect;
this.Shape = shape;
}

public void Update(float totalElapsedTime){
float localElapsedTime = (totalElapsedTime - this.StartTime);
float normalizedTime = localElapsedTime / this.LifeTime;
float clippedNormalizedTime = Mathf.Clamp01(normalizedTime);

if(localElapsedTime > this.LifeTime){
this.effect.killParticle(this);
return;
}

Vector3 position = this.StartPosition + this.Velocity * this.Speed * localElapsedTime + 0.5f * this.Acceleration * localElapsedTime * localElapsedTime;
this.Shape.transform.position = position;
this.Shape.transform.Rotate(this.SpinFactor * Time.deltaTime);
this.Shape.scale = this.Scale * MathS.Lerp(this.StartScaleFactor, this.FinalScaleFactor, clippedNormalizedTime);
Color shapeColor = this.Shape.color;
shapeColor = MathS.ColorLerp(this.Tint, this.FinalTint, clippedNormalizedTime);
shapeColor.a = Mathf.Clamp(MathS.Lerp(this.StartAlpha, this.FinalAlpha, clippedNormalizedTime), 0f, 255f);
this.Shape.color = shapeColor;
}
}

[System.Serializable]
public class ParticleEffect{
public bool isEnabled;
//EmiterProperties
public float EmitStart;
public float EmitStop;
public float Rate = 3.0f;
public bool InheritMovement = false;
public EmitterType EmitterType;

//ParticleProperties
public tk2dSprite Shape;
public float LifeSpan = 30.0f;
public float LifeSpanVariation = 20.0f;

public Vector3 Direction;
public Vector3 DirectionVariation;
public float Speed = 1.0f;
public float SpeedVariation = 0.0f;
public Vector3 Acceleration;
public Vector3 Spin;
public Vector3 SpinVariation;
public Vector3 Rotation;
public Vector3 RotationVariation;

public float Scale = 1.0f;
public float ScaleVariation = 0.0f;
public float FinalScale = 1.0f;
public float FinalScaleVariation;

public float Alpha = 255.0f;
public float AlphaVariation;
public float FinalAlpha = 255.0f;
public float FinalAlphaVariation;

public Color Tint = Color.white;
public Color FinalTint = Color.white;


private List<tk2dParticle> availableParticles;
private List<tk2dParticle> activeParticles;
private List<tk2dParticle> particlesToDeactivate;

private float timePerParticleEmision;
private float timeSinceLastEmision;
private ParticleEngine engine;

public ParticleEffect(){
this.EmitStart = 0;
this.EmitStop = 10;
this.Rate = 3.0f;
}

public void Initialize(ParticleEngine engine){
this.engine = engine;
float maxParticles = 0.0f;
if(this.EmitStop == -1)
maxParticles = (this.LifeSpan + LifeSpanVariation +1) * this.Rate;
else
maxParticles = (this.EmitStop - this.EmitStart) * this.Rate;

this.timeSinceLastEmision = 0.0f;
this.timePerParticleEmision = 1.0f / this.Rate;

this.availableParticles = new List<tk2dParticle>((int)maxParticles);
this.activeParticles = new List<tk2dParticle>((int)maxParticles);
this.particlesToDeactivate = new List<tk2dParticle>((int)maxParticles);

tk2dParticle tempParticle = null;
for(int i=0; i < maxParticles; i++){
tk2dSprite obj = (tk2dSprite)Instantiate(this.Shape);
tempParticle = new tk2dParticle(this, obj);
this.killParticle(tempParticle);
}
}

internal void EmitParticle(){
if(this.availableParticles.Count == 0)
return;
tk2dParticle particle = this.availableParticles[0];
this.availableParticles.RemoveAt(0);
this.activeParticles.Add(particle);

GameObject obj = particle.Shape.gameObject;
obj.SetActiveRecursively(true);
if(!this.InheritMovement)
obj.transform.parent = null;
obj.transform.position = this.engine.selfTransform.position;

particle.LifeTime = Random.Range(this.LifeSpan - this.LifeSpanVariation, this.LifeSpan + this.LifeSpanVariation);
particle.StartTime = Time.time;
particle.StartPosition = obj.transform.position;
particle.Velocity = Quaternion.Euler(
Random.Range(this.DirectionVariation.x * -1, this.DirectionVariation.x),
Random.Range(this.DirectionVariation.y * -1, this.DirectionVariation.y),
Random.Range(this.DirectionVariation.z * -1, this.DirectionVariation.z)) * this.Direction;
particle.Velocity.Normalize();
particle.Speed = Random.Range(this.Speed - this.SpeedVariation, this.Speed + this.SpeedVariation);
particle.Acceleration = this.Acceleration;
particle.Shape.transform.Rotate(
Random.Range(this.Rotation.x - this.RotationVariation.x, this.Rotation.x + this.RotationVariation.x),
Random.Range(this.Rotation.y - this.RotationVariation.y, this.Rotation.y + this.RotationVariation.y),
Random.Range(this.Rotation.z - this.RotationVariation.z, this.Rotation.z + this.RotationVariation.z));
particle.SpinFactor = new Vector3(
Random.Range(this.Spin.x - this.SpinVariation.x, this.Spin.x + this.SpinVariation.x),
Random.Range(this.Spin.y - this.SpinVariation.y, this.Spin.y + this.SpinVariation.y),
Random.Range(this.Spin.z - this.SpinVariation.z, this.Spin.z + this.SpinVariation.z));

particle.Shape.scale = this.Shape.scale;
particle.Scale = particle.Shape.scale;
particle.StartScaleFactor = Random.Range(this.Scale - this.ScaleVariation, this.Scale + this.ScaleVariation);
particle.FinalScaleFactor = Random.Range(this.FinalScale - this.FinalScaleVariation, this.FinalScale + this.FinalScaleVariation);

particle.StartAlpha = Random.Range(this.Alpha - this.FinalAlphaVariation, this.Alpha + this.AlphaVariation);
particle.FinalAlpha = Random.Range(this.FinalAlpha - this.FinalAlphaVariation, this.FinalAlpha + this.FinalAlphaVariation);

particle.Tint = this.Tint;
particle.FinalTint = this.FinalTint;
}

internal void killParticle(tk2dParticle particle){
this.particlesToDeactivate.Add(particle);
}

private void deactivateParticle(tk2dParticle particle){
GameObject obj = particle.Shape.gameObject;
obj.SetActiveRecursively(false);
obj.transform.parent = this.engine.selfTransform;
this.activeParticles.Remove(particle);
this.availableParticles.Add(particle);
}

internal void Update (float elapsedTime) {
if(this.engine.isRunning){
bool isEmitting = false;
if(this.EmitStop == -1)
isEmitting = elapsedTime >= this.EmitStart;
else
isEmitting = elapsedTime >= this.EmitStart && elapsedTime <= EmitStop;
if(isEmitting){
this.timeSinceLastEmision += Time.deltaTime;
if(this.timeSinceLastEmision >= this.timePerParticleEmision){
float numOfEmisions = this.timeSinceLastEmision / this.timePerParticleEmision;
numOfEmisions = Mathf.CeilToInt(numOfEmisions);
for(int i=0; i< numOfEmisions; i++)
this.EmitParticle();
this.timeSinceLastEmision = 0.0f;
}
}
}
if(this.activeParticles.Count>0){
foreach(tk2dParticle particle in this.activeParticles)
particle.Update(elapsedTime);
}
if(this.particlesToDeactivate.Count >0){
foreach(tk2dParticle particle in this.particlesToDeactivate)
this.deactivateParticle(particle);
this.particlesToDeactivate.Clear();
}
}
}


public bool AutoStartEngine = true;
public ParticleEffect[] Effects;

internal Transform selfTransform;
internal bool isRunning;
private float startTime;

public void StartEngine(){
this.startTime = Time.time;
this.isRunning = true;
}

public void StopEngine(){
this.isRunning = false;
}

// Use this for initialization
void Start () {
this.selfTransform = this.gameObject.transform;
if(Effects == null)
return;
for(int i=0 ; i< this.Effects.Length; i++)
if(this.Effects[i].isEnabled)
this.Effects[i].Initialize(this);
}

void Awake(){
if(this.AutoStartEngine)
this.StartEngine();
}

// Update is called once per frame
void Update () {
if(Effects == null)
return;
float elapsedTime = (Time.time - this.startTime);
for(int i=0 ; i< this.Effects.Length; i++)
if(this.Effects[i].isEnabled)
this.Effects[i].Update(elapsedTime);
}
}


Any improvement or sugestion would be appreciated

unikronsoftware

  • Administrator
  • Hero Member
  • *****
  • Posts: 9709
    • View Profile
Re: Recommended method for simple particle effects?
« Reply #19 on: December 20, 2012, 03:10:24 am »
Thats awesome. You only need to do any further work if you run into performance issues with this current one.

Tochas

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 15
    • View Profile
Re: Recommended method for simple particle effects?
« Reply #20 on: February 21, 2013, 05:10:34 am »
Hi Unikron,

We have been working for a few weeks now and we have a working prototype now, so we just did a few tests with real devices
on iphone and ipads everything is ok between 40 to 60 fps but with 4g ipods we sometimes have drops below 20 fps.
We are still using the built in physics collision detection for bullets

You mentioned earlier there could be another ways to test if a sprite (aka a bullet) hits another sprite (aka a character)
could you please elaborate on those, so we can see if we can get better performance on older devices

Thanks

unikronsoftware

  • Administrator
  • Hero Member
  • *****
  • Posts: 9709
    • View Profile
Re: Recommended method for simple particle effects?
« Reply #21 on: February 21, 2013, 11:55:08 pm »
Easy thing to try is to use raycasts for bullets instead of colliders. Hopefully they are small enough that you'll get away with doing this. Basically for each frame step raycast forward to where the bullet will be in the current frame and see if it hits anything. If you set up your collision masks properly (i.e. don't try to cast bullets with other bullets and so on) your performance should improve. There are other options but will require more work than this, so I'd try this first to see if it helps.