Here is the HeatMap source code. Not really polished and by far not optimized. It’s more of a debug tool I put together quickly, to cover my current needs. Feel free to draw inspiration and reuse in your projects.
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using UnityEngine;
[System.Serializable]
public struct HeatmapNode
{
public Vector3[] pos;
}
[System.Serializable]
public struct HeatmapQuad
{
public Vector3 pos;
public Vector3 nor;
public int use;
public void Use()
{
use++;
}
}
[System.Serializable]
public class HeatmapFile : StorageData
{
public const int Version = 1;
public int m_version;
public int m_level;
public int m_playersCount;
public float m_sampleFrequency;
public List<HeatmapNode> m_nodes;
public List<HeatmapQuad> m_quads;
public HeatmapFile()
{
m_nodes = new List<HeatmapNode>();
m_quads = new List<HeatmapQuad>();
}
}
public class Heatmap : MonoBehaviourSingleton<Heatmap>
{
public enum Mode
{
Off,
Play,
Record
}
public Mode m_mode;
public float m_playSpeed = 16;
public bool m_drawNodes = false;
public bool m_drawQuads = true;
public Color m_nodesColor = new Color(1.0f, 1.0f, 1.0f, 0.1f);
public Color m_quadsColor = new Color(1.0f, 1.0f, 1.0f, 0.1f);
public float m_quadsAlpha = 1.0f;
public Mesh m_quadMesh;
public Material m_quadMaterial;
private bool m_active;
private float m_timer;
private float m_sampleFrequency = 0.5f;
private List<HeatmapFile> m_files;
private HeatmapFile m_current;
private Dictionary<Vector3, HeatmapQuad> m_quadMap;
public void OnUpdate()
{
if (m_mode == Mode.Play)
{
m_timer += Time.deltaTime * m_playSpeed;
}
else if (m_mode == Mode.Record)
{
m_timer += Time.deltaTime;
if (m_timer > m_sampleFrequency)
{
m_timer -= m_sampleFrequency;
RecordQuad();
RecordNode();
}
}
}
void RecordQuad()
{
for (int i = 0; i < Game.PlayersCount; i++)
{
if (Game.instance.IsPlayerActive(i))
{
Player p = Game.instance.GetPlayer(i);
Vector3 pos = p.transform.position;
pos.x = Mathf.Floor(pos.x / 2) * 2;
//pos.y = Mathf.Floor(pos.y); // / 0.1f) * 0.1f;
pos.z = Mathf.Floor(pos.z / 2) * 2;
int layerMask = 1; // default bit 0
RaycastHit hit;
if (!Physics.Raycast(pos + Vector3.up, Vector3.down, out hit, 2.0f, layerMask))
continue;
pos = hit.point;
pos.x = Mathf.Round(pos.x);
pos.y = Mathf.Floor(pos.y / 0.1f) * 0.1f;
pos.z = Mathf.Round(pos.z);
// Debug.Log(hit.collider.name + " at y=" + hit.point.y + " nor=" + hit.normal + " >> " + pos);
if (m_quadMap.ContainsKey(pos))
{
HeatmapQuad hq = m_quadMap[pos];
hq.use++;
m_quadMap[pos] = hq;
}
else
{
HeatmapQuad qv;
qv.pos = pos;
qv.nor = hit.normal;
qv.use = 1;
m_quadMap[pos] = qv;
}
}
}
}
void RecordNode()
{
HeatmapNode s;
s.pos = new Vector3[4];
int pi = 0;
for (int i = 0; i < Game.PlayersCount; i++)
{
if (Game.instance.IsPlayerActive(i))
{
Player p = Game.instance.GetPlayer(i);
s.pos[pi] = p.transform.position;
pi++;
}
}
m_current.m_nodes.Add(s);
}
void OnDrawGizmos()
{
if (m_active && m_mode == Mode.Play && m_files!=null)
{
if (m_drawNodes)
DrawNodes();
if (m_drawQuads)
DrawQuads();
}
}
void DrawNodes()
{
int t = Mathf.RoundToInt(m_timer / m_sampleFrequency);
for (int i = 0; i < t; i++)
{
foreach (var f in m_files)
{
if (i < f.m_nodes.Count && i > 0)
{
for (int p = 0; p < Game.PlayersCount; p++)
{
Vector3 b = f.m_nodes[i].pos[p] + Vector3.up * 0.25f;
Vector3 a = f.m_nodes[i - 1].pos[p] + Vector3.up * 0.25f;
if ((b - a).magnitude < 4)
Debug.DrawLine(a, b, m_nodesColor);
}
}
}
}
}
void DrawQuads()
{
m_quadMaterial.enableInstancing = true;
foreach (var f in m_files)
{
foreach (var q in f.m_quads)
{
Color c = m_quadsColor;
c.a *= (float)q.use / m_quadsAlpha;
Quaternion rot = Quaternion.FromToRotation(Vector3.up, q.nor);
//Gizmos.color = c;
//Gizmos.DrawMesh(m_quadMesh, q.pos + Vector3.up * 0.15f, rot, new Vector3(2.0f, 1.0f, 2.0f));
Matrix4x4 mat = Matrix4x4.TRS(q.pos + Vector3.up * 0.15f, rot, new Vector3(2.0f, 1.0f, 2.0f));
m_quadMaterial.color = c;
m_quadMaterial.SetPass(0);
Graphics.DrawMeshNow(m_quadMesh, mat, 0);
}
}
}
public void OnStartLevelSession()
{
m_active = true;
m_timer = 0.0f;
if (m_mode == Mode.Play)
{
LoadAll(Game.instance.m_progression.m_level + 1);
}
else if (m_mode == Mode.Record)
{
m_current = new HeatmapFile();
m_quadMap = new Dictionary<Vector3, HeatmapQuad>();
}
}
public void OnEndLevelSession()
{
m_active = false;
if (m_mode == Mode.Record)
{
Save(Game.instance.m_progression.m_level + 1);
}
// clean
m_files = null;
m_current = null;
m_quadMap = null;
}
void Save(int level)
{
m_current.m_version = HeatmapFile.Version;
m_current.m_level = level;
m_current.m_playersCount = Game.instance.GetActivePlayersCount();
m_current.m_sampleFrequency = m_sampleFrequency;
foreach (var q in m_quadMap)
m_current.m_quads.Add(q.Value);
string date = System.DateTime.Now.ToString("yyMMdd_hhmmss");
string folder = "Heatmap/Level" + level.ToString("00");
Storage.Save<HeatmapFile>(ref m_current, (folder + "/heatmap_" + date + ".hmp"));
Debug.Log("Saved heatmap.");
}
void LoadAll(int level)
{
m_files = new List<HeatmapFile>();
string[] files = Directory.GetFiles(Application.persistentDataPath + "/Heatmap/Level" + level.ToString("00"));
foreach(var f in files)
{
string f2 = f.Replace('\\', '/');
f2 = f2.Replace(Application.persistentDataPath + "/", "");
HeatmapFile hf = new HeatmapFile();
if (Storage.Load<HeatmapFile>(ref hf, f2))
{
if (hf.m_version == HeatmapFile.Version)
m_files.Add(hf);
else
Debug.Log("Heatmap old version " + f);
}
}
Debug.Log("Loaded " + m_files.Count + " heatmaps.");
}
}
And you will also need this storage utility class which I also use to store config settings.
/////////////////////////////////////////////////////////////////////////////////////////////////
// GF - Storage
// Game data storage
// Use:
// - have the YourGameDataStructure class serializable ([System.Serializable]) and derived from StrageData
// - implement override YourGameDataStructure.IsValid for versioning or validation
// - call Storage.SaveSlot<YourGameDataStructure>(ref m_gameData, slot)
// - call Storage.LoadSlot<YourGameDataStructure>(ref m_gameData, slot)
// References:
// http://wiki.unity3d.com/index.php?title=Saving_and_Loading_Data:_XmlSerializer
/////////////////////////////////////////////////////////////////////////////////////////////////
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Xml.Serialization;
[System.Serializable]
public class StorageData
{
public virtual bool IsValid() { return true; }
}
public class Storage
{
public static bool SaveSlot<T>(ref T data, int slot) where T : StorageData
{
return Save(ref data, "savedgame" + slot + ".sav");
}
public static bool LoadSlot<T>(ref T data, int slot) where T : StorageData
{
return Load(ref data, "savedgame" + slot + ".sav");
}
public static bool Save<T>(ref T data, string fileName) where T : StorageData
{
bool ok = false;
string pathName = Application.persistentDataPath + "/" + fileName;
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(pathName));
FileStream file = File.Create(pathName);
if(file != null)
{
ok = true;
try
{
// BinaryFormatter serializer = new BinaryFormatter();
XmlSerializer serializer = new XmlSerializer(typeof(T));
serializer.Serialize(file, data);
// Debug.Log("save successful");
}
catch(System.Exception e)
{
Debug.Log("SAVE to file '" + pathName + "' FAILED with EXCEPTION: " + e);
ok = false;
}
file.Close();
}
return ok;
}
public static bool Load<T>(ref T data, string fileName) where T : StorageData /* , new() */
{
bool ok = false;
string pathName = Application.persistentDataPath + "/" + fileName;
if(File.Exists(pathName))
{
FileStream file = File.Open(pathName, FileMode.Open);
if(file != null)
{
try
{
// BinaryFormatter serializer = new BinaryFormatter();
XmlSerializer serializer = new XmlSerializer(typeof(T));
data = (T)serializer.Deserialize(file);
ok = (data != null) && data.IsValid();
}
catch(System.Exception e)
{
Debug.Log("LOAD from file '" + pathName + "' FAILED with EXCEPTION: " + e);
}
file.Close();
}
}
// @NOTE: optional re-construct
//if(!ok)
// data = new T();
// data = default(T);
return ok;
}
public static bool LoadFromText<T>(ref T data, string text) where T : StorageData /* , new() */
{
bool ok = false;
StringReader stream = new StringReader(text);
try
{
var serializer = new XmlSerializer(typeof(T));
data = (T)serializer.Deserialize(stream);
ok = (data != null) && data.IsValid();
}
catch(System.Exception e)
{
Debug.Log("LOAD from text '" + text + "' FAILED with EXCEPTION: " + e);
}
// @NOTE: optional re-construct
//if(!ok)
// data = new T();
// data = default(T);
return ok;
}
}