ParaLily Update #4: Improving Level Design with Player Analytics and Heatmaps

ParaLily Update #4: Improving Level Design with Player Analytics and Heatmaps

In our last update we showed a bunch of Player Analytics that we collected from MAGFest including playtime, death variations, levels beaten, etc. Now we’d like to share how we collected, summarized, and utilized all that data to help improve ParaLily.

As a two man team we spend a lot of time not only designing and creating ParaLily, but also playing testing, ParaLily. Unfortunately all that testing really skews our perception of difficulty within the game. In order to combat this we have to let people test our game and see for ourselves where things are too hard or confusing. Fortunately conventions are great place to get feedback! We can watch players firsthand, and see where they get stuck or lost. Even better, we can make sure we don’t miss a beat, by keeping track of our players events in an audit log file.

Logging

The backbone of our analytic system relies heavily on Logging. Specifically Audit Logging, which is a method of recording chronological events. For an everyday example someone might log when they ate an apple. It’s important to note that we don’t care how many apples they ate, we just log each one as a separate entry like so:

  1. ate apple
  2. ate apple
  3. ate apple
ate apple
ate apple
ate apple

Now if we want to know where each apple was eaten, or what kind of specific apple it was, then we just add that to our log entry:

  1. ate apple fuji kitchen
  2. ate apple gala office
  3. ate apple fuji bedroom
ate apple fuji kitchen
ate apple gala office
ate apple fuji bedroom

Looking at the logs we can see that three apples were eaten. Two of which were fuji, and were eaten at home. So from this bit of data we can assume that at home there are fuji apples and at work there are gala apples.

In ParaLily we apply this same concept where each line in our log file is simply an event that the Player executed. It ends up looking something like this:

  1. 04d17h27m00s Game Start 0f4fbaf6-9e9a-4740-a01a-86369f74419e
  2. 04d17h27m24s 1-1 Level Start
  3. 04d17h27m48s 1-1 r1 81.6,7.7 Glyph
  4. 04d17h27m54s 1-1 r1 131.4,1.3 Glyph
  5. 04d17h28m00s 1-1 Level Win 35.92s
  6. 04d17h28m01s 1-2 Level Start
  7. 04d17h28m08s 1-2 r1 -4.8,-5.0 SXB
  8. 04d17h28m08s 1-2 r1 -0.3,-24.3 Death Fall
  9. 04d17h28m09s 1-2 Level Lose 7.45s
  10. 04d17h28m10s 1-2 Level Start
  11. 04d17h28m14s 1-2 r1 -25.8,-1.9 SB
  12. 04d17h28m31s 1-2 r2 16.6,-3.6 SF
  13. 04d17h28m35s 1-2 r1 16.9,-3.2 Glyph
  14. 04d17h28m37s 1-2 r1 16.9,-0.1 SB
  15. 04d17h28m42s 1-2 r2 33.9,0.5 SF
  16. 04d17h28m47s 1-2 r1 63.8,-2.1 SB
  17. 04d17h28m53s 1-2 r2 87.9,0.3 SF
  18. 04d17h28m54s 1-2 r1 89.9,-3.0 Death Thorn
  19. 04d17h28m54s 1-2 Level Lose 44.25s
  20. 04d17h28m56s 1-2 Level Start
  21. 04d17h29m02s 1-2 r1 -19.2,-0.3 SB
  22. 04d17h29m11s 1-2 r2 35.1,-1.1 SF
  23. 04d17h29m17s 1-2 r1 65.8,0.0 SB
  24. 04d17h29m22s 1-2 r2 71.6,-5.8 SF
  25. 04d17h29m23s 1-2 r1 71.6,-5.3 Glyph
  26. 04d17h29m25s 1-2 r1 75.9,-3.4 SB
  27. 04d17h29m27s 1-2 r2 82.4,-2.6 SF
  28. 04d17h29m31s 1-2 r1 93.6,-4.5 SB
  29. 04d17h29m32s 1-2 r2 98.2,-20.5 SF
  30. 04d17h29m33s 1-2 r1 98.9,-24.1 Death Fall
  31. 04d17h29m33s 1-2 Level Lose 37.52s
  32. 04d17h29m35s 1-2 Level Start
  33. 04d17h29m39s 1-2 r1 -23.6,-1.1 SB
  34. 04d17h29m49s 1-2 r2 41.3,-0.6 SF
  35. 04d17h30m00s 1-2 r1 101.8,-1.3 SB
  36. 04d17h30m06s 1-2 r2 130.2,1.1 SF
  37. 04d17h30m10s 1-2 Level Win 35.15s
  38. 04d17h30m11s 1-3 Level Start
  39. 04d17h30m19s 1-3 r1 19.8,1.8 Glyph
  40. 04d17h30m22s 1-3 r1 31.3,0.7 SB
  41. 04d17h30m31s 1-3 r2 61.2,-0.3 SB
  42. 04d17h30m42s 1-3 r3 90.8,-6.7 Glyph
  43. 04d17h30m43s 1-3 r3 94.6,-9.6 SF
  44. 04d17h30m47s 1-3 r2 110.6,-2.5 Glyph
  45. 04d17h30m52s 1-3 r2 146.5,-1.3 SB
  46. 04d17h30m54s 1-3 r3 151.2,-10.9 SF
  47. 04d17h30m55s 1-3 r2 152.6,-24.2 Death Fall
  48. 04d17h30m55s 1-3 Level Lose 43.77s
  49. 04d17h30m57s 1-3 Level Start
  50. 04d17h31m06s 1-3 r1 31.4,0.4 SB
  51. 04d17h31m13s 1-3 r2 59.8,0.0 SB
  52. 04d17h31m21s 1-3 r3 98.0,-12.4 SXF
  53. 04d17h31m21s 1-3 r3 98.1,-12.9 Death LavaFlow
  54. 04d17h31m21s 1-3 Level Lose 24.46s
  55. 04d17h31m23s 1-3 Level Start
  56. 04d17h31m32s 1-3 r1 31.3,-0.1 SB
  57. 04d17h31m41s 1-3 r2 59.4,-0.5 SB
  58. 04d17h31m49s 1-3 r3 95.2,-7.3 SF
  59. 04d17h32m02s 1-3 r2 154.0,-24.2 Death Fall
  60. 04d17h32m03s 1-3 Level Lose 40.07s
  61. 04d17h32m04s 1-3 Level Start
  62. 04d17h32m14s 1-3 r1 31.8,0.0 SB
  63. 04d17h32m19s 1-3 r2 59.1,-2.0 SF
  64. 04d17h32m20s 1-3 r1 59.2,-2.3 SB
  65. 04d17h32m22s 1-3 r2 64.4,-0.2 SF
  66. 04d17h32m24s 1-3 r1 67.0,-24.3 Death Fall
  67. 04d17h32m24s 1-3 Level Lose 20.14s
  68. 04d17h32m26s 1-3 Level Start
  69. 04d17h32m35s 1-3 r1 30.4,0.4 SB
  70. 04d17h32m42s 1-3 r2 60.6,-0.5 SB
  71. 04d17h32m49s 1-3 r3 91.5,-7.1 SF
  72. 04d17h33m00s 1-3 r2 145.0,-1.8 SF
  73. 04d17h33m01s 1-3 r1 146.7,-1.0 SB
  74. 04d17h33m02s 1-3 r2 148.8,-1.7 SF
  75. 04d17h33m05s 1-3 Level Win 39.18s
  76. 04d17h33m06s 1-4 Level Start
  77. 04d17h33m13s 1-4 r1 14.4,2.4 Glyph
  78. 04d17h33m17s 1-4 r1 34.2,5.0 SB
  79. 04d17h33m22s 1-4 r2 49.2,1.6 SB
  80. 04d17h33m24s 1-4 r3 49.2,2.0 SF
  81. 04d17h33m25s 1-4 r2 49.2,1.6 SF
  82. 04d17h33m26s 1-4 r1 49.2,0.4 SXB
  83. 04d17h33m31s 1-4 r1 71.5,-24.2 Death Fall
  84. 04d17h33m32s 1-4 Level Lose 25.30s
  85. 04d17h33m33s 1-4 Level Start
  86. 04d17h33m43s 1-4 r1 34.8,4.9 SB
  87. 04d17h33m46s 1-4 r2 48.3,1.8 SF
  88. 04d17h33m49s 1-4 r1 49.2,-1.2 SXB
  89. 04d17h33m50s 1-4 r1 49.2,-1.6 SB
  90. 04d17h33m51s 1-4 r2 49.2,-1.6 Glyph
  91. 04d17h33m54s 1-4 r2 51.7,-4.4 SF
  92. 04d17h33m56s 1-4 r1 55.9,-1.3 SB
  93. 04d17h34m00s 1-4 r2 51.5,3.7 SB
  94. 04d17h34m24s 1-4 r3 80.2,-5.8 SF
  95. 04d17h34m25s 1-4 r2 80.2,-7.0 SF
  96. 04d17h34m26s 1-4 r1 80.2,-12.5 SXB
  97. 04d17h34m26s 1-4 r1 80.2,-24.2 Death Fall
  98. 04d17h34m27s 1-4 Level Lose 53.39s
  99. 04d17h34m28s 1-4 Level Start
  100. 04d17h34m36s 1-4 r1 29.0,6.0 SB
  101. 04d17h34m40s 1-4 r2 53.1,3.5 SB
  102. 04d17h34m46s 1-4 r3 79.6,-9.2 SF
  103. 04d17h34m49s 1-4 r2 87.7,3.2 SB
  104. 04d17h34m51s 1-4 r3 84.3,3.3 Glyph
  105. 04d17h34m52s 1-4 r3 88.0,2.6 SF
  106. 04d17h34m59s 1-4 r2 134.0,2.6 SF
  107. 04d17h35m02s 1-4 Level Win 33.86s
  108. 04d17h35m03s 1-5 Level Start
  109. 04d17h35m11s 1-5 r1 -78.5,-1.2 Death Bunny
  110. 04d17h35m11s 1-5 Level Lose 7.72s
  111. 04d17h35m13s 1-5 Level Start
  112. 04d17h35m30s 1-5 r1 -60.4,2.3 SB
  113. 04d17h35m35s 1-5 r2 -43.6,2.1 SB
  114. 04d17h35m36s 1-5 r3 -38.5,-12.0 Death LavaFlow
  115. 04d17h35m37s 1-5 Level Lose 24.10s
  116. 04d17h35m38s 1-5 Level Start
  117. 04d17h35m48s 1-5 r1 -59.9,2.1 SB
  118. 04d17h35m52s 1-5 r2 -43.3,2.1 SF
  119. 04d17h35m53s 1-5 r1 -41.8,0.5 SB
  120. 04d17h35m55s 1-5 r2 -45.5,1.6 SF
  121. 04d17h35m56s 1-5 r1 -45.5,1.6 Glyph
  122. 04d17h35m56s 1-5 r1 -45.4,1.1 SB
  123. 04d17h35m57s 1-5 r2 -44.0,-5.8 SB
  124. 04d17h35m58s 1-5 r3 -42.8,-12.1 Death LavaFlow
  125. 04d17h35m58s 1-5 Level Lose 20.09s
  126. 04d17h36m00s 1-5 Level Start
  127. 04d17h36m05s 1-5 r1 -86.8,-3.0 Death Bunny
  128. 04d17h36m05s 1-5 Level Lose 5.64s
  129. 04d17h36m07s 1-5 Level Start
  130. 04d17h36m16s 1-5 r1 -60.7,2.1 SB
  131. 04d17h36m19s 1-5 r2 -43.9,2.1 SB
  132. 04d17h36m20s 1-5 r3 -42.0,1.4 SF
  133. 04d17h36m22s 1-5 r2 -34.1,3.5 SB
  134. 04d17h36m23s 1-5 r3 -31.2,3.6 SF
  135. 04d17h36m24s 1-5 r2 -29.4,0.6 SF
  136. 04d17h36m27s 1-5 r1 -14.2,-1.6 Glyph
  137. 04d17h36m28s 1-5 r1 -12.7,-1.6 SB
  138. 04d17h36m32s 1-5 r2 1.5,2.3 SF
  139. 04d17h36m33s 1-5 r1 6.0,-0.7 SB
  140. 04d17h36m34s 1-5 r2 7.1,-3.6 SB
  141. 04d17h36m35s 1-5 r3 7.5,-5.5 Death SkullSpider
  142. 04d17h36m35s 1-5 Level Lose 28.16s
  143. 04d17h36m37s 1-5 Level Start
  144. 04d17h36m46s 1-5 r1 -60.0,2.1 SB
  145. 04d17h36m49s 1-5 r2 -42.8,1.7 SB
  146. 04d17h36m50s 1-5 r3 -40.7,-1.5 SXF
  147. 04d17h36m50s 1-5 r3 -37.7,-12.1 Death LavaFlow
  148. 04d17h36m50s 1-5 Level Lose 13.77s
  149. 04d17h36m52s 1-5 Level Start
  150. 04d17h37m01s 1-5 r1 -59.1,1.7 SB
  151. 04d17h37m05s 1-5 r2 -34.9,3.4 SF
  152. 04d17h37m08s 1-5 r1 -14.7,-1.1 SB
  153. 04d17h37m12s 1-5 r2 1.1,2.3 SB
  154. 04d17h37m14s 1-5 r3 6.6,-5.6 Death SkullSpider
  155. 04d17h37m14s 1-5 Level Lose 22.10s
  156. 04d17h37m15s 1-5 Level Start
  157. 04d17h37m24s 1-5 r1 -59.8,1.7 SB
  158. 04d17h37m28s 1-5 r2 -36.3,2.7 SXF
  159. 04d17h37m29s 1-5 r2 -31.8,-1.3 SF
  160. 04d17h37m32s 1-5 r1 -14.0,-1.4 SB
  161. 04d17h37m36s 1-5 r2 8.1,-0.6 SB
  162. 04d17h37m37s 1-5 r3 9.7,-6.0 Death SkullSpider
  163. 04d17h37m37s 1-5 Level Lose 22.04s
  164. 04d17h37m39s 1-5 Level Start
  165. 04d17h37m48s 1-5 r1 -58.7,1.4 SB
  166. 04d17h37m52s 1-5 r2 -36.3,2.6 SXF
  167. 04d17h37m52s 1-5 r2 -32.8,-0.6 SF
  168. 04d17h37m56s 1-5 r1 -14.0,-1.1 SB
  169. 04d17h37m59s 1-5 r2 3.0,4.0 SF
  170. 04d17h38m00s 1-5 r1 4.1,4.2 SB
  171. 04d17h38m01s 1-5 r2 7.1,1.5 SB
  172. 04d17h38m02s 1-5 r3 9.7,-5.8 Death SkullSpider
  173. 04d17h38m02s 1-5 Level Lose 23.44s
  174. 04d17h38m04s 1-5 Level Start
  175. 04d17h38m13s 1-5 r1 -59.6,1.9 SB
  176. 04d17h38m17s 1-5 r2 -34.6,3.4 SF
  177. 04d17h38m20s 1-5 r1 -14.1,-1.0 SB
  178. 04d17h38m23s 1-5 r2 0.3,3.5 SB
  179. 04d17h38m24s 1-5 r3 1.8,4.0 SF
  180. 04d17h38m25s 1-5 r2 3.1,3.6 SB
  181. 04d17h38m26s 1-5 r3 7.3,-5.8 Death SkullSpider
  182. 04d17h38m27s 1-5 Level Lose 22.79s
  183. 04d17h38m28s 1-5 Level Start
  184. 04d17h38m37s 1-5 r1 -61.9,1.5 SB
  185. 04d17h38m41s 1-5 r2 -35.9,3.2 SXF
  186. 04d17h38m42s 1-5 r2 -30.9,-1.8 SF
  187. 04d17h38m45s 1-5 r1 -14.0,-1.1 SB
  188. 04d17h38m49s 1-5 r2 2.2,4.1 SB
  189. 04d17h38m53s 1-5 r3 17.6,-0.9 SF
  190. 04d17h38m55s 1-5 r2 22.8,0.0 SF
  191. 04d17h38m56s 1-5 r1 20.9,-24.3 Death Fall
  192. 04d17h38m57s 1-5 Level Lose 28.61s
  193. 04d17h38m58s 1-5 Level Start
  194. 04d17h39m07s 1-5 r1 -58.6,1.4 SB
  195. 04d17h39m12s 1-5 r2 -33.3,2.0 SF
  196. 04d17h39m15s 1-5 r1 -15.5,-1.4 SB
  197. 04d17h39m18s 1-5 r2 0.5,-0.3 SF
  198. 04d17h39m19s 1-5 r1 2.8,2.3 SB
  199. 04d17h39m21s 1-5 r2 6.0,-3.4 SB
  200. 04d17h39m22s 1-5 r3 4.8,-6.2 Death SkullSpider
  201. 04d17h39m22s 1-5 Level Lose 24.21s
  202. 04d17h39m24s 1-5 Level Start
  203. 04d17h39m33s 1-5 r1 -59.2,1.8 SB
  204. 04d17h39m37s 1-5 r2 -33.7,1.6 SF
  205. 04d17h39m41s 1-5 r1 -13.5,-1.1 SB
  206. 04d17h39m44s 1-5 r2 0.0,2.9 SF
  207. 04d17h39m45s 1-5 r1 1.0,3.8 SB
  208. 04d17h39m46s 1-5 r2 3.8,-5.2 SB
  209. 04d17h39m50s 1-5 r3 16.0,-1.6 SF
  210. 04d17h39m52s 1-5 r2 20.9,3.1 SB
  211. 04d17h39m55s 1-5 r3 30.1,2.9 SF
  212. 04d17h39m57s 1-5 r2 34.5,3.7 SB
  213. 04d17h39m59s 1-5 r3 40.3,3.7 SF
  214. 04d17h40m04s 1-5 r2 38.1,8.3 SB
  215. 04d17h40m06s 1-5 r3 38.2,-5.4 SF
  216. 04d17h40m07s 1-5 r2 38.8,-6.2 SB
  217. 04d17h40m09s 1-5 r3 38.5,-7.5 SF
  218. 04d17h40m10s 1-5 r2 37.8,-6.3 Glyph
  219. 04d17h40m11s 1-5 r2 37.7,-3.3 SF
  220. 04d17h40m12s 1-5 r1 37.8,-0.8 SB
  221. 04d17h40m16s 1-5 r2 56.0,3.2 SB
  222. 04d17h40m17s 1-5 r3 59.1,4.6 SF
  223. 04d17h40m19s 1-5 r2 62.1,1.1 SF
  224. 04d17h40m25s 1-5 r1 99.8,-24.3 Death Fall
  225. 04d17h40m25s 1-5 Level Lose 60.89s
  226. 04d17h40m26s 1-5 Level Start
  227. 04d17h40m35s 1-5 r1 -59.3,1.8 SB
  228. 04d17h40m40s 1-5 r2 -35.4,3.1 SF
  229. 04d17h40m43s 1-5 r1 -15.4,-1.2 SB
  230. 04d17h40m46s 1-5 r2 0.2,1.6 SB
  231. 04d17h40m49s 1-5 r3 4.0,-5.8 Death SkullSpider
  232. 04d17h40m49s 1-5 Level Lose 22.35s
  233. 04d17h40m50s 1-5 Level Start
  234. 04d17h40m59s 1-5 r1 -59.8,2.1 SB
  235. 04d17h41m03s 1-5 r2 -35.5,3.1 SF
  236. 04d17h41m07s 1-5 r1 -14.5,-1.0 SB
  237. 04d17h41m10s 1-5 r2 -2.1,2.9 SF
  238. 04d17h41m11s 1-5 r1 3.0,-2.0 SB
  239. 04d17h41m12s 1-5 r2 3.7,-14.2 SB
  240. 04d17h41m13s 1-5 r3 3.7,-14.2 Death LavaFlow
  241. 04d17h41m13s 1-5 Level Lose 23.19s
  242. 04d17h41m15s 1-5 Level Start
  243. 04d17h41m24s 1-5 r1 -59.4,1.8 SB
  244. 04d17h41m28s 1-5 r2 -33.3,-0.6 SF
  245. 04d17h41m32s 1-5 r1 -13.6,-1.2 SB
  246. 04d17h41m35s 1-5 r2 0.7,2.3 SB
  247. 04d17h41m40s 1-5 r3 18.0,-3.3 SF
  248. 04d17h41m42s 1-5 r2 20.5,3.4 SB
  249. 04d17h41m44s 1-5 r3 27.0,3.6 SF
  250. 04d17h41m50s 1-5 r2 56.4,1.8 SF
  251. 04d17h42m00s 1-5 Level Win 45.15s
  252. 04d17h42m02s 1-6 Level Start
  253. 04d17h42m14s 1-6 r1 34.6,-3.5 Death Bunny
  254. 04d17h42m14s 1-6 Level Lose 11.95s
  255. 04d17h42m16s 1-6 Level Start
  256. 04d17h42m29s 1-6 r1 49.8,-2.0 SB
  257. 04d17h42m31s 1-6 r2 46.9,1.6 Death Creep
  258. 04d17h42m31s 1-6 Level Lose 15.42s
  259. 04d17h42m33s 1-6 Level Start
  260. 04d17h42m44s 1-6 r1 35.5,2.8 SB
  261. 04d17h42m51s 1-6 r2 76.2,0.8 SB
  262. 04d17h42m55s 1-6 r3 98.4,1.3 SF
  263. 04d17h42m59s 1-6 r2 120.3,-8.4 SB
  264. 04d17h43m00s 1-6 r3 120.7,-10.0 SXF
  265. 04d17h43m00s 1-6 r3 120.9,-12.1 Death LavaFlow
  266. 04d17h43m01s 1-6 Level Lose 27.53s
  267. 04d17h43m02s 1-6 Level Start
  268. 04d17h43m14s 1-6 r1 35.5,2.8 SB
  269. 04d17h43m21s 1-6 r2 76.8,1.0 SB
  270. 04d17h43m25s 1-6 r3 96.4,0.9 SF
  271. 04d17h43m31s 1-6 r2 137.1,0.5 SF
  272. 04d17h43m33s 1-6 r1 152.8,0.5 SB
  273. 04d17h43m35s 1-6 r2 160.0,0.4 SB
  274. 04d17h43m38s 1-6 r3 175.8,0.2 SF
  275. 04d17h43m42s 1-6 r2 197.3,1.1 SB
  276. 04d17h43m44s 1-6 r3 211.2,-2.7 SF
  277. 04d17h43m46s 1-6 r2 220.6,1.2 SB
  278. 04d17h43m52s 1-6 r3 250.8,2.7 Death LavaPool
  279. 04d17h43m52s 1-6 Level Lose 49.95s
  280. 04d17h43m54s 1-6 Level Start
  281. 04d17h44m06s 1-6 r1 36.4,2.8 SB
  282. 04d17h44m12s 1-6 r2 76.2,1.1 SB
  283. 04d17h44m16s 1-6 r3 96.5,0.9 SF
  284. 04d17h44m22s 1-6 r2 135.2,2.5 SF
  285. 04d17h44m25s 1-6 r1 152.9,0.7 SB
  286. 04d17h44m27s 1-6 r2 159.1,1.0 SB
  287. 04d17h44m30s 1-6 r3 175.5,1.4 SF
  288. 04d17h44m33s 1-6 r2 197.8,1.1 SB
  289. 04d17h44m36s 1-6 r3 211.5,-2.5 SF
  290. 04d17h44m38s 1-6 r2 221.8,2.8 SB
  291. 04d17h44m41s 1-6 r3 237.5,-3.0 Death SkullSpider
  292. 04d17h44m42s 1-6 Level Lose 47.46s
  293. 04d17h44m43s 1-6 Level Start
  294. 04d17h44m55s 1-6 r1 36.8,2.8 SB
  295. 04d17h45m02s 1-6 r2 76.6,1.0 SB
  296. 04d17h45m05s 1-6 r3 96.6,1.5 SF
  297. 04d17h45m12s 1-6 r2 136.3,2.6 SF
  298. 04d17h45m15s 1-6 r1 153.6,0.7 SB
  299. 04d17h45m17s 1-6 r2 161.1,1.3 SB
  300. 04d17h45m20s 1-6 r3 175.3,1.6 SF
  301. 04d17h45m23s 1-6 r2 197.1,1.0 SB
  302. 04d17h45m26s 1-6 r3 212.5,-3.0 SF
  303. 04d17h45m27s 1-6 r2 218.5,0.9 SB
  304. 04d17h45m32s 1-6 r3 231.1,-2.1 Death Creep
  305. 04d17h45m32s 1-6 Level Lose 48.82s
  306. 04d17h45m34s 1-6 Level Start
  307. 04d17h45m46s 1-6 r1 37.1,2.7 SB
  308. 04d17h45m52s 1-6 r2 76.3,1.0 SB
  309. 04d17h45m55s 1-6 r3 98.1,1.7 SF
  310. 04d17h46m01s 1-6 r2 135.6,2.6 SF
  311. 04d17h46m04s 1-6 r1 151.5,0.1 SB
  312. 04d17h46m06s 1-6 r2 160.4,1.3 SB
  313. 04d17h46m08s 1-6 r3 172.9,2.8 SF
  314. 04d17h46m12s 1-6 r2 197.5,0.9 SB
  315. 04d17h46m15s 1-6 r3 210.6,-2.4 SF
  316. 04d17h46m17s 1-6 r2 221.9,2.0 SB
  317. 04d17h46m25s 1-6 r3 254.8,5.2 Glyph
  318. 04d17h46m31s 1-6 r3 291.6,-5.6 SF
  319. 04d17h46m35s 1-6 r2 316.3,-2.3 SF
  320. 04d17h46m38s 1-6 Level Win 63.54s
  321. 04d17h46m38s Game End 1177.96s 0f4fbaf6-9e9a-4740-a01a-86369f74419e
04d17h27m00s Game Start 0f4fbaf6-9e9a-4740-a01a-86369f74419e
04d17h27m24s 1-1 Level Start
04d17h27m48s 1-1 r1 81.6,7.7 Glyph
04d17h27m54s 1-1 r1 131.4,1.3 Glyph
04d17h28m00s 1-1 Level Win 35.92s
04d17h28m01s 1-2 Level Start
04d17h28m08s 1-2 r1 -4.8,-5.0 SXB
04d17h28m08s 1-2 r1 -0.3,-24.3 Death Fall
04d17h28m09s 1-2 Level Lose 7.45s
04d17h28m10s 1-2 Level Start
04d17h28m14s 1-2 r1 -25.8,-1.9 SB
04d17h28m31s 1-2 r2 16.6,-3.6 SF
04d17h28m35s 1-2 r1 16.9,-3.2 Glyph
04d17h28m37s 1-2 r1 16.9,-0.1 SB
04d17h28m42s 1-2 r2 33.9,0.5 SF
04d17h28m47s 1-2 r1 63.8,-2.1 SB
04d17h28m53s 1-2 r2 87.9,0.3 SF
04d17h28m54s 1-2 r1 89.9,-3.0 Death Thorn
04d17h28m54s 1-2 Level Lose 44.25s
04d17h28m56s 1-2 Level Start
04d17h29m02s 1-2 r1 -19.2,-0.3 SB
04d17h29m11s 1-2 r2 35.1,-1.1 SF
04d17h29m17s 1-2 r1 65.8,0.0 SB
04d17h29m22s 1-2 r2 71.6,-5.8 SF
04d17h29m23s 1-2 r1 71.6,-5.3 Glyph
04d17h29m25s 1-2 r1 75.9,-3.4 SB
04d17h29m27s 1-2 r2 82.4,-2.6 SF
04d17h29m31s 1-2 r1 93.6,-4.5 SB
04d17h29m32s 1-2 r2 98.2,-20.5 SF
04d17h29m33s 1-2 r1 98.9,-24.1 Death Fall
04d17h29m33s 1-2 Level Lose 37.52s
04d17h29m35s 1-2 Level Start
04d17h29m39s 1-2 r1 -23.6,-1.1 SB
04d17h29m49s 1-2 r2 41.3,-0.6 SF
04d17h30m00s 1-2 r1 101.8,-1.3 SB
04d17h30m06s 1-2 r2 130.2,1.1 SF
04d17h30m10s 1-2 Level Win 35.15s
04d17h30m11s 1-3 Level Start
04d17h30m19s 1-3 r1 19.8,1.8 Glyph
04d17h30m22s 1-3 r1 31.3,0.7 SB
04d17h30m31s 1-3 r2 61.2,-0.3 SB
04d17h30m42s 1-3 r3 90.8,-6.7 Glyph
04d17h30m43s 1-3 r3 94.6,-9.6 SF
04d17h30m47s 1-3 r2 110.6,-2.5 Glyph
04d17h30m52s 1-3 r2 146.5,-1.3 SB
04d17h30m54s 1-3 r3 151.2,-10.9 SF
04d17h30m55s 1-3 r2 152.6,-24.2 Death Fall
04d17h30m55s 1-3 Level Lose 43.77s
04d17h30m57s 1-3 Level Start
04d17h31m06s 1-3 r1 31.4,0.4 SB
04d17h31m13s 1-3 r2 59.8,0.0 SB
04d17h31m21s 1-3 r3 98.0,-12.4 SXF
04d17h31m21s 1-3 r3 98.1,-12.9 Death LavaFlow
04d17h31m21s 1-3 Level Lose 24.46s
04d17h31m23s 1-3 Level Start
04d17h31m32s 1-3 r1 31.3,-0.1 SB
04d17h31m41s 1-3 r2 59.4,-0.5 SB
04d17h31m49s 1-3 r3 95.2,-7.3 SF
04d17h32m02s 1-3 r2 154.0,-24.2 Death Fall
04d17h32m03s 1-3 Level Lose 40.07s
04d17h32m04s 1-3 Level Start
04d17h32m14s 1-3 r1 31.8,0.0 SB
04d17h32m19s 1-3 r2 59.1,-2.0 SF
04d17h32m20s 1-3 r1 59.2,-2.3 SB
04d17h32m22s 1-3 r2 64.4,-0.2 SF
04d17h32m24s 1-3 r1 67.0,-24.3 Death Fall
04d17h32m24s 1-3 Level Lose 20.14s
04d17h32m26s 1-3 Level Start
04d17h32m35s 1-3 r1 30.4,0.4 SB
04d17h32m42s 1-3 r2 60.6,-0.5 SB
04d17h32m49s 1-3 r3 91.5,-7.1 SF
04d17h33m00s 1-3 r2 145.0,-1.8 SF
04d17h33m01s 1-3 r1 146.7,-1.0 SB
04d17h33m02s 1-3 r2 148.8,-1.7 SF
04d17h33m05s 1-3 Level Win 39.18s
04d17h33m06s 1-4 Level Start
04d17h33m13s 1-4 r1 14.4,2.4 Glyph
04d17h33m17s 1-4 r1 34.2,5.0 SB
04d17h33m22s 1-4 r2 49.2,1.6 SB
04d17h33m24s 1-4 r3 49.2,2.0 SF
04d17h33m25s 1-4 r2 49.2,1.6 SF
04d17h33m26s 1-4 r1 49.2,0.4 SXB
04d17h33m31s 1-4 r1 71.5,-24.2 Death Fall
04d17h33m32s 1-4 Level Lose 25.30s
04d17h33m33s 1-4 Level Start
04d17h33m43s 1-4 r1 34.8,4.9 SB
04d17h33m46s 1-4 r2 48.3,1.8 SF
04d17h33m49s 1-4 r1 49.2,-1.2 SXB
04d17h33m50s 1-4 r1 49.2,-1.6 SB
04d17h33m51s 1-4 r2 49.2,-1.6 Glyph
04d17h33m54s 1-4 r2 51.7,-4.4 SF
04d17h33m56s 1-4 r1 55.9,-1.3 SB
04d17h34m00s 1-4 r2 51.5,3.7 SB
04d17h34m24s 1-4 r3 80.2,-5.8 SF
04d17h34m25s 1-4 r2 80.2,-7.0 SF
04d17h34m26s 1-4 r1 80.2,-12.5 SXB
04d17h34m26s 1-4 r1 80.2,-24.2 Death Fall
04d17h34m27s 1-4 Level Lose 53.39s
04d17h34m28s 1-4 Level Start
04d17h34m36s 1-4 r1 29.0,6.0 SB
04d17h34m40s 1-4 r2 53.1,3.5 SB
04d17h34m46s 1-4 r3 79.6,-9.2 SF
04d17h34m49s 1-4 r2 87.7,3.2 SB
04d17h34m51s 1-4 r3 84.3,3.3 Glyph
04d17h34m52s 1-4 r3 88.0,2.6 SF
04d17h34m59s 1-4 r2 134.0,2.6 SF
04d17h35m02s 1-4 Level Win 33.86s
04d17h35m03s 1-5 Level Start
04d17h35m11s 1-5 r1 -78.5,-1.2 Death Bunny
04d17h35m11s 1-5 Level Lose 7.72s
04d17h35m13s 1-5 Level Start
04d17h35m30s 1-5 r1 -60.4,2.3 SB
04d17h35m35s 1-5 r2 -43.6,2.1 SB
04d17h35m36s 1-5 r3 -38.5,-12.0 Death LavaFlow
04d17h35m37s 1-5 Level Lose 24.10s
04d17h35m38s 1-5 Level Start
04d17h35m48s 1-5 r1 -59.9,2.1 SB
04d17h35m52s 1-5 r2 -43.3,2.1 SF
04d17h35m53s 1-5 r1 -41.8,0.5 SB
04d17h35m55s 1-5 r2 -45.5,1.6 SF
04d17h35m56s 1-5 r1 -45.5,1.6 Glyph
04d17h35m56s 1-5 r1 -45.4,1.1 SB
04d17h35m57s 1-5 r2 -44.0,-5.8 SB
04d17h35m58s 1-5 r3 -42.8,-12.1 Death LavaFlow
04d17h35m58s 1-5 Level Lose 20.09s
04d17h36m00s 1-5 Level Start
04d17h36m05s 1-5 r1 -86.8,-3.0 Death Bunny
04d17h36m05s 1-5 Level Lose 5.64s
04d17h36m07s 1-5 Level Start
04d17h36m16s 1-5 r1 -60.7,2.1 SB
04d17h36m19s 1-5 r2 -43.9,2.1 SB
04d17h36m20s 1-5 r3 -42.0,1.4 SF
04d17h36m22s 1-5 r2 -34.1,3.5 SB
04d17h36m23s 1-5 r3 -31.2,3.6 SF
04d17h36m24s 1-5 r2 -29.4,0.6 SF
04d17h36m27s 1-5 r1 -14.2,-1.6 Glyph
04d17h36m28s 1-5 r1 -12.7,-1.6 SB
04d17h36m32s 1-5 r2 1.5,2.3 SF
04d17h36m33s 1-5 r1 6.0,-0.7 SB
04d17h36m34s 1-5 r2 7.1,-3.6 SB
04d17h36m35s 1-5 r3 7.5,-5.5 Death SkullSpider
04d17h36m35s 1-5 Level Lose 28.16s
04d17h36m37s 1-5 Level Start
04d17h36m46s 1-5 r1 -60.0,2.1 SB
04d17h36m49s 1-5 r2 -42.8,1.7 SB
04d17h36m50s 1-5 r3 -40.7,-1.5 SXF
04d17h36m50s 1-5 r3 -37.7,-12.1 Death LavaFlow
04d17h36m50s 1-5 Level Lose 13.77s
04d17h36m52s 1-5 Level Start
04d17h37m01s 1-5 r1 -59.1,1.7 SB
04d17h37m05s 1-5 r2 -34.9,3.4 SF
04d17h37m08s 1-5 r1 -14.7,-1.1 SB
04d17h37m12s 1-5 r2 1.1,2.3 SB
04d17h37m14s 1-5 r3 6.6,-5.6 Death SkullSpider
04d17h37m14s 1-5 Level Lose 22.10s
04d17h37m15s 1-5 Level Start
04d17h37m24s 1-5 r1 -59.8,1.7 SB
04d17h37m28s 1-5 r2 -36.3,2.7 SXF
04d17h37m29s 1-5 r2 -31.8,-1.3 SF
04d17h37m32s 1-5 r1 -14.0,-1.4 SB
04d17h37m36s 1-5 r2 8.1,-0.6 SB
04d17h37m37s 1-5 r3 9.7,-6.0 Death SkullSpider
04d17h37m37s 1-5 Level Lose 22.04s
04d17h37m39s 1-5 Level Start
04d17h37m48s 1-5 r1 -58.7,1.4 SB
04d17h37m52s 1-5 r2 -36.3,2.6 SXF
04d17h37m52s 1-5 r2 -32.8,-0.6 SF
04d17h37m56s 1-5 r1 -14.0,-1.1 SB
04d17h37m59s 1-5 r2 3.0,4.0 SF
04d17h38m00s 1-5 r1 4.1,4.2 SB
04d17h38m01s 1-5 r2 7.1,1.5 SB
04d17h38m02s 1-5 r3 9.7,-5.8 Death SkullSpider
04d17h38m02s 1-5 Level Lose 23.44s
04d17h38m04s 1-5 Level Start
04d17h38m13s 1-5 r1 -59.6,1.9 SB
04d17h38m17s 1-5 r2 -34.6,3.4 SF
04d17h38m20s 1-5 r1 -14.1,-1.0 SB
04d17h38m23s 1-5 r2 0.3,3.5 SB
04d17h38m24s 1-5 r3 1.8,4.0 SF
04d17h38m25s 1-5 r2 3.1,3.6 SB
04d17h38m26s 1-5 r3 7.3,-5.8 Death SkullSpider
04d17h38m27s 1-5 Level Lose 22.79s
04d17h38m28s 1-5 Level Start
04d17h38m37s 1-5 r1 -61.9,1.5 SB
04d17h38m41s 1-5 r2 -35.9,3.2 SXF
04d17h38m42s 1-5 r2 -30.9,-1.8 SF
04d17h38m45s 1-5 r1 -14.0,-1.1 SB
04d17h38m49s 1-5 r2 2.2,4.1 SB
04d17h38m53s 1-5 r3 17.6,-0.9 SF
04d17h38m55s 1-5 r2 22.8,0.0 SF
04d17h38m56s 1-5 r1 20.9,-24.3 Death Fall
04d17h38m57s 1-5 Level Lose 28.61s
04d17h38m58s 1-5 Level Start
04d17h39m07s 1-5 r1 -58.6,1.4 SB
04d17h39m12s 1-5 r2 -33.3,2.0 SF
04d17h39m15s 1-5 r1 -15.5,-1.4 SB
04d17h39m18s 1-5 r2 0.5,-0.3 SF
04d17h39m19s 1-5 r1 2.8,2.3 SB
04d17h39m21s 1-5 r2 6.0,-3.4 SB
04d17h39m22s 1-5 r3 4.8,-6.2 Death SkullSpider
04d17h39m22s 1-5 Level Lose 24.21s
04d17h39m24s 1-5 Level Start
04d17h39m33s 1-5 r1 -59.2,1.8 SB
04d17h39m37s 1-5 r2 -33.7,1.6 SF
04d17h39m41s 1-5 r1 -13.5,-1.1 SB
04d17h39m44s 1-5 r2 0.0,2.9 SF
04d17h39m45s 1-5 r1 1.0,3.8 SB
04d17h39m46s 1-5 r2 3.8,-5.2 SB
04d17h39m50s 1-5 r3 16.0,-1.6 SF
04d17h39m52s 1-5 r2 20.9,3.1 SB
04d17h39m55s 1-5 r3 30.1,2.9 SF
04d17h39m57s 1-5 r2 34.5,3.7 SB
04d17h39m59s 1-5 r3 40.3,3.7 SF
04d17h40m04s 1-5 r2 38.1,8.3 SB
04d17h40m06s 1-5 r3 38.2,-5.4 SF
04d17h40m07s 1-5 r2 38.8,-6.2 SB
04d17h40m09s 1-5 r3 38.5,-7.5 SF
04d17h40m10s 1-5 r2 37.8,-6.3 Glyph
04d17h40m11s 1-5 r2 37.7,-3.3 SF
04d17h40m12s 1-5 r1 37.8,-0.8 SB
04d17h40m16s 1-5 r2 56.0,3.2 SB
04d17h40m17s 1-5 r3 59.1,4.6 SF
04d17h40m19s 1-5 r2 62.1,1.1 SF
04d17h40m25s 1-5 r1 99.8,-24.3 Death Fall
04d17h40m25s 1-5 Level Lose 60.89s
04d17h40m26s 1-5 Level Start
04d17h40m35s 1-5 r1 -59.3,1.8 SB
04d17h40m40s 1-5 r2 -35.4,3.1 SF
04d17h40m43s 1-5 r1 -15.4,-1.2 SB
04d17h40m46s 1-5 r2 0.2,1.6 SB
04d17h40m49s 1-5 r3 4.0,-5.8 Death SkullSpider
04d17h40m49s 1-5 Level Lose 22.35s
04d17h40m50s 1-5 Level Start
04d17h40m59s 1-5 r1 -59.8,2.1 SB
04d17h41m03s 1-5 r2 -35.5,3.1 SF
04d17h41m07s 1-5 r1 -14.5,-1.0 SB
04d17h41m10s 1-5 r2 -2.1,2.9 SF
04d17h41m11s 1-5 r1 3.0,-2.0 SB
04d17h41m12s 1-5 r2 3.7,-14.2 SB
04d17h41m13s 1-5 r3 3.7,-14.2 Death LavaFlow
04d17h41m13s 1-5 Level Lose 23.19s
04d17h41m15s 1-5 Level Start
04d17h41m24s 1-5 r1 -59.4,1.8 SB
04d17h41m28s 1-5 r2 -33.3,-0.6 SF
04d17h41m32s 1-5 r1 -13.6,-1.2 SB
04d17h41m35s 1-5 r2 0.7,2.3 SB
04d17h41m40s 1-5 r3 18.0,-3.3 SF
04d17h41m42s 1-5 r2 20.5,3.4 SB
04d17h41m44s 1-5 r3 27.0,3.6 SF
04d17h41m50s 1-5 r2 56.4,1.8 SF
04d17h42m00s 1-5 Level Win 45.15s
04d17h42m02s 1-6 Level Start
04d17h42m14s 1-6 r1 34.6,-3.5 Death Bunny
04d17h42m14s 1-6 Level Lose 11.95s
04d17h42m16s 1-6 Level Start
04d17h42m29s 1-6 r1 49.8,-2.0 SB
04d17h42m31s 1-6 r2 46.9,1.6 Death Creep
04d17h42m31s 1-6 Level Lose 15.42s
04d17h42m33s 1-6 Level Start
04d17h42m44s 1-6 r1 35.5,2.8 SB
04d17h42m51s 1-6 r2 76.2,0.8 SB
04d17h42m55s 1-6 r3 98.4,1.3 SF
04d17h42m59s 1-6 r2 120.3,-8.4 SB
04d17h43m00s 1-6 r3 120.7,-10.0 SXF
04d17h43m00s 1-6 r3 120.9,-12.1 Death LavaFlow
04d17h43m01s 1-6 Level Lose 27.53s
04d17h43m02s 1-6 Level Start
04d17h43m14s 1-6 r1 35.5,2.8 SB
04d17h43m21s 1-6 r2 76.8,1.0 SB
04d17h43m25s 1-6 r3 96.4,0.9 SF
04d17h43m31s 1-6 r2 137.1,0.5 SF
04d17h43m33s 1-6 r1 152.8,0.5 SB
04d17h43m35s 1-6 r2 160.0,0.4 SB
04d17h43m38s 1-6 r3 175.8,0.2 SF
04d17h43m42s 1-6 r2 197.3,1.1 SB
04d17h43m44s 1-6 r3 211.2,-2.7 SF
04d17h43m46s 1-6 r2 220.6,1.2 SB
04d17h43m52s 1-6 r3 250.8,2.7 Death LavaPool
04d17h43m52s 1-6 Level Lose 49.95s
04d17h43m54s 1-6 Level Start
04d17h44m06s 1-6 r1 36.4,2.8 SB
04d17h44m12s 1-6 r2 76.2,1.1 SB
04d17h44m16s 1-6 r3 96.5,0.9 SF
04d17h44m22s 1-6 r2 135.2,2.5 SF
04d17h44m25s 1-6 r1 152.9,0.7 SB
04d17h44m27s 1-6 r2 159.1,1.0 SB
04d17h44m30s 1-6 r3 175.5,1.4 SF
04d17h44m33s 1-6 r2 197.8,1.1 SB
04d17h44m36s 1-6 r3 211.5,-2.5 SF
04d17h44m38s 1-6 r2 221.8,2.8 SB
04d17h44m41s 1-6 r3 237.5,-3.0 Death SkullSpider
04d17h44m42s 1-6 Level Lose 47.46s
04d17h44m43s 1-6 Level Start
04d17h44m55s 1-6 r1 36.8,2.8 SB
04d17h45m02s 1-6 r2 76.6,1.0 SB
04d17h45m05s 1-6 r3 96.6,1.5 SF
04d17h45m12s 1-6 r2 136.3,2.6 SF
04d17h45m15s 1-6 r1 153.6,0.7 SB
04d17h45m17s 1-6 r2 161.1,1.3 SB
04d17h45m20s 1-6 r3 175.3,1.6 SF
04d17h45m23s 1-6 r2 197.1,1.0 SB
04d17h45m26s 1-6 r3 212.5,-3.0 SF
04d17h45m27s 1-6 r2 218.5,0.9 SB
04d17h45m32s 1-6 r3 231.1,-2.1 Death Creep
04d17h45m32s 1-6 Level Lose 48.82s
04d17h45m34s 1-6 Level Start
04d17h45m46s 1-6 r1 37.1,2.7 SB
04d17h45m52s 1-6 r2 76.3,1.0 SB
04d17h45m55s 1-6 r3 98.1,1.7 SF
04d17h46m01s 1-6 r2 135.6,2.6 SF
04d17h46m04s 1-6 r1 151.5,0.1 SB
04d17h46m06s 1-6 r2 160.4,1.3 SB
04d17h46m08s 1-6 r3 172.9,2.8 SF
04d17h46m12s 1-6 r2 197.5,0.9 SB
04d17h46m15s 1-6 r3 210.6,-2.4 SF
04d17h46m17s 1-6 r2 221.9,2.0 SB
04d17h46m25s 1-6 r3 254.8,5.2 Glyph
04d17h46m31s 1-6 r3 291.6,-5.6 SF
04d17h46m35s 1-6 r2 316.3,-2.3 SF
04d17h46m38s 1-6 Level Win 63.54s
04d17h46m38s Game End 1177.96s 0f4fbaf6-9e9a-4740-a01a-86369f74419e

So what’s going on in all this mess? Just like in the apple scenario a space is our delimiter for each column. So our columns are roughly:

  1. DATE/TIME LEVEL ROW PLAYER_POSITION EVENT
DATE/TIME LEVEL ROW PLAYER_POSITION EVENT

Date and time are great to have because it let’s us see just how long it took players to go from one place to another. Then in ParaLily we not only have levels like world 1 level 1, but also Universes or Rows as we call them, which are r1, r2, and r3. Logging the Players exact position gives us a lot of opportunities like generating heatmaps. Lastly, we log the event itself: Death, Shift Forward, Shift Back, or Glyph Collected.

We also have special scenarios like the game starting or ending, a level starting or ending. Timestamps for key things like when the game ends, in this case they beat it after 1177.96s. A Unique Identifier is generated per play-through, and helps us find specific logs if something crazy happens in game.

Then when the next player comes along we create a new log file. So each log file itself is a single play-through. Our folder by the end of a convention:

For developers looking to setup their own loggers it’s pretty straightforward. My biggest tip would be to make sure you are not re-opening the log file every time you write to it. Here’s the code we used for our Audit Logger:

  1. using System;
  2. using System.IO;
  3. using System.Text;
  4. using UnityEngine;
  5. public static class Logger
  6. {
  7. private static readonly string[] Row = new string[] { " r1 ", " r2 ", " r3 " };
  8. private static readonly string LogDir = Path.Combine(Application.persistentDataPath, "Logs/");
  9. private static readonly string FileName = "_ParaLily_audit.log";
  10. private static readonly string FileDateTimeFormat = "yyyy-MM-dd'T'HH'h'mm'm'ss's'";
  11. private static readonly string LogDateTimeFormat = "dd'd'HH'h'mm'm'ss's'";
  12. public static Guid Uid { get; set; }
  13. private static string auditLogPath;
  14. private static float gameStart;
  15. private static float playStart;
  16. private static StreamWriter logWriter;
  17. static Logger()
  18. {
  19. SetNewGame();
  20. if (!Directory.Exists(LogDir))
  21. {
  22. Directory.CreateDirectory(LogDir);
  23. }
  24. }
  25. public static void AuditGame()
  26. {
  27. SetNewGame();
  28. StringBuilder sb = new StringBuilder();
  29. sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" Game Start ").Append(Uid.ToString());
  30. WriteLog(sb.ToString());
  31. }
  32. public static void AuditGame(string text)
  33. {
  34. StringBuilder sb = new StringBuilder();
  35. sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" Game ").Append(text).Append(" ")
  36. .Append(GameDurationInSecondsToString()).Append(" ").Append(Uid.ToString());
  37. WriteLog(sb.ToString());
  38. }
  39. public static void AuditLevelStart()
  40. {
  41. StringBuilder sb = new StringBuilder();
  42. playStart = Time.time;
  43. sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" ").Append(GameManager.instance.CurrentSceneName).Append(" Level Start");
  44. WriteLog(sb.ToString());
  45. }
  46. public static void AuditLevel(string text)
  47. {
  48. StringBuilder sb = new StringBuilder();
  49. sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" ").Append(GameManager.instance.CurrentSceneName).Append(" Level ").Append(text).Append(" ").Append(PlayDurationInSecondsToString());
  50. WriteLog(sb.ToString());
  51. }
  52. public static void AuditFull(string text)
  53. {
  54. StringBuilder sb = new StringBuilder();
  55. sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" ").Append(GameManager.instance.CurrentSceneName).Append(Row[RowManager.instance.CurrentParallel])
  56. .Append(PlayerController.instance.transform.position.ToStringXY()).Append(" ").Append(text);
  57. WriteLog(sb.ToString());
  58. }
  59. private static void WriteLog(string text)
  60. {
  61. logWriter.WriteLine(text);
  62. logWriter.Flush();
  63. }
  64. public static float GameDurationInSeconds()
  65. {
  66. return Time.time - gameStart;
  67. }
  68. public static float PlayDurationInSeconds()
  69. {
  70. return Time.time - playStart;
  71. }
  72. private static string GameDurationInSecondsToString()
  73. {
  74. return GameDurationInSeconds().ToString("0.00s");
  75. }
  76. private static string PlayDurationInSecondsToString()
  77. {
  78. return PlayDurationInSeconds().ToString("0.00s");
  79. }
  80. private static void SetNewGame()
  81. {
  82. Uid = Guid.NewGuid();
  83. gameStart = Time.time;
  84. auditLogPath = Path.Combine(LogDir, DateTime.Now.ToString(FileDateTimeFormat) + FileName);
  85. Dispose();
  86. logWriter = new StreamWriter(auditLogPath);
  87. logWriter.AutoFlush = false;
  88. }
  89. public static void Dispose()
  90. {
  91. logWriter?.Dispose();
  92. }
  93. }
using System;
using System.IO;
using System.Text;
using UnityEngine;

public static class Logger
{
	private static readonly string[] Row = new string[] { " r1 ", " r2 ", " r3 " };

	private static readonly string LogDir = Path.Combine(Application.persistentDataPath, "Logs/");
	private static readonly string FileName = "_ParaLily_audit.log";
	private static readonly string FileDateTimeFormat = "yyyy-MM-dd'T'HH'h'mm'm'ss's'";
	private static readonly string LogDateTimeFormat = "dd'd'HH'h'mm'm'ss's'";

	public static Guid Uid { get; set; }

	private static string auditLogPath;
	private static float gameStart;
	private static float playStart;
	private static StreamWriter logWriter;

	static Logger()
	{
		SetNewGame();
		if (!Directory.Exists(LogDir))
		{
			Directory.CreateDirectory(LogDir);
		}
	}

	public static void AuditGame()
	{
		SetNewGame();
		StringBuilder sb = new StringBuilder();
		sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" Game Start ").Append(Uid.ToString());
		WriteLog(sb.ToString());
	}

	public static void AuditGame(string text)
	{
		StringBuilder sb = new StringBuilder();
		sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" Game ").Append(text).Append(" ")
		  .Append(GameDurationInSecondsToString()).Append(" ").Append(Uid.ToString());
		WriteLog(sb.ToString());
	}

	public static void AuditLevelStart()
	{
		StringBuilder sb = new StringBuilder();
		playStart = Time.time;
		sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" ").Append(GameManager.instance.CurrentSceneName).Append(" Level Start");
		WriteLog(sb.ToString());
	}

	public static void AuditLevel(string text)
	{
		StringBuilder sb = new StringBuilder();
		sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" ").Append(GameManager.instance.CurrentSceneName).Append(" Level ").Append(text).Append(" ").Append(PlayDurationInSecondsToString());
		WriteLog(sb.ToString());
	}

	public static void AuditFull(string text)
	{
		StringBuilder sb = new StringBuilder();
		sb.Append(DateTime.Now.ToString(LogDateTimeFormat)).Append(" ").Append(GameManager.instance.CurrentSceneName).Append(Row[RowManager.instance.CurrentParallel])
		  .Append(PlayerController.instance.transform.position.ToStringXY()).Append(" ").Append(text);
		WriteLog(sb.ToString());
	}

	private static void WriteLog(string text)
	{
		logWriter.WriteLine(text);
		logWriter.Flush();
	}

	public static float GameDurationInSeconds()
	{
		return Time.time - gameStart;
	}

	public static float PlayDurationInSeconds()
	{
		return Time.time - playStart;
	}

	private static string GameDurationInSecondsToString()
	{
		return GameDurationInSeconds().ToString("0.00s");
	}

	private static string PlayDurationInSecondsToString()
	{
		return PlayDurationInSeconds().ToString("0.00s");
	}

	private static void SetNewGame()
	{
		Uid = Guid.NewGuid();
		gameStart = Time.time;
		auditLogPath = Path.Combine(LogDir, DateTime.Now.ToString(FileDateTimeFormat) + FileName);
		Dispose();
		logWriter = new StreamWriter(auditLogPath);
		logWriter.AutoFlush = false;
	}

	public static void Dispose()
	{
		logWriter?.Dispose();
	}
}

Summarizing with Shell Scripting

So what do we do with all of this data? There’s hundreds of lines per file and hundreds of files. It would take forever to sift through and tally all this up by hand. Fear not! Scripting is here to save us from all that misery. To get all the important bits of data we want, we created a shell script to do all the heavy lifting, and generate a clean summarized output of all that player data. After it runs it leaves us with a folder with all of these files:

The files each look similar to this:

  1. Game Start 109
  2. Game End 7
  3. Game Timeout 59
  4. Game Reset 27
  5. Level Start 1707
  6. Level Win 191
  7. Level Lose 1427
  8. SB 2893
  9. SF 1996
  10. SXB 322
  11. SXF 360
  12. Glyph 576
  13. Death 1427
  14. Death Thorn 373
  15. Death Fall 790
  16. Death SkullSpider 57
  17. Death LavaPool 24
  18. Death LavaFlow 116
  19. Death Creep 12
  20. Death Bunny 16
  21. Death Quicksand 39
Game Start 109
Game End 7
Game Timeout 59
Game Reset 27
Level Start 1707
Level Win 191
Level Lose 1427
SB 2893
SF 1996
SXB 322
SXF 360
Glyph 576
Death 1427
Death Thorn 373
Death Fall 790
Death SkullSpider 57
Death LavaPool 24
Death LavaFlow 116
Death Creep 12
Death Bunny 16
Death Quicksand 39

*Data collected from Cecil Con

And here’s the Shell Script we developed to do all the summarizing:

  1. #!/bin/bash
  2. function date_time_format {
  3. date "+%Y-%m-%d"
  4. }
  5. SUMMARY_DIR="summary_$(date_time_format)"
  6. LEVELS=( "1-1" "1-2" "1-3" "1-4" "1-5" "1-6" "1-7" "1-8" )
  7. ROWS=( "r1" "r2" "r3" )
  8. DAYS=( "14d" ) # Days the event ran
  9. HOURS=( "10h" "11h" "12h" "13h" "14h" "15h" "16h" "17h" )
  10. #HOURS=( "00h" "01h" "02h" "03h" "04h" "05h" "06h" "07h" "08h" "09h" "10h" "11h" "12h" "13h" "14h" "15h" "16h" "17h" "18h" "19h" "20h" "21h" "22h" "23h" )
  11. KEYWORDS_BY_LEVEL_BY_ROW=( "SB" "SF" "SXB" "SXF" "Glyph" "Death" )
  12. KEYWORDS_BY_LEVEL=( "Level Start" "Level Win" "Level Lose" "Level Reset" "Level Timeout" "SB" "SF" "SXB" "SXF" "Glyph" "Death" "Death Thorn" "Death Fall" "Death SkullSpider" "Death LavaPool" "Death LavaFlow" "Death Creep" "Death Bunny" "Death Quicksand" )
  13. KEYWORDS_BY_ROW=( "SB" "SF" "SXB" "SXF" "Glyph" "Death" )
  14. KEYWORDS_OVERALL=( "Game Start" "Game End" "Game Timeout" "Game Reset" "Level Start" "Level Win" "Level Lose" "SB" "SF" "SXB" "SXF" "Glyph" "Death" "Death Thorn" "Death Fall" "Death SkullSpider" "Death LavaPool" "Death LavaFlow" "Death Creep" "Death Bunny" "Death Quicksand" )
  15. KEYWORDS_BY_COORDINATE=( "SB" "SF" "SXB" "SXF" "Glyph" "Death" )
  16. KEYWORDS_BY_HOUR=( "Game Start" "Game End" "Game Timeout" "Game Reset" "Level Start" "Level Win" "Level Lose" "SB" "SF" "SXB" "SXF" "Glyph" "Death" "Death Thorn" "Death Fall" "Death SkullSpider" "Death LavaPool" "Death LavaFlow" "Death Creep" "Death Bunny" "Death Quicksand" )
  17. cd '.\Logs'
  18. mkdir $SUMMARY_DIR
  19. echo "Building Analytic Summary to $SUMMARY_DIR..."
  20. #--Totals by Level by Row Summary--
  21. TOTALS_BY_LEVEL_BY_ROW_FILE="$SUMMARY_DIR\\totals_by_level_by_row.txt"
  22. echo "Generating File $TOTALS_BY_LEVEL_BY_ROW_FILE..."
  23. for (( i=0; i<${#LEVELS[@]}; i++ ))
  24. do
  25. for (( k=0; k<${#ROWS[@]}; k++ ))
  26. do
  27. for (( j=0; j<${#KEYWORDS_BY_LEVEL_BY_ROW[@]}; j++ ))
  28. do
  29. COUNT[$j]=$(grep "${LEVELS[$i]}.*${ROWS[$k]}.*${KEYWORDS_BY_LEVEL_BY_ROW[$j]}" *audit* | wc -l )
  30. echo "${LEVELS[$i]} ${ROWS[$k]} ${KEYWORDS_BY_LEVEL_BY_ROW[$j]} ${COUNT[$j]}" | tee -a $TOTALS_BY_LEVEL_BY_ROW_FILE
  31. done
  32. done
  33. done
  34. #--Totals by Row Summary--
  35. TOTALS_BY_ROW_FILE="$SUMMARY_DIR\\totals_by_row.txt"
  36. echo "Generating File $TOTALS_BY_ROW_FILE..."
  37. for (( i=0; i<${#ROWS[@]}; i++ ))
  38. do
  39. for (( k=0; k<${#KEYWORDS_BY_ROW[@]}; k++ ))
  40. do
  41. COUNT[$k]=$(grep "${ROWS[$i]}.*${KEYWORDS_BY_ROW[$k]}" *audit* | wc -l )
  42. echo "${ROWS[$i]} ${KEYWORDS_BY_ROW[$k]} ${COUNT[$k]}" | tee -a $TOTALS_BY_ROW_FILE
  43. done
  44. done
  45. #--Totals by Level Summary--
  46. TOTALS_BY_LEVEL_FILE="$SUMMARY_DIR\\totals_by_level.txt"
  47. echo "Generating File $TOTALS_BY_LEVEL_FILE..."
  48. for (( i=0; i<${#LEVELS[@]}; i++ ))
  49. do
  50. for (( k=0; k<${#KEYWORDS_BY_LEVEL[@]}; k++ ))
  51. do
  52. COUNT[$k]=$(grep "${LEVELS[$i]}.*${KEYWORDS_BY_LEVEL[$k]}" *audit* | wc -l )
  53. echo "${LEVELS[$i]} ${KEYWORDS_BY_LEVEL[$k]} ${COUNT[$k]}" | tee -a $TOTALS_BY_LEVEL_FILE
  54. done
  55. done
  56. #--Totals Overall Summary--
  57. TOTALS_FILE="$SUMMARY_DIR\\totals_overall.txt"
  58. echo "Generating File $TOTALS_FILE..."
  59. for (( i=0; i<${#KEYWORDS_OVERALL[@]}; i++ ))
  60. do
  61. COUNT[$i]=$(grep "${KEYWORDS_OVERALL[$i]}" *audit* | wc -l )
  62. echo "${KEYWORDS_OVERALL[$i]} ${COUNT[$i]}" | tee -a $TOTALS_FILE
  63. done
  64. #--Coordinate Summary--
  65. echo "Generating Coordinate Files..."
  66. for (( i=0; i<${#LEVELS[@]}; i++ ))
  67. do
  68. coordinate_file="$SUMMARY_DIR\\${LEVELS[$i]}_coordinates.txt"
  69. for (( k=0; k<${#KEYWORDS_BY_COORDINATE[@]}; k++ ))
  70. do
  71. for (( j=0; j<${#ROWS[@]}; j++ ))
  72. do
  73. grep "${LEVELS[$i]} ${ROWS[j]}.*${KEYWORDS_BY_COORDINATE[$k]}" *audit* | awk '{print $5,$3,$4;}' >> "$coordinate_file"
  74. done
  75. done
  76. echo "Generated $coordinate_file."
  77. done
  78. #--Hourly Totals Summary--
  79. HOURLY_FILE="$SUMMARY_DIR\\totals_by_day_by_hour.txt"
  80. echo "Generating File $HOURLY_FILE..."
  81. for (( i=0; i<${#DAYS[@]}; i++))
  82. do
  83. for (( k=0; k<${#HOURS[@]}; k++ ))
  84. do
  85. for (( j=0; j<${#KEYWORDS_BY_HOUR[@]}; j++ ))
  86. do
  87. count=$(grep "${DAYS[$i]}${HOURS[$k]}.*${KEYWORDS_BY_HOUR[$j]}" *audit* | wc -l)
  88. echo "${DAYS[$i]}${HOURS[$k]} ${KEYWORDS_BY_HOUR[$j]} $count" | tee -a "$HOURLY_FILE"
  89. done
  90. # Game time by Day by Hour
  91. hourly_game_time=$(grep "${DAYS[$i]}${HOURS[$k]}.*Game End\|${DAYS[$i]}${HOURS[$k]}.*Game Timeout\|${DAYS[$i]}${HOURS[$k]}.*Game Reset" *audit* | sed 's/s//g' | awk '{sum += $4} END {print sum}')
  92. echo "${DAYS[$i]}${HOURS[$k]} Playtime $hourly_game_time" | tee -a "$HOURLY_FILE"
  93. done
  94. done
  95. #--Playtime Summary--
  96. PLAYTIME_FILE="$SUMMARY_DIR\\playtime.txt"
  97. echo "Generating File $PLAYTIME_FILE..."
  98. for (( i=0; i<${#LEVELS[@]}; i++ ))
  99. do
  100. # Playtime by Level
  101. plays=$(grep "${LEVELS[$i]}.*Level Start" *audit* | wc -l)
  102. playtime=$(grep "${LEVELS[$i]}.*Level" *audit* | sed 's/s//g' | awk '{sum += $5} END {print sum}')
  103. average_playtime=$(awk -v x="$playtime" -v y="$plays" 'BEGIN {print x / y}')
  104. echo "${LEVELS[$i]} Total Plays $plays" | tee -a $PLAYTIME_FILE
  105. echo "${LEVELS[$i]} Total Playtime $playtime" | tee -a $PLAYTIME_FILE
  106. echo "${LEVELS[$i]} Average Playtime $average_playtime" | tee -a $PLAYTIME_FILE
  107. done
  108. total_games=$(grep "Game Start" *audit* | wc -l)
  109. total_game_time=$(grep "Game End\|Game Timeout\|Game Reset" *audit* | sed 's/s//g' | awk '{sum += $4} END {print sum}')
  110. average_game_time=$(awk -v x="$total_game_time" -v y="$total_games" 'BEGIN {print x / y}')
  111. echo "Total Games $total_games" | tee -a $PLAYTIME_FILE
  112. echo "Total Game Time $total_game_time" | tee -a $PLAYTIME_FILE
  113. echo "Average Game Time $average_game_time" | tee -a $PLAYTIME_FILE
  114. mv $SUMMARY_DIR $USERPROFILE/Desktop
  115. echo "Analytic Summary Completed."
  116. sleep 10
#!/bin/bash
function date_time_format {
  date "+%Y-%m-%d"
}

SUMMARY_DIR="summary_$(date_time_format)"

LEVELS=( "1-1" "1-2" "1-3" "1-4" "1-5" "1-6" "1-7" "1-8" )
ROWS=( "r1" "r2" "r3" )
DAYS=( "14d" ) # Days the event ran 
HOURS=( "10h" "11h" "12h" "13h" "14h" "15h" "16h" "17h" )
#HOURS=( "00h" "01h" "02h" "03h" "04h" "05h" "06h" "07h" "08h" "09h" "10h" "11h" "12h" "13h" "14h" "15h" "16h" "17h" "18h" "19h" "20h" "21h" "22h" "23h" )

KEYWORDS_BY_LEVEL_BY_ROW=( "SB" "SF" "SXB" "SXF" "Glyph" "Death" )
KEYWORDS_BY_LEVEL=( "Level Start" "Level Win" "Level Lose" "Level Reset" "Level Timeout" "SB" "SF" "SXB" "SXF" "Glyph" "Death" "Death Thorn" "Death Fall" "Death SkullSpider" "Death LavaPool" "Death LavaFlow" "Death Creep" "Death Bunny" "Death Quicksand" )
KEYWORDS_BY_ROW=( "SB" "SF" "SXB" "SXF" "Glyph" "Death" )
KEYWORDS_OVERALL=( "Game Start" "Game End" "Game Timeout" "Game Reset" "Level Start" "Level Win" "Level Lose" "SB" "SF" "SXB" "SXF" "Glyph" "Death" "Death Thorn" "Death Fall" "Death SkullSpider" "Death LavaPool" "Death LavaFlow" "Death Creep" "Death Bunny" "Death Quicksand" )
KEYWORDS_BY_COORDINATE=( "SB" "SF" "SXB" "SXF" "Glyph" "Death" )
KEYWORDS_BY_HOUR=( "Game Start" "Game End" "Game Timeout" "Game Reset" "Level Start" "Level Win" "Level Lose" "SB" "SF" "SXB" "SXF" "Glyph" "Death" "Death Thorn" "Death Fall" "Death SkullSpider" "Death LavaPool" "Death LavaFlow" "Death Creep" "Death Bunny" "Death Quicksand" )

cd '.\Logs'
mkdir $SUMMARY_DIR

echo "Building Analytic Summary to $SUMMARY_DIR..."

#--Totals by Level by Row Summary--
TOTALS_BY_LEVEL_BY_ROW_FILE="$SUMMARY_DIR\\totals_by_level_by_row.txt"
echo "Generating File $TOTALS_BY_LEVEL_BY_ROW_FILE..."
for (( i=0; i<${#LEVELS[@]}; i++ ))
do
  for (( k=0; k<${#ROWS[@]}; k++ ))
  do
    for (( j=0; j<${#KEYWORDS_BY_LEVEL_BY_ROW[@]}; j++ ))
    do
      COUNT[$j]=$(grep "${LEVELS[$i]}.*${ROWS[$k]}.*${KEYWORDS_BY_LEVEL_BY_ROW[$j]}" *audit* | wc -l )
      echo "${LEVELS[$i]} ${ROWS[$k]} ${KEYWORDS_BY_LEVEL_BY_ROW[$j]} ${COUNT[$j]}" | tee -a $TOTALS_BY_LEVEL_BY_ROW_FILE
    done
  done
done

#--Totals by Row Summary--
TOTALS_BY_ROW_FILE="$SUMMARY_DIR\\totals_by_row.txt"
echo "Generating File $TOTALS_BY_ROW_FILE..."
for (( i=0; i<${#ROWS[@]}; i++ ))
do
  for (( k=0; k<${#KEYWORDS_BY_ROW[@]}; k++ ))
  do
    COUNT[$k]=$(grep "${ROWS[$i]}.*${KEYWORDS_BY_ROW[$k]}" *audit* | wc -l )
    echo "${ROWS[$i]} ${KEYWORDS_BY_ROW[$k]} ${COUNT[$k]}" | tee -a $TOTALS_BY_ROW_FILE
  done
done

#--Totals by Level Summary--
TOTALS_BY_LEVEL_FILE="$SUMMARY_DIR\\totals_by_level.txt"
echo "Generating File $TOTALS_BY_LEVEL_FILE..."
for (( i=0; i<${#LEVELS[@]}; i++ ))
do
  for (( k=0; k<${#KEYWORDS_BY_LEVEL[@]}; k++ ))
  do
    COUNT[$k]=$(grep "${LEVELS[$i]}.*${KEYWORDS_BY_LEVEL[$k]}" *audit* | wc -l )
    echo "${LEVELS[$i]} ${KEYWORDS_BY_LEVEL[$k]} ${COUNT[$k]}" | tee -a $TOTALS_BY_LEVEL_FILE
  done
done

#--Totals Overall Summary--
TOTALS_FILE="$SUMMARY_DIR\\totals_overall.txt"
echo "Generating File $TOTALS_FILE..."
for (( i=0; i<${#KEYWORDS_OVERALL[@]}; i++ ))
do
  COUNT[$i]=$(grep "${KEYWORDS_OVERALL[$i]}" *audit* | wc -l )
  echo "${KEYWORDS_OVERALL[$i]} ${COUNT[$i]}" | tee -a $TOTALS_FILE
done

#--Coordinate Summary--
echo "Generating Coordinate Files..."
for (( i=0; i<${#LEVELS[@]}; i++ ))
do
  coordinate_file="$SUMMARY_DIR\\${LEVELS[$i]}_coordinates.txt"
  for (( k=0; k<${#KEYWORDS_BY_COORDINATE[@]}; k++ ))
  do
    for (( j=0; j<${#ROWS[@]}; j++ ))
    do
      grep "${LEVELS[$i]} ${ROWS[j]}.*${KEYWORDS_BY_COORDINATE[$k]}" *audit* | awk '{print $5,$3,$4;}' >> "$coordinate_file"
    done
  done
  echo "Generated $coordinate_file."
done

#--Hourly Totals Summary--
HOURLY_FILE="$SUMMARY_DIR\\totals_by_day_by_hour.txt"
echo "Generating File $HOURLY_FILE..."
for (( i=0; i<${#DAYS[@]}; i++))
do
  for (( k=0; k<${#HOURS[@]}; k++ ))
  do
    for (( j=0; j<${#KEYWORDS_BY_HOUR[@]}; j++ ))
    do
      count=$(grep "${DAYS[$i]}${HOURS[$k]}.*${KEYWORDS_BY_HOUR[$j]}" *audit* | wc -l)
      echo "${DAYS[$i]}${HOURS[$k]} ${KEYWORDS_BY_HOUR[$j]} $count" | tee -a "$HOURLY_FILE"
    done
    # Game time by Day by Hour
    hourly_game_time=$(grep "${DAYS[$i]}${HOURS[$k]}.*Game End\|${DAYS[$i]}${HOURS[$k]}.*Game Timeout\|${DAYS[$i]}${HOURS[$k]}.*Game Reset" *audit* | sed 's/s//g' | awk '{sum += $4} END {print sum}')
    echo "${DAYS[$i]}${HOURS[$k]} Playtime $hourly_game_time" | tee -a "$HOURLY_FILE"
  done
done

#--Playtime Summary--
PLAYTIME_FILE="$SUMMARY_DIR\\playtime.txt"
echo "Generating File $PLAYTIME_FILE..."
for (( i=0; i<${#LEVELS[@]}; i++ ))
do
  # Playtime by Level
  plays=$(grep "${LEVELS[$i]}.*Level Start" *audit* | wc -l)
  playtime=$(grep "${LEVELS[$i]}.*Level" *audit* | sed 's/s//g' | awk '{sum += $5} END {print sum}')
  average_playtime=$(awk -v x="$playtime" -v y="$plays" 'BEGIN {print x / y}')
  echo "${LEVELS[$i]} Total Plays $plays" | tee -a $PLAYTIME_FILE
  echo "${LEVELS[$i]} Total Playtime $playtime" | tee -a $PLAYTIME_FILE
  echo "${LEVELS[$i]} Average Playtime $average_playtime" | tee -a $PLAYTIME_FILE
done
total_games=$(grep "Game Start" *audit* | wc -l)
total_game_time=$(grep "Game End\|Game Timeout\|Game Reset" *audit* | sed 's/s//g' | awk '{sum += $4} END {print sum}')
average_game_time=$(awk -v x="$total_game_time" -v y="$total_games" 'BEGIN {print x / y}')
echo "Total Games $total_games" | tee -a $PLAYTIME_FILE
echo "Total Game Time $total_game_time" | tee -a $PLAYTIME_FILE
echo "Average Game Time $average_game_time" | tee -a $PLAYTIME_FILE

mv $SUMMARY_DIR $USERPROFILE/Desktop
echo "Analytic Summary Completed."

sleep 10

The script running in action with data from MAGFest:

Take a minute or two to figure out what’s going on. Essentially we are looping through various arrays that contain the keywords found in our logs files, and use grep along with wc (word count) to tally up our totals. Then we output the totals to corresponding text files. Using this method we can tally up just about anything that we can key off of. So when we said that Logging is the backbone of our analytics, it really is! Without properly formatted log files this summary script would have been much harder to develop.

Utilizing the Analytics in Unity

Out of all the data we collected and summarized, what we find to be the most useful is the coordinate data. Meaning where the Player did the thing. While the totals and times are great for a quick glance as to how players are performing; the coordinate data is what gives us that extra bit of detail as to what exactly needs to be changed. Our Shell Script generates a coordinate file per level, and they look like this:

  1. SB r1 29.4,6.9
  2. SB r1 28.0,5.5
  3. SB r1 19.2,2.0
  4. SB r1 98.8,-18.2
  5. SB r1 28.1,4.3
  6. SB r1 27.5,6.3
  7. SB r1 28.3,6.7
  8. SB r1 28.3,6.7
  9. SB r1 49.3,-4.3
  10. SB r1 69.5,-4.8
  11. SB r1 29.2,6.9
  12. SB r1 33.9,5.3
  13. SB r1 49.0,-4.3
  14. SB r1 49.0,-1.9
  15. SB r1 55.1,-1.3
  16. SB r1 28.8,3.5
  17. SB r1 37.4,5.5
  18. SB r1 30.0,4.6
  19. SB r1 24.8,3.4
  20. SB r1 24.5,3.3
  21. SF r2 50.7,3.3
  22. SF r2 52.6,-2.9
  23. SF r2 132.9,2.3
  24. SF r2 53.0,0.0
  25. SF r2 51.4,-4.3
  26. SF r2 55.0,-3.6
  27. SF r2 48.2,1.8
  28. SF r2 49.6,1.6
  29. SF r2 49.6,-4.0
  30. SF r2 135.0,0.8
  31. SXB r1 95.2,-9.2
  32. SXB r2 55.5,-4.0
  33. SXB r2 54.8,-2.7
  34. SXB r2 85.4,-1.8
  35. SXB r2 48.5,1.7
  36. SXB r2 49.9,-1.4
  37. SXB r2 60.2,-6.1
  38. SXB r2 47.8,1.8
  39. SXB r2 47.8,1.8
  40. SXB r2 47.8,1.8
  41. SXB r2 47.8,1.8
  42. SXF r2 27.0,-4.6
  43. SXF r2 26.5,-1.3
  44. Glyph r1 12.4,1.6
  45. Glyph r1 12.6,2.1
  46. Glyph r1 12.6,2.5
  47. Glyph r1 12.2,1.6
  48. Glyph r1 12.5,2.0
  49. Glyph r1 12.3,1.6
  50. Glyph r1 12.4,1.0
  51. Glyph r1 12.6,2.4
  52. Glyph r1 12.7,2.6
  53. Glyph r1 12.5,2.2
  54. Glyph r1 12.4,2.4
  55. Glyph r1 12.7,2.5
  56. Glyph r1 12.4,2.3
  57. Glyph r1 12.5,2.1
  58. Glyph r1 12.6,2.3
  59. Glyph r1 12.3,2.0
  60. Glyph r1 12.4,2.4
  61. Glyph r1 12.5,2.1
  62. Glyph r1 12.3,2.0
  63. Glyph r1 12.5,2.7
  64. Glyph r1 12.8,2.1
  65. Glyph r1 12.4,2.2
  66. Glyph r1 12.4,2.2
  67. Glyph r1 12.4,1.9
  68. Glyph r1 12.3,1.8
  69. Glyph r1 12.5,2.3
  70. Glyph r1 12.8,2.6
  71. Glyph r1 12.4,2.1
  72. Glyph r1 12.3,2.2
  73. Glyph r2 26.9,-4.0
  74. Glyph r2 50.1,-1.8
  75. Glyph r2 49.4,-4.0
  76. Glyph r2 49.0,-4.0
  77. Glyph r2 49.1,-3.8
  78. Glyph r2 26.5,-4.0
  79. Glyph r2 49.1,-3.8
  80. Glyph r2 49.2,-1.2
  81. Glyph r2 49.6,-1.4
  82. Glyph r2 49.6,-2.3
  83. Glyph r2 48.7,-4.0
  84. Glyph r2 49.9,-2.2
  85. Glyph r2 48.9,-1.9
  86. Glyph r2 49.3,-2.2
  87. Glyph r2 49.6,-1.4
  88. Glyph r2 49.5,-1.7
  89. Glyph r2 49.7,-1.3
  90. Glyph r2 26.1,-4.6
  91. Glyph r2 49.4,-4.0
  92. Glyph r2 49.4,-4.0
  93. Glyph r2 49.6,-1.0
  94. Glyph r2 50.2,-3.4
  95. Glyph r2 48.7,-4.0
  96. Glyph r2 49.8,-4.0
  97. Death r1 12.5,-2.2
  98. Death r1 5.6,-2.4
  99. Death r1 42.5,-24.2
  100. Death r1 97.0,-24.1
  101. Death r1 15.8,-2.4
  102. Death r1 2.6,-1.9
  103. Death r1 73.9,-24.0
  104. Death r1 20.4,-1.4
  105. Death r1 1.9,-2.0
  106. Death r1 12.5,-2.2
  107. Death r1 4.5,-2.5
  108. Death r1 90.2,-23.9
  109. Death r1 7.4,-2.3
  110. Death r1 62.7,-24.2
  111. Death r1 19.8,-1.9
  112. Death r1 20.4,-1.4
  113. Death r1 2.6,-1.9
  114. Death r1 11.0,-2.3
  115. Death r1 14.7,-2.0
  116. Death r1 20.4,-1.4
  117. Death r1 15.8,-2.4
  118. Death r1 100.8,-23.9
  119. Death r1 12.5,-2.2
  120. Death r1 15.2,-2.0
  121. Death r1 12.5,-2.2
  122. Death r1 61.2,-24.1
  123. Death r1 5.8,-2.3
  124. Death r1 127.5,-24.2
  125. Death r1 12.5,-2.2
  126. Death r1 2.5,-1.9
  127. Death r1 20.4,-1.4
  128. Death r1 20.4,-1.4
  129. Death r1 20.4,-1.4
  130. Death r1 23.3,-1.0
  131. Death r1 155.9,-24.2
  132. Death r1 20.4,-1.4
  133. Death r1 21.4,-1.0
  134. Death r1 7.4,-2.3
  135. Death r1 7.4,-2.3
  136. Death r1 13.6,-2.4
  137. Death r1 141.1,-24.1
  138. Death r1 20.4,-1.4
  139. Death r1 20.4,-1.4
  140. Death r1 12.5,-2.2
  141. Death r1 12.5,-2.2
  142. Death r1 2.6,-1.9
  143. Death r1 7.4,-2.3
  144. Death r1 47.7,-24.2
  145. Death r1 12.5,-2.2
  146. Death r1 2.6,-1.9
  147. Death r1 22.6,-1.1
  148. Death r1 45.5,-24.0
  149. Death r1 20.4,-1.4
SB r1 29.4,6.9
SB r1 28.0,5.5
SB r1 19.2,2.0
SB r1 98.8,-18.2
SB r1 28.1,4.3
SB r1 27.5,6.3
SB r1 28.3,6.7
SB r1 28.3,6.7
SB r1 49.3,-4.3
SB r1 69.5,-4.8
SB r1 29.2,6.9
SB r1 33.9,5.3
SB r1 49.0,-4.3
SB r1 49.0,-1.9
SB r1 55.1,-1.3
SB r1 28.8,3.5
SB r1 37.4,5.5
SB r1 30.0,4.6
SB r1 24.8,3.4
SB r1 24.5,3.3
SF r2 50.7,3.3
SF r2 52.6,-2.9
SF r2 132.9,2.3
SF r2 53.0,0.0
SF r2 51.4,-4.3
SF r2 55.0,-3.6
SF r2 48.2,1.8
SF r2 49.6,1.6
SF r2 49.6,-4.0
SF r2 135.0,0.8
SXB r1 95.2,-9.2
SXB r2 55.5,-4.0
SXB r2 54.8,-2.7
SXB r2 85.4,-1.8
SXB r2 48.5,1.7
SXB r2 49.9,-1.4
SXB r2 60.2,-6.1
SXB r2 47.8,1.8
SXB r2 47.8,1.8
SXB r2 47.8,1.8
SXB r2 47.8,1.8
SXF r2 27.0,-4.6
SXF r2 26.5,-1.3
Glyph r1 12.4,1.6
Glyph r1 12.6,2.1
Glyph r1 12.6,2.5
Glyph r1 12.2,1.6
Glyph r1 12.5,2.0
Glyph r1 12.3,1.6
Glyph r1 12.4,1.0
Glyph r1 12.6,2.4
Glyph r1 12.7,2.6
Glyph r1 12.5,2.2
Glyph r1 12.4,2.4
Glyph r1 12.7,2.5
Glyph r1 12.4,2.3
Glyph r1 12.5,2.1
Glyph r1 12.6,2.3
Glyph r1 12.3,2.0
Glyph r1 12.4,2.4
Glyph r1 12.5,2.1
Glyph r1 12.3,2.0
Glyph r1 12.5,2.7
Glyph r1 12.8,2.1
Glyph r1 12.4,2.2
Glyph r1 12.4,2.2
Glyph r1 12.4,1.9
Glyph r1 12.3,1.8
Glyph r1 12.5,2.3
Glyph r1 12.8,2.6
Glyph r1 12.4,2.1
Glyph r1 12.3,2.2
Glyph r2 26.9,-4.0
Glyph r2 50.1,-1.8
Glyph r2 49.4,-4.0
Glyph r2 49.0,-4.0
Glyph r2 49.1,-3.8
Glyph r2 26.5,-4.0
Glyph r2 49.1,-3.8
Glyph r2 49.2,-1.2
Glyph r2 49.6,-1.4
Glyph r2 49.6,-2.3
Glyph r2 48.7,-4.0
Glyph r2 49.9,-2.2
Glyph r2 48.9,-1.9
Glyph r2 49.3,-2.2
Glyph r2 49.6,-1.4
Glyph r2 49.5,-1.7
Glyph r2 49.7,-1.3
Glyph r2 26.1,-4.6
Glyph r2 49.4,-4.0
Glyph r2 49.4,-4.0
Glyph r2 49.6,-1.0
Glyph r2 50.2,-3.4
Glyph r2 48.7,-4.0
Glyph r2 49.8,-4.0
Death r1 12.5,-2.2
Death r1 5.6,-2.4
Death r1 42.5,-24.2
Death r1 97.0,-24.1
Death r1 15.8,-2.4
Death r1 2.6,-1.9
Death r1 73.9,-24.0
Death r1 20.4,-1.4
Death r1 1.9,-2.0
Death r1 12.5,-2.2
Death r1 4.5,-2.5
Death r1 90.2,-23.9
Death r1 7.4,-2.3
Death r1 62.7,-24.2
Death r1 19.8,-1.9
Death r1 20.4,-1.4
Death r1 2.6,-1.9
Death r1 11.0,-2.3
Death r1 14.7,-2.0
Death r1 20.4,-1.4
Death r1 15.8,-2.4
Death r1 100.8,-23.9
Death r1 12.5,-2.2
Death r1 15.2,-2.0
Death r1 12.5,-2.2
Death r1 61.2,-24.1
Death r1 5.8,-2.3
Death r1 127.5,-24.2
Death r1 12.5,-2.2
Death r1 2.5,-1.9
Death r1 20.4,-1.4
Death r1 20.4,-1.4
Death r1 20.4,-1.4
Death r1 23.3,-1.0
Death r1 155.9,-24.2
Death r1 20.4,-1.4
Death r1 21.4,-1.0
Death r1 7.4,-2.3
Death r1 7.4,-2.3
Death r1 13.6,-2.4
Death r1 141.1,-24.1
Death r1 20.4,-1.4
Death r1 20.4,-1.4
Death r1 12.5,-2.2
Death r1 12.5,-2.2
Death r1 2.6,-1.9
Death r1 7.4,-2.3
Death r1 47.7,-24.2
Death r1 12.5,-2.2
Death r1 2.6,-1.9
Death r1 22.6,-1.1
Death r1 45.5,-24.0
Death r1 20.4,-1.4

Then we bring all these coordinate files into Unity under a Resources -> Editor folder

Next we use our custom made “Heatmap” tool, that’s actually more of a Density Scatterplot, that will plot each piece of data for the corresponding level, parenting it to the appropriate row, and giving it the correct icon for the event.

In this Screenshot you can see how it’s definitely a Scatterplot and we call it a Density Scatterplot because we reduced the transparency for each icon. So they get brighter when there’s more stacked. This helps us detect the events of many as opposed to a stray event from a single person or two.

Notice this is plotting all the events for all our different Universes, which can get confusing quick. Luckily, our Script is setup to create the icons in a parenting structure that easily allows us to toggle the events we want to look at on/off.

Now we are just showing Deaths that occurred in the first universe aka r1. In our Hierarchy it looks like this:

It’s great reassurance when we come across an odd or challenging section of a level, and can toggle on the heatmap to see how other players have been performing in the same spot.

To make all this work and have the right icons go the right points, we developed two C# Scripts. The first is used to store the icon, row, and coordinate for each plot point, and the second is an EditorWindow that can load/unload the heatmap on the fly whether we’re in the editor or the game view:

  • 1: HeatmapData.cs
  • 2: HeatmapManager.cs
  1. using System;
  2. using UnityEngine;
  3. namespace Heatmap
  4. {
  5. public class HeatmapData
  6. {
  7. public HeatmapIcon Icon { get; private set; }
  8. public int Row { get; private set; }
  9. public Vector2 Coordinate { get; private set; }
  10. public HeatmapData(string data)
  11. {
  12. string[] fields = data.Split(' ');
  13. Icon = (HeatmapIcon) Enum.Parse(typeof(HeatmapIcon), "Heatmap_" + fields[0]);
  14. Row = int.Parse(fields[1].Remove(0, 1));
  15. string[] coordinates = fields[2].Split(',');
  16. Coordinate = new Vector2(float.Parse(coordinates[0]), float.Parse(coordinates[1]));
  17. }
  18. }
  19. }
using System;
using UnityEngine;

namespace Heatmap
{
	public class HeatmapData
	{
		public HeatmapIcon Icon { get; private set; }
		public int Row { get; private set; }
		public Vector2 Coordinate { get; private set; }

		public HeatmapData(string data)
		{
			string[] fields = data.Split(' ');
			Icon = (HeatmapIcon) Enum.Parse(typeof(HeatmapIcon), "Heatmap_" + fields[0]);
			Row = int.Parse(fields[1].Remove(0, 1));
			string[] coordinates = fields[2].Split(',');
			Coordinate = new Vector2(float.Parse(coordinates[0]), float.Parse(coordinates[1]));
		}
	}
}

This second script handles the bulk of the logic. Parsing the text files, creating the plot points, loading the correct icons, and then scattering the plot points across the level in an organized hierarchical fashion.

A lot of design, testing, and effort goes into making sure each level in ParaLily is an exciting, rewarding, and fair experience. With this method of logging everything/anything, summarizing it up, and bringing it all back in Unity; we can ensure our levels meet our high quality standards!


Thank you,
ParaLily Dev Team
Nate & Jeff
 
 
P.S. We skimmed over a lot of the technical setup and coding portions, so if you have any questions or comments feel free to email us info@ParaLilyGame.com and we'll get back to you when we can!