Understanding C# Interfaces in Game Design
In an ARPG (Action Role-Playing Game) – The left mouse button is the single most important codified system. It handles movement, inventory management, interactions with NPCs, enemies, loot, and more. It’s a system completely designed with one word in mind – intent. Everything a character intends to do is first handled by the left mouse button.
When coding for left mouse button interactions, we could create systems in C# based on if/else statements or, better yet, switch/case statements.
However, the code for these interactions would be long and take up so much code space. Additionally, it’s not a performant way of handling
interactions. Would it work? Sure! Would it be efficient codified and performance wise? No, not at all. Luckily, C# has an amazing feature called interfaces. Let’s talk about how we can implement all of this with an interface.
First, let’s understand what an interface is.
When we talk about code, we generally talk about implementation, or the “how” of code. How do we implement an interaction for a loot chest? We code the character’s movement, we code the loot chest, we create movement functions to move to the chest, and then we create an interaction method to interact with the loot chest.
If implementation is the “how” of code, then interfaces are the “what” of code. The “how”, or implementation, is up to the developer. Broken down, an interface is a contract that requires an object to inherit specific methods, properties, events, and indexers that a class or struct must implement if it adopts the interface.
Interfaces are a blueprint of what an object’s structure is at its core.
Let’s use our loot chest as an example. We know that the loot chest can be interacted with. We could implement a method that is very specific to a loot chest and codify the interaction of the player with the loot chest. For instance, what happens when we click on the chest? Perhaps the chest is locked, or perhaps the chest requires the character to be a specific level. If we have to codify all these conditions, it surely makes for messy code in a single file.
Interfaces save the day!
Since interfaces are a blueprint, or a contract of “what” an object looks like at its core, we can define methods, properties and other things in one, then require our loot chest to inherit the interface. Once the loot chest inherits the interface, any method or property inside the interface MUST BE inherited by the loot chest.
Interfaces can be extremely complex, but they can also define the most simplistic blueprints of an object. Interfaces are meant to simplify and reduce the toil of creating multiple objects that would typically use similar properties, indexes, and methods. Doors, chests, enemies, NPCs are all things we can interact with. What if we created a blueprint that defined “what” an interactable object should look like? What would we call that interface?
The additional “I” before the word “Interactable” lets us know that this is an interface. It’s short for “Interface Interactable”. How do we define the IInteractable Interface in code? We start by creating the interface itself, then we place properties and methods inside that create the “blueprint” for the object(s) that will inherit the interface:
public interface IInteractable
This interface includes a method called Interact(), which will be called when the player clicks on an object that implements IInteractable. One of the most interesting aspects of interfaces, again, is that it defines “what” properties and methods an object should have, not “how” they are used. For different interactable objects (enemies, items, chests, NPCs, etc.), each class will have its own version of the Interact() method, defining what happens when it’s interacted with. The interface only requires the class to have an Interact() method. So long as we have the Interact() method, the interface does not care “how” we interact.
Now that we have an interface set up, let’s create a way for the player to interact with an interactable:
if (Input.GetMouseButtonDown(0)) // If left mouse button is pressed down
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // Cast a ray to the mouse position
RaycastHit hit; // Create a variable to store what we hit with the ray
if (Physics.Raycast(ray, out hit)) // If we hit something with the ray
IInteractable interactable = hit.collider.GetComponent<IInteractable>(); // Get the IInteractable interface
if (interactable != null) // If the object doesn't have an IInteractable, ignore
interactable.Interact(); // If we evaluated to true, execute the Interact() method for the interactable object
In the above example, if an interactable is clicked, we perform the Interact() method for that interactable. This structure maintains clean code, makes it easy to add new interactable objects, and keeps interaction logic encapsulated within each object class. So, what does an interactable object look like? We have the interface created, and we have a way for the player to interact with an interactable object, but we don’t actually have object(s) to interact with. Let’s implement the interface on an enemy and an item that drops on the ground. Both can be interacted with, but the way we interact with them are different. For enemies, we may need to run to the enemy and attack. For items, we simply need to pick up the item and place it in our inventory.
Let’s start with an enemy:
public class Enemy : MonoBehaviour, IInteractable
public void Interact()
// Walk up to the enemy
// Attack the enemy with our weapon
While the code for actually completing the task above is not fully implemented, we’ve successfully satisfied the requirement of the interface. The enemy has an Interact() method, which our interface requires, and our code inside can be implemented however we see fit to walk up to the enemy and attack it. Now, let’s look at how we satisfy the requirement for an item:
public class Item : MonoBehaviour, IInteractable
public void Interact()
// Walk up to the item
// Check if we have enough inventory space to place the item in our inventory
// If we don't have enough space, produce an error message on screen, "Not enough inventory space".
// If we do have enough space, pick up the item and place it into our inventory.
Just like that, we have created an interface for any type of interactable, created the logic for our player to interact, and implemented the requirements of our interface of two example interactables!
Almost! We’ve explained the benefit of interfaces and how invaluable they can be in simplifying code, but we haven’t explained one of the most important pitfalls of interfaces – over-usage. Since interfaces help to blueprint and simplify classes, it’s very easy to start implementing lots of interfaces and end up with many overcomplicated systems. Let’s use our enemy as an example. Currently, our enemy only inherits the IInteractable interface. What if we want to create a number of enemies in our ARPG? Should we create an IEnemy interface? What if we have different families of enemies, like Ghosts, or Werewolves? Should we then create another interface called IGhostEnemy and IWerewolfEnemy? This can get complicated very quickly. It’s likely safe to assume a developer has encountered a situation where an interface would have made sense, and in contrast, a situation where using an interface didn’t save any time or code, and instead made a trivial issue much
more complex, resulting in a non-trivial solution or even multiple solutions. Understanding when to use an interface and when not to, is itself, a very complex debate. It is true, however, that deciding to use an interface when one isn’t needed overcomplicates your code significantly. Choosing not to use an interface, which is typically deemed a more complex solution, simply for the sake of not wanting to make things more complicated from a code perspective, can cause the same over-complications. How do we get comfortable knowing when we should or shouldn’t use an interface?
The solutions aren’t always straightforward. For example, we may encounter a specific condition in our ARPG where a Ghost enemy can revive after it dies, resulting in the player needing to engage them again. It seems like we could use an IInteractable, IEnemy, and IGhost interface. The IInteractable would handle how we interact with the Ghost enemy. The IEnemy interface would handle how every enemy we interact with are able to take damage and eventually die. Lastly, the IGhost interface may handle how fast our Ghost enemy is, what skills it has, and what loot it might be able to drop.
This is more complicated than having just a single interface, but are these three interfaces enough? Maybe we should have another interface for IEnemyGhostActions, which might be a list of actions or skills the ghost could perform. What about, IEnemyLootTable – where we decide what items an enemy can drop. Surely, every enemy would benefit from having these interfaces – would you agree or disagree? The complexity of interfaces often stems from how complex our ARPG can get. What’s the best approach?
While many methods exist to give a one-size-fits-all answer to the question at hand, there are three words to help decide whether or not an interface should be used:
M – Multiple
C – Complexity
F – Futureproof
Will multiple classes inherit this? If so, how many? If it’s just one class, it’s unlikely that an interface is needed. If it’s more than one, then perhaps an interface would help to make implementation less complicated overall.
Would implementing an interface for two different enemies be more complex than it would to write thirty lines of code for each one individually? If so, then it’s likely an interface isn’t solving a potentially complex problem or organizational issue. If it’s less complicated to create an interface and save twenty of the total thirty lines of code, then perhaps this is a good use-case for an interface.
Would writing an interface now ensure less complexity for multiple classes in the future? For example, we may have decided previously that writing thirty lines of code for two Ghost enemies was more appropriate, which meant we chose not to use an interface. However, what if we know there are future plans to add ten additional types of Ghost enemies? Would writing an interface now save us time and complexity in the future? It’s likely. Attempting to understand how complex your project may get in the future and what your plans are to add complexity can often give a clear indication.
These very specific words can help determine whether or not you should or shouldn’t implement an interface, but it’s important to note that these are simply guidelines.
Programming is an art, and in art, there are no rules. It’s important to remember that interfaces exist to simplify and reduce the toil of creating multiple objects that would typically use similar properties, indexes, and methods. If efficiency, performance, and organization are highly valued, then interfaces seek to provide that value. It’s possible that being more efficient, performant, and organized provide us additional time to enjoy the art that we’ve created; equally as important, it’s possible we may find additional time to create something new.