Clone wiki

heatmaps / v1

This is legacy documentation for Heatmaps v1. For the new version, click here

These documents outline, from a developer’s perspective, the Unity Analytics Heatmaps plugin.

Sign up


Heatmaps are an incredibly useful (and rather beautiful) visualization of spatial events. Where do players die? Where do they score kills? Where do they get stuck? By collecting the data from hundreds or thousands of plays, you begin to assemble a large-scale picture of how your users experience your game. The patterns that emerge can help you tune the game and improve things like game economy, difficulty curve, and even good ol’ fashioned fun.

Example Heatmap

The Unity Analytics heatmap system is built on top of existing Analytics technologies. The heatmap is not itself a product so much as an exploration and demonstration of something that can be built by leveraging Custom Events and the new Data Export feature.

Heatmaps allow the recording, reading and rendering of spatial data via a three-step process.

  1. Track data using UnityAnalyticsHeatmap.HeatmapEvent.Send()
  2. Fetch and process raw event data using Data Export and the Aggregate Events section of the Heatmapper inspector.
  3. Render the heatmap with the Render section of the Heatmapper inspector.

Steps 2 and 3 occur inside the Heatmapper inspector in Unity.

Step 0

Activate Analytics

If you haven't already done so, activate Unity Analytics for your project.

Clone this repo

The Unity Analytics Heatmap repo contains a lot of stuff. The important items are:

  • Heatmaps: A Unity project containing all the plugin code, including the Heatmapper inspector
    • Assets
      • Editor
        • Heatmaps (contains classes that operate the Heatmapper inspector)
      • Plugins
        • Heatmaps
          • Examples (contains examples of how you might implement HeatmapEvent.Send()
          • HeatmapEvent.cs - Use this class to send heatmap events to the Analytics server
          • Lib (contains MiniJSON.cs, for parsing. NB: We've had one report of a namespace clash with MiniJSON...presumably because more than one library is using it. Please let us know if you encounter the same issue.)
          • Renderer (contains classes for rendering the heatmap)
          • Shader (contains a shader used by the HeatmapMeshRenderer)
  • Heatmaps_Installer.unitypackage


Double-click Heatmaps_Installer.unitypackage to install the plugin in your Project. This places the following directories:

  • Assets/Plugins/Heatmaps
  • Assets/Editor/Heatmaps

Step 1: Track data

HeatmapEvent API

In order to allow you to capture heatmap data simply, we’ve created an API specifically for heatmaps. Note that UnityAnalyticsHeatmap.HeatmapEvent.Send() is simply a typesafe adapter for the standard UnityAnalytics.CustomEvent() method (so you could choose to ignore HeatmapEvent.Send(), but it adds the advantages of simplicity and type-safety, meaning you’re less likely to send data that the post-processor can't interpret).

Send heatmap events in one of the following forms:

//Simply send Vector3 position info
UnityAnalyticsHeatmap.HeatmapEvent.Send(string eventName, Vector3 position);

//Simply send Vector2 position info
UnityAnalyticsHeatmap.HeatmapEvent.Send(string eventName, Vector2 position);

//Send Vector3 position plus time
UnityAnalyticsHeatmap.HeatmapEvent.Send(string eventName, Vector3 position, float time);

//Send Vector2 position plus time
UnityAnalyticsHeatmap.HeatmapEvent.Send(string eventName, Vector2 position, float time);

//Send Vector3 position plus time and X rotation (suitable for 2D)
UnityAnalyticsHeatmap.HeatmapEvent.Send(string eventName, Vector3 position, float time, float rotation);

//Send position plus rotation (no time)
UnityAnalyticsHeatmap.HeatmapEvent.Send(string eventName, Transform transform);

//Send Vector3 position, rotation and time
UnityAnalyticsHeatmap.HeatmapEvent.Send(string eventName, Transform transform, float time);


//Send the position, rotation and time at the moment of player death
UnityAnalyticsHeatmap.HeatmapEvent.Send(“PlayerDeath”, transform, Time.time);

All heatmap events require an eventName to distinguish the specific spatial event you’re mapping. Note that the Send() method automatically prepends the word "Heatmap." on your event name, so "PlayerDeath" will be processed as "Heatmap.PlayerDeath". Vector2’s or Vector3’s may be used for position to map in two or three dimensions respectively (consider using Vector2’s both for 2D games and for mapping UI interactions). Including time allows you to render results based on when an event occurred during gameplay. Depending on your use case, you might want to use Time.time, Time.fixedTime, Time.realtimeSinceStartup, Time.timeSinceLevelLoad, or Time.unscaledTime. See UnityEngine.Time documentation for details.

Note that 'time' is pretty arbitrary. If your game is turn-based, you could send the number of the turn (expressed as a float) and use that for time instead.

The following data can be sent with Heatmap events

  • x - World spatial coordinates
  • y
  • z
  • rx - World rotational components
  • ry
  • rz
  • t - An indicator for time

Adding arbitrary data

We do not yet fully support aggregating on arbitrary data but we plan to. In order to facilitate this, the HeatmapEvent supports sending a Dictionary of arbitrary values as a final argument. You could, for example, attach a Dictionary like this:

var dict = new Dictionary dict<string, object>();
dict["level'] = currentLevel;
UnityAnalyticsHeatmap.HeatmapEvent.Send("PlayerDeath", transform, Time.time, dict);

By doing so, you will eventually (though not yet today) be able to aggregate based on the arbitrary 'level' parameter to sort heatmaps from different levels.

Note that Custom Events are limited to 10 parameters. If you choose to add arbitrary parameters, ensure that you don't add any more than the Custom Event will support for the HeatmapEvent form you choose. Events with > 10 parameters are rejected by the Analytics SDK.

About sending heatmap events

As you know, apps using Unity Analytics have a Custom Event limit of 100 events per hour per device. Heatmap events are just like any other custom event, but in order to make heatmap data useful to you, we are increasing this limit for your game to 5000 events per hour per device. This allows you to send a bit more than one event/second...which is a lot of data.

PLEASE BE RESPECTFUL OF THIS PRIVILEGE. Your app should send heatmap data ONLY for development builds.

Step 2: Fetch and process raw event data

Data Export is a new feature from Unity Analytics that allows you to get raw access to your events. As of this writing, access to Data Export is by invitation only (but signing up for the heatmaps project automatically adds you to raw data export!). We’ll roll this out to a larger audience as the product matures.

Getting to raw data

Navigate to your project on the Analytics dashboard. Navigate to the Project Settings page. To get there from the Projects page, click the gear icon of your app.

Getting to settings from Projects page

From any other page, find the gear icon in the upper right corner of the page.

Getting to settings from most pages.

If you’ve been invited to use the data export feature, you should see a checkbox saying “Data Export Enabled” together with a link that loops something like this:

Data Export Enabled
List API:

Copy the entire link.

Warning: This link is the key to your data. Keep it secure!

Retrieving data in the Heatmapper inspector

Open Unity and the Heatmapper inspector. (Windows/Heatmapper if you installed the plugin correctly)

Open the Heatmapper

The Heatmapper is divided into three sections. The top section is a set of three buttons that simply allow you to reset the system and find this documentation.

For now we’re interested in the second section: Aggregate Events.

Aggregate Events

Click to open that section. Select the Data Export URL field and paste the URL you copied in the previous step.

Paste the URL

By default, the inspector downloads events from the past five days. If you want, you can change this to reflect any date range you like using the format YYYY-MM-DD (any ISO-8601 date will work).

Press "Fetch & Process". How long this takes depends on the amount of data your clients have sent. Obviously the Heatmapper inspector can only download data once the data exists. At present, raw events are moved into retrievable files every two hours or so, but note that this is an estimate that depends on many factors.

Manual raw data retrieval

The Heatmapper inspector is provided for your convenience, but if you want to see the inner workings of the Data Export API, simply copy/paste the Data Export link into a browser window. This leads to a manifest of available data batches. By "walking" these URLs forward, you can see all the data, understand the Data Export API, and download the data sets manually.

Data aggregation

Once data is downloaded, it is automatically aggregated.

You can modify how the data is processed by trimming dates, by smoothing space or direction (rotation), by disaggregating time, or by narrowing the processed data by event name ("Limit to Events").

Trim dates Ignores info before or after a specific timestamp.

Space smooth Specifies a divisor for smoothing x, y and z coordinates.

Aggregate time When selected, all times are treated the same. When unselected, different times will "smooth" so playback over time may be observed.

Time smooth Value by which to smooth disaggregated time.

Aggregate Direction When selected, all rotations are treated the same. When unselected, different angles will "smooth" so directionality may be observed (see Arrows on the Rendering page).

Limit to Events Clicking this button allows you to build a list of specific events to include (this allows you to filter out heatmap events you don't care about). An empty list will include all valid heatmap events.

How often do I download data?

The Heatmapper is designed to automate as much as possible. When you click "Fetch and Process", it determines what needs to be downloaded and what needs to be aggregated, based on the settings in the "Aggregate Events" subpanel. Therefore, you can always look at heatmaps -- even when offline -- for data you've already downloaded. The system will intelligently update raw data when it can (see the flowchart below).

Upload and aggregation flow

If you know you don't want to check the server for updates, it's faster to check the "Local only" toggle, which instructs the Heatmapper to bypass the server check, and go directly to aggregation using only locally cached files.

Local only mode

Step 3: Render the heatmap

So, you’ve sent a bunch of data, retrieved it, aggregated it. Here’s where the fun begins!

The Heatmapper inspector allows you to load, view, and manipulate heatmap data within Unity. It looks like this:

The render pane

If you followed everything so far, you should already be seeing a heatmap, which looks something like this:

A typical heatmap

The heatmap is a Mesh, just like any other GameObject mesh. You can rotate around it, zoom in on it...even convert it to a prefab for use in a game!

OK, you’ve had your fun...back to work! Let’s look at all these controls to see how you can modify what you’re looking at.

Heatmapper Renderer Controls

The render pane


Depending on exactly how you ran the aggregator script, the “Option” drop-down might display a list of custom event names, or else a list of individual device IDs (we may try to add individual play sessions at some point).

Colors and Thresholds

Individual events occurring at exactly the same location were aggregated in the previous step. What counts as "exactly" depends on some of the settings with which you ran the aggregator. For example, setting the space value to 100 in the inspector will aggregate the following events to "exactly" the same place (all get smoothed to 50, 50, 50):

(x, y, z) .1, .1, .1 50.7, 27.2, 86.0 * 99.9, 99.9, 99.9

You could re-run the aggregator with the space value set to 1 or 1000 or .2 or 72.5. In each case, the space value indicates a unit divisor across all three dimensions for splitting up and smoothing your data.

The more often an event occurs at the same place, the higher its density, represented by d in the JSON. The inspector (and the renderer) displays three colors, representing high-, medium- and low-density occurrence of events. The thresholds are values from 0.0 to 1.0 specifying the transition points between high-/medium-density events, and medium-/low-density events. In other words, the thresholds are the transition points between colors.

We've found that tweaking the colors and thresholds are necessary to make your heatmap visible and useful within your unique environment.

Play controls

When you sent your original event information, you had the option of sending it with a time parameter. If you did so — and we hope you did — you can use these sliders to filter out early or late events, and you can drag the control to "scrub" through all the data (as you might do in a movie editor), which can reveal developing trends over time. Note that this only works if you've switched off "Aggregate Time" in the "Aggregate Events" subpanel.

Play controls

The play controls allow you to view your heatmap as if it were a movie. Simply set your play speed and click the Play (">") button. You can tweak the play speed while the "movie" is running, or use the Rewind ("<<") button to return to the start.

Particle Size

A number to adjust the render size of each heatmap point. We find it often helps to set this to the same value as the space smoothing parameter you used when aggregating.

Particle Shape

Set this to Cube, Square, Triangle, or Arrow to change the display of each data point.

Arrow shape

Using this shape will allow you to display rotational information.

A heatmap showing directional information

Billboard plane (visible only for square and triangle)

Set to YZ, XZ or XY. This is useful in 2D if you need to adjust the direction of the Square or Triangle particles.

Same data in three billboard planes

Points in current set

The total number of points in the dataset you're currently viewing. If this number is 0, there's a good chance something went wrong during data download/aggregation.

Points in set

Points currently displayed

The number of points currently being rendered.


Heatmap visualizations are naturally data-heavy, and you might find that rendering so many points in your scene creates a performance hit. With this in mind, we’ve provided a few optimization tricks that might help you get the most out of this feature.


When aggregating, use the space, time, and direction values to round out individual data points. Higher smoothing values tend to yield fewer individual points.

You can also use date trimming to limit the number of input events.


The heatmap prototype uses a dynamically drawn mesh to render the data. It can render a theoretically infinite number of vertices, but at some point this is bound to affect performance. There are a couple render settings in the Heatmap Inspector that can help you render faster and with less performance impact.

Particle Shape

The Particle Shape drop-down allows you to pick a shape with which to render each data point. At present, the renderer supports four shapes: Cube, Square, Triangle and Arrow.

Not surprisingly, a triangle requires only 3 vertices. A square requires more, and cubes more still, so naturally they are heavier on rendering. If you finding performance lagging, try using triangles and see if that helps.

Time filtering

The Heatmap Inspector allows you to narrow the range of rendered points by game time (this is one good reason for using a form of HeatmapEvent.Send() that includes time).

Doing so can dramatically limit rendering impact, since Unity only needs to draw the points within the provided game time window.

Optimizing the sending of events

Only send what you need to send

If you only need x and y information, and don't care about time or rotation, use the form of the HeatmapEvent that sends the least data. This will minimize any potential impact on your game performance and yield smaller (and therefore faster) datasets when you download the raw data.

Only send when you need to sed

We've upped your events/device limit to enable you to send a LOT of events, but resist the temptation to instrument every possible thing, and try to send multiple Heatmap events every second. Critically examine what you're mapping and how often you need to map it. Too much data can be just as confusing as not enough data.

Faster aggregation with 'Local only'

Whenever you 'Fetch and Process', the Heatmapper needs to check the server to see if there are new files to download. If you know for sure that you don't want to download any new data, toggle on 'Local only' mode in the 'Aggregate Events' panel. This bypasses the server check and goes directly to examining the locally cached files.

Local only mode

Troubleshooting & FAQ

Q: Do I need to sign up with Unity Analytics to use heatmaps?

A: Yes. Heatmaps operate using raw data collected from the Unity Analytics service.

Q: I've sent events and retrieved them, but I don't see my heatmap.

There can be many causes for this. Let's walk through a few possibilities, working backwards from the moment you clicked "Fetch and Process"

What are your renderer settings?

One common reason for not seeing the heatmap is simply visual: how big is your Particle Size? Are you rendering a shape and color that assist you in seeing your map against your game? Try fiddling with the render settings to see if anything appears. Also, in the hierarchy, try typing the word "heatmap". Not only does this isolate the heatmap GameObject, it grays back and makes transparent everything else. This can help if, say, your heatmap is hiding behind another GameObject.

Do you have any data?

At the very bottom of the Heatmapper, you should see "Points in current set" and "Points currently displayed" if you don't see this text, or if "Points in current set" reads "0", there's no actual data to display.

Are you actually downloading data correctly?

When raw data is recovered from the server, you should see output in the Console. Usually is says "Downloaded x/y. Note: Download complete." If you don't see that output, or perhaps you see and error, that might indicate that you're not receiving any data from the server. Double-check that you've entered the correct Data Export URL and that the Start and End Dates correspond to times you expect data to have been flowing.

If you're not downloading data...

...are you sure there's data to download? Raw data can take anywhere from two hours to a day to be available, depending on our server load. While you're waiting, check the validator on your app's dashboard to ensure that the events are being received. If you see them in the validator, they'll eventually show up in your raw data download. If you don't it probably means that you haven't implemented the HeatmapEvent properly.

Q: Is there any way to save my raw/processed data?

There is. The raw and processed data are stored in your persistent data path. If you go to this location, you can copy the data from there to wherever you like.

Q: I'd like to make a feature request or file a bug report.

Please create an issue here.

The provided renderer uses a mesh to display the heatmap data. We've experimented with other approaches, such as using the Shuriken particle system. You don't need to understand the renderer in order to use the system, but you might want to learn about it, especially if you're thinking about attempting an alternate rendering solution.

Writing a custom renderer


HeatmapMeshRenderer was written with the presumption that developers might want to write their own custom renderers. With this in mind, we created the IHeatmapRenderer interface. By default, the HeatmapInspector starts up with an instance of HeatmapMeshRenderer, but if you want, you can swap in a custom renderer to override the display. Any MonoBehaviour that implements IHeatmapRenderer is permissible, and the only line you need to change (outside of your custom renderer, of course) is this one in Heatmapper (currently on line 140):

//Change this to add your custom renderer
heatMapInstance.AddComponent<HeatmapMeshRenderer> ();

Here’s the required API for an IHeatmapRenderer:

void UpdatePointData(HeatPoint[] data, float maxDensity);

Sets an array of heatmap data. See HeatPoint below. maxDensity is the highest d value in your data set.

void UpdateColors(Color[] colors);

Defines the colors that draw the heatmap. HeatmapMeshRenderer currently supports exactly 3 colors (low, medium and high), but the interface is designed to allow you to handle more.

void UpdateThresholds(float[] thresholds);

Sets the thresholds between medium/low and high/medium event density. This array should have one value less than the values in UpdateColors.

void UpdateTimeLimits(float startTime, float endTime);

Allows the user to limit the display of data by game time.

void RenderHeatmap();

Tell the renderer to perform a new render.

void UpdateRenderStyle(RenderShape style, RenderDirection direction);

Allows the user to set a type of rendering. At the moment, the allowed styles are RenderShape.CUBE, RenderShape.SQUARE, and RenderShape.TRI together with directions RenderDirection.YZ, RenderDirection.XZ, and RenderDirection.XY.

float pointSize { get; set; }

Get and set the size of individual data points.

bool allowRender { get; set; }

When true, renderer is allowed to render. When false, rendering is suppressed.

int currentPoints { get; }

The number of points currently being rendered.

int totalPoints { get; }

The total number of points in the current data set.


The HeatmapRenderer uses a struct called HeatPoint, which is simply a holder for all the relevant data for a sample point:

public Vector3 position;
public Vector3 rotation;
public float density;
public float time;

Note that in cases where only 2 dimensions of data are sent (i.e., data was sent using Vector2), the HeatPoint still renders using Vector3, with only the x and y ordinates specified. In this case, the z ordinate is 0.

Runtime heatmaps

HeatmapMeshRenderer produces a mesh, just like any other mesh, so the easiest way to get a runtime heatmap is simply to save the editor-time mesh as a prefab and just plain use it!

But let's say you wanted to display your heatmap — live and able to update — to your players. To do this, you'd want to use the actual HeatmapMeshRenderer at runtime, and it's pretty easy to create a runtime controller class make that happen. In fact, we've included a simple one in the repo for you to peruse (Heatmaps/Assets/Plugins/Heatmaps/Renderer/HeatmapController.cs). This isn't an all-singing, all-dancing controller, but it gives you everything you'd need to understand how to use the HeatmapMeshRender at runtime.

To use HeatmapController, complete these steps:

  1. If you don’t have a Resources folder, create one.
  2. In the Resources folder, place a copy of the aggregation output (i.e., a JSON file).
  3. In your game, create an empty GameObject. Name it 'MyRuntimeHeatmap'.
  4. Add the HeatmapController MonoBehaviour to 'MyRuntimeHeatmap'.
  5. Look at the Inspector for 'MyRuntimeHeatmap'. Under Heatmap Controller, find the Data Path field.
  6. Type in the name of your JSON file.
  7. Hit Play!

Now, to really make this work the way we described — to update the heat map on-the-fly for your players — you'd have to build in a bit more infrastructure. Specifically, you'd collect your raw data and aggregate it on a server, then allow your controller to fetch that server-side JSON file. It's outside the purview of this document to explain that process, but hopefully the idea is clear enough.