<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>luminance &#187; achievements</title>
	<atom:link href="http://www.luminance.org/tag/achievements/feed" rel="self" type="application/rss+xml" />
	<link>http://www.luminance.org</link>
	<description>Programming and Game Development - Kevin Gadd's Personal Blog</description>
	<lastBuildDate>Thu, 29 Apr 2010 17:20:57 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.2</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Achievements and player data II</title>
		<link>http://www.luminance.org/gruedorf/2009/09/03/achievements-and-player-data-ii</link>
		<comments>http://www.luminance.org/gruedorf/2009/09/03/achievements-and-player-data-ii#comments</comments>
		<pubDate>Thu, 03 Sep 2009 12:51:37 +0000</pubDate>
		<dc:creator>Kael</dc:creator>
				<category><![CDATA[Gruedorf]]></category>
		<category><![CDATA[achievements]]></category>
		<category><![CDATA[analysis]]></category>
		<category><![CDATA[csharp]]></category>
		<category><![CDATA[data]]></category>
		<category><![CDATA[heatmap]]></category>
		<category><![CDATA[xna]]></category>

		<guid isPermaLink="false">http://www.luminance.org/?p=824</guid>
		<description><![CDATA[In my previous post I gave an introduction to achievements and player data collection. In this post, I&#8217;ll cover the remaining two significant pieces: Services and front-ends. Unfortunately neither of these will be quite as complete as the coverage in the previous post, since I haven&#8217;t completely finished implementing these parts of my achievement system. [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://www.luminance.org/gruedorf/2009/08/28/achievements-and-player-data-i">In my previous post</a> I gave an introduction to achievements and player data collection. In this post, I&#8217;ll cover the remaining two significant pieces: Services and front-ends. Unfortunately neither of these will be quite as complete as the coverage in the previous post, since I haven&#8217;t completely finished implementing these parts of my achievement system. Whoops!</p>
<h2>Services</h2>
<p>Boy, that&#8217;s a generic heading. Anyway, so your game is collecting important data on gameplay events, and it&#8217;s reporting that data periodically to a web service on your server. Unfortunately, since you haven&#8217;t actually written that service yet, it doesn&#8217;t seem to be working. Odd how that goes, isn&#8217;t it?</p>
<p>The primary issue you have to deal with when building a service to recieve collected data from your game is storing the data. There are other incidental issues &#8211; security, performance, serialization, etc &#8211; but none of them are of any significance if you can&#8217;t find a way to reliably store and access the data. This isn&#8217;t a simple problem, but there are ways to handle it.</p>
<p>Essentially, the biggest issue you have to confront here is that you are going to have a lot of data. You may not have a lot of data now, but you will eventually. On one hand, you could spend weeks and weeks trying to design the perfect solution for storing all of this data &#8211; but doing that doesn&#8217;t actually get your game any closer to shipping, and doesn&#8217;t actually guarantee that you won&#8217;t have scalability issues down the road. On the other hand, if you completely ignore the problem, you risk losing some of the data you&#8217;ve already gathered by discovering significant design issues after you ship. In practice, you probably want to prioritize shipping over the longevity of your data, since (assuming you ship and your game isn&#8217;t terrible) you can always get more data.</p>
<p>As a starting point, I decided to build my services on Google App Engine, since it was relatively easy to get up and running and provides fairly generous quotas for free.</p>
<p><span id="more-824"></span></p>
<p>I built my services in Python, using the provided datastore and JSON modules to handle most of the work. For those unfamiliar with how App Engine works, essentially your process is this:</p>
<ul>
<li>Modify your app.yaml file to specify the URLs of your services and the python scripts that implement them.</li>
<li>Create one or more Python classes to be used for storing your data in the GAE datastore.</li>
<li>Implement your services by creating python WSGI modules to process incoming requests and store the resulting data in the datastore.</li>
</ul>
<p>For those more familiar with traditional databases, working with the App Engine datastore can be a challenge &#8211; its write performance borders on terrible, and the documentation is somewhat sparse. However, the actual API is very easy to use and since you&#8217;re using Python, most of the problems you&#8217;re dealing with are easy to solve. It helps that most of Django is included with the GAE APIs, since that means you have access to all sorts of useful tools like their HTML template module.</p>
<p>So, for the event format I showed in my previous post, the Python class looks like this:</p>
<pre>class Event(db.Expando):
  playerId = db.IntegerProperty()
  sessionId = db.IntegerProperty()
  timeMs = db.IntegerProperty(indexed=False)
  eventType = db.StringProperty()</pre>
<p>Fairly straightforward. Note that I&#8217;m deriving from <strong>Expando</strong> and not <strong>Model</strong> &#8211; to get an idea of what this means you&#8217;ll need to read the GAE documentation, but essentially this lets me add additional, unindexed fields to my instances as necessary. Since I only plan to do most queries based on event type and player ID, this is perfect.</p>
<p>Given that class definition, in my service, I can simply unpack the JSON I recieve from a game client and convert it into one or more Event instances, ready to send to the datastore:</p>
<pre>      playerId = int(body["PlayerId"])
      sessionId = int(body["SessionId"])
      events = body["Events"]

      put_list = []

      for event in events:
        eventData = event["Data"]
        extraArgs = {}

        while eventData:
          if isinstance(eventData, types.DictType):
            d = eventData
            eventData = None
            for k in d.iterkeys():
              value = d[k]
              if k == "Data":
                eventData = value
              elif isinstance(value, types.DictType):
                continue
              else:
                key = ("%s" % (k,)).encode("ascii")
                extraArgs[key] = value
          else:
            extraArgs["eventData"] = eventData
            eventData = None

        evt = Event(
          playerId=playerId,
          sessionId=sessionId,
          timeMs=event["Time"],
          eventType=event["Type"],
          **extraArgs
        )
        put_list.append(evt)

      db.put(put_list)</pre>
<p>The resulting data format doesn&#8217;t allow you to answer every question you could possibly have, but does leave you with a relatively easy to query volume of data in the google datastore. It&#8217;s also very simple, which makes it relatively straightforward to debug failures and make changes.</p>
<p>As mentioned before, the datastore&#8217;s write performance borders on terrible. If you attempt to perform a <strong>put</strong> on multiple objects in sequence, its write performance goes from bad to earth-shatteringly horrible. However, the datastore API allows you to hand it a list of objects, and it will attempt to put them all at once, which significantly improves your performance so that it&#8217;s at least tolerable. If I were using a relational database like MySQL this would be somewhat equivalent to trying to insert all my rows in a single multi-line query to reduce round-trip time.</p>
<p>Also of note are the somewhat ridiculous contortions I do here to convert event data into fields for the Event instance. Essentially, if the incoming event&#8217;s Data field is just one value, I shove it into a single field inside the Event &#8211; but if it&#8217;s a dictionary, I dig through it looking for values and store them into corresponding fields on the Event instance. This ensures that all the values attached to the event end up with corresponding columns in the datastore.</p>
<p>This also illustrates some slightly interesting design decisions: Events don&#8217;t have any global timestamp, nor do they have a sequence number &#8211; all they have is a player ID, session ID, and timestamp relative to the beginning of the session *in game time*. This is mostly a pragmatic decision; I could provide a global timestamp and get some benefits out of it, but the costs associated with that are quite significant, since accurate timestamps interact badly with batching and the general problem of internet latency.</p>
<p>One particularly nasty sticking point is that GAE has fairly rigid limits on how much CPU and &#8216;API CPU&#8217; time you can consume in a given request, or in a given minute, or in a given hour, or in a given day. These limits are <strong>extremely</strong> easy to hit if you are using the datastore in the manner I am &#8211; so it&#8217;s important to make sure that your game isn&#8217;t sending large batches, or your requests will time out and google will become very angry with you for wasting precious CPU milliseconds. In practice, I&#8217;ve found that batch sizes between 6 and 12 reduce network traffic without hitting the built-in GAE limits. Your mileage may vary based on the nature of your data.</p>
<p>It&#8217;s also worth pointing out that the HistoryBlock approach I described in the previous approach is a bit problematic here: You&#8217;re basically forced to store all those position values within a single event, because turning them into multiple events would absolutely destroy the datastore. Unfortunately, it appears that storing a long list of values within a single event <strong>also</strong> absolutely destroys the datastore. Oh well, what can you do? (Other than switch to another hosting provider)</p>
<p>Also note that this doesn&#8217;t account for security, which means you probably can&#8217;t deploy it in production, because people like breaking your services. Oh well!</p>
<p>Now that we&#8217;re all familiar with how frustrating web service development is, let&#8217;s venture into the comforting realm of data analysis:</p>
<h2>Presenting Your Data (Front-ends)</h2>
<p>Mmm, much better.</p>
<p>Now that you&#8217;ve got all this data sitting around in the GAE datastore, what are you going to do with it?</p>
<p>Among the many things you can do, here are a few good ideas:</p>
<ul>
<li>You can perform high level queries against your event data to generate general &#8216;gameplay statistics&#8217; for player profiles.</li>
<li>You can perform statistical analysis of your event data to detect patterns in usage and player behavior.</li>
<li>You can take the position values attached to your event data, and use it to generate heatmaps representing certain events or behaviors &#8211; player movement, player death, creature death, etc.</li>
</ul>
<p>Since I&#8217;m uneducated, I&#8217;m going to skip the statistical analysis and describe the other two.</p>
<p>Creating gameplay statistics from your data is actually relatively easy, as long as you&#8217;re not worried about performance (and right now, you probably shouldn&#8217;t be). Even if performance is an issue, you can often solve this easily by track counters and statistics in a summary table, either by maintaining a count in your service when storing events, or by writing a <a href="http://en.wikipedia.org/wiki/Cron">cron job</a> to periodically update your summaries from the most recently stored events. Doing this will allow you to maintain statistics without having to query your entire event history.</p>
<p>For example, given my event format, I can determine how many times a given player has killed the Drowned Queen by querying the datastore like so:</p>
<pre>player_events = db.Query(Event).filter(
  'playerId =', playerId
)
drowned_queen_kill_events = player_events.filter(
  'eventType =', 'PlayerKilledCreature'
).filter(
  'CreatureName =', 'DrownedQueen'
)
drowned_queen_kill_count = drowned_queen_kill_events.count()</pre>
<p>Essentially, what I&#8217;m doing here is taking my entire event data set, and iteratively filtering it down &#8211; first I start with the player ID, then I filter by the event type. Both of these columns are indexed, and each of these filters will eliminate a huge number of rows. After this I can filter by CreatureName; as long as I don&#8217;t have thousands of PlayerKilledCreature events this will be relatively cheap to do. After that it&#8217;s simple to ask the datastore how many events match my filter.</p>
<p>Given statistics like these, if you choose you can then come up with achievements and other information to display on a player&#8217;s profile page. Players tend to love these things, even though they&#8217;re not directly integral to a game experience. They give players a way to gauge their own skill at a game versus others, and also provide them with interesting &#8216;carrots&#8217; to run after, whether it&#8217;s finding all the secrets in a game or defeating a boss without taking any damage.</p>
<p>So! Now for <a href="http://en.wikipedia.org/wiki/Heat_map">heat maps</a>. If you&#8217;re not familiar with them, you might want to glance at the linked Wikipedia page, but they&#8217;re fairly easy to understand, at least conceptually. You essentially take a large volume of data (in our case, events) and use it to generate a bitmap that you can overlay on an image of a space that you care about. To give a real-world example, if you had a list of recent car accidents, you could generate a heat map from that list and overlay it on a map of your neighborhood, and possibly see clusters around particularly dangerous intersections. You can&#8217;t always rely on a heat map for drawing conclusions, but they make it much easier to spot patterns and trends because our brains are great at analyzing data when it&#8217;s presented in this format.</p>
<p>In my case, I want to take all that HistoryBlock data and convert it into a heat map, so I can figure out where players are spending most of their time. To do this, first I needed to get images of all my levels &#8211; this took some work, since I hadn&#8217;t previously done any offline rendering of my game content. In some cases you can do this sort of thing offline during your build process, but in my case since my renderer relies on Direct3D and XNA, I simply start up an instance of the game engine in a &#8216;batch processing mode&#8217;, and it scans through a level, using the 3D card to generate screen-size &#8217;tiles&#8217; of the level, one at a time, and save them out to disk. After that I scan through the tiles and scale them down to create a &#8216;minimap&#8217; that represents the level at a given magnification.</p>
<p>Once I&#8217;ve got images for each of the levels, I can begin to generate a heat map for them. First, I query a service that pulls down all of the HistoryBlock events from the server and returns them as JSON. This sort of thing is likely to have scalability issues, so in practice you might want to only pull down a subset of your events &#8211; the most recent ones, or a random sampling &#8211; and use them instead. Right now my dataset is small enough that this isn&#8217;t a problem.</p>
<p>Once I&#8217;ve got the list of positions, I can scan over them and create the heat map. Essentially, what you want to do is create a 2D bitmap that is roughly the same size as your minimap, or some even multiple of it. Then, you can through all your positions and map them to points within that bitmap. Every time you successfully map a position to a point, you increment the value at that point, so that the most frequently reported locations have higher values.</p>
<p>Once you&#8217;ve done that, you can compute a minimum, maximum, etc. for the heat map, and use that to translate those &#8216;heat&#8217; values into colors at each point. Then, all you have to do layer the resulting color bitmap onto your mini map, and you have a heat map. Here&#8217;s a few ones I just generated, based on the events I&#8217;ve been recording while testing out my services and achievement system. You may need to click on them to view them at full size:</p>
<p style="text-align: center;"><a href="http://www.luminance.org/wp-content/uploads/2009/09/troupe.png"><img class="aligncenter size-full wp-image-831" title="troupe" src="http://www.luminance.org/wp-content/uploads/2009/09/troupe.png" alt="troupe" width="778" height="185" /></a></p>
<p style="text-align: center;">This heat map was generated from a dozen or so play-throughs of the first level in the Level Up 2009 demo.</p>
<p style="text-align: left;">There are a few interesting conclusions you can draw from this heatmap &#8211; for example, you can easily spot the locations of switches/cranks, because those points are orange or red (from the player tending to stand in front of them to manipulate the object). Because of the short number of playthroughs, you can actually see faint traces of me dying by falling into the water.</p>
<p style="text-align: left;">The data also gives you a general idea of typical paths through the level, and shows you where players spend more time &#8211; for example, the ledge on the right side of the water is much brighter, because the player tends to stand there for a while fighting the monsters that are spawned there.</p>
<p style="text-align: left;">This presents some good opportunities to improve the flow of the level, address difficult platforming challenges, or make it easier for the player to find objectives (keen-eyed observers will note that there&#8217;s a secret passage in this level that I did not enter in any of my playthroughs. (: )</p>
<p style="text-align: center;"><a href="http://www.luminance.org/wp-content/uploads/2009/09/drowned_queen_arena.png"><img class="aligncenter size-full wp-image-830" title="drowned_queen_arena" src="http://www.luminance.org/wp-content/uploads/2009/09/drowned_queen_arena.png" alt="drowned_queen_arena" width="332" height="332" /></a></p>
<p style="text-align: left;">This one was generated from the small arena in which the player fights the boss from the demo, the Drowned Queen.</p>
<p style="text-align: left;">Even a quick glance at this heat map reveals some interesting patterns, despite the fact that it is based on a small amount of data.</p>
<p style="text-align: left;">I rarely died from falling into the water below, which indicates that the water is not particularly effective as a hazard (or that I&#8217;m particularly adept at avoiding it).</p>
<p style="text-align: left;">The extremely bright clusters to the left side indicate that I rarely moved around the arena while fighting the Queen, tending to instead dart back and forth within the same area. This might indicate problems with the design of the level or the boss, since you would usually expect players to be running around in an arena like this while fighting a well-designed foe.</p>
<p style="text-align: left;">The big bright red point up in the top left is aligned perfectly with one of the arena&#8217;s grip plates. While you&#8217;re supposed to use the grip plates to avoid one of the Queen&#8217;s special attacks, the huge amount of time spent there suggests that the boss&#8217;s AI is leading me to remain on the plate for a long period of time, looking for a safe opportunity to drop down, instead of actively fighting her and having a good time.</p>
<p style="text-align: left;">Keen-eyed viewers who&#8217;ve played the demo will also note that there are yellowish regions at each of the locations where a dialogue sequence occurs. This indicates that I tend to stand still while progressing through the dialogue sequence, despite the fact that the player is allowed to move freely. Interesting!</p>
<p style="text-align: center;"><a href="http://www.luminance.org/wp-content/uploads/2009/09/aqueduct.png"><img class="aligncenter size-full wp-image-829" title="aqueduct" src="http://www.luminance.org/wp-content/uploads/2009/09/aqueduct.png" alt="aqueduct" width="695" height="302" /></a></p>
<p style="text-align: center;">This final one is generated from the second level in the demo, the aqueducts.</p>
<p style="text-align: left;">Rather embarassingly, this demonstrates the perils of trying to draw conclusions from player data: Almost all the points in this heatmap are barely visible.</p>
<p style="text-align: left;">Why? Well, most likely, I left the game running with the player standing in that position for a long time, which resulted in that point being far, far brighter than the rest of the level.</p>
<p style="text-align: left;">Of course, there are ways to address these sorts of problems with your data &#8211; but if you&#8217;re not careful, they may influence your conclusions without you even realizing it. In this case I would probably want to discard or ignore any points that have values vastly out of whack compared to the rest of the heat map, so that the heat map will demonstrate general trends instead of only showing outliers.</p>
<p style="text-align: left;">
<p style="text-align: left;">I hope these posts have given you an idea of how you can go about gathering player data in your games, and how you can put that data to work &#8211; both towards improving your game, and giving your players access to data they want.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.luminance.org/gruedorf/2009/09/03/achievements-and-player-data-ii/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Achievements and player data I</title>
		<link>http://www.luminance.org/gruedorf/2009/08/28/achievements-and-player-data-i</link>
		<comments>http://www.luminance.org/gruedorf/2009/08/28/achievements-and-player-data-i#comments</comments>
		<pubDate>Fri, 28 Aug 2009 09:35:37 +0000</pubDate>
		<dc:creator>Kael</dc:creator>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Gruedorf]]></category>
		<category><![CDATA[achievements]]></category>
		<category><![CDATA[csharp]]></category>
		<category><![CDATA[data]]></category>
		<category><![CDATA[JSON]]></category>
		<category><![CDATA[xna]]></category>

		<guid isPermaLink="false">http://www.luminance.org/?p=814</guid>
		<description><![CDATA[One of the things I&#8217;ve been working on lately is a way to collect and store data on play sessions, so I can track achievements for players and track where players spend the most time in a level, or where they die the most. There are lots of details to get right for a system [...]]]></description>
			<content:encoded><![CDATA[<p>One of the things I&#8217;ve been working on lately is a way to collect and store data on play sessions, so I can track achievements for players and track where players spend the most time in a level, or where they die the most. There are lots of details to get right for a system like this, but I&#8217;ve at least gotten a prototype working and started experimenting with ways to visualize the data.</p>
<p>For a system like this you have a few important pieces:</p>
<ul>
<li>Your game needs to collect data during play sessions &#8211; in my case, I track important events like player/creature death, and periodically sample the player&#8217;s position to get a general idea of where players are in a given level during their session.</li>
<li>You need a way for your game to periodically report collected data to a remote server. You have to handle lots of edge cases here &#8211; for example, it&#8217;s likely some people will play without an active internet connection or suffer temporary loss of connection, so you need to batch up events to report later in this case &#8211; you definitely don&#8217;t want the game to fall over and choke without access to the internet, and if possible you want to avoid losing data too.</li>
<li>You need to build a set of services to collect data from the game. This typically means you also need services to handle things like uniquely identifying individual play sessions and computers, so that you can track achievements for individual players and perform analysis on individual sessions instead of only on a player&#8217;s entire play history.</li>
<li>You need to build an in-game frontend, to expose collected data to a player. This means turning your event data into user-friendly statistics &#8211; like a kill counter, or an achievement for killing a particular boss. One interesting challenge here is that you probably want to expose this information online, too, so that players can share their profiles and achievements.</li>
</ul>
<p>In this post, I&#8217;m going to cover the first two.</p>
<p><span id="more-814"></span></p>
<h2>Collecting Data</h2>
<p>So, first, you need a way to collect important player data. For it to be useful, you need to collect it in as consistent a format as possible; you don&#8217;t want every piece of data to have a unique set of parameters attached to it, for example, because that would make it impossible to perform any generic analysis of your data. Likewise, you want to try and keep your data &#8216;denormalized&#8217;, by storing as much relevant data together as possible. If you achieve both of these goals, you end up with a stream of &#8216;events&#8217; that represent the things you care about in a form that lets you inspect them individually or analyze them in large groups.</p>
<p>In my case, this step was actually quite straightforward. I already had a simple &#8216;event bus&#8217; integrated into the game, that I was previously using to synchronize animation and sound effects with gameplay. Given this, it was simple to create a data collector that attaches to the event bus and listens for events that it wants to collect.</p>
<p>I did, however, have to make some changes &#8211; many of my events didn&#8217;t expose the information necessary to be useful; for example, the event representing a player death didn&#8217;t contain any information to tell you who (or what) killed the player, so I had to tweak the game code to attach that to the event. Likewise, many other events didn&#8217;t include position information, which made it hard to get meaningful information about them &#8211; knowing that the player grabbed onto a ledge isn&#8217;t particularly useful if you don&#8217;t know *where* or *which ledge*. In some cases, the data collector attached important parameters into the events so that game objects wouldn&#8217;t have to; for example any event coming from the Player automatically has PlayerX and PlayerY parameters attached to it (since the Player is the source of the event, it is simple to extract a position and attach it to the event).</p>
<p>One thing that required some special treatment as well was recording the player&#8217;s position over time &#8211; while attaching position information to events gives you a general idea of the player&#8217;s location, it&#8217;s not very useful for figuring out where players are getting stuck or confused. To address this, I built a simple class that periodically samples the player&#8217;s position and adds it to a list. Every time the list reaches a certain size, its contents are batched up into a single event &#8211; a &#8216;HistoryBatch&#8217; &#8211; and sent to the data collector. This allows me to get fairly high-resolution position data without having to report hundreds of individual events every minute (which would have contained lots and lots of redundant information).</p>
<h2>Reporting Data</h2>
<p>Once you have your player data, you need to report it to your server (and probably store some of it locally, too). I honestly haven&#8217;t tackled the latter part yet, but here&#8217;s how I handled the former:</p>
<p>To handle periodically reporting events, I created a simple &#8216;Event Reporter&#8217; class that collaborates with the data collector. Essentially, the event reporter maintains a queue of pending events, and runs a thread that is responsible for sending batches of events to the remote server once the queue gets large enough. It automatically waits a certain amount of time between requests, and attempts to batch events into groups instead of reporting them one at a time as they occur. This allows me to get relatively low latency (if I want it) but still remain relatively efficient in my use of the network. Whenever the data collector gets an event, it adds it to the event reporter&#8217;s queue.</p>
<p>Once I have a batch of events ready to send to the server, I pack them together into a class that&#8217;s laid out optimally for network transmission. I then use .NET 3.5&#8217;s built in JSON serializer to convert the batch into a blob of JSON data ready to send to my remote server. A typical blob looks like this:</p>
<pre>{
  "PlayerId":xxxx,
  "SessionId":yyyy,
  "Events":[
    {"Type":"ActiveControllerChanged","Time":0,"Data":{
      "IsKeyboard":true,
      "ConnectedGamepads":0,
      "ActiveController":0
    }},
    {"Type":"LevelLoaded","Time":5436,"Data":"troupe"},
    {"Type":"HistoryBlock","Time":7788,"Data":{
      "LevelName":"troupe",
      "PlayerX":[-136,-74,35,192,209,209],
      "PlayerY":[588,875,920,920,920,920]
    }}
  ]
}</pre>
<p>As you can see, events are small when possible &#8211; they simply store the event type and the timestamp at which the event occurred, along with their associated information. In cases where an event has lots of information attached, I send a dictionary, but in some cases I can just send a single value. In the case of a history block, I make sure to send the X and Y coordinates as individual lists, so that I don&#8217;t have to waste time encoding &#8216;PlayerX&#8217; and &#8216;PlayerY&#8217; along with every individual coordinate. A batch of events has a player ID and a session ID attached, which allows me to analyze individual play sessions and track achievements. (Note that both of these IDs are randomly generated on the server, so they&#8217;re anonymous.)</p>
<p>The reporter starts an HTTP POST to send the JSON to the server, and goes to sleep until it&#8217;s finished. If the send fails, the events are left on the queue and the thread sleeps for a while so it can retry sending them later. Once a send completes, the reporter checks to see if the queue is empty &#8211; if the queue is now empty, it goes to sleep until the queue contains items again. If the queue still contains items, it sleeps for a short while and prepares to send another batch &#8211; this allows the event reporter to spend the majority of its time asleep, but still handle large bursts of events without sending a huge blob of events at the server in one POST (since that would be very likely to fail or time out.)</p>
<p>One important detail is that the reporter needs to be able to correctly report events even if the player quits. In my case, the player can quit at any time, and I can end up with a very large number of events in the queue at the time. To deal with this, once the game has finished tearing down, the event reporter is given around 30 seconds to report any remaining events to the server. This usually allows events to get through, but prevents the game from hanging indefinitely on exit (in the event that there&#8217;s some sort of network failure, or the event queue is <strong>really</strong> full). Since this occurs after teardown, the game window is gone so the player doesn&#8217;t get the impression that the game has hung, and since the reporter spends most of its time sleeping, they won&#8217;t see any significant CPU usage either.</p>
<p>The reporter also runs in its own isolated thread and uses its own task scheduler, instead of sharing resources with the game. This increases the likelihood that the game will be able to successfully report events in the case of a bug or crash, and also means that if the reporter fails for some reason &#8211; probably a network outage &#8211; it won&#8217;t take the game down with it.</p>
<p>In my next post, I&#8217;ll cover the remaining two items and show you some of the things you can do once you&#8217;ve gathered player data. In the interim, you can <a href="http://inferus-data.luminance.org/player/view?id=1115">take a look at my player profile</a>!</p>
<hr />
]]></content:encoded>
			<wfw:commentRss>http://www.luminance.org/gruedorf/2009/08/28/achievements-and-player-data-i/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>
