The explanation of these three key concepts has always been a bit vague to me, often using technical terms that are either confusing or impractical for truly understanding what they’re meant for.
interface
A player interacts with many objects in a game. These objects could be projectiles, lives, enemies, bullets, portals, doors etc. At any point when a player collides with the collider of these other objects, something is expected to happen. In plain language, what is expected to happen depends on which object a player interacts/collides with. However, how a developer chooses to implement it, is what we need to talk about.
if condition/switch is often the first option for handling collisions/triggers as has been demonstrated in the code below.
void OnTriggerEnter(Collider other)
{
switch (other.gameObject.tag)
{
case "Enemy":
// handle enemy trigger
Debug.Log("enemy entered trigger");
break;
case "Life":
// handle life trigger
Debug.Log("gained a life");
break;
case "Bullet":
// handle bullet trigger
Debug.Log("got shot");
break;
default:
// handle unknown trigger
Debug.Log("unknown object entered trigger");
break;
}
}
This clearly does the job but the question is, is it efficient enough?, especially when several other entities are added; how many checks would have to be done in that case, is it decoupled, the questions are endless.
This is where interfaces come in handy to cleanup the situation. An interface is basically like a class but with methods and other members that are not yet implemented and the word "interface" in place of "class". The cool thing is, any class can inherit as many interfaces as they want and would then be responsible for the implementation of the methods the way its wants. For example, an interface called IBird can has Fly(), Sing(), Eat() as methods to be implemented by Eagle, Parrot and Crow classes. How these classes decide to define these methods depend on how they fly, sing or eat. "I" is mostly the first letter of the interface's name.
public interface ISomething
{
// c# rule, it is wrong to add public here, the implementing class must add public
void DoThis();
void DoThat();
}
It is now the responsibility of any class that chooses to inherit this interface to define what the DoThis() and DoThat() methods should do. that is all !!! that simple !
now let's choose to do this for Enemy, Life and Bullet classes
public class Enemy: MonoBehaviour, ISomething
{
public void DoThis()
{
// define what Enemy's DoThis() should do
}
public void DoThat()
{
// define what Enemy's DoThat() should do
}
}
public class Life: MonoBehaviour, ISomething
{
public void DoThis()
{
// define what Life's DoThis() should do
}
public void DoThat()
{
// define what Life's DoThat() should do
}
}
public class Bullet: MonoBehaviour, ISomething
{
public void DoThis()
{
// define what Bullet's DoThis() should do
}
public void DoThat()
{
// define what Bullet's DoThat() should do
}
}
Each class handles responses its own way decoupled from the player.
The OnTriggerEnter function now looks much cleaner and scalable:
void OnTriggerEnter(Collider other)
{
ISomething entity = other.GetComponent<ISomething>();
entity?.DoThis(); // take notice the null-conditional operator(?) :)
}
The code is simpler, decoupled and easy to maintain. Also note that interface names typically describe what they do. For example IInteractable, IKillable, or IDamageable. With this, I believe you can now revisit the topic with a different perspective. Good luck!
delegate and event
There is a GameManager or SoundManager that runs an action or actions when something happens, for example when a player dies. Delegates and events are ideal for this. They allow classes to subscribe to events without being tightly coupled.
A delegate defines a method signature (what a method returns and what parameters it takes), and can reference or point to any method that matches this signature. Event acting as a broadcaster, notifiers those methods that have subscribed to the delegates to take action when an action happens in the order they were added. In practical terms, five methods somewhere are called when a player dies.
public class Player
{
public delegate void PlayerDied();
public static event PlayerDied OnPlayerDied;
// this is static because it is accessing a static event; c# rules :)
public static void PlayerDiesHere()
{
OnPlayerDied?.Invoke();
}
}
// subscribers, GameManager and SoundManager
public class GameManager : MonoBehaviour
{
void OnEnable()
{
// the FunctionForPlayerDied() has the same signature as the PlayerDied delegate
Player.OnPlayerDied += FunctionForPlayerDied;
}
void FunctionForPlayerDied()
{
// whatever must be done here
}
void OnDisable()
{
// it is important to unsubscribe
Player.OnPlayerDied -= FunctionForPlayerDied;
}
}
public class SoundManager : MonoBehaviour
{
void OnEnable()
{
Player.OnPlayerDied += SoundForPlayerDied;
}
void SoundForPlayerDied()
{
// do as you please :)
}
void OnDisable()
{
// never forget to unsubscribe :)
}
}
It is important to unsubscribe in either OnDisable() or OnDestroy to avoid unexpected behaviour.
In the example above, i defined delegate separately from the event. While that is fine, there is a simpler approach using the built-in Action type.
This:
public class Player
{
public delegate void PlayerDied();
public static event PlayerDied OnPlayerDied;
can be replaced with:
public class Player
{
public static event Action OnPlayerDIed;
// and if the function should have parameters, you write it this way
public static event Action<string> OnPlayerDIed; //one parameter
public static event Action<string, int> OnPlayerDIed; //two parameters
// ie Action<T1,T2> in that order
I hope this gives a practical perspective to these three concepts.
Have fun building cool stuff! cheers!!
Top comments (1)
Nice mention :).