Skip to content

Commit

Permalink
Add README
Browse files Browse the repository at this point in the history
  • Loading branch information
knokko committed Feb 9, 2024
1 parent d5c5195 commit 1072599
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 0 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Update loop
## Handles the timing for game loops and other periodic tasks
### Motivation
Managing game loops properly can be significantly more
complicated than you might expect.
[First of all, should you put your update and render
functions in the same loop?](./splitting-update-from-render.md).
Furthermore, there are many ways to time your loops, and
[each approach has its own
drawbacks](./update-loops-overview.md). This mini library
provides a convenient class to handle the timing of game
loops.

### Usage
The usage is best shown using example code:
```java
class SampleGame {
public static void main(String[] args) {
long updatePeriod = 50_000_000L; // 50 milliseconds = 50M nanoseconds

new Thread(new UpdateLoop(updatePeriod, updateLoop -> {
synchronized (gameState) {
gameState.update();
}
if (!shouldContinue) updateLoop.stop();
})).start();

while (shouldContinue) {
synchronized (gameState) {
gameState.render();
}
}
}
}
```
This example would update approximately 20 times per second
and render with maximum FPS. Note that this is not
necessarily the smartest game loop: the parallelization
could be improved significantly with smarter synchronization.

For convenience, this library also provides an `UpdateCounter`
class that can count updates or frames:
```java
class Sample {
void test() {
UpdateCounter counter = new UpdateCounter();
while (shouldContinue) {
counter.increment();
render();
System.out.println("FPS is " + counter.getValue());
}
}
}
```
For demonstration purposes, this project also has a `testbench` [module
](./testbench/src/main/java/com/github/knokko/update/UpdateMonitor.java).
![](./update-monitor.png)

### Add as dependency
Since this library only has 4 small classes, you could just
copy & paste them into your own project. If you prefer
proper dependency management, you can use Gradle or Maven
and Jitpack.
192 changes: 192 additions & 0 deletions splitting-update-from-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Splitting the update loop from the render loop
Most games need to both update and render periodically.
There are basically 2 ways to accomplish this:
- Call both the update and render function in 1 loop
- Spawn 1 thread for the render loop and 1 for the update loop

## Using one loop
Using one loop is the easiest, and the easiest way to
implement this is basically:
```
while (shouldContinue) {
update();
render();
}
```

The drawback of this approach is that your update frequency
is tied to your framerate. The faster your computer, the
more updates you will get per second. All in-game characters
will literally move faster on faster computers.

### Using delta time for the update
A common 'solution' to this problem is adding a `deltaTime`
parameter to the update function:
```
long previousTime = System.currentTimeMillis();
while (shouldContinue) {
long currentTime = System.currentTimeMillis();
double deltaTime = (currentTime - previousTime) / 1000.0;
previousTime = currentTime;
update(deltaTime);
render();
}
```
At least the speed of the in-game characters no longer
depends on the framerate, but it still has plenty of flaws
left:
1. It uses `System.currentTimeMillis()` instead of
`System.nanoTime()`.
2. The update frequency is still tied to the framerate.
3. The physics is non-deterministic and its precision
depends on the framerate: lower framerate =
sloppy physics.

Problem 1 is easy to solve, but still relevant to point out.
The drawbacks of `System.currentTimeMillis()` are that it is
less precise than `System.nanoTime()`, and that it is
synchronized with the real time. The latter means that the
physics/update will go wild whenever the system time changes.
`System.nanoTime()` doesn't suffer from this problem.

Problem 2 is smaller than it used to be, but still not great.
To increase the framerate, both the update and render
performance need to be improved. Since the update performance
typically strongly depends on the CPU, buying a better
graphics card will only improve the framerate slightly.

Problem 3 is most visible when the framerate is low. When
the collision detection is simplistic, it even allows
characters to move through walls when the framerate is
sufficiently low (when
`deltaTime * speed > characterWidth + wallWidth`). When
games have glitches that can only occur at low framerates,
this stupid update loop is probably the reason. This
particular bug can also be solved by using better
collision detection, but it won't solve the fundamental
problem of your non-deterministic physics: if two players
are in the same situation, the outcome will depend on
their framerate. For instance, some jumps could be
easier or harder if the framerate is higher or lower.

### Skipping physics based on time
An alternative to the `deltaTime` mess is using the
time to determine when physics should be skipped.
Something like this, for instance:
```
long lastUpdate = System.nanoTime();
while (shouldContinue) {
long currentTime = System.nanoTime();
if (currentTime - lastUpdate > updatePeriod) {
lastUpdate = currentTime;
update();
}
render();
}
```
Personally, I would consider this solution to be a lot
better than the `deltaTime` approach: the physics is
deterministic, and it's easier to achieve higher FPS
since you don't need to `update` between every `render`.
Still, there are 2 problems left:
1. When the framerate is low, the update frequency is also
low: all characters will be slower when the framerate is low.
2. Updating and rendering can't be done in parallel.

Problem 1 can be solved with a bit more effort, but
problem 2 is a fundamental problem when the update
loop and render loop are combined. Since almost any
somewhat-modern computer has at least 2 cores, this is
quite wasteful.

## Using separate loops
The aforementioned problems vanish when you use
separate loops for rendering and updating. A simplistic
approach would be:
```
new Thread(() -> {
while (shouldContinue) {
update();
maybeSleep();
}
}).start();
while (shouldContinue) {
render();
}
```
TODO Describe maybeSleep.
The advantages of this approach are:
- The update period can be chosen and is completely
independent of the framerate.
- The application can update and render at the
same time (great for performance).

The drawback of this approach is that the
application can update and render at the same time.
All kinds of weird things and errors can occur when
the rendering is reading the game state while the
updater is changing it. For instance, a
`ConcurrentModificationException` could be thrown
if the rendering is reading the entity list while
the updater adds an entity. Generally, there are
countless consistency hazards that could go wrong.

### Synchronizing access to the game state
These (consistency) problems can be solved by
synchronizing on the game state (or some other
lock). A simple solution would be:
```
new Thread(() -> {
while (shouldContinue) {
synchronized(gameState) {
update();
}
maybeSleep();
}
}).start();
while (shouldContinue) {
synchronized(gameState) {
render();
}
}
```
This solves all consistency problems, but it also has
a drawback: updating and rendering can't be done
in parallel, which beats one of the main purposes of
splitting the loops in the first place...

### Copying the game state
Fortunately, this solution can be improved by using
smarting synchronization and some copies. For instance,
the renderer could copy the game state in the
synchronized block, after which it renders that copy
after the synchronized block:
```
new Thread(() -> {
while (shouldContinue) {
synchronized(gameState) {
update();
}
maybeSleep();
}
}).start();
while (shouldContinue) {
GameState localGameState;
synchronized(gameState) {
localGameState = copy(gameState);
}
render(localGameState);
}
```
This solution allows the updater to update the game state
while the renderer is rendering a copy of it. This
is an improvement over the previous situation, but still
not perfect. For instance, when the renderer wants to
start its next frame, it needs to wait until the updater
finishes the current tick. There are plenty of ways to
solve this problem, but that's out of scope. The
purpose of this text was just to explain why splitting
the update and render loop is a good idea.
Loading

0 comments on commit 1072599

Please sign in to comment.