1

I'm implementing an FPS cap for my game, but it's not very precise.

public static volatile int FPS_CAP = 60;

@Override
public void run() {
    long lastTime = System.nanoTime();
    double amountOfTicks = 60.0;
    double ns = 1000000000 / amountOfTicks;
    double delta = 0;
    long timer = System.currentTimeMillis(), lastRender;
    while (running) {
        long now = System.nanoTime();
        delta += (now - lastTime) / ns;
        lastTime = now;
        while (delta >= 1) {
            tick();
            delta--;
        }
        lastRender = System.currentTimeMillis();
        draw.render();
        draw.fps++;
        
        if (FPS_CAP != -1) {
            try {
                int nsToSleep = (int) ((1000 / FPS_CAP) - (System.currentTimeMillis() - lastRender));

                if (nsToSleep > 1 / FPS_CAP) {
                    Thread.sleep(nsToSleep);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        if (System.currentTimeMillis() - timer > 1000) {
            timer += 1000;
            draw.lastFPS = draw.fps;
            draw.fps = 0;
            // updates = 0;
        }
    }
}

The result is:

enter image description here

As you can see it's really not accurate, sometimes it's a lot lower than 60 and sometimes even higher!

I want it to be as precise as possible.

Thanks in advance.

David Gomes
  • 650
  • 2
  • 10
  • 34
  • 2
    Hmm might this be due to the fact that `System.currentTimeMillis` is not always very accurate? While it's time in ms, its resolution depends on the OS and it can increase in steps of 10 or 15 ms. At 60 fps, you're talking about 16.7 ms per frame so that might make a difference? – Arjan Jun 24 '16 at 00:59
  • 1
    Java is not precise enough to achieve such precision mechanisms. All rendering libraries actually rely on a C or C++ layer to manage real-time precision. – Guillaume F. Jun 24 '16 at 01:02
  • @Arjan It can't be because of that because when I cap it at 100 (which is 10ms per frame) isn't accurate too. – David Gomes Jun 24 '16 at 01:11
  • @GuillaumeF. Is there any workaround? – David Gomes Jun 24 '16 at 01:12
  • 2
    That's silly to suggest "don't use Java..." There are many Java gaming libraries out there, he should be pointed towards one that will work well for his needs. I'm also curious if anything in the new Java 8 Time Library could be of any use? https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html – XaolingBao Jun 24 '16 at 02:43
  • 1
    @XaolingBao I dont think java 8 time api will help. Problem is not the elapsed time, `System.nanoTime()` is precise enough to measure elapsed time. Problem is `Thread.sleep()` is not precise because it is a native method and most of time JNI is the bottleneck of java. – Onur Jun 24 '16 at 03:07
  • @Onur thanks, I figured I'd share it just in case, but it seems you figured it out. – XaolingBao Jun 24 '16 at 09:23
  • fwiw there's [a thread about this](http://www.java-gaming.org/index.php?topic=24220.0) on Java-Gaming.org, it seems the code in the accepted answer is from game loop code posted there by a developer named Eli Delventhal. There's more interesting stuff to be found on that site. – Arjan Jun 24 '16 at 12:50

3 Answers3

3

Java is not precise enough to achieve such precision mechanisms. All rendering libraries actually rely on a C or C++ layer to manage real-time precision.

In your case, the best workaround would be to avoid the use of Thread.sleep().

You can rely on Timer events instead to run the updates during the TimerTask. Your game will have a heartbeat approximately 60 times per second.

Another solution, if 60fps is good for you, is to wait for a VSync before rendering your screen. Most LCD screens are 60 or 120fps. The VSync should be managed by your graphical library (swing, JavaFX or other).

A last solution, you could look into the code of a specialized engine (like JMonkey) as a reference.

Guillaume F.
  • 5,905
  • 2
  • 31
  • 59
1

First of all I see you mixed System.currentTimeMilis() and System.nanoTime() not really a good idea, use only either one of them. Better only use System.nanoTime() since you are working on a high precision.

What's causing your issue is Thread.sleep() is not precise enough. So you need to avoid sleeping. Change your sleeping code to this;

lastRender = System.nanoTime(); //change it to nano instead milli
draw.render();
draw.fps++;

if (FPS_CAP > 0) {
    while ( now - lastRender < (1000000000 / FPS_CAP))
    {
        Thread.yield();

        //This stops the app from consuming all your CPU. It makes this slightly less accurate, but is worth it.
        //You can remove this line and it will still work (better), your CPU just climbs on certain OSes.
        //FYI on some OS's this can cause pretty bad stuttering. 
        try {Thread.sleep(1);} catch(Exception e) {}

        now = System.nanoTime();
    }
}

About how to enable VSYNC, your application need to be full screen and you should call Toolkit.sync() after every render.

Onur
  • 5,617
  • 3
  • 26
  • 35
  • Well I see that there's not a perfect solution so ok thanks, it is pretty accurate now and thank you to say that the sync methods to be fullscreen. – David Gomes Jun 24 '16 at 02:45
  • also put Thread.sleep(0, 100); so that it becomes more accurate and still doesn't eat much CPU. – David Gomes Jun 24 '16 at 02:52
  • `Thread.sleep(millis, nanos)` actually check's if `nanos` param bigger than 500000 and increases millis param accordingly. So you are actually calling `Thread.sleep(0);` :D Check source code at [Thread.java](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/Thread.java#Thread.sleep%28long%2Cint%29) Now the thing is `Thread.sleep(int)` is a [native method](http://stackoverflow.com/questions/6101311/what-is-the-native-keyword-in-java-for?lq=1). So calling `Thread.sleep(0);` is not sleeping but waiting for native call to complete. – Onur Jun 24 '16 at 03:04
  • @Onur that is not entirely correct. The relevant code is this, `if (nanos >= 500000 || (nanos != 0 && millis == 0)) {millis++;} sleep(millis);`. You're describing the condition left of the OR operator which is indeed `false`, but the right part is `true`. So `Thread.sleep(0, 100);` makes it also increments millis, and it's calling native `Thread.sleep(1)`. Which, @DavidKenz also means that it's not one bit more accurate than just calling `Thread.sleep(1);`. – Arjan Jun 24 '16 at 12:32
  • @Arjan Well it seems so I really won't change :p, but thanks for the info! – David Gomes Jun 24 '16 at 19:38
1

JavaFX is based upon a pulse mechanism.

A pulse is an event that indicates to the JavaFX scene graph that it is time to synchronize the state of the elements on the scene graph with Prism. A pulse is throttled at 60 frames per second (fps) maximum and is fired whenever animations are running on the scene graph. Even when animation is not running, a pulse is scheduled when something in the scene graph is changed. For example, if a position of a button is changed, a pulse is scheduled.

When a pulse is fired, the state of the elements on the scene graph is synchronized down to the rendering layer. A pulse enables application developers a way to handle events asynchronously. This important feature allows the system to batch and execute events on the pulse.

. . .

The Glass Windowing Toolkit is responsible for executing the pulse events. It uses the high-resolution native timers to make the execution.

To understand the implementation of the pulse mechanism, it is best to study the JavaFX source code. Some key classes are:

Timer.java WinTimer.java win/Timer.h win/Timer.cpp

The above links are for the generic Timer class and the window specific timer implementations. The JavaFX source code includes implementations for other platforms, such as OS X, GTK, iOS, Android, etc.

Some implementations (such as OS X it would seem) allow for vsync synchronization of the Timer implementation, others (such as Windows it would seem) do not allow for vsync synchronization. Though, these systems can get complicated, so I guess it is possible that, in some cases, vsync synchronization might be achieved via the hardware graphics pipeline rather than via the timer.

The windows native code is based upon a timeSetEvent call.

By default, the JavaFX framerate is capped at 60fps, though it is adjustable via undocumented properties.

If you are not using JavaFX (and it doesn't seem you are), you could still examine the JavaFX source code and learn about its implementation there in case you wanted to port any of the concepts or code for use in your application. You might also be able to shoehorn the JavaFX timer mechanism into a non-JavaFX application by making your application subclass the JavaFX Application class or creating a JFXPanel to initiate the JavaFX toolkit, then implementing your timer callback based upon AnimationTimer.

Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • I can't use the undocumented properties, it just doesn't work but it may be because it's undocumented right? – David Gomes Jun 24 '16 at 19:58
  • Could be, I guess support for them could be dropped at anytime. They have worked in the past for me. They are JavaFX specific, so they will only work in relation to a JavaFX application. – jewelsea Jun 24 '16 at 20:20