Thursday, April 21, 2016

Avoiding Code Coupling: Messaging Systems

A pretty common feature of, well most any game, is clicking on some object to select it or get information about it. Or mousing over it for a tooltip, etc. In whichever case there needs to be some connection between the visual game object displayed, and the behavior and data behind it.

Let's examine a case from JumpShip, the game I'm working on: you have ship/fleet icons displayed on the screen. When you mouse over them a section of the lower screen displays some useful stats on the unit(s). Clicking on the icon selects that fleet. If you right click on the icon it brings up a dialog with detailed information and actions.

Clicking on the icon First Fleet (1 below) selects it (adding the colored markers around it).  Right clicking brings up the details dialog (2).  Having moused over the icon it to selected it, the lower left hand corner of the screen updates to display some general info (3).

The Problem

1 Ship Icon.  2 Details Dialog.  3 UI Info updated on mouseover.
The question is, how do we get all these UI and other elements to react to the mouse events on the icon (assume a collider on the icon, to enable the ordinary OnMouseEnter and OnMouseUp, etc. behavior).

Assuming that we have some sort of general script (StarSystemSceneControl.cs in my case) driving the scene we have to somehow link the icon, control script, UI and the pop-up dialog.

The initial, and kind of the 'Unity' way would be for each of the control scripts (ShipHandler in this case) on the icons to hold a reference to the main control script and the dialog:

public class ShipHandler : MonoBehaviour
    {
    public BaseOrderable Orderable;
    public TextMeshPro ShipLabel;
    public Material ShipDestLineMaterial;
    public Trail Trail;

    public SystemScreenSceneControl sceneControl;
    public OrdersPanelHandler ordersPanelHandler;
    public OrderablePanelDisplayHandler orderablePanelDisplayHandler;

Presumably we'd assign these values in the prefab we use to instantiate the ship icon.  Thus our code in the icon handler would look something like:


    //Note we only handle the primary/left mouse button (0)
    //here.  This even simply does not trigger on any other
    //button as it turns out.
    public void OnMouseUpAsButton()
        {

        //Don't handle this if we are over a UI object.
        if (EventSystem.current.IsPointerOverGameObject())
            return;
 
        selected = true;
        sceneControl.SetSelectedUnit(Orderable.ID);
 
        (snip...)
 


    public void OnMouseOver()
        {

        //Don't handle this if we are over a UI object.
        if (EventSystem.current.IsPointerOverGameObject())
            return;

        //right mouse button.  Doesn't work with OnMouseUp or OnMouseUpAsButton
        if (Input.GetMouseButtonUp(1) && selected)
            {
            ordersPanelHandler.SetSelectedUnit(Orderable.ID);
            ordersPanelHandler.gameObject.SetActive(True);
            }
        }


    //Set the data in the UI panel in the bottom left of the
    //screen to show a summary for this unit on mouseover.
    public void OnMouseEnter()
        {
        if (!EventSystem.current.IsPointerOverGameObject())
            orderablePanelDisplayHandler.SetDisplay(Orderable);
        }

So, that will work.  And it will probably work fine.  Of course there's also the problem of having to assign and keep track of all these references, but that's minor on the scale of things.

Further Problems

The main trouble is this:  now ShipHandler.cs (on the icon gameObject) has to know the inner details of StarSystemSceneControl.cs.  If I decided to make any changes in StarSystemSceneControl (say reorganizing these to a UI/selection bits to a different script, i.e. ShipSelectionHandler.cs or something) or even just rename these functions... it breaks ShipHandler.

Worse, we've now worked some UI and game logic code into what is really a script that's just here to display the ship icon and respond to clicks.

The class on the icon shouldn't have to know about hiding and showing some other dialogs, or setting data on UI objects, etc.  All it should care about is that 'somebody clicked me'.

And another problem:  If I've got twenty ship icons on the screen and I select one, I have to somehow notify all the others that they aren't the selected ship anymore (so they will clear the selection indicator, the orange-ish brackets in the picture above).  So now every ship icon object needs to know about every other one!  Or at a minimum the sceneControl script needs to keep a list of them all and run through them when we do sceneControl.SetSelectedUnit().

If you don't already know where I'm going, this is called 'coupling'.  We've made object A of our software dependent on the exact inner details of object B.  Now any time we edit or change object B we risk breaking A.  Worse, we've made A have to know various details that it really doesn't even need to do it's job, cluttering up the code.  To some extent you could also call this a violation of the Single Responsibility Principle, which is another very useful rule to live by..

Ideally, we want the shipHandler object to simply say 'I've been clicked... everybody that cares: do stuff'.  Since that 'stuff' isn't the responsibility of this object, it shouldn't care or know about the details.  Ideally the various other object that care about ship icons being clicked should do their own work.

So, how do we do this?  Unity does have some built in messaging features (SendMessage), but that only works for passing messages to other components on one game object.  Not what we want.

What it sounds like we need is some kind of publish/subscribe message system.  In other words, some objects could state 'I am interested in XYZ types of events', and other objects would broadcast 'XYZ has happened', and, besides that there would be no code dependencies or linking.

And, fortunately, someone has long ago written a nice package of code for Unity to do this:  the wiki.unity3d.com: Advanced C# Messenger by Ilya Suzdalnitski (this is an improvement and extension of several previous versions of this code).

The Messenger

I have to say I pretty much live and die by the Messenger.    I'll go into details in a bit, but switching to this simplifies our code a lot:


    //Note we only handle the primary/left mouse button (0)
    //here.  This even simply does not trigger on any other
    //button as it turns out.
    public void OnMouseUpAsButton()
        {

        //Don't handle this if we are over a UI object.
        if (EventSystem.current.IsPointerOverGameObject())
            return;
 
        Messenger.Broadcast("SelectOrderable", Orderable);
 
        (snip...)
 


    public void OnMouseOver()
        {

        //Don't handle this if we are over a UI object.
        if (EventSystem.current.IsPointerOverGameObject())
            return;

        //right mouse button.  Doesn't work with OnMouseUp or OnMouseUpAsButton
        if (Input.GetMouseButtonUp(1) && selected)
            Messenger.Broadcast("SetOrderablePanelDisplay", Orderable);
        }

    //Set the data in the UI panel in the bottom left of the
    //screen to show a summary for this unit on mouseover.
    public void OnMouseEnter()
        {
        if (!EventSystem.current.IsPointerOverGameObject())
           Messenger.Broadcast("SetOrderablePanelDisplay", Orderable);
        }

Now, instead of having to hold references to various different objects, we just fire off a message, and anyone who cares deals with it.  ShipHandler.cs no longer needs to know the inner workings of other classes, and is thus far simpler and less prone to break.

Now, even if I muck up some other script (delete the functions that handle these events, rename them, whatever) ShipHandler.cs will still work. Something else may fail, but that something is not the responsibility of this script.

Better, now when we instantiate all these icons and UI objects, we don't have to fill each one out with references to everything else, simplifying our code even further.

The one (pointlessly, trivially, minor) downside is that now instead of a direct call to whatever object, you're essentially doing lookups into a function table to find delegates to call.   I can't see any case where this would be anything but well below the threshold of detectability.  If it's a problem, you definitely have other, bigger problems.

So:  decoupling good, single responsiblity good, messaging good.  I'll talk a bit more about how I use the messenger in the next post.

No comments:

Post a Comment