1

I'm making a small asteroids game, and I'm having some trouble controlling the animation speed.

For example, let's say I have 20 asteroids in my game, when I destroy an asteroid, the amount of asteroids goes down (obviously). Because there are fewer objects in the game, the fps goes up and the animation speed of the asteroids is getting faster and faster.

I fixed it by adjusting the animation speed according to the amount of asteroids I have in the game, but I'm also facing another problem with the explosions when I destroy an asteroid. I could do the same thing I did with the asteroids I suppose, but I just think it's not a very wise way to "solve" it and just seems like bad practice to me.

I thought of capping the fps, but I'm not really sure how to do it. I'd like to get some advices and what's the best way to deal with such situations.

I'll post here my main game class including the game loop, and an example of the explosion class so you'll get the general idea of the code.

Game class and loop:

import com.asteroids.view.*;

public class Game extends Canvas implements Runnable {

private static final long serialVersionUID = -8921419424614180143L;
public static final int WIDTH = 1152, HEIGHT = WIDTH / 8 * 5;

private Thread thread;
private boolean isRunning;
private LoadImages loadImages = new LoadImages();
private Player player = new Player();
private AllObjects objects;
private KeyInput keyInput;
private long delay = 80;
private long currentTime = System.currentTimeMillis();
private long expectedTime = currentTime + delay;
public static BufferedImage test;

public Game() {
    new Window(WIDTH, HEIGHT, "Asteroids!", this);
    objects = new AllObjects();
    objects.addObject(player);
    for (int i = 0; i < 20; i++) {
        objects.addObject(new Rock((int) (Math.random() * (Game.WIDTH - 64) + 1),
                (int) (Math.random() * (Game.HEIGHT - 64) + 1)));
    }
    keyInput = new KeyInput(player);
    this.addKeyListener(keyInput);
}

public void run() {
    this.requestFocus();
    long lastTime = System.nanoTime();
    double amountOfTicks = 60.0;
    double ns = 1000000000 / amountOfTicks;
    double delta = 0;
    long timer = System.currentTimeMillis();
    int frames = 0;

    // main game loop.
    while (isRunning) {
        adjustAsteroidsSpeed();
        destroyAsteroids();
        collisionLoop();

        // used to set delay between every bullet(milliseconds)
        currentTime = System.currentTimeMillis();
        if (KeyInput.shoot && currentTime >= expectedTime) {

            // calculates the accurate position of the x,y on the "circumference" of the
            // player
            float matchedX = player.getX() + 1 + (float) ((player.getRadius() + 32) * Math.cos(player.getRadian()));
            float matchedY = player.getY() - 7 + (float) ((player.getRadius() + 32) * Math.sin(player.getRadian()));
            objects.addObject(new Bullet(matchedX, matchedY, player));
            expectedTime = currentTime + delay;
        }
        destroyBullets();
        long now = System.nanoTime();
        delta += (now - lastTime) / ns;
        lastTime = now;
        while (delta >= 1) {
            tick();
            delta--;
        }
        if (isRunning)
            render();
        frames++;
        if (System.currentTimeMillis() - timer > 1000) {
            timer += 1000;
            System.out.println("FPS: " + frames);
            frames = 0;
        }
    }

    render();

    stop();
    System.exit(1);

}

private void stop() {
    try {
        thread.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.exit(1);

}

private void render() {
    BufferStrategy bs = this.getBufferStrategy();
    if (bs == null) {
        this.createBufferStrategy(3);
        return;
    }

    Graphics g = bs.getDrawGraphics();
    g.drawImage(LoadImages.getbackground(), 0, 0, getWidth(), getHeight(), this);
    objects.render(g);
    player.render(g);
    g.dispose();
    bs.show();

}

private void tick() {
    player.tick();
    objects.tick();
}

// starting thread and game loop.
public void start() {
    thread = new Thread(this);
    thread.start();
    isRunning = true;
}

// minimum and maximum possible position for object.
public static float Bounds(float value, float min, float max) {
    if (value >= max) {
        return value = max;
    }
    if (value <= min) {
        return value = min;
    } else {
        return value;
    }

}

// detects collision between two objects
public boolean collision(GameObject a, GameObject b) {
    return (b.getX() - a.getX() + 10) * (b.getX() - a.getX() + 10)
            + (b.getY() - a.getY() + 10) * (b.getY() - a.getY() + 10) < (a.getRadius() + b.getRadius())
                    * (a.getRadius() + b.getRadius());
}

// destroys bullets once they go out of the screen
public void destroyBullets() {
    for (int i = 0; i < objects.getSize(); i++) {
        if (objects.get(i).getId() == ID.BULLET) {
            GameObject bullet = objects.get(i);
            if (bullet.getX() > Game.WIDTH || bullet.getX() < 0 || bullet.getY() > Game.HEIGHT
                    || bullet.getY() < 0) {
                objects.removeObject(bullet);
            }
        }
    }
}

// whenever a collision between an asteroid and a bullet occurs, the asteroid and the bullets are destroyed
public void destroyAsteroids() {
    GameObject bullet = null;
    GameObject bigRock = null;
    for (int i = 0; i < objects.getSize(); i++) {
        if (objects.get(i).getId() == ID.BULLET) {
            bullet = (Bullet) objects.get(i);
            for (int q = 0; q < objects.getSize(); q++) {
                if (objects.get(q).getId() == ID.BIGROCK) {
                    bigRock = objects.get(q);
                    if (bullet != null && bigRock != null) {
                        if (collision(bigRock, bullet)) {
                            objects.addObject(new Explosion(bigRock.getX(), bigRock.getY(), objects));
                            objects.removeObject(bigRock);
                            objects.removeObject(bullet);
                        }
                    }
                }
            }
        }
    }
}

// calculates the amount of asteroids in the game and adjust the asteroids speed
public void adjustAsteroidsSpeed() {
    int rocksCount = 0;
    Rock rock;
    for (GameObject object : objects.link()) {
        if (object.getId() == ID.BIGROCK) {
            rocksCount++;
        }
    }
    for (GameObject object : objects.link()) {
        if (object.getId() == ID.BIGROCK) {
            rock = (Rock) object;
            rock.setAnimSpeed(rocksCount * 0.002f);
        }
    }
 }

Explosion class:

package com.asteroids.model;

import java.awt.Graphics;
import java.awt.Image;

import com.asteroids.controller.*;
import com.asteroids.view.LoadImages;


public class Explosion extends GameObject {

private AllObjects objects;
private Image explosion;
private float frame = 0;
private float animSpeed = 0.09f;
private int frameCount = 48;

public Explosion(float x, float y, AllObjects objects) {
    super(x, y, ID.EXPLOSION, 1);
    this.objects = objects;
}

public void render(Graphics g) {
    explosion(g);
}

public void explosion(Graphics g) {
    frame += animSpeed;
    if (frame > frameCount) {
        frame -= frameCount;
    }
    explosion = LoadImages.getExplosion().getSubimage((int) frame * 256, 0, 256, 256);
    g.drawImage(explosion, (int) x, (int) y, 110, 110, null);
    if (frame >= 47.8f) {
        objects.removeObject(this);
    }
}

public void tick() {
    
}

public void setAnimSpeed(float animSpeed) {
    this.animSpeed = animSpeed;
}
}
mkrieger1
  • 19,194
  • 5
  • 54
  • 65
msacco
  • 152
  • 2
  • 13
  • Your game loop timing seems to be wrong (I'm pretty sure it should't be producing 10's of thousands of frames a second), this is producing an uneven amount of updates. You need to devise a solution which tries to aim for a specific number of "renders" per second and tries to do so evenly – MadProgrammer Jun 10 '18 at 23:26
  • I just read about timestep in this article : https://gafferongames.com/post/fix_your_timestep/ but Im not really sure which one I'd want for my situation and why. Do you know anything about it and able to guide me to which one I should look for? Thanks. – msacco Jun 10 '18 at 23:31

1 Answers1

2

Your main loop is generating uneven updates. If I do nothing, I get anywhere between 7799913 and 8284754 fps, however, if I throw in a 8 millisecond delay (to simulate some work), it drops to around 115-120 fps.

Your intention is to try and get the frame rate to be as even as possible, this will ensure that the animation speed remains the same

Personally, I don't like the "free-wheeling" style of game loop, it means that the loop is been allowed to consume CPU cycles without actually doing anything, where those cycles could be been used to do more important work, like update the UI.

In most cases, I just use a Swing Timer set to something like 5 millisecond intervals and then make use of the date/time API to calculate the difference between now and the last update and make choices about what to do, but, this assumes you're using a Swing based painting path. If you're doing a direct painting path (ie BufferStrategy), you could use a similar idea with a "loop" instead...

public void run() throws InterruptedException {

    int frames = 0;
    Duration threashold = Duration.ofMillis(1000 / 59);
    Duration cycle = Duration.ofSeconds(1);

    Instant cycleStart = Instant.now();

    // main game loop.
    while (isRunning) {
        Instant start = Instant.now();
        // Some update function...

        Thread.sleep(rnd.nextInt(32));

        Duration processTime = Duration.between(start, Instant.now());
        Duration remainingTime = threashold.minusMillis(processTime.toMillis());
        long delay = remainingTime.toMillis();
        if (delay > 0) {
            Thread.sleep(delay);
        } else {
            System.out.println("Dropped frame");
        }

        frames++;
        // Render the output

        Duration cycleTime = Duration.between(cycleStart, Instant.now());
        if (cycleTime.compareTo(cycle) >= 0) {
            cycleStart = Instant.now();
            System.out.println(frames);
            frames = 0;
        }
    }

}

In this example, your update and paint scheduling code simply have 16 milliseconds to get there job done, otherwise it will drop frames. If the work takes less then 16 milliseconds, the loop will "wait" the remaining time in order to provide some breathing room for the CPU to give time to other threads (and not take update unnecessary time on the CPU)

In the example above, I generate a "random" delay of up to 32 milliseconds for testing. Set it back to 16 and you should get (roughly) 60fps.

Now, I know people are extraordinarily passionate about these things, so if using Thread.sleep and Duration make your skin crawl, you "could" use a "free wheeling" loop, something like the one presented in Java Main Game Loop

Below is a sample implementation, I've set the number of updates and frames per second to 60, but you can change those values to suit your needs...

public void run() throws InterruptedException {

    double ups = 60;
    double fps = 60;

    long initialTime = System.nanoTime();
    final double timeU = 1000000000 / ups;
    final double timeF = 1000000000 / fps;
    double deltaU = 0, deltaF = 0;
    int frames = 0, ticks = 0;
    long timer = System.currentTimeMillis();

    while (isRunning) {

        long currentTime = System.nanoTime();
        deltaU += (currentTime - initialTime) / timeU;
        deltaF += (currentTime - initialTime) / timeF;
        initialTime = currentTime;

        if (deltaU >= 1) {
            Thread.sleep(rnd.nextInt(32));
            //getInput();   
            //update();
            ticks++;
            deltaU--;
        }

        if (deltaF >= 1) {
            Thread.sleep(rnd.nextInt(32));
            //render();
            frames++;
            deltaF--;
        }

        if (System.currentTimeMillis() - timer > 1000) {
            System.out.println(String.format("UPS: %s, FPS: %s", ticks, frames));
            frames = 0;
            ticks = 0;
            timer += 1000;
        }
    }
}

Again, the Thread.sleep here is just to inject a random amount of "work". Because it allows for more then 16ms of delay, you will also find it "drops" frames. Your job would be to get you work down to under 16ms per pass

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • First thanks a lot for the detailed answer, really appreciate it! I realized that the problem is really with the game loop generating unsteady fps, and I started looking for some gameloops etc, I will definitely try to use what you gave and see how it works, generally, Im currently reading an article about game loops here: http://www.java-gaming.org/index.php?topic=24220.0 and he said that using timers and thread.sleep is not good, but I suppose there are different needs for every game, so I'll just try it and see how it works, thanks again. – msacco Jun 10 '18 at 23:57
  • The problem with timer's and `Thread.sleep` is they only guarantee at least the specified amount of time, that is, they can sleep/take longer then you've requested, but free wheeling a loop is also departmental, so it's trade off – MadProgrammer Jun 10 '18 at 23:59
  • I tried using your gameloop and another game loop from the article I posted before, both of them works just well, and I do not feel any difference between them, so first of all thanks for the help, it worked and solved my problem, my question is if you could take a look at the other gameloop, and tell me which one is better in your opinion, and why. This is the gameloop: https://pastebin.com/jwj65cQ4, thanks :] – msacco Jun 11 '18 at 01:09
  • My opinions not worth squat ;) - I still don't like the free wheeling loops (although `Thread.yield` is a nice touch). The `while (now - lastUpdateTime > TIME_BETWEEN_UPDATES && updateCount < MAX_UPDATES_BEFORE_RENDER) {` is consuming thread cycles without actually doing any work, so it's kind of wasting resources; the `if (!paused) {` could be done with a lock object instead, which would put the thread to sleep, again, not wasting CPU cycles, but the basic intention is still the same, even out the fps, so I'd lean towards the one which you can best reason about – MadProgrammer Jun 11 '18 at 01:20
  • @MadProgrammmer hey, Im actually facing another problem now, its not as crucial, but for example if I destroy many asteroids at in a short period of time, the game itself starts to run slower, the thing is, the fps is still constant(I've set it to 144 fps, cause I have 144 hz screen, but even with 60, or higher than 144, the result is the same), no matter what fps I set, it stays constant, but the game itself lags, what could cause that? will multithreading solve that? – msacco Jun 11 '18 at 12:11
  • 1
    If the FPS are remaining the same, then the game loop is running fine, it will most likely be in the update/rendering pass. You could also be suffering from a GC overhead (especially if you're creating/destroying lost of objects). I had a similar issue with [another question](https://stackoverflow.com/questions/14886232/swing-animation-running-extremely-slow/14902184#14902184) which was solved by using a object cache to cache re-usable objects. If that doesn't help, then you need to investigate you update/collision/rendering code to further see where the slow down might be – MadProgrammer Jun 11 '18 at 20:14
  • I was actually totally wrong, the problem is indeed fps drops, I forgot to change something back after changing it, and it showed a wrong fps counter. Can you elaborate a bit more about the cache thing? someone else told me that as well, but I couldn't really find anything in google regarding that, can you point me to where exactly it is used in the link you showed? and just generally explain about it a bit? Thanks. – msacco Jun 12 '18 at 00:05
  • 1
    You have a bunch or asteroids which come and go. Rather then creating new asteroids every time you need them, you simply remove from the "active" (on screen) list and place them in a "cache" list. When you need another astroid, you pull it from this cache and put it back in the active list - if nothing is in the cache, you create a new one. The idea is to reduce the GC (garbage collection) overhead, which can produce a slow down in your system – MadProgrammer Jun 12 '18 at 00:08
  • Ohhh I see, well its actually a problem with the explosions more than the asteroids, as I just destroy asteroids, but Im adding explosions each time an asteroid is destroyed, so yeah I might try that, ty :] – msacco Jun 12 '18 at 00:40