Refactored Game Analytics

When I integrated an analytics system using Mixpanel, the event senders were cluttered in just one class. It was my first time integrating an analytics system. Honestly, the code looks like a “getting started” tutorial. There’s a big switch statement for some game notifications. Each ‘case’ clause calls a function that sends the Mixpanel event. Each notification uses a unique function with some common code among other functions. The class has reached around 600 lines of code and I intended to add more events. It’s time to refactor. The following is the ugly implementation:

public void HandleNotification(Notification notification) {
    switch(notification.GetId()) {
    case NotificationIds.LEVEL_STARTED:
        OnLevelStarted();
        break;

    case NotificationIds.LEVEL_COMPLETED:
        OnLevelComplete();
        break;

    case NotificationIds.LEVEL_FAILED:
        OnLevelFailed();
        break;

    case NotificationIds.PLAYER_CREATED_BUILDING:
        OnPlayerCreatedBuilding(notification);
        break;

    case NotificationIds.PLAYER_UPGRADED_BUILDING:
        OnPlayerUpgradedBuilding(notification);
        break;

    case NotificationIds.WAVE_COMPLETED:
        OnWaveCompleted();
        break;

    case NotificationIds.BUILDING_RECOVERED:
        OnBuildingRecovered(notification);
        break;

    case NotificationIds.PLAYER_SET_RALLY_STARTED:
        OnSetRallyStarted(notification);
        break;

    case NotificationIds.PLAYER_SET_RALLY_ENDED:
        OnSetRallyEnded(notification);
        break;

    case NotificationIds.EXECUTE_SHOCK_POWER_UP:
        OnShockPowerUp(notification);
        break;

    case NotificationIds.EXECUTE_CHILL_POWER_UP:
        OnChillPowerUp(notification);
        break;

    case NotificationIds.EXECUTE_HEAL_POWER_UP:
        OnHealPowerUp(notification);
        break;

    case NotificationIds.FREEZE_POWER_UP_REQUESTED:
        OnFreezePowerUp();
        break;

    case NotificationIds.DAMAGE_ALL_POWER_UP_REQUESTED:
        OnDamageAllPowerUp();
        break;

    case NotificationIds.PLAYER_TIME_SCALE:
        OnTimeScale(notification);
        break;

    case NotificationIds.PLAYER_STARTED_TARGETTING:
        OnStartedTargetting(notification);
        break;

    case NotificationIds.PLAYER_CANCELLED_TARGETTING:
        OnCancelledTargetting(notification);
        break;
    }
}

My main goal is to remove the long switch statement and reduce the LOC of the GameAnalytics class. I used an interface called AnalyticsEventSender that has a method Send(Notification notif).

/**
 * Interface for classes that send an analytics event.
 */
public interface AnalyticsEventSender {

    /**
     * Sends the analytics event.
     */
    void Send(Notification notification);

}

Each function within each case clause will now be refactored into a class implementing the AnalyticsEventSender interface. The parameters and values that the sender may required can be found in the specified Notification instance. Thus, the so many classes below:

Each one sends a certain kind of Mixpanel event
Each one sends a certain kind of Mixpanel event

This is one of the sender classes:

/**
 * Event sender when player creates a building
 */
public class CreatedBuilding : MixPanelEventSender {

    /**
     * Constructor
     */
    public CreatedBuilding(MxpSystem mixPanel) : base(mixPanel, "Created a Building") {
    }

    #region implemented abstract members of MixPanelEventSender
    public override void Send(Notification notification) {
        MxpEventRequest mxpEvent = mixPanel.StartEvent(this.Description);

        CommonAnalyticsProperties.AddCommonProperties(mxpEvent);
        CommonAnalyticsProperties.AddLevelName(mxpEvent);
        CommonAnalyticsProperties.AddWaveNumber(mxpEvent);

        CommonAnalyticsProperties.AddPosition(mxpEvent, notification);
        CommonAnalyticsProperties.AddBuildingId(mxpEvent, notification);
        
        mixPanel.SendEvent(mxpEvent);
    }
    #endregion

}

I then store an instance of each type of AnalyticsEventSender in a Dictionary. The key of each entry is the corresponding constant used in the original case clause.

public class GameAnalytics : MonoBehaviour, NotificationHandler {
    ...

    private Dictionary<string, AnalyticsEventSender> eventSenderMap;

    ...

    void Awake() {
        ...
        this.eventSenderMap = new Dictionary<string, AnalyticsEventSender>();
        PopulateAnalyticsSenders();
        ...
    }

    private void PopulateAnalyticsSenders() {
        AddSender(NotificationIds.LEVEL_STARTED, new LevelStarted(this.mixPanel));
        AddSender(NotificationIds.LEVEL_COMPLETED, new LevelCompleted(this.mixPanel));
        AddSender(NotificationIds.LEVEL_FAILED, new LevelFailed(this.mixPanel));
        AddSender(NotificationIds.PLAYER_CREATED_BUILDING, new CreatedBuilding(this.mixPanel));
        AddSender(NotificationIds.PLAYER_CREATED_BUILDING_BUT_CANT_AFFORD, new CreatedBuildingButCantAfford(this.mixPanel));
        AddSender(NotificationIds.PLAYER_UPGRADED_BUILDING, new UpgradedBuilding(this.mixPanel));
        AddSender(NotificationIds.WAVE_COMPLETED, new WaveCompleted(this.mixPanel));
        AddSender(NotificationIds.BUILDING_RECOVERED, new RecoveredBuilding(this.mixPanel));
        AddSender(NotificationIds.PLAYER_SET_RALLY_STARTED, new SetRallyStarted(this.mixPanel));
        AddSender(NotificationIds.PLAYER_SET_RALLY_ENDED, new SetRallyEnded(this.mixPanel));
        AddSender(NotificationIds.EXECUTE_SHOCK_POWER_UP, new UsedTargetedSpell(this.mixPanel, "Shock"));
        AddSender(NotificationIds.EXECUTE_CHILL_POWER_UP, new UsedTargetedSpell(this.mixPanel, "Chill"));
        AddSender(NotificationIds.EXECUTE_HEAL_POWER_UP, new UsedTargetedSpell(this.mixPanel, "Heal"));
        AddSender(NotificationIds.FREEZE_POWER_UP_REQUESTED, new UsedNonTargetedSpell(this.mixPanel, "Freeze"));
        AddSender(NotificationIds.DAMAGE_ALL_POWER_UP_REQUESTED, new UsedNonTargetedSpell(this.mixPanel, "Firestorm"));
        AddSender(NotificationIds.PLAYER_TIME_SCALE, new TimeScaled(this.mixPanel));
        AddSender(NotificationIds.PLAYER_STARTED_TARGETTING, new StartedTargeting(this.mixPanel));
        AddSender(NotificationIds.PLAYER_CANCELLED_TARGETTING, new CancelledTargeting(this.mixPanel));
    }

    private void AddSender(string notifId, AnalyticsEventSender sender) {
        this.eventSenderMap[notifId] = sender;
    }

    ...
}

The long switch is then simply replaced with a dictionary lookup during notification handling.

public void HandleNotification(Notification notification) {
    // let map of senders try to handle the notification
    if(this.eventSenderMap.ContainsKey(notification.GetId())) {
        this.eventSenderMap[notification.GetId()].Send(notification);
    }
}

Now, every time I add a new event, I just create a sender class for that event and add an entry in PopulateAnalyticsSenders(). LOC is down and ugly switch statement is removed. Pretty neat huh?

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s