I'm a beginning game developer and I'm trying to make a game in Java, but for some reason, it keeps stuttering at random times. I believe what I'm experiencing could also be called microstuttering.
At first, I though my framerate of 60 fps was the problem, so I've tried setting it to 30 and 120. In both cases, however, did not only the microstutters persist, but the player also started moving shakingly, as if it were moving back and forth while moving in one direction, the same way pigeons move their heads when walking.
Next, I've tried changing my game loop. I've read that sleep based game loops are very unreliable, so I changed it to a timer based game loop. Unfortunately, that didn't work either. The microstutters still persisted, the player still moved shakingly and for some reason, my framerate also seemed to have halved.
I've been looking into this for a while now, but I haven't been able to fix the problem. Is there anyone with more experience who could help me, please?
Here's my game class for the sleep based loop:
package collisiondetection;
import java.applet.Applet;
import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.FileNotFoundException;
import java.net.URL;
@SuppressWarnings("serial")
public class Game extends Applet implements KeyListener, Runnable {
final boolean DEBUG = true;
public static int health = 13;
public static Player player;
public static Terrain terrain;
public boolean running = true;
public Graphics graphics;
public Line line;
public Image background, image;
public URL base;
@Override
public void init() {
setSize(1280, 720);
setBackground(Color.WHITE);
setFocusable(true);
addKeyListener(this);
Frame frame = (Frame) this.getParent().getParent();
frame.setTitle("Collision Detection");
base = getDocumentBase();
background = getImage(base, "data/background.png");
}
@Override
public void start() {
player = new Player();
try {
terrain = new Terrain();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Thread thread = new Thread(this);
thread.start();
}
@Override
public void stop() {
}
@Override
public void destroy() {
}
// fix lag
@Override
public void run() {
while (running) {
if (health <= 0)
running = false;
player.update();
terrain.update();
repaint();
try {
Thread.sleep(1000 / 60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void update(Graphics g) {
if (image == null) {
image = createImage(this.getWidth(), this.getHeight());
graphics = image.getGraphics();
}
graphics.setColor(getBackground());
graphics.fillRect(0, 0, getWidth(), getHeight());
graphics.setColor(getForeground());
paint(graphics);
g.drawImage(image, 0, 0, this);
}
@Override
public void paint(Graphics g) {
if (running) {
g.drawImage(background, 0, 0, this);
} else {
// handle death
}
if (DEBUG) {
g.setColor(Color.BLUE);
g.fillRect(player.hitbox.x, player.hitbox.y, player.hitbox.width, player.hitbox.height);
g.setColor(Color.RED);
for (int i = 0; i < terrain.lines.size(); i++) {
line = terrain.lines.get(i);
g.drawLine((int) line.line.getX1(), (int) line.line.getY1(), (int) line.line.getX2(),
(int) line.line.getY2());
}
}
}
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
player.moveLeft();
break;
case KeyEvent.VK_RIGHT:
player.moveRight();
break;
case KeyEvent.VK_UP:
player.jump();
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
player.stopLeft();
break;
case KeyEvent.VK_RIGHT:
player.stopRight();
break;
}
}
@Override
public void keyTyped(KeyEvent e) {
}
public static Player getPlayer() {
return player;
}
public static Terrain getTerrain() {
return terrain;
}
}
And my game class for the timer based loop:
package collisiondetection;
import java.applet.Applet;
import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.FileNotFoundException;
import java.net.URL;
import java.util.Timer;
import java.util.TimerTask;
@SuppressWarnings("serial")
public class Game extends Applet implements KeyListener {
final boolean DEBUG = true;
public static int health = 13;
public static Player player;
public static Terrain terrain;
public boolean running = true;
public Graphics graphics;
public Line line;
public Image background, image;
public Timer timer;
public URL base;
@Override
public void init() {
setSize(1280, 720);
setBackground(Color.WHITE);
setFocusable(true);
addKeyListener(this);
Frame frame = (Frame) this.getParent().getParent();
frame.setTitle("Collision Detection");
base = getDocumentBase();
background = getImage(base, "data/background.png");
}
@Override
public void start() {
player = new Player();
try {
terrain = new Terrain();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
timer = new Timer();
timer.schedule(new TimerTask() {
// fix lag
@Override
public void run() {
if (health <= 0) {
running = false;
timer.cancel();
}
player.update();
terrain.update();
repaint();
}
}, 0, (long) (1000.0 / 60.0));
}
@Override
public void stop() {
}
@Override
public void destroy() {
}
@Override
public void update(Graphics g) {
if (image == null) {
image = createImage(this.getWidth(), this.getHeight());
graphics = image.getGraphics();
}
graphics.setColor(getBackground());
graphics.fillRect(0, 0, getWidth(), getHeight());
graphics.setColor(getForeground());
paint(graphics);
g.drawImage(image, 0, 0, this);
}
@Override
public void paint(Graphics g) {
if (running) {
g.drawImage(background, 0, 0, this);
} else {
// handle death
}
if (DEBUG) {
g.setColor(Color.BLUE);
g.fillRect(player.hitbox.x, player.hitbox.y, player.hitbox.width, player.hitbox.height);
g.setColor(Color.RED);
for (int i = 0; i < terrain.lines.size(); i++) {
line = terrain.lines.get(i);
g.drawLine((int) line.line.getX1(), (int) line.line.getY1(), (int) line.line.getX2(),
(int) line.line.getY2());
}
}
}
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
player.moveLeft();
break;
case KeyEvent.VK_RIGHT:
player.moveRight();
break;
case KeyEvent.VK_UP:
player.jump();
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
player.stopLeft();
break;
case KeyEvent.VK_RIGHT:
player.stopRight();
break;
}
}
@Override
public void keyTyped(KeyEvent e) {
}
public static Player getPlayer() {
return player;
}
public static Terrain getTerrain() {
return terrain;
}
}
If you want to import the game in Eclipse to see how it works for yourself, you can download both the sleep and timer based projects from here.
Thanks in advance.
System.nanoTime
to calculate how much time has passed since the last iteration of the loop. I understand that this would be a more reliable option, but it would also cause the host device to work constantly at maximum power, since the thread wouldn't sleep. Any suggestions on how to work this out in a more battery friendly way? \$\endgroup\$player.update()
? Are you applying a fixed movement every time, or are you checking how much time has actually passed and moving the right distance based on that? \$\endgroup\$