Alpha Accesswebsite_badges_steamwishlistwebsite_badges_steamwishlist

Tag results

Better parabolic motion for bounces


bouncingDebris2.gif.32

In a previous post, I detailed a methods of using tweens to give explosion debris some bounce and life. I knew at the time that this would be expensive for a large number of particles – but it was the right look and I at least wanted the temporary solution in the game. Consider it a living mockup, waiting to be optimized.

Recently I came across this post on gamasutra by Mohan Rajagopalan describing design philosophies and techniques behind 2D jumps. Sure enough, the parabolic arc equation was exactly what I needed for my debris bounce, which could be considered debris “jumping.” Here’s the classic formula:

y(t) = v_0 + \frac{gt^2}{2}

The implementation of this as a Unity component is quite straightforward , and here’s a simple version of the Update() function of the behavior:

public void Bounce(int numberOfBounces)
{
	// init
	startTime = Time.time;
	lastYOffset = 0.0f;
	bounceNumber = numberOfBounces;
	bounceVelocity *= 0.5f;
}

void Update()
{
	// if done with bounces, stop
	if (bounceNumber <= 0)
	{
		return;
	}

	// otherwise, calculate current yoffset
	if (bounceNumber > 0)
	{
		// apply classic parabolic formula: h(t) = vt + (gt^2 / 2)
		float time = (Time.time - startTime);
		yOffset = (bounceVelocity * time) + ((gravity * time * time) / 2);
		
		// add to the current position, but subtract last y offset (additive behavior)
		// since this could be moving in the y-axis from other forces as well
		transform.position = new Vector3
		(
			transform.position.x,
			transform.position.y + yOffset - lastYOffset,
			transform.position.z
		);
		lastYOffset = yOffset;

		// if hitting the "floor", bounce again
		if (yOffset <= 0f)
		{
			BounceTimes(bounceNumber - 1);
		}
	}
}

One thing to note is that I subtract the previously calculated y-offset since I am adding the new y-offset to the current position each update. This allows this parabolic offset to be additive to any other motion the game object is already undergoing. This is perfect in my case, since the debris is being ejected by an explosive force, and I am just adding this to the y-axis to simulate bouncing in overhead 2D space. As stated in the original post, the root of all this is the simple fact that Unity doesn’t allow gravity in the z-axis for 2D projects.

Object pooling for performance in Unity


objectPoolExample64

Object pooling is a technique that you’re bound to need at some point in your game dev career. In fact, you may even be using some form of it (or your game engine might) without realizing it. In a nutshell, it is equivalent to caching. Rather than instantiating objects at runtime as you need them, you should instantiate everything you will need (or estimate you will need) beforehand, and pull from this “pool” when you need them. Instead of destroying objects when they are “dead,” you simply disable them and put them back into the pool.

The classic example in games are bullets: rather than instantiating a new bullet every time characters fire their weapons, and then destroying them when off screen or hitting another character, you simply disable them and move them off screen, then enable and move them back on screen as needed. Obviously, it doesn’t just apply to bullets, so keep an eye out for anything in your game that is instantiated frequently. The most common cases are projectiles and effects, such as explosions, dust, debris, etc, however you could even go as far as using it for common shared UI components for the ultimate in snappy performance.

The benefits to object pooling are: fewer game hiccups and lag due to expensive instantiation and destruction, and better estimation of memory needs up front since pooled objects are always loaded. If you have a lot of objects, you will definitely notice and feel these improvements right away. The only downside is from the developer’s perspective: it requires more attention to initialization of variables in objects as they are enabled and disabled, rather than created and destroyed. In Unity, this means you should handle this logic in OnEnable() and OnDisable() in game objects. Similarly, rather than creating and destroying, you are enabling and disabling the pooled objects.

There are many object pooling scripts in the Asset Store, but it’s easy enough to create your own if you understand the concept. As with all in-house solutions, you can tailor it slightly to better fit your exact needs when crafting your own. I’m including the full script here in case you’d like to use it in your own projects (or improve upon it). Some notes about my implementation:

  • The ObjectPool.cs¬†script is attached to an empty game object in the scene.
  • It’s developed as a singleton, so it has a static reference: ObjectPool.shared
  • It’s designed to store any number of pooled object types, with each containing a game object, the amount to start in the pool, and whether or not the amount can dynamically grow or not. You set these up simply on the game object (see the screenshot example below).
  • To facilitate storing anything, the main data structure is a Dictionary full of Lists, with the keys of the dictionary being the names of the prefabs / game objects to be stored.
  • GetPooledObject()¬†returns a regular instantiated prefab if there is no associated pool, so it’s safe to call from scripts that don’t know if there’s a pool or not.

objectPool

Note: This hasn’t really been stress tested too much, but so far it seems to be working great. I’m not sure if making it too flexible (a Dictionary of Lists) is actually more expensive than instantiating and destroying, thus defeating the purpose, but I imagine it isn’t and I’ll run some tests later.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[System.Serializable]
public class PooledObject
{
	public Object objectToPool;
	public int pooledAmount = 20;
	public bool canGrow = true;
}

public class ObjectPool : MonoBehaviour
{
	public static ObjectPool shared;
	public PooledObject[] pooledObjectTypes;

	private Dictionary<string, List<GameObject>> pool;

	void Awake()
	{
		// set up singleton reference
		if (shared == null)
		{
			shared = this;
		}
	}

	void Start()
	{
		// set up the pool and instantiate all initial objects
		pool = new Dictionary<string, List<GameObject>>();
		for (int i = 0; i < pooledObjectTypes.Length; i++)
		{
			PooledObject pooledObjectType = pooledObjectTypes[i];
			pool.Add(pooledObjectType.objectToPool.name, new List<GameObject>());
			for (int i2 = 0; i2 < pooledObjectType.pooledAmount; i2++)
			{
				AddPooledObject(pooledObjectType.objectToPool);
			}
		}
	}

	GameObject AddPooledObject(Object newObject)
	{
		GameObject pooledObject = Instantiate(newObject) as GameObject;
		if (shared.gameObject.transform != null)
		{
			pooledObject.transform.parent = shared.gameObject.transform;
		}
		pooledObject.SetActive(false);
		pool[newObject.name].Add(pooledObject);
		return pooledObject;
	}

	public void DisableAllPooledObjects()
	{
		foreach(string key in pool.Keys)
		{
		    foreach(GameObject pooledObject in pool[key])
		    {
		    	pooledObject.SetActive(false);
		    }
		}
	}

	public List<GameObject> GetPooledObjects(Object gameObject)
	{
		if (!pool.ContainsKey(gameObject.name))
		{
			return null;
		}
		return pool[gameObject.name];
	}

	public GameObject GetPooledObject(Object gameObject)
	{
		// return a regular non-pooled version if there is no pool for this type
		List<GameObject> pooledObjects = GetPooledObjects(gameObject);
		if (pooledObjects == null)
		{
			Debug.Log("Returning non-pooled instance of: " + gameObject.name);
			return (Instantiate(gameObject) as GameObject);
		}

		// return first available inactive pooled object
		for (int i = 0; i < pooledObjects.Count; i++)
		{
			if (!pooledObjects[i].activeInHierarchy)
			{
				return pooledObjects[i];
			}
		}

		// grow the pool if needed and return the new object
		for (int i = 0; i < pooledObjectTypes.Length; i++)
		{
			if (pooledObjectTypes[i].objectToPool.name == gameObject.name)
			{
				if (pooledObjectTypes[i].canGrow)
				{
					return AddPooledObject(gameObject);
				}
				else
				{
					return pooledObjects[0];
				}
			}
		}
		
		// return null if none was created
		// this should be handled by the caller script
		return null;
	}
}
©2017 Sombr Studio LLC