This past week I’ve been looking into demo file playback and refining the movement code. Movement up until this point relied on my previous knowledge of Quake’s movement system. The constants were eyeballed and while totally playable, it doesn’t quite feel like “Half-Life”. And of course I’d like to preserve bunny hopping.
I decided that having the ability to replay demo files would help me consistently test changes to nmdist, and give me an opportunity to compare footage of nmdist against Half-Life. This also has the bonus of being useful for more consistent performance testing down the line.
While there doesn’t seem to be great documentation for the demo file format, others have written parsers for it. I ended up getting the struct definitions from YaLTeR/hldemo-rs. I didn’t want to deal with more dependencies, so I replaced the usage of nom with my own parser that I’ve used in the rest of the engine.
After some tweaking, I was able to achieve demo playback! As a bonus I also added a very crude console and autoexec system. The following is nmdist playing back a demo file that was recorded in Half-Life:
Demo files contain two types of information that are relevant to us right now. First, there are the positions and orientations of the camera on each frame. This information is used by the playback system, and thus is not effected by differences in nmdist vs Half-Life. Second, there are the player inputs. While not used in demo playback, we can use this to replay the actions of the player in nmdist and see if they match the position on that frame in Half-Life.
I added a command to analyze demo files, comparing the position from the demo file against a simulation of the demo file inputs running in nmdist. The blue line represents the positions found in the demo file (e.g. the positions recorded by Half-Life), and the orange line represents nmdist. Ignore the pink line for now.

Right away you can see there’s a significant difference between the two implementations. In the first segment (up until the first red point/cube in each line), movement is purely being driven by the mouse look and the forward key. So the difference comes down to a difference in speed. It’s easier to see on a chart. The following is a chart that plots the speed on each frame. Blue is Half-Life and orange is nmdist:

It takes nmdist far longer to get up to the max ground speed than it does Half-Life. If you’re wondering why the blue line has some weird spikes in it, it’s because I’m computing the speed on each frame based on the position from the previous frame and the time that elapsed. Velocity isn’t present in the demo file. This computation seems to have an artifact, or it’s revealing some quirk of GoldSrc. But it’s not relevant to us right now.
My first thought was that this difference in speed is likely down to a difference in friction between nmdist and Half-Life. I modified the demo analysis system to include a second simulation, but this time taking many of the constants (e.g. friction, max speed, acceleration) from the demo file. As you might have guessed, that’s what the pink line is in the previous picture of nmdist.
This gets much closer! But there’s still some differences, largely when the player is in the air.

The next steps are to take a closer look at collisions and air physics. I’ve found a great resource online that has documented Half-Life’s physics and movement as plainly and mathematically as possible, so that’ll be a big help. But that’s it for now. Happy holidays!