I’m not going to get into the debate on if you should do this, I’m just going to go into how. For me in particular… I’ll rely on dependency inversion principal in some scenarios, but not all, because at the end of the day I’m here to make a game now, not later. Things like dependency inversion principal rely heavily on the idea that the software is going to be maintained over long periods of time… think enterprise settings. Games can sometimes have this (especially these days with games that get released and then upgraded and have seasons and what not), but most games traditionally don’t really benefit as much from it.
So…
Dependency Inversion Principle relies on the idea that you should not write your code to depend directly on a concretion, but rather to depend on an abstraction.
What this means is create an abstracted layer between the concrete implementation and the code that depends on it.
Visually…
Don’t do this:

Do this:

In a language like C# this is usually facilitated by an interface (but an abstract class could also be used).
Take for example how collections all implement interfaces like IList and ICollection and etc. This way code that depends on collections can be coded in a manner to rely on these interfaces (abstractions) rather than be forced to use only the predefined concrete Array or List.
…
So lets give a pretty simple example in Unity terms.
And I’ll go to the good old “interaction” scenario.
A simple example could be something like:
public class PlayerOpenDoorScript : MonoBehaviour
{
public float Radius; //distance from self we are allowed to interact
private Collider[] _buffer = new Collider[16];
void Update()
{
if (Input.GetButtonDown("Interact"))
{
var curpos = this.transform.position;
int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.DOOR_LAYER_MASK, QueryTriggerInteraction.Collide);
float dist = float.PositiveInfinity;
Door nearest = null;
for (int i = 0; i < count; i++)
{
var door = _buffer[i].GetComponent<Door>();
if (door == null) continue;
float ds = (door.transform.position - curpos).sqrMagnitude;
if (ds < dist)
{
nearest = door;
dist = ds;
}
}
if (nearest != null)
{
nearest.Open();
}
}
}
}
public class Door
{
public void Open()
{
//do whatever the heck we do for opening
}
}
This code is the simplest. It’s a very basic script that allows us to open doors by overlapping with its collider on some door layer.
Downside is that it only really supports doors. Nothing else. The dependency is pretty darn strict.
Arguably you could call it “Interactable” and then exploit UnityEvent. But still you’re relying on this very explicit concrete type… so not dependency inversion. Also that heads down a completely different abstraction topic about using UnityEvent driven design.
So lets start expanding it…
public class PlayerInteractController : MonoBehaviour
{
public float Radius; //distance from self we are allowed to interact
private Collider[] _buffer = new Collider[16];
void Update()
{
if (Input.GetButtonDown("Interact"))
{
var curpos = this.transform.position;
int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.INTERACTABLE_LAYER_MASK, QueryTriggerInteraction.Collide);
float dist = float.PositiveInfinity;
GameObject nearest = null;
for (int i = 0; i < count; i++)
{
float ds = (_buffer[i].transform.position - curpos).sqrMagnitude;
if (ds < dist)
{
nearest = _buffer[i].gameObject;
dist = ds;
}
}
if (nearest != null)
{
nearest.SendMessage("InteractWith");
}
}
}
}
This code is pretty simple and relies on a very subtle amount of abstraction. That minor amount of abstraction is that we don’t actually care what is being interacted with we just send a message saying “InteractWith”.
Now anything that wants to be interacted with could just have a “InteractWith” method on one of its script. And it’ll do a thing.
Downside is that we’re relying on this magic string that we just have to know about. But it’s a good start.
This is probably about as simple I could make of a dependency inverted situation in Unity… but it’s relying on abstracting to the point where it’s really just using dynamics. This could get really hard to debug… and doesn’t really hold to other standards practices that are favored in a language like C#.
So… lets try to be more distinct.
public interface IInteractable
{
float Priority { get; }
void Interact(PlayerInteractController actor);
}
public class PlayerInteractController : MonoBehaviour
{
public float Radius; //distance from self we are allowed to interact
private Collider[] _buffer = new Collider[16];
void Update()
{
if (Input.GetButtonDown("Interact"))
{
var curpos = this.transform.position;
int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.INTERACTABLE_LAYER_MASK, QueryTriggerInteraction.Collide);
IInteractable bestmatch = (from c in _buffer.Take(count)
let ic = c.GetComponent<IInteractable>()
where ic != null
orderby (c.transform.position - curpos).sqrMagnitude descending
orderby ic.Priority descending
select ic).FirstOrDefault();
if (bestmatch != null)
{
bestmatch.Interact(this);
}
}
}
}
Now we’ve done a similar situation but now we’ve abstracted it into an interface to interact with. This has also allowed us to add some extra properties to the interactable like priority. I’ve also added the ability to pass along who interacted with it (this could have been done in SendMessage… but in a loser manner).
So now we’ve decoupled PlayerInteractController from what its interacting. You could implement IInteractable in many ways. From doors, to chests, to whatever the heck you want. If you want a new thing to interact with… just implement and add it to the scene.
But we could abstract more. Currently this implies that ONLY the player can interact with things. What if we wanted mobs to interact with things? Mobs don’t use the Input system or anything. So what then?
public interface IInteractable
{
float Priority { get; }
void Interact(IInteractionBehaviour actor);
}
public interface IInteractionBehaviour
{
void AttemptInteraction();
}
public class PlayerInteractionBehaviour : MonoBehaviour
{
public float Radius; //distance from self we are allowed to interact
private Collider[] _buffer = new Collider[16];
void Update()
{
if (Input.GetButtonDown("Interact"))
{
this.AttemptInteraction();
}
}
public void AttemptInteraction()
{
var curpos = this.transform.position;
int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.INTERACTABLE_LAYER_MASK, QueryTriggerInteraction.Collide);
IInteractable bestmatch = (from c in _buffer.Take(count)
let ic = c.GetComponent<IInteractable>()
where ic != null
orderby (c.transform.position - curpos).sqrMagnitude descending
orderby ic.Priority descending
select ic).FirstOrDefault();
if (bestmatch != null)
{
bestmatch.Interact(this);
}
}
}
public class MobInteractionBehaviour : MonoBehaviour
{
public void AttemptInteraction()
{
IInteractable bestmatch = * WHO KNOWS, MAYBE MOB FINDS TARGETS A DIFFERENT WAY THAN PLAYER??? *;
if (bestmatch != null)
{
bestmatch.Interact(this);
}
}
}
The abstraction can keep going of course.
public class Entity : MonoBehaviour
{
//other info that defines an entity
}
public interface IInteractable
{
float Priority { get; }
void Interact(IEntity actor);
}
public abstract class InteractionBehaviour : MonoBehaviour
{
void AttemptInteraction(IEntity entity);
}
public class AttemptInteractOnPlayerInput : MonoBehaviour
{
public Entity entity;;
public InteractionBehaviour interactionBehaviour;
void Update()
{
if (Input.GetButtonDown("Interact"))
{
interactionBehaviour.AttemptInteraction(entity);
}
}
}
public class MobAI : MonoBehaviour
{
public Entity entity;
public InteractionBehaviour interactionBehaviour;
void MobAIRoutine()
{
//implement AI however you want...
//reach line where we do interaction
interactionBehaviour.AttemptInteraction(entity);
}
}
public class NearestTargetInteraction : InteractionBehaviour
{
public float Radius; //distance from self we are allowed to interact
private Collider[] _buffer = new Collider[16];
public override void AttemptInteraction(IEntity entity)
{
var curpos = this.transform.position;
int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.INTERACTABLE_LAYER_MASK, QueryTriggerInteraction.Collide);
IInteractable bestmatch = (from c in _buffer.Take(count)
let ic = c.GetComponent<IInteractable>()
where ic != null
orderby (c.transform.position - curpos).sqrMagnitude descending
orderby ic.Priority descending
select ic).FirstOrDefault();
if (bestmatch != null)
{
bestmatch.Interact(entity);
}
}
}
You can abstract to your hearts content.
…
But here in lies the issue.
When do you stop abstracting?
What’s the final layer?
Are we forever engineering a solution and never making a game???