Replay System - epic failure?
Introduction
Earth Matters has 135 levels of varying difficulty. Since I designed all the levels, I know how to solve them. However, I suspected that some were probably too difficult. I thought it would be useful to build a replay system, so that when a player(tester) failed at a level I could see where they were struggling, and try to fix it before publicly releasing the game. I wanted to make the replay system extremely lightweight.
By lightweight, I mean minimize the amount of data that needs to be transferred, low CPU overhead while “recording”, and easy to build from a coding perspective. I didn't want 10+ second uploads of replay data, in fact I wanted it to be unnoticeable to the user. (The target for a typical payload-size was less than 1k, keeping transfer times under 1 second.)
Approach
The basic approach was to capture the UI events, each with a timestamp, for the current level. In other words, what button was pressed when. Then I created a playback mode where if I passed a replay event log, I could watch the level play out. I figured this approach would work because the only “randomness” is when and how players interact with the game. The beauty with this approach is that it is basically replaying/simulating user actions, with no custom viewer code to write. The replay-log reader is just firing UI events at the specified times.
The game was never designed to require pixel-perfect placement nor overly precise timing. It is more about solving puzzles, despite there being a timing aspect to several of levels. In Earth Matters you click on an Elemental's button and it spawns, and then does its thing. On any given level, new Elementals launch from the level's portal and start moving in the same direction. Elementals continue to move in the same direction until they encounter terrain, other Elementals, or a reaction. All of the movement of the game characters (Elementals and reactions) is deterministic, meaning there is no randomness. Or so I thought...
Determinism
Enter Unity, and different devices with varying screen resolutions. The game world of each level is a grid of 16x10 world units. All of the physics, movement, collisions and collision responses, regardless of device or screen resolution takes place on that 16x10 floating point grid. I figured that the display is just a rendering of the game world state. Since time passes at the same rate on all devices, and positional updates are mathematical calculations based on elapsed time, then regardless of screen resolution, everything will behave the same.
Oh how naive I was. For starters the frame-rate across all devices is not guaranteed to be the same, which makes total sense and was anticipated. I used a running game clock based on the system-clock and log its value when a UI event occurs. To that end, I didn't write code that tracked things frame by frame. Had I logged things frame by frame, I would have had to build a replay viewer app or mode. By keeping it to injecting UI events into the game engine, a replay system didn't require much code.
Unfortunately, I quickly discovered that the little floating point differences add up, resulting in different outcomes. Heartbreakingly, Unity used this way is not deterministic. I could totally understand slight differences in time as UI events are triggered, but that wasn't what was happening. It was more like characters accelerating at different rates due to gravity, and collision responses had characters separating at different angles.
The only way to make a replay-system that would show what the user experienced would be to log the position of all GameObjects at some interval. Decreasing the interval would increase the precision at the cost of a larger replay-file. Of course maximum precision would be to just do a screen recording.
Silver Lining
I'm not including any code with this post, because ultimately it wasn't usable. Even though I never got the replay-system working in a way that was useful, meaning being able to see where players were struggling, all was not lost.
In an effort to test the quirkiness of Unity across different devices, I did identify problematic level configurations that led to varying results. Seeing how Unity behaved differently across devices allowed me to modify levels so the physics behaved in a more consistent fashion.
Typically it was shortening the jump height required. The end result of the level changes was to make them more forgiving, and ultimately make Earth Matters a better game.