Why you should use Event Systems

Hello Readers!

So I have received a ton of questions about event systems from both C# classes I teach as well as from Unity game development classes I teach.  So I have decided to build this blog post generically enough to apply to both.  If you aren’t using event systems, your code probably looks a lot like something below…

    /// <summary>
    /// Moves the character on the ladder
    /// </summary>
    /// <param name="position">place touched.</param>
    /// <param name="other">the object that collided with us.</param>
    private void MoveOnLadder(Vector3 position, Collider2D other)
    {
        GameObject profSuite = other.gameObject.transform.parent.gameObject;
        //I used 10 because that is how far the camera is from the scene
        Vector3 worldPos = Camera.main.ScreenToWorldPoint(new Vector3(position.x, position.y, 10));
        Vector2 touchPos = new Vector2(worldPos.x, worldPos.y);
        Collider2D col = Physics2D.OverlapCircle(touchPos, 0.2f, this.whatIsInteractive);
        if (null == col)
        {
            //they didn't touch the ladder, don't move.
            return;
        }
        //double check make sure they didnt cheat and did click on this ladder
        if (col.gameObject.transform.position == this.gameObject.transform.position
            || col.gameObject.transform.position == this.parent.transform.position)
        {
            //move the player to the top
            Vector3 nTansform = profSuite.transform.position;
            nTansform.x = this.top.transform.position.x;
            nTansform.y = this.top.transform.position.y + 5;
            profSuite.transform.position = nTansform;
            profSuite.GetComponent<Rigidbody2D>().AddForce(new Vector2(0.0f, this.HopUpPower));
        }
    }

So that is obviously way too much responsibility for our ladder.  What if we change the way our Professor Suite code works, maybe he changes color.  Maybe we want to shake the camera as well and add new features.  All of that code would then have to live here as our code currently exists.  This is absolutely terrible.  Not to mention inefficient.  We have to iterate object hierarchies and find those objects and call methods and make modifications.  Just not good stuff.

So…This is where Event Systems are extremely helpful.  First we need to build an Eventer!

The Eventer

I like to call it the “Eventer”, but it probably has a more technically correct name.  I have no idea what that name is.  This is a piece of code that just holds onto what events are available to you.  For this instance, it looks like below.  Notice that I already added events for the Ladder along with triggers.

using UnityEngine;
using System.Collections;

public class GPlayEventer {

    #region Singleton Setup
    //the variable is declared to be volatile to ensure that 
    //assignment to the instance variable completes before the 
    //instance variable can be accessed.
    private static GPlayEventer instance;

    //object for locking
    private static object syncRoot = new System.Object();
    public static GPlayEventer Instance
    {
        get
        {
            //check to see if it doesnt exist
            if (instance == null)
            {
                //lock access, if it is already locked, wait.
                lock (syncRoot)
                {
                    //the instance could have been made between 
                    //checking and waiting for a lock to release.
                    if (instance == null)
                    {
                        //create a new instance
                        instance = new GPlayEventer();
                    }
                }
            }
            //return either the new instance or the already built one.
            return instance;
        }
    }
    #endregion Singleton Setup

    #region Events
    /// <summary>
    /// Definition of a game event
    /// </summary>
    /// <param name="sender">Who triggered this event.</param>
    /// <param name="args">What arguments are there.</param>
    public delegate void GameEvent(object sender, object args);

    /// <summary> Set up all game events here -- feel free to add more </summary>
    public event GameEvent LevelLoaded, GameStarted, PlayerFiredFromCannon, PlayerClimbedLadder;

    #endregion Events

    #region Triggers

    /// <summary>
    /// Trigger the event PlayerFiredFromCannon
    /// </summary>
    /// <param name="sender">who triggered this event.</param>
    /// <param name="eventArgs">Fire Direction and previous gravity amount.</param>
    public void TriggerPlayerFiredFromCannon(object sender, ActiveCannon.CannonFireArgs eventArgs)
    {
        //check to see if the event exists
        if (null != this.PlayerFiredFromCannon)
        {
            //cool, trigger it!
            this.PlayerFiredFromCannon(sender, eventArgs);
        }
    }

    /// <summary>
    /// Trigger the event PlayerClimbedLadder
    /// </summary>
    /// <param name="sender">who triggered this event.</param>
    /// <param name="eventArgs">Ladder specific arguments.  See struct definition</param>
    public void TriggerPlayerClimbedLadder(object sender, Transform eventArgs)
    {
        //check to see if the event exists
        if (null != this.PlayerClimbedLadder)
        {
            //cool, trigger it!
            this.PlayerClimbedLadder(sender, eventArgs);
        }
    }
    #endregion Triggers
}

Refactored Ladder Code

So we take the ladder and rebuild the code to be more appropriate for just the ladder.  It should only do things to itself and then trigger the event.  You will notice it only checks for the touch from the player and then it triggers the event in which other objects (our player) can listen to.

    /// <summary>
    /// Moves the character on the ladder
    /// </summary>
    /// <param name="position">place touched.</param>
    /// <param name="other">the object that collided with us.</param>
    private void MoveOnLadder(Vector3 position, Collider2D other)
    {
        GameObject profSuite = other.gameObject.transform.parent.gameObject;
        //I used 10 because that is how far the camera is from the scene
        Vector3 worldPos = Camera.main.ScreenToWorldPoint(new Vector3(position.x, position.y, 10));
        Vector2 touchPos = new Vector2(worldPos.x, worldPos.y);
        Collider2D col = Physics2D.OverlapCircle(touchPos, 0.2f, this.whatIsInteractive);
        if (null == col)
        {
            //they didn't touch the ladder, don't move.
            return;
        }
        //double check make sure they didnt cheat and did click on this ladder
        if (col.gameObject.transform.position == this.gameObject.transform.position
            || col.gameObject.transform.position == this.parent.transform.position)
        {
            GPlayEventer.Instance.TriggerPlayerClimbedLadder(this, this.top.transform);
        }
    }

Where did the other code go?

Ok, so now we just trigger an event, but the code for climbing the ladder had to go somewhere.  It went to our player of course!  My player has a piece of code on it that manages state that is always active, so that is where I put the code.  You will notice that I have to subscribe to the events, and pass it a handler of the same form of the delegate we defined in our eventer.  The code looks like this:

    /// <summary>
    /// First Initialization phase.  Only ever called once.
    /// </summary>
    private void Awake()
    {
        //Get references to state and input scripts
        this.nInput = this.GetComponent<NormalInput>();
        this.iInput = this.GetComponent<InteractInput>();
        this.cState = this.GetComponent<CharacterState>();

        //set initial state of input scripts
        this.nInput.enabled = true;
        this.iInput.enabled = false;

        //listen to events.
        GPlayEventer.Instance.PlayerFiredFromCannon += new GPlayEventer.GameEvent(this.DealWithFiredFromCannon);
        GPlayEventer.Instance.PlayerClimbedLadder += new GPlayEventer.GameEvent(this.DealWithLadderClimbed);
    }

    /// <summary>
    /// Deal with ladder being climbed from the character's perspective.
    /// </summary>
    /// <param name="sender">who is sending this.</param>
    /// <param name="args">transform of where to teleport to.</param>
    private void DealWithLadderClimbed(object sender, object args)
    {
        Transform t = (Transform) args;
        //move the player to the correct ladder position.
        Vector3 nTansform = this.gameObject.transform.position;
        nTansform.x = t.position.x;
        nTansform.y = t.position.y;
        this.gameObject.transform.position = nTansform;
    }

Summary

Cool, so we went over not just how to implement a simple eventing system, but also why it is important and how it can be implemented.  You should try taking this example a bit further and add another object that listens to the same event and maybe rocks the camera as the player climbs.  I hope everybody enjoyed this.

Leave a Reply

Your email address will not be published. Required fields are marked *