Software Design Practices for Unity3D based Development (Part2)

Software Design Practices for Unity3D based Development (Part2)
COMMENTS (0)
Tweet

Hi guys,

This is a continuation of my previous blog post titled ‘Software Design Practices for Unity3D based Development’. In this post we’ll discuss some of the best practices you should follow when creating your app’s architecture in Unity 3D.

Application Architecture

Code vs Inspector

Working on a Unity3D app can be a little difficult if you have multiple people working on a same scene file, since Unity3D still doesn’t offer any mechanism for scene versioning and conflict resolution. As such, problem typically occur when you’re attaching your own custom components to game objects and are assigning references between game object instances. Another common issue that occurs in such situations is the notorious ‘Reference Missing’ exception thrown by the code inspector.

In such situations, where you have multiple people working on the same scene file, the recommended practice is to avoid using the Code Inspector as much as possible, and move all initialization, assignments and component attachments to the code itself.

By using the best practices mentioned below, you can reduce your dependency on the Code Inspector significantly.

Event System

Another best practice is to use a custom event system, especially in cases where you are accessing a large number of external scripts and calling methods from various places. In such a scenario, it’s a good idea to use a custom event system to decouple the code and to avoid issues arising from spaghetti code.

Shown below is an example of tightly coupled code, which should be avoided as much as possible.

The solution to this problem, lies in a custom event system, and there are multiple ways you can implement that. In the discussion below, we’ll look at one of these methods, which is also the easiest.

Our custom event system is similar to the approach Unity uses in MonoBehaviour scripts. We will use the same approach i.e. event functions encapsulated inside interfaces.

Our custom event system is made up of 3 components.

  • Event Publisher
  • Event Listener (Usually integrated with some Controller component).
  • Event Manager

Event Listener

In order to listen for events, we can introduce a number of interfaces, each of which is intended to listen for a different type of event. A general description for such interfaces is depicted in the illustration below.

As you can see, for each type of event listener interface, we will have separate types of EventData. Whereas each EventData class, will be derived from a single BaseEventData class. So any script that implements any version of the ICustomEventListener, will receive an event of the event type it is listening for.

For example, supposing we have 3 types of events which we want to propagate i.e. Button Click event, XP Gained event and Ammo Fired event. In this case we’ll have three separate interfaces providing these three event functions (as shown in the snippet below).

public interface IButtonEvent
    {
        void OnButtonEvent(ButtonEvent buttonEvent);
    }
    public interface IXPEvent
    {
        void OnXPEvent(XPEvent XPEvent);
    }
    public interface IFireEvent
    {
        void OnFireEvent(FireEvent fireEvent);
    }

 

We will also have to define 3 separate event data types, which will all inherit their properties from a common parent class (as shown below).

public abstract class BaseEventData
    { }

    public class ButtonEvent : BaseEventData
    {
        public string ButtonName;
    }
    public class XPEvent : BaseEventData
    {
        public int GainedXP;
        public int TotalXP;
    }

    public class FireEvent : BaseEventData
    {
        public FireType FireType;
    }

    //.........//
    public enum FireType
    {
        GLOCK,
        AK47
    }

 

In order to use this event system inside any script, you just need to implement the appropriate interface for receiving that particular event.

For example, below is a test script that we are using to receive all three of above mentioned event types, by implementing all three event interfaces.

public class Test : MonoBehaviour, IButtonEvent, IFireEvent, IXPEvent
{
    public void OnButton(ButtonEvent buttonEvent)
    {
        Debug.Log(buttonEvent.ButtonName);
    }

    public void OnFireEvent(FireEvent fireEvent)
    {
        Debug.Log(fireEvent.FireType);
    }

    public void OnXPEvent(XPEvent XPEvent)
    {
        Debug.Log(XPEvent.GainedXP);
    }
}

 

But to make it work, this script must first register itself with the EventManager, which is one of the three components of our event system. So what is an event manager?

Event Manager

An EventManager is basically a static class which is responsible for registering event listeners and bridging the gap between event publishers and event listeners. A simple EventManager class can be described as follows:

 

The Event Manager will contain a list of delegates for different types of events. We can implement this using a dictionary (as shown below).

public static class EventManager
{
        private static readonly Dictionary<string, object> EventMap = new Dictionary<string, object>();
}

 

This class will provide the following three public methods.

 

  • A Register method by which any script can register itself for receiving desired events.
  • An Unregister method to stop receiving events.
  • A Publish method to trigger an event.
public static void Register(object obj)
{
       SubscribeEvents(obj, true);
}
public static void Unregister(object obj)
{
       SubscribeEvents(obj, false);
}
public static void PublishEvent<T>(T eventData) where T : BaseEventData
{
       string key = typeof(T).Name;
       if (!EventMap.ContainsKey(key))
           return; // no subscribers

       var action = (Action<T>)EventMap[key];
       if (action == null)
           return;

       action.Invoke(eventData);
}

 

The other supportive methods that will be used are:

/// <summary>
/// Subscribes or unsubscribes to all events implemented on obj
/// Note: when you add a new event interface, add the subcription lines here
/// </summary>
private static void SubscribeEvents(object obj, bool subscribe)
{
       var buttonEvent = obj as IButtonEvent;
       if (buttonEvent != null) SubscribeEvent<ButtonEvent>(buttonEvent.OnButtonEvent, subscribe);

       var xpEvent = obj as IXPEvent;
       if (xpEvent != null) SubscribeEvent<XPEvent>(xpEvent.OnXPEvent, subscribe);

       var fireEvent = obj as IFireEvent;
       if (fireEvent != null) SubscribeEvent<FireEvent>(fireEvent.OnFireEvent, subscribe);
            
}

 

/// <summary>
/// Subscribes or unsubscribes a consumer to an event
/// </summary>
static void SubscribeEvent<T>(Action<T> onEvent, bool subscribe)
{
      string key = typeof(T).Name;
      bool containsKey = EventMap.ContainsKey(key);
      Action<T> action = containsKey ? (Action<T>)EventMap[key] : null;

      if (subscribe)
      {
           // if key already exists than do delegate addition.
           // otherwise just make the first entry for the key.
           EventMap[key] = containsKey
                    ? action + onEvent
                    : onEvent;
           return;
      }

      if (!containsKey)
           return;

      // unsubscribe
      if (action.GetInvocationList().GetLength(0) <= 1)
      {
           // no more subscribers, remove key completely from the map
           EventMap.Remove(key);
           return;
      }

      // remove the subscriber
      EventMap[key] = action - onEvent;
}

 

So, any script implementing the ICustomEventListener must first register itself to the EventManager to start receiving events, and must unregister itself upon destruction.

Event Publisher

The third component of our event system is the event publisher, which is the simplest component of the event system. It can be a generic component or can be embedded inside another script. It’s purpose, is just to call the PublishEvent method with the appropriate EventData (as depicted in the snippet below).

[RequireComponent(typeof (Button))]
public class ButtonEventDispatcher : MonoBehaviour
{
     private void Start()
     {
         GetComponent<Button>().onClick.AddListener(() =>
         {
             var buttonEvent = new ButtonEvent
             {
                  ButtonName = gameObject.name,
             };
             EventManager.PublishEvent(buttonEvent);
         });
     }
}

 

Now, in order to make our test class work, we just need to make 2 changes. First, we need to register our test class with the event system on Awake, and unregister the test class on Destroy. It is important that we unregister our test class on destroy, otherwise it may result in exceptions being thrown. Let’s look at an example.

Example

public class Test : MonoBehaviour, IButtonEvents, IFireEvent, IXPEvent
{
    public void Awake()
    {
        EventManager.Register(this);
    }

    public void OnButton(ButtonEvent buttonEvent)
    {
        Debug.Log(buttonEvent.ButtonName);
    }

    public void OnFireEvent(FireEvent fireEvent)
    {
        Debug.Log(fireEvent.FireType);
    }

    public void OnXPEvent(XPEvent XPEvent)
    {
        Debug.Log(XPEvent.GainedXP);
    }

    public void OnDestroy()
    {
        EventManager.Unregister(this);
    }
}

 

As you can see from the code snippet above, our event system is basically looking for the list of event interfaces our test script is implementing, upon which it will register that script for all those respective events.

Hope you find this post useful.

 

CALL

USA408 365 4638

VISIT

1301 Shoreway Road, Suite 160,

Belmont, CA 94002

Contact us

Whether you are a large enterprise looking to augment your teams with experts resources or an SME looking to scale your business or a startup looking to build something.
We are your digital growth partner.

Tel: +1 408 365 4638
Support: +1 (408) 512 1812