Unity Interfaces

Unity Interfaces – Getting Started

Lately, I’ve realized that many Unity developers have never programmed outside of Unity projects.
While there’s nothing wrong with that, it does seem to leave some holes in the average Unity developers skill set.
There are some great features and techniques that aren’t commonly used in Unity but are staples for typical c# projects.

That’s all fine, and they can be completely productive, but some of the things I see missing can really help, and I want to make sure to share those things with you.

Because of this, I’ve decided to write a few articles covering some core c# concepts that can really improve your code if you’re not using them already.

The first in this series will cover c# interfaces.

If you google c# interfaces, you’ll come across the msdn definition

An interface contains definitions for a group of related functionalities that a class or a struct can implement.

Personally, I prefer to use an example to explain them though, so here’s one from an actual game.

The ICanBeShot interface

In Armed Against the Undead, you have guns and shoot zombies..Armed Against the Undead
But you can also shoot other things like Ammo pickups, Weapon unlocks, Lights, etc.

Shooting things is done with a standard raycast from the muzzle of the gun.  Any objects on the correct layer and in range can be shot.

If you’ve used Physics.Raycast before, you’ll know that it returns a bool and outputs a RayCastHit object.

The  RayCastHit has a .collider property that points to the collider your raycast found.

In Armed, the implementation of this raycast looks like this:

private bool TryHitEnvironment(Ray ray)
{
	RaycastHit hitInfo;

    if (Physics.Raycast(ray, out hitInfo, _weaponRange, LayerMask.GetMask("EnvironmentAndGround")) == false)
        return false;

    ICanBeShot shootable = hitInfo.collider.GetComponent<ICanBeShot>();

    if (shootable != null)
		shootable.TakeShot(hitInfo.point);
    else
        PlaceBulletHoleBillboardOnHit(hitInfo);

    return true;
}

Here you can see that we do a raycast on the EnvironmentAndGround layer (where I place things you can shoot that aren’t enemies).

If we find something, we attempt to get an ICanBeShot component.

That component is not an actual implementation but rather an Interface which is on a variety of components.

It’s also very simple with a single method named TakeShot defined on it as you can see here:

public interface ICanBeShot
{
    void TakeShot(Vector3 hitPosition);
}

If you’ve never used an interface before, it may seem a little strange that there’s no actual code or implementation.  In the interface, we only define how the methods look and not the implementation.  We leave that part to the classes implementing our interface.

How the Interface is used

So now that I have my interface, and I have a method that will search for components implementing that interface, let me show you some of the ways I’m using this interface.

Implementation #1 – Ammo Pickups

public class AmmoBox : MonoBehaviour, ICanBeShot
{
    public void TakeShot(Vector3 hitPosition)
    {
		PickupAmmo();

		if (_isSuperWeaponAmmo)
			FindObjectOfType<Inventory>().AddChargeToSuperWeapon();
		else
			FindObjectOfType<Inventory>().AddAmmoToWeapons();
	}
}

This ammo script is placed on an Ammo prefab.

Ammo Scene and Inspector

Ammo Scene and Inspector

Notice the box collider that will be found by the raycast in TryHitEnvironment above (line 5).

 

Ammo Inspector

Ammo Inspector

In the case of the AmmoBox, the TakeShot method will add ammo to the currently equipped weapon.  But an AmmoBox isn’t the only thing we want the player to shoot at.

Implementation #2 – Weapon Unlocks

public class WeaponUnlocker : MonoBehaviour, ICanBeShot
{
    public void TakeShot(Vector3 hitPosition)
    {
        WeaponUnlocks.UnlockWeapon(_weaponToUnlock);
        PlayerNotificationPanel.Notify(string.Format("<color=red>{0}</color> UNLOCKED", _weaponToUnlock.name));

        if (_particle != null)
            Instantiate(_particle, transform.position, transform.rotation);

        Destroy(this.gameObject);
    }
}

Compare the AmmoBox to the WeaponUnlocker.  Here you see that we have a completely different implementation of TakeShot.  Instead of adding ammo to the players guns, we’re unlocking a weapon and notifying the player that they’ve unlocked it.

And remember, our code to deal with shooting things didn’t get any more complicated, it’s still just calling TakeShot.  This is one of the key benefits, we can add countless new implementations, without complicating or even editing the code to handle shooting.  As long as those components implement the interface, everything just works.

Implementation #3 – Explosive Boxes

These are crates that when shot will explode and kill zombies.

Implementation #4 – Destructible Lights

In addition to everything else, the lights can also take a shot (in which case they explode and turn off the light source component)

Recapping

Again to make the benefits of Unity interfaces clear, re-examine our code in TryHitEnvironment.

ICanBeShot shootable = hitInfo.collider.GetComponent<ICanBeShot>();

if (shootable != null)
	shootable.TakeShot(hitInfo.point);

We simply look for any collider on the right layer then search for the ICanBeShot interface.  We don’t need to worry about which implementation it is.  If it’s an ammo box, the ammo box code will take care of it.  If it’s a weapon unlock, that’s covered as well.  If we add a new object that implements the interface, we don’t need to touch our current code.

Other Benefits

While I won’t cover everything that’s great about interfaces in depth here, I feel I should at least point out that there are other benefits you can take advantage of.

  1. Unit Testing – If you ever do any unit testing, interfaces are a key component as they allow you to mock out dependencies when you write your tests.
  2. Better Encapsulation – When you code to interfaces, it becomes much more obvious what should be public, and your code typically becomes much better encapsulated.
  3. Loose Coupling – Your code no-longer needs to rely on the implementations of methods it calls, which usually leads to code that is more versatile and changeable.