Update 2014.08.22
I did figure out a much better approach. Read the update here!
Update: 2014.07.22
So... my hunch “this is probably too expensive in the end” turned out to be true. I’ve optimized it a little, but this is too expensive to apply to large numbers of game objects (but perfectly fine for just a few). I’ll figure out a more efficient approach for the same effect since I’ll require a lot of debris, and include the new method in a future post.
I love debris. Physical particle debris is one of the things that, in my opinion, brings modern low-res games to life. It's also a technique that simply wasn't feasible in the 8- and 16-bit eras due to hardware limitations. This is another reason why I would wholeheartedly argue that low-res is not simply "retro," but a legitimate, timeless aesthetic style all its own. It is familiar yet evolving. I digress!
There are two limitations in Unity 4.3+ that I've had to work around to get the effect that I wanted.
The first is that the built in shuriken particle system does not support 2D physics. This means that I must use individual game objects for my debris if I want them to physically interact with the rest of my 2D colliders. So far, this hasn't been a performance issue as I am keeping my debris in a shared object pool – a topic for a future post.
The second issue is that Unity won't let you specify gravity in the z-direction; it's strictly limited to x and y for 2D. My project is top-down, so to get gravity effects along the z-axis, I'm forced to use old school visual tricks to sell it. One such technique is growing sprites as they are closer to you and shrinking as they move away, and also moving them along parabolic arcs in the y direction (while adding this to their "real" y-position).
Going back to my introduction, debris should feel "alive" and dynamic, and in my case I wanted each particle to collide with the environment, and to slightly bounce until they came to rest. You can see in this example that the bounce is quite subtle, but it serves to "ground" everything and when you see it without the bounce, you'll realize just how much it adds. I've always loved this sort of attention to detail in my favorite games, and they really add polish and "glue" to otherwise static objects.
For the bouncing itself, I'm running some simple tweens (using the excellent GoKit tweening library) in the y direction on each particle. The rest of their motion path all comes from a physical force I apply when they are spawned. I created a simple vertical bounce behavior script that I can attach to game objects to simulate this effect, and you can see a version of the script here. This script actually has some issues and will most likely change dramatically before the game is done, but the general idea will be the same. In fact, this is probably too expensive in the end, but so far my performance is still good:
using UnityEngine;
using System.Collections;
public class VerticalBounce : MonoBehaviour
{
public float minAmplitude = 0.25f;
public float maxAmplitude = 0.5f;
public float minBounceTime = 0.25f;
public float maxBounceTime = 0.5f;
public int numberOfBounces = 2;
public float bounceChance = 0.33f;
private float amplitude;
private float bounceTime;
private Vector3 currentPosition;
private float _offset;
[HideInInspector]
public float offset
{
get { return _offset; }
set { _offset = value; }
}
void OnEnable()
{
// sometimes, don't bounce at all
if (Random.value > bounceChance)
{
return;
}
// choose a random amplitude and time between the specified range
amplitude = Random.Range(minAmplitude, maxAmplitude);
bounceTime = Random.Range(minBounceTime, maxBounceTime);
// create a tween chain that raises and lowers the offset x numberOfBounces times
GoTweenChain bounceChain = new GoTweenChain();
for (int i = 1; i <= numberOfBounces; i++)
{
_offset = 0f;
GoTween riseTween = new GoTween(this, bounceTime, new GoTweenConfig()
.floatProp("offset", amplitude, false)
.setEaseType(GoEaseType.SineOut)
);
GoTween fallTween = new GoTween(this, bounceTime, new GoTweenConfig()
.floatProp("offset", -amplitude, false)
.setEaseType(GoEaseType.SineIn)
);
bounceChain.append(riseTween).append(fallTween);
// each bounce should have a lower amplitude and take less time
amplitude /= 2;
bounceTime /= 2;
}
bounceChain.setOnCompleteHandler((x) => {
_offset = 0f;
});
bounceChain.play();
}
void FixedUpdate()
{
// add the fake vertical bounce on top of the current y position (additive)
currentPosition = gameObject.transform.localPosition;
gameObject.transform.localPosition = new Vector3(currentPosition.x, (currentPosition.y + _offset), currentPosition.z);
}
}