The picture is starting to clear up for me. I think in practice, you will never have an object of type Collider because this is the base class of all other colliders. It’s not clear from the documentation if it is an abstract class, so I suppose it is possible you could end up with one at runtime. Anyways, assume for the moment that a Collider is always one of the following:
It looks like each of these has its own way of detecting the actual size of the collider. For basic shapes, we can get a precise size and radius. For the MeshCollider, the best we can do is calculate the bounding box.
Since we want a common property across all of these types, the best we can do is the most generic shape which can contain every type of collider; a bounding box. Let’s come up with a common way to access this bounding box.
The simplest way to do this is runtime type analysis and an ugly block of if statements. I’ll start with that and refactor later.
class ColliderMeasurer
{
public Vector3 GetBoundingBox(Collider collider)
{
if (collider is BoxCollider)
return ((BoxCollider)collider).size;
else if (collider is SphereCollider)
{
var radius = ((SphereCollider)collider).radius;
return new Vector3(radius * 2, radius * 2, radius * 2);
}
else if (collider is CapsuleCollider)
{
var radius = ((CapsuleCollider)collider).radius;
var height = ((CapsuleCollider)collider).height;
var direction = ((CapsuleCollider)collider).direction;
var directionArray = new Vector3[] { Vector3.right, Vector3.up, Vector3.forward };
var result = new Vector3();
for (int i = 0; i < 3; i ++)
{
if (i == direction)
result += directionArray[i] * height;
else
result += directionArray[i] * radius * 2;
}
return result;
}
else if (collider is MeshCollider)
{
return ((MeshCollider)collider ).sharedMesh.bounds.size;
}
}
}
So that’s ugly… but I think it will work. Now let’s put our refactoring hat on and se if we can tidy it up. The next bit is pretty long so read on if you’re feeling brave…
Whenever I see a block of else if, or a case statement, I consider replacing it with a strategy pattern. In this case, I think it’s a good fit. Let’s create a base strategy and replace each if statement with a concrete implementation of that strategy.
abstract class ColliderMeasureStrategy
{
public abstract Vector3 GetBoundingBox(Collider collider);
}
class BoxColliderMeasureStrategy : ColliderMeasureStrategy
{
public override Vector3 GetBoundingBox(Collider collider)
{
return GetBoundingBox(collider as BoxCollider);
}
public Vector3 GetBoundingBox(BoxCollider collider)
{
return collider.size;
}
}
class SphereColliderMeasureStrategy : ColliderMeasureStrategy
{
public override Vector3 GetBoundingBox(Collider collider)
{
return GetBoundingBox(collider as SphereCollider);
}
public Vector3 GetBoundingBox(SphereCollider collider)
{
return new Vector3(collider.raidus * 2, collider.raidus * 2, collider.raidus * 2);
}
}
class CapsuleColliderMeasureStrategy : ColliderMeasureStrategy
{
public override Vector3 GetBoundingBox(Collider collider)
{
return GetBoundingBox(collider as CapsuleCollider);
}
public Vector3 GetBoundingBox(CapsuleCollider collider)
{
var directionArray = new Vector3[] { Vector3.right, Vector3.up, Vector3.forward };
var result = new Vector3();
for (int i = 0; i < 3; i ++)
{
if (i == direction)
result += directionArray[i] * height;
else
result += directionArray[i] * radius * 2;
}
return result;
}
}
class MeshColliderMeasureStrategy : ColliderMeasureStrategy
{
public override Vector3 GetBoundingBox(Collider collider)
{
return GetBoundingBox(collider as MeshCollider);
}
public Vector3 GetBoundingBox(MeshCollidercollider)
{
return collider.sharedMesh.bounds.size;
}
}
This is looking better to me but I want to make a few more changes…
- I will create a namespace specifically for this strategy, so to clear up my main namespace
- I will change the class names a bit - I don’t typically like putting the name of design patterns into my class names
- I will refactor the CapsuleColliderStrategy class to make it easier to understand
namespace ColliderMeasureStrategy
{
abstract class ColliderMeasurer
{
public abstract Vector3 GetBoundingBox(Collider collider);
}
class BoxColliderMeasurer : ColliderMeasurer
{
public override Vector3 GetBoundingBox(Collider collider)
{
return GetBoundingBox(collider as BoxCollider);
}
public Vector3 GetBoundingBox(BoxCollider collider)
{
return collider.size;
}
}
class SphereColliderMeasurer : ColliderMeasurer
{
public override Vector3 GetBoundingBox(Collider collider)
{
return GetBoundingBox(collider as SphereCollider);
}
public Vector3 GetBoundingBox(SphereCollider collider)
{
return new Vector3(collider.raidus * 2, collider.raidus * 2, collider.raidus * 2);
}
}
class CapsuleColliderMeasurer : ColliderMeasurer
{
static readonly Vector3[] directions = new Vector3[] { Vector3.right, Vector3.up, Vector3.forward };
public override Vector3 GetBoundingBox(Collider collider)
{
return GetBoundingBox(collider as CapsuleCollider);
}
public Vector3 GetBoundingBox(CapsuleCollider collider)
{
var result = new Vector3();
for (int i = 0; i < 3; i ++)
{
if (i == direction)
result += directions[i] * height;
else
result += directions[i] * radius * 2;
}
return result;
}
}
class MeshColliderMeasurer : ColliderMeasurer
{
public override Vector3 GetBoundingBox(Collider collider)
{
return GetBoundingBox(collider as MeshCollider);
}
public Vector3 GetBoundingBox(MeshCollidercollider)
{
return collider.sharedMesh.bounds.size;
}
}
}
Finally, we need an entry point to use our strategy… Normally I would leave it up to the owner of the Collider to determine what kind of ColliderMeasurer it needs. I would use an abstract factory to create both the Collider and ColliderMeasurer at the same time. However, in this case the Collider is created by Unity and just handed to us, so we’ll need something else.
Lets add a static convenience creation method to our base strategy which can create the correct measuring strategy based on type. There is still some type checking here but at least it is done using a registration model.
abstract class ColliderMeasurer
{
static Dictionary<Type, ColliderMeasurer> registeredMeasurers = new Dictionary<Type, ColliderMeasurer>()
{ { typeof(BoxCollider), new BoxColliderMeasurer() },
{ typeof(SphereCollider), new SphereColliderMeasurer() },
{ typeof(CapsuleCollider), new CapsuleColliderMeasurer() },
{ typeof(MeshCollider), new MeshColliderMeasurer() } };
public static ColliderMeasurer ForCollider(Collider collider)
{
if (registeredMeasurers.ContainsKey(collider.GetType())
return registeredMeasurers[collider.GetType()];
else
throw new NotImplementedException(string.Format("The measurer strategy for collider type {0} is not implemented", collider.GetType().FullName));
}
public abstract Vector3 GetBoundingBox(Collider collider);
}
We could keep going - maybe make the constructors for each concrete strategy private so that people don’t accidentally use this pattern incorrectly but I think that’s far enough for now… Especially since I’m not even 100% sure this is going to do what you need!