The holy, seamless, integrated matrimony of two powerhouses.

Google Analytics in Unity

As I was previously lamenting the lack of information from our beta testing survey, I mentioned that the analytics we collected were instrumental in tuning our game before release. There are lots of analytics providers out there – some paid, some free – but the wealth of tools and analysis that Google provides is unparalleled. My quest for Google Analytics integration into our Unity3d game had begun (much like I assume yours has). I read this post on including Google Analytics in Unity applications and snagged the code from Almar’s site – which appears to be missing now. So for your convenience, I’ve included it below.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

/// Google analytics class v 1.1
/// v1.0 (c) by Almar Joling
/// E-mail: [email]unitydev@persistentrealities.com[/email]
/// Website: [url]http://www.persistentrealities.com[/url]

/// Performs a google analytics request with given  parameters
/// Important: account id and domain id are required.
/// Make sure that your site is verified, so that tracking is enabled! (it needs a file to be placed somewhere in a directory online)

/// Example usage:
/// One time initialize:
/// Use a different hostname if you like.
/// GoogleAnalyticsHelper.Instance.Settings("UA-yourid", "127.0.0.1");

/// Then, for an "event":
///  GoogleAnalyticsHelper.Instance.LogEvent("levelname", "pickups", "pickupname", "optionallabel", optionalvalue);

/// For a "page" (like loading a level?)
/// GoogleAnalyticsHelper.Instance.LogPage("Mainscreen");

/// If you made any improvements please let me know :).

public sealed class GoogleAnalyticsHelper : MonoBehaviour {
    private static string accountid;
    private static string domain = "";
    public static GoogleAnalyticsHelper instance;

	public void Awake() {
        GoogleAnalyticsHelper.instance = this;
    }

	public static GoogleAnalyticsHelper Instance {
        get {
            return instance;
        }
    }

    /// Init class with given site id and domain name
    public static void Settings(string p_accountid, string p_domain){
        domain = p_domain;
        accountid = p_accountid;
    }

    public static void LogPage(string page){
        LogEvent(page, "","","", 0);
    }

    /// Perform a log call, if only page is specified, a page visit will be tracked
    /// With category, action, and optionally opt_label and opt_value, there will be an event added instead
    /// Note that the statistics can take up to 24h before showing up at your Google Analytics account!
    private static Hashtable requestParams = new Hashtable();

    public static void LogEvent(string page, string category, string action, string opt_label, int opt_value){
        if (domain.Length == 0){
            //Debug.Log("GoogleAnalytics settings not set!");
            return;
        }
        long utCookie = Random.Range(10000000,99999999);
        long utRandom = Random.Range(1000000000,2000000000);
        long utToday = GetEpochTime();
        string encoded_equals = "%3D";
        string encoded_separator = "%7C";

        string _utma = utCookie + "." + utRandom + "." + utToday + "." + utToday + "." + utToday + ".2" + WWW.EscapeURL (";") + WWW.EscapeURL ("+");
        string cookieUTMZstr = "utmcsr" + encoded_equals + "(direct)" + encoded_separator + "utmccn"+ encoded_equals +"(direct)" + encoded_separator + "utmcmd" + encoded_equals + "(none)" + WWW.EscapeURL (";");

        string _utmz = utCookie + "." + utToday + "2.2.2." + cookieUTMZstr;

        /// If no page was given, use levelname:
        if (page.Length == 0) {
            page = Application.loadedLevelName;
        }

        requestParams.Clear();
        requestParams.Add("utmwv", "4.6.5");
        requestParams.Add("utmn", utRandom.ToString());
        requestParams.Add("utmhn", WWW.EscapeURL(domain));
        requestParams.Add("utmcs", "ISO-8859-1");
        requestParams.Add("utmsr", Screen.currentResolution.width.ToString() + "x" + Screen.currentResolution.height.ToString());
        requestParams.Add("utmsc", "24-bit");
        requestParams.Add("utmul", "nl");
        requestParams.Add("utmje", "0");
        requestParams.Add("utmfl", "-");
        requestParams.Add("utmdt", WWW.EscapeURL(page));
        requestParams.Add("utmhid", utRandom.ToString());
        requestParams.Add("utmr", "-");
        requestParams.Add("utmp", page);
        requestParams.Add("utmac", accountid);
        requestParams.Add("utmcc", "__utma" + encoded_equals +_utma + "__utmz" + encoded_equals + _utmz );

        /// Add event if available:
        if (category.Length > 0 && action.Length > 0){
            string eventparams = "5(" + category + "*" + action;
            if (opt_label.Length > 0){
                eventparams += "*" + opt_label + ")(" + opt_value.ToString() + ")";
            } else {
                eventparams += ")";
            }
            requestParams.Add("utme", eventparams);
            requestParams.Add("utmt", "event");
        }
        /// Create query string:
        ArrayList pageURI = new ArrayList();
        foreach( string key in requestParams.Keys ){
                pageURI.Add( key  + "=" + requestParams[key]) ;
        }

		string url =   "http://www.google-analytics.com/__utm.gif?" + string.Join("&", (string [])pageURI.ToArray(typeof(string)));

        /// Log url:
        //Debug.Log("[Google URL]" + url);

        /// Execute query:
#if UNITY_WEBPLAYER
		Application.ExternalCall ("gaLogEvent", "BlockBlastersWebV1", ""+action, ""+opt_label, opt_value, true);
#else
        new WWW(url);
#endif
    }

    private static long GetEpochTime(){
        System.DateTime dtCurTime = System.DateTime.Now;
        System.DateTime dtEpochStartTime = System.Convert.ToDateTime("1/1/1970 0:00:00 AM");
        System.TimeSpan ts = dtCurTime.Subtract(dtEpochStartTime);

        long epochtime;
        epochtime = ((((((ts.Days * 24) + ts.Hours) * 60) + ts.Minutes) * 60) + ts.Seconds);

        return epochtime;
    }
}

A BIG thanks to him for making this available. With a few tweaks it worked perfectly. Here’s how I set it up:

Inside my Start() (or Awake()) function:

if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork){
    GoogleAnalyticsHelper.Settings("UA-12345678-9", "http://www.g3zarstudios.com");
    logEvent ("game", "start", 0);
}

Obviously, you’ll need to replace UA-12345678-9 with your Google Analytics ID and the G3zar URL with your own. I defined a logEvent() function to wrap their GoogleAnalyticsHelper.LogEvent call, here’s what that looks like:

private void logEvent(string category, string action, int value){
    if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork){
        GoogleAnalyticsHelper.LogEvent (gaStudio, gaGameName, category, action, value);
    }
}
 

**EDIT** as noted by Thormor in the comments below, this code only checks for a LAN connection to the internet and your analytics will not function if the device in question is connected via 3G, 4G, or any other cellular network.  To submit analytics via these other channels, check out his comment below.  Thanks, Thormor!

I realize I’m checking for network connectivity twice in this example, but usually, I just call logEvent() on its own without being inside a check. The only reason I have the check in Start() is for the GoogleAnalyticsHelper initialization. Log event makes it super simple to track events for my game. I just pass a category for an event, an action and a value. We’ll show how these map to the actual data in a second. To the actual GoogleAnalyticsHelper LogEvent() function I pass gaStudio which is “G3zarStudios” and gaGameName which is “BlockBlastersBeta.” This allows me to not only re-use this code between games easily through a single domain name, but also allows me to track different versions of the game separately if I want. During development, I set gaGameName to “BlockBlastersDev” so that my usage doesn’t clutter up beta testing results – but I can still ensure that my code changes aren’t breaking the analytics logging.

Here’s an example of logging an event when a player beats a level and earns 3 stars:

logEvent ("level_" + curLevel, "win", 3);

So here’s how it maps to data:

  • gaStudio – not tracked (as far as I can tell) in Google Analytics
  • gaGameName – highest level “Category” name in Google Analytics for event tracking
  • catgory – shows as Event Action in Google Analytics
  • action – shows as Event Label in Google Analytics
  • value – optional parameter for tracking numerical values. Google Analytics provides sums and averages of these.

Let’s take a look at what some of this looks like. For each level I log “start”,”win” and “lose” events. These are tracked as actions in logEvent() and as Event Labels in Google Analytics. This allows me to group them inside categories or Event Actions as Google likes to call them. So level_1 would log a “win” with a value showing 3 stars earned. Here’s a high level view of my data:

This does a great job of breaking down activity for me by level. It’s easy to see which levels have the most events (and therefore the most plays). We can see that people must struggle the most with level 19 and 17 so we can use this information to redesign some of the more challenging levels. All without users needing to tell me that level 19 was too difficult.

Here’s a breakdown of a level and what the individual events look like:

Again, we get some really useful information from this. The timeline shows us when people are playing this level (and our game). We can see the number of times this level has been started, beaten and lost. We can also tell the number of times that users have given up on this level (decided to restart it without losing or winning). For the win event, we can see that the average stars earned on this level is 2.69/3. All of this information is useful for fine tuning level design.

Something else I added with analytics was tracking a start event for when people start playing the game itself. We saw this in the Start() code for Unity. When exiting the game, I log the total time that’s passed since they started playing the game so I can see how long people play the game before they get bored. Here’s what that code looks like:

private void OnApplicationQuit(){
    logEvent ("game","exit",(int)Time.fixedTime);
}

So not only can I see that the average amount of time spent playing is 736 seconds (or 12 minutes) but I can also see that the game must have crashed 4 times (since we can see 36 starts and only 32 exits). Drilling into the start events will also turn our timeline graph into a more accurate depiction of how many people played the game each day.

Using Google Analytics From Within Unity Webplayer

One of our eagle-eyed readers, akratic, spotted this compiler directive inside GoogleAnalyticsHelper.cs:

#if UNITY_WEBPLAYER
      Application.ExternalCall (
            "gaLogEvent",
            "BlockBlastersWebV1",
            "" + action,
            "" + opt_label,
            opt_value,
            true
      );

This code is executed in place of the normal event logging when your app is run inside a webplayer.  The only change you’ll have to make is replacing “BlockBlastersWebV1″ with whatever name you’d like your app to log under.

Usually, a Google Analytics event is logged by requesting a small 1px __utm.gif file from Google and passing your event parameters in the query string.  However, when running inside a webplayer, the WWW() call doesn’t operate as we want it to.  In this case, we need to call a JavaScript function on the page which will in turn make the Google Analytics call for us.  Here’s what the JavaScript function looks like:

function gaLogEvent(category, action, label, value, noninteract)
{
      _gaq.push(['_trackEvent', category, action,
                label, value, noninteract]);
}

This relies on the _gaq variable being set which happens when you initialize your Google Analytics code on your web page just as you normally would.  It shouldn’t require any extra work on your part beyond defining this gaLogEvent JavaScript function on your page.  You can retrieve this code from your Google Analytics account if you don’t already have it on your web player page.

So to summarize, instead of requesting the gif directly from within our Unity app, we need to call a JavaScript function which exists on the page that is hosting our webplayer app.  That function, in turn, will request the gif from Google and pass along the event parameters we defined in our app.

In Conclusion

Hopefully this helps you implement your own Google Analytics tracking in Unity. After gaining access to this information, I can’t imagine trying to play test without it. Watching people over their shoulder is a great way of getting user feedback but it’s impossible to sample a large pool of users that way (unless you have way more free time than I do). What have been your experiences with Google Analytics in Unity?

MetricsWant more? Check out our list of 5 game metrics that you should track.  It’s got code snippets and snapshots of how the data will appear in Google Analytics!

Also, take a look at our Block Blasters Post Mortem to see how useful your Google Analytics data can be.

37 thoughts on “Google Analytics in Unity”

  1. Thanks for this information. I’ve been looking for something like this. I do have a question. When calling using the GoogleAnalyticsHelper.Settings() or the GoogleAnalyticsHelper.LogEvent call, are these in the same GoogleAnalyticsHelper.cs file, or are these being used in separate .cs files? Thanks for any clarity you can give.

    1. Thanks for the question! I’m sorry I wasn’t more clear in the article. Yes – calling both GoogleAnalyticsHelper.Settings() and GoogleAnalyticsHelper.LogEvent() will result in a call to the functions within GoogleAnalyticsHelper.cs (lines 44 and 58 to be exact). I’d written a wrapper for the LogEvent call (probably should have named it something else to avoid confusion) which exists in a global.cs file I can call from anywhere in my code. You could absolutely leave this bit out if you want – or completely bypass it by calling GoogleAnalyticsHelper.LogEvent() like you say. I hope this helps clear things up for you – please don’t hesitate to ask any other questions! Happy coding!

      1. Thanks G3zar Studios. So, let me make sure I understand. 1. Copy and paste the GoogleAnalyticsHelper code to GoogleAnalyticsHelper.cs, 2. Create a Start() and copy & paste the if (Application.internetReachability… code in it , 3. copy & paste the logEvent() code somewhere in it. 4. I then created public string variables for gaStudio, gaGameName, category, action, and value.

        When I did this I didn’t get any pings on Google Analytics, but when I changed the value setting in the logEvent (“game”, “start”, 0); to 1 instead of 0, I got a ping. This seems to work, but am I doing this correctly? I am not a C# guru, but I am learning bit by bit. Thanks for your insight.

        1. Yes that sounds correct! For clarity, here’s a stripped down version of my global.cs script that I have attached to a single Game Object in my scene:


          using UnityEngine;
          using System.Collections;
          using System.IO;
          using System.Collections.Generic;
          using System;

          public class global : MonoBehaviour {

          private static string gaId = "UA-12345678-9";
          private static string gaURL = "http://www.yoururlhere.com";
          private static string gaStudio = "yourDevName";
          private static string gaGameName = "yourGameName";

          void Awake () {
          if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork){
          GoogleAnalyticsHelper.Settings(gaId, gaURL);
          logEvent ("game", "start", 0);
          logEvent ("device", SystemInfo.deviceModel, 0);
          }
          }

          private void logEvent(string category, string action, int value){
          if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork){
          GoogleAnalyticsHelper.LogEvent (gaStudio, gaGameName, category, action, value);
          }
          }
          }

          Sounds like you’ve already added GoogleAnalyticsHelper.cs so you should be all set. I’m not sure why an event with a value of 1 would post while an event with value 0 would not. That sounds odd to me – they should both work. Sometimes it takes a while (hours) before Google records an event to http://analytics.google.com. If it’s still not working with a value of 0, you can always check your console for errors and feel free to post them here. I’ll try to help you sort through them. Good luck!

          1. Thanks G3zar Studios. It looks like my home iMac or wifi is not letting any signals from my game go through. But, when I tested it at work, perfecto! Any ideas on that? Thanks.

          2. Do your indie development at work? ;-) Only kidding. The GA call from your app is just a normal HTTP request for a 1px GIF image from Google – so you don’t have to open any ports on your router to allow the analytics traffic to go through. A good test would be to uncomment line 116 in GoogleAnalyticsHelper.cs – Debug.Log(“[Google URL]” + url); then run your app and check your console for the [Google URL]. Copy and paste this url into your browser. If the call succeeds, you know it’s an issue with your Unity app and not your network. If it’s an issue with the app, check to make sure you’ve allowed Unity access through any firewalls (OSX / Little Snitch / etc.) If it’s STILL not working, check out Unity’s doc on their WWW object. I believe there’s a way to view any error messages that are being returned by HTTP requests.

  2. It seems that I needed to update my copy of Unity at home to match to updated one at work. I seem to be able to get the Analytics results now. Thanks for all your help. Peace.

  3. Hello!

    When I implemented Google Analytic for our game, I had a very similar code base as to yours. However our mobile game was unable to send data to the mobile profile, only to the web one…
    Were you able to go around this problem? (if your game was for mobile)

    1. Hi Thormor, yes ours was implemented for our Android/iOS game Block Blasters and was able to submit Analytics information w/out issues. To clarify the difference between “mobile profile” and “web profile” – are you saying that your app analytics are tracking w/in your website’s section of Google Analytics and not w/in a separate mobile profile? Ours track under our web traffic as well – but are all tracked as events (as opposed to content) so it’s easy to separate the data for reporting purposes. I’m sure it’s possible to set up separate Analytics tokens to track individual apps, but we thought there would be value in being able to track all of our apps under one account. Is this what you’re asking?

      1. Under one profile you can have both a web and a mobile one. I was only seeing my events on the web profile, and never on the mobile profile.

        It doesn’t change much in term of reports, but having the mobile profile working would simplify reports as which device is being used and some data would be more accurate, like timing.

        Also, small comment on your code snipset :
        if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork)
        Doing so you exclude 3GS connection to send telemetry event, as they will never be setup =)

        1. Oh very cool tip about analytics – I will look into that and update the blog if I figure anything out!

          Thanks for the cellular data suggestion too! I’ll look into that as well! :-)

          1. For the cellular data you could do : UnityEngine.Application.internetReachability != UnityEngine.NetworkReachability.NotReachable

            or : UnityEngine.Application.internetReachability == UnityEngine.NetworkReachability.ReachableViaCarrierDataNetwork || UnityEngine.Application.internetReachability == UnityEngine.NetworkReachability.ReachableViaLocalAreaNetwork

            I’ll follow your blog if you find anything else. Thanks!

    1. I’m thinking your analytics needs might be a bit more advanced than how we’ve been using Google Analytics at G3zar. :-) When we log an event, we pass a custom label and action which are effectively used by Google as the dimension (https://developers.google.com/analytics/devguides/platform/features/customdimsmets). So in effect, we’re defining ALL of the dimensions w/in the event tracking portion of Google Analytics. At least this is my understanding of what they mean by dimensions. I tried to log in to take some additional screenshots but GA seems to be having issues at the moment. I blame Google IO. ;-)

    1. Yeah that’s a really good question! Thanks! I’ll try to get an update done to the post. For the analytics to work via web player you essentially have to call out to a JavaScript function that exists on the page which is hosting your unity app. I’ll update the post in more detail. Very good suggestion!

    2. Hey akratic – thanks again for the question! I’ve updated the post with additional information about webplayer event logging under a section called “Using Google Analytics From Within Unity Webplayer.” Hopefully this helps explain things for you – but if you have any other questions, don’t hesitate to ask!

      1. Hey, thanks a lot for this write-up, I’m finding it extremely helpful. This point seems problematic for me, because I’m trying to use Google Analytics for a Facebook game. As far as I know, I can’t count on or add any external Javascript functions to the webpage, since that is all handled by Facebook. If there was a clever way to do this strictly from within the webplayer, that’d be great.

        Thanks for this awesome post!

  4. With this code each event is registered as a new visit, right? There’s a way to make analytics understand that all events triggered by the same app instance belong to the same session?

    1. Yup yup you are correct. I’m not sure if there’s a way to attach multiple events to one session while still having the ability to compare data across all sessions. You CAN use the gaGameName parameter of logEvent() in order to track different app instances or platforms (for example myIphoneApp, myPCApp, myKongregateApp) but this will only group your data by platform and not by player session – and it will make cross platform queries of your data more challenging. If anyone who reads this post knows how to do this within the event structure of Google Analytics, please add your two-cents! Thanks for the comment!

      1. I could create an random number at the Start/Awake and use it as gaGameName. It would separate the game sessions. But wait i’m really trying to avoid is the erroneous number of visits, especially in the real time stats that says I have many users online at the moment, but I know I’m the unique one. :)

        1. Oh I see what you’re saying. Yeah I can see that being a problem. It’s difficult to know when users are “offline” or stop playing the game from an analytics perspective because mobile and web players don’t always report an exit from the app and give you a chance to log that information. You might be able to get away with firing an event every minute that reports how long the player has been active and reset the value when they close your game. That would generate a nice graph showing how many minutes people play before closing the app – it would also show individual events under your real time stats (but not give an active user count). A problem with doing that though is that if someone backgrounds your app on a mobile device after playing for 9 minutes it will just pick back up at 10 the next time they open it – assuming they didn’t explicitly close out of it. If you are able to find a good option to track the metric you’re after, definitely let us know!

    1. Hi Martin – yes this is definitely related and you probably could implement something like this via Unity (though calling iOS specific code from Unity can be tricky). Since Unity makes building cross platform apps really easy, we wanted to keep our analytics solution as cross platform as possible. Using an HTTP post via Unity’s WWW() function allowed us to do this. If you end up using the method you linked, we’re definitely interested in hearing how it goes for you. Let us know and happy indie dev-ing!!

    1. That sounds like flash has some built in XSS controls to prevent malicious use. You might need to treat flash like you treat the web player and not perform an HTTP post directly from there. Instead, call out to a JS function on your page that’s hosting the flash build and do your analytics event tracking from that function. Check out line 118 of GoogleAnalyticsHelper.cs. You might need to add a check to that compiler directive so that the analytics code makes an external call for web player AND flash player. Pretty sure Almar Joling wrote this before Unity built for flash so that could be a legit upgrade to his script. Let me know if that does the trick for you!

    1. Sorry for the super delayed response! While I haven’t built an app in Unity for Windows Phone, the Google Analytics tools will work seamlessly with any platform that supports HTTP posts from w/in an app. This should include any Windows based platform.

    2. I hop G3zar Studios won’t mind if I pitch in here with my alternative solution… (or if that’s not ok – just delete this msg).

      Since you asked about WP8 / W8 – I’d like to mention my “Google Universal Analytics for Unity” which is available in Asset Store: http://j.mp/UASGUA

      It supports also Windows Phone 8 and Windows 8 platforms, as well as other desktop and mobile platforms and even the webplayer plugin.

      Thormor asked earlier in comments about custom dimensions, those are supported as well, and actually there is full API available for the “Measurement Protocol” specification.

      There’s also support for caching hits when device is offline, automatic internet access verification (better than just checking the Application.internetReachability), and throttled background sending of the cached hits when there is connectivity.

What do you think?