paint-brush
How to Optimize Work With Collisions in Unityby@doomowenok
216 reads

How to Optimize Work With Collisions in Unity

by Uladzislau DaroshkaJuly 16th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The solution is to cache the components that we want to take in case of a collision. To do this, we create an object that will participate in a constant collision, register it by ID, and we will take the ID from the collider using **GetInstanceID. This is what our code for this service will look like.
featured image - How to Optimize Work With Collisions in Unity
Uladzislau Daroshka HackerNoon profile picture

Hello everyone, I would like to share a solution that, for some reason, I have not seen anywhere before, but it does a good job of optimizing the work with collisions in Unity.


Let's start with the problem and its solution: you need to detect a collision between a bullet and a character and call the necessary logic on the collision object; the first thing that comes to mind is to do the following code:


public class Projectile : MonoBehaviour
{
  public float Damage { get; private set; }
}

public class Character : MonoBehaviour
{
	private float _health;

	private void OnCollisionEnter(Collision collision)
	{
		if(collision.TryGetComponent(out Projectile projectile)
		{
			ChangeHealth(-projectile.Damage);
		}
	}
	
	private void ChangeHealth(float value)
	{
		_health -= value;
	}
}


And it would seem that everything is fine, but we pull the object, then run through its components in the hope of finding the one; in the case, when we have a tight fight on the stage, the performance of our game will begin to gradually go down.




The solution is to cache the components that we want to take in case of a collision.


To do this, we don’t need much, create an object that will participate in a constant collision, register it in some service, from which we will take it by ID, and we will take the ID from the collider using GetInstanceID method from GameObject. This is what our code for this service will look like:


public interface IColliderRegistry<TComponent>
{
    void Register(int id, TComponent component);
    TComponent Get(int id);
    bool Contains(int id);
    void Remove(int id);
    void Clear();
}

// Implementation
public class EnemyProjectileColliderRegistry : IColliderRegistry<Projectile>
{
    private readonly Dictionary<int, Projectile> _registry = new Dictionary<int, Projectile>(32);
        
    public void Register(int id, Projectile component) => _registry.Add(id, component);
    public EnemyProjectile Get(int id) => _registry[id];
	public bool Contains(int id) => _registry.ContainsKey(id);
	public void Remove(int id) => _registry.Remove(id);
    public void Clear() => _registry.Clear();
}


What each method does should be clear, but let’s go over it anyway:


  • Register - registers a component in the dictionary by ID (Use on object creation)
  • Get - returns a cached element component by ID (Use on collision detection)
  • Contains - returns a bool that tells whether the element is in the list (Use on collision detection)
  • Remove - removes an element from the cache (Use when destroying or disabling)
  • Clear - clears the entire list (Use when clear all gameplay scenes)


For each type of object that we will search for in bulk during a collision, we create an implementation of the IColliderRegistry<TComponent> interface.


After all, our manipulations we get the following code:

// Example where we can create and register projectile.
public class ProjectileFactory
{
	private readonly IColliderRegistry<Projectile> _registry;
	
	public ProjectileFactory(IColliderRegistry<Projectile> registry)
	{
		_registry = registry;
	}
	
	public Projectile CreateProjectile()
	{
		Projectile projectile = // Create projectile ;
		_registry.Register(projectile.GetInstanceID(), projectile);
		return projectile;
	}
}

// Updated logic of character.
public class Character : MonoBehaviour
{
	private IColliderRegistry<Projectile> _projectileColliderRegistry;

	private float _health;

	private void OnCollisionEnter(Collision collision)
	{
		int id = collision.GetInstanceID();
		if(_projectileColliderRigistry.Contains(id))
		{
		    ChangeHealth(_projectileColliderRigistry.Get(id).Damage);			
		}
	}
	
	private void ChangeHealth(float value)
	{
		_health -= value;
	}
}


It is also worth considering that the IColliderRegistry instance for each type must be in a single copy; you can forward it to the necessary objects using the DI principle, or if you do not use DI, then have a controller that will give you the required implementation.


The solution, in my opinion, is convenient, and what advantages do we ultimately have:


  1. Faster access to component locating and correspondingly improved performance.


  2. It’s clearer what components we want to work with in the event of a collision, simply by having an additional Registry as a dependency