Popular Posts

Wednesday, July 6, 2011

On engine design

I'm not sure how many of you know this, but NullpoMino 8 was originally going to feature a millisecond-based engine.

Not the engine design I'm talking about.
There are many games right now which operate on millisecond-based engines. For example, Tetris Online Japan's internals work on milliseconds, as does Tetris Friends' engine. Guideline games in general are stated to work on milliseconds in their requirements.

Now, NullpoMino has been one of the games which runs on a frame-based engine. Many other fan games such as Texmaster and Blockbox work the same way. In the original design of NullpoMino 8, we were throwing around the idea of switching around the engine type in order to deal with some of the performance issues in older versions.

If Nullpo could not keep 60 frames per second, it would slow down, which was undesirable. There were two different ways to go about fixing this. One involved decoupling the rendering from the logic, and the other involved using a millisecond-based engine. When Noname laid down the first code for Nullpo 8, he used milliseconds as an original attempt to fix this.

Millisecond-based engines are not coupled to frames which are assumed to take a certain length of time, so they are certainly a solution to the performance issue because they do logic simply by simulating whatever should have happened since last time they did logic, given the inputs entered and the status of DAS and such. However, other problems were instantly raised. NullpoMino is a descendant of a line of TGM clones, and making the engine millisecond-based would be horrible for implementing modes with TGM-type functionality, even though it might work for more guideline-oriented behavior.

To solve this issue, Noname made the engine able to run on frames or milliseconds, essentially by treating the frames as 1/60 second blocks that were elementary, as opposed to 1 ms. Along with this, the game was given the ability to do logic multiple times per render in order to "catch up" if it fell behind, without needing to render each frame explicitly.

So, we had to deal with two engine types now. Kitaru and I decided that there were some issues with the current engine design after looking it over, and we came up with a few points at which the millisecond-based engine was likely to behave in an unsuitable manner when compared to one that ran on frames. We tried to think of the best solutions we could to them, but some of them were not easily tackled, and it wouldn't make sense to keep this other engine which was both harder to maintain and did not perform as well.

In the next portion of the article, I use the term delta to mean the difference in milliseconds between one logic loop and the next. Also, keep in mind that these objections may not necessarily apply to all millisecond-based engines, but they do apply to games which are written like TOJ or TF.
  1. Timing issues resulting from variable frame rate.
    • What happens in the millisecond-based engine? Suppose the delta is temporarily increased by the computer hiccuping, or something like that. This will potentially cause inputs to be processed out of order, because they will all be grouped together in that time span since the last render, and the engine keeps a specific processing order for each logic loop. The result is that things that should happen first because they are due to happen sooner could potentially happen later, because they occur within the same time window. This could lead to things like replay desyncs if the hiccup is not present when playing back the game, for example.
    • How does the frame-based engine handle this? In a frame-based engine, the elementary unit of time is the frame, and one frame is defined to be the time difference between two logic loops. So, nothing will ever be enacted out of order, because the inputs are tied to specific frames. Nothing can happen "before" something else in a frame, because all timings are in terms of frames, and because everything is already expected to obey the processing order. If the computer hiccups, the logic will either slow down (old way) or disregard the rendering (new way) until everything clears up. No information is lost, and no replays desync as a result.
  2. Timing issues resulting from players operating on different frame rates.
    • What happens in the millisecond-based engine? Like before, two different engines splitting things into different parts will cause them to interpret inputs in different ways. Replays recorded on one engine will potentially look different (via desync) on another. This could be an issue if some players have performance problems that lead them to not be able to run at 60 frames per second.
    • How does the frame-based engine handle this? See above.
  3. Multiple events happening during one logic loop will be compressed into one event.
    • What happens in the millisecond-based engine? Suppose that you had your DAS rate (or, as it has come to be known, ARR) set to 5 ms. The expectation is that every 5 ms, your piece will shift if DAS is activated. Now consider what happens when your delta is 16~17ms as is usual. Even though you would expect to have roughly 3G DAS (that is, your piece would move about 3 spaces for every frame DAS is active), you will still be capped at 1G! This is because the processing order only takes into account each event happening once.
      • What are some of the ramifications of this? We vastly overcomplicate the current method of implementing Instant DAS, which is what we call the "Frame Condensation" or "Frame Compression" algorithm: essentially, we just run complete logic loops until DAS can no longer move the piece. This was done specifically to allow Instant DAS to behave stably in high gravity and prevent it from breaking TGM modes.
      • Increasing the delta ends up slowing the game down, because it can only produce one movement due to DAS each logic loop, and increasing the delta spreads out the logic loops.
      • How might the millisecond-based engine fix this? The only reasonable method would be to simulate multiple smaller time-sections of, say, 16~17ms, and doing one logic loop for each, but at this point you may as well be using a frame-based engine, because that's pretty much what it already does for you. Why try to emulate the other style of engine when you already implemented it?
    • How does the frame-based engine handle this? In the new system, the engine will simulate as many frames as it needs to in order to catch up to "real" time, but this time, you're getting it for free instead of hacking it on top.
The engine discussion included something else which was specific to the NullpoMino implementation of a millisecond-based engine, and I'm not going to reproduce it unless people are seriously interested in hearing what it is.

When we presented these objections to Noname, he noted that he had agreed and tried to implement the fix for #3 before realizing that the engine would just be better off as a frame-based engine to begin with. So, all millisecond-related engine code was removed, and the engine returned to its state of operation in terms of frames.

This is obviously because frame-based engines are the master race.

1 comment: