Designer friendly component scripts with Unity events
Unity recently introduced a serializable UnityEvent
class which allows scripts to expose
events in the inspector allowing designers to wire up handlers without coding. This is a
really powerful feature because it allows developers to implement components that can now
be assembled much more freely by designers… almost like a basic form of visual scripting.
Events that have no parameters can be exposed using the ready made UnityEvent
class.
Here is an example of a MonoBehaviour
that exposes an event that is triggered after the
specified delay has elapsed:
public sealed class DelayedEvent : MonoBehaviour
{
[SerializeField]
private float delayInSeconds = 1f;
[SerializeField]
private bool repeat = false;
[SerializeField]
private int maximumRepeatCount = 0;
[SerializeField]
private UnityEvent triggeredEvent = null;
public float DelayInSeconds {
get { return this.delayInSeconds; }
set { this.delayInSeconds = value; }
}
public bool Repeat {
get { return this.repeat; }
set { this.repeat = value; }
}
public int MaximumRepeatCount {
get { return this.maximumRepeatCount; }
set { this.maximumRepeatCount = value; }
}
public UnityEvent TriggeredEvent {
get { return this.triggeredEvent; }
}
private void OnEnable()
{
this.StartCoroutine(this.StartDelay());
}
private void OnTriggered()
{
this.TriggeredEvent.Invoke();
}
private IEnumerator StartDelay()
{
int count = 0;
do {
yield return new WaitForSeconds(this.DelayInSeconds);
this.OnTriggered();
if (this.MaximumRepeatCount > 0) {
count += 1;
}
}
while (this.Repeat && count <= this.MaximumRepeatCount);
this.enabled = false;
}
}
Alone this component doesn’t seem particularly useful; however, if you implement action components, like the following, then it becomes possible to wire them up using the inspector.
public sealed class SpawnPrefabAction : MonoBehaviour
{
[SerializeField]
private GameObject prefab = null;
[SerializeField]
private Transform parent = null;
[SerializeField]
private bool inheritPosition = true;
[SerializeField]
private bool inheritRotation = true;
[SerializeField]
private bool inheritScale = true;
public GameObject Prefab {
get { return this.prefab; }
set { this.prefab = value; }
}
public Transform Parent {
get { return this.parent; }
set { this.parent = value; }
}
public bool InheritPosition {
get { return this.inheritPosition; }
set { this.inheritPosition = value; }
}
public bool InheritRotation {
get { return this.inheritRotation; }
set { this.inheritRotation = value; }
}
public bool InheritScale {
get { return this.inheritScale; }
set { this.inheritScale = value; }
}
public void ExecuteAction()
{
// In a real-world implementation I would suggest using
// a pooling solution rather than making new ones each time.
var instanceTransform = Instantiate(this.prefab, this.Parent).transform;
if (this.InheritPosition) {
instanceTransform.position = this.transform.position;
}
if (this.InheritRotation) {
instanceTransform.rotation = this.transform.rotation;
}
if (this.InheritScale) {
instanceTransform.localScale = this.transform.localScale;
}
}
}
public sealed class PlaySoundEffectAction : MonoBehaviour
{
[SerializeField]
private AudioClip clip = null;
[SerializeField]
private float volume = 1f;
public AudioClip Clip {
get { return this.clip; }
set { this.clip = value; }
}
public float Volume {
get { return this.volume; }
set { this.volume = Mathf.Clamp01(value); }
}
public void ExecuteAction()
{
AudioSource.PlayClipAtPoint(this.Clip, this.transform.position, this.Volume);
}
}
Which can then be wired up like demonstrated below using the inspector:
Sometimes we need to be able to provide additional information about an event. Unity
provides a selection of generic UnityEvent
implementations that take varying quantities
of custom parameters. Due to limitations with the Unity serialization mechanism it is
necessary to create a little boilerplate for each specialization.
The following demonstrates one way to create a component to maintain the health of an actor (any object that can take damage):
[Serializable]
public sealed class HealthDamagedEvent : UnityEvent<HealthComponent, float, object> { }
[Serializable]
public sealed class HealthDestroyedEvent : UnityEvent<HealthComponent, object> { }
public sealed class HealthComponent : MonoBehaviour
{
[SerializeField]
private float initialHealth = 100f;
[SerializeField]
private HealthDamagedEvent damageTakenEvent = null;
[SerializeField]
private HealthDestroyedEvent destroyedEvent = null;
private float health;
public float InitialHealth {
get { return this.initialHealth; }
set { this.initialHealth = value; }
}
public float Health {
get { return this.health; }
set { this.health = value; }
}
public bool IsAlive {
get { return this.Health > 0f; }
}
public bool IsDead {
get { return this.Health <= 0f; }
}
public HealthDamagedEvent DamageTakenEvent {
get { return this.damageTakenEvent; }
}
public HealthDestroyedEvent DestroyedEvent {
get { return this.destroyedEvent; }
}
private void Start()
{
this.RestoreInitialHealth();
}
public void RestoreInitialHealth()
{
this.Health = this.InitialHealth;
}
public void TakeDamage(float damage, object actuator = null)
{
if (this.IsDead) {
// Already dead! please no more!
return;
}
this.Health = Mathf.Max(0f, this.Health - damage);
this.DamageTakenEvent.Invoke(this, damage, actuator);
if (this.IsDead) {
this.DestroyedEvent.Invoke(this, actuator);
}
}
}
For more complex objects it is useful to create empty objects to group related actions
together and to workaround the limitation whereby UnityEvent
cannot determine which
component to invoke when the game object contains multiple components of the same type:
Finally I’ll demonstrate how you might create a component that inflicts damage upon whatever it collides with. Collision can be controlled using the physics layer matrix feature of Unity although you could obviously add various constraints to your components.
[Serializable]
public sealed class DamagerDamagedTargetEvent : UnityEvent<DamagerComponent, float, HealthComponent> { }
[Serializable]
public sealed class DamagerDestroyedTargetEvent : UnityEvent<DamagerComponent, HealthComponent> { }
public sealed class DamagerComponent : MonoBehaviour
{
[SerializeField]
private float damageAmount = 10f;
[SerializeField]
private DamagerDamagedTargetEvent damagedTargetEvent = null;
[SerializeField]
private DamagerDestroyedTargetEvent destroyedTargetEvent = null;
public float DamageAmount {
get { return this.damageAmount; }
set { this.damageAmount = value; }
}
public DamagerDamagedTargetEvent DamagedTargetEvent {
get { return this.damagedTargetEvent; }
}
public DamagerDestroyedTargetEvent DestroyedTargetEvent {
get { return this.destroyedTargetEvent; }
}
private void OnCollisionEnter2D(Collision2D other) {
var health = other.gameObject.GetComponent<HealthComponent>();
if (health != null && health.IsAlive) {
health.TakeDamage(this.DamageAmount, this.gameObject);
this.DamagedTargetEvent.Invoke(this, this.DamageAmount, health);
if (health.IsDead) {
this.DestroyedTargetEvent.Invoke(this, health);
}
}
}
}
I hope that this post is useful for people that are looking for a lightweight approach for wiring up their components visually. I used this approach in my most recent client project and they said that they found the game’s components flexible and easy to use.