1

I've posted a video on Youtube, where you can see the problem. I've slowed the video down, so you really can see the jumps.

LINK: https://www.youtube.com/watch?v=17Wftj2-MRM&feature=youtu.be

The problem is most visible in the last part of the video. You can clearly see the frame jumps, it's like it skipped a frame or two..

I have none problems with lag or performance, as the computer i'm running the games on got the latest tech..

I thought it may had something to do with the game loop, like a rounding error or something, but I can't seem to find any bug in the code.. I've looked for a similar problem online, but can't find it..

So here's my code The GUI

import java.awt.Container;
import java.awt.Graphics2D;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;

public class Gui extends JFrame implements Runnable {

private static final long serialVersionUID = 1L;
public static final int FRAMES_PR_SEC = 60;
public static final int TIME_PR_FRAME = 1000 / FRAMES_PR_SEC;
public static final int MARGIN_X = 3;
public static final int MARGIN_Y = 32;
public static final int WIDTH = 800;
public static final int HEIGHT = 600;
public static int TILE_SIZE = 32;
public static int SPEED = 5;

public static void main(final String[] args) {
    new Gui();
}

private Game _game;

private Container _cp;
private BufferedImage _img;

private Graphics2D _g;

public Gui() {
    super("Tile Based Game");
    setSize(WIDTH + MARGIN_X + 3, HEIGHT + MARGIN_Y - 4);
    setResizable(false);
    setVisible(true);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    _cp = getContentPane();
    _game = new Game();
    newGraphics();
    addMouseListener(new MouseAdapter() {

        @Override
        public void mousePressed(final MouseEvent arg0) {
            _game.mP(arg0.getX() - MARGIN_X, arg0.getY() - MARGIN_Y);
        }

        @Override
        public void mouseReleased(final MouseEvent arg0) {
            _game.mR(arg0.getX() - MARGIN_X, arg0.getY() - MARGIN_Y);
        }
    });
    addKeyListener(new KeyAdapter() {

        @Override
        public void keyPressed(final KeyEvent arg0) {
            _game.keyP(arg0.getKeyCode());
        }

        @Override
        public void keyReleased(final KeyEvent arg0) {
            _game.keyR(arg0.getKeyCode());
        }
    });
    new Thread(this).start();
}

private void newGraphics() {
    _img = getGraphicsConfiguration().createCompatibleImage(WIDTH, HEIGHT);
    _g = _img.createGraphics();
}

@Override
public void run() {
    while (true) {
        long before = System.currentTimeMillis();
        _game.update();
        newGraphics();
        _game.draw(_g);
        _cp.getGraphics().drawImage(_img, 0, 0, null);
        long after = System.currentTimeMillis();
        long sleep = TIME_PR_FRAME - (after - before);
        System.out.println(sleep);
        if (sleep < 5) {
            sleep = 5;
        }
        try {
            Thread.sleep(sleep);
        } catch (InterruptedException e) {
        }
    }
}
}

I suppose that everything you will need is in the GUI class, but I've included the other classes in case you need anything from them.

The Game class

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;

public class Game {
private Map[] _maps;
private String[] _mapNames;
private int _currentMap;

private boolean _r;
private boolean _l;
private boolean _u;
private boolean _d;

public Game() {
    _mapNames = new String[0];
    _maps = new Map[0];
    addMap("test.map");

}

public void addMap(final String s) {
    String[] stringTemp = _mapNames;
    _mapNames = new String[stringTemp.length + 1];
    for (int i = 0; i < stringTemp.length; i++) {
        _mapNames[i] = stringTemp[i];
    }
    _mapNames[_mapNames.length - 1] = s.replace(".map", "");
    Map[] mapTemp = _maps;
    _maps = new Map[mapTemp.length + 1];
    for (int i = 0; i < mapTemp.length; i++) {
        _maps[i] = mapTemp[i];
    }
    _maps[_maps.length - 1] = new Map(s);
}

public void draw(final Graphics2D _g) {
    _maps[_currentMap].draw(_g);
    _g.setColor(Color.WHITE);
    _g.drawString(_mapNames[_currentMap], 5, 595);
}

public void keyP(final int keyCode) {
    if (keyCode == KeyEvent.VK_RIGHT) {
        _r = true;
    }
    if (keyCode == KeyEvent.VK_LEFT) {
        _l = true;
    }

    if (keyCode == KeyEvent.VK_UP) {
        _u = true;
    }
    if (keyCode == KeyEvent.VK_DOWN) {
        _d = true;
    }
}

public void keyR(final int keyCode) {
    if (keyCode == KeyEvent.VK_SPACE) {
        if (_currentMap < _maps.length - 1) {
            _currentMap++;
        } else {
            _currentMap = 0;
        }
    }
    if (keyCode == KeyEvent.VK_RIGHT) {
        _r = false;
    }
    if (keyCode == KeyEvent.VK_LEFT) {
        _l = false;
    }

    if (keyCode == KeyEvent.VK_UP) {
        _u = false;
    }
    if (keyCode == KeyEvent.VK_DOWN) {
        _d = false;
    }
}

public void mP(final int i, final int j) {

}

public void mR(final int i, final int j) {

}

public void update() {

    if (_r) {
        _maps[_currentMap].incOffsetX(Gui.SPEED);
    }
    if (_l) {
        _maps[_currentMap].decOffsetX(Gui.SPEED);
    }
    if (_u) {
        _maps[_currentMap].decOffsetY(Gui.SPEED);
    }
    if (_d) {
        _maps[_currentMap].incOffsetY(Gui.SPEED);
    }
    _maps[_currentMap].update();
}
}

And finally the Map class (handles loading of .map files and such)

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

import javax.imageio.ImageIO;

public class Map {

private class Tile {
    private Image _img;
    private int _x;
    private int _y;

    public Tile(final Image img) {
        _img = img;
    }

    public Image getImg() {
        return _img;
    }

    public int getX() {
        return _x;
    }

    public int getY() {
        return _y;
    }

    public void setLoc(final int x, final int y) {
        _x = x * Gui.TILE_SIZE;
        _y = y * Gui.TILE_SIZE;
    }
}

private int _offsetX;
private int _offsetY;

private String[][] _map;

private Tile[][] _tileMap;

private BufferedImage _terrain;

private Color _waterColor;

public Map(final String s) {
    _terrain = loadImg("terrain.png");

    _waterColor = new Color(21, 108, 153);

    _map = loadMap(s);

    _offsetX = _map[0].length * Gui.TILE_SIZE / 2 - Gui.WIDTH / 2;
    _offsetY = _map.length * Gui.TILE_SIZE / 2 - Gui.HEIGHT / 2;

    _tileMap = new Tile[_map.length][_map[0].length];

    for (int i = 0; _map.length > i; i++) {
        for (int j = 0; _map[0].length > j; j++) {
            Image img;
            switch (_map[i][j]) {
            case "o":
                img = getSubimage(_terrain, 0, 0, 3, 6);
                break;
            case "0":
                img = getSubimage(_terrain, 0, 1, 3, 6);
                break;
            case "a":
                img = getSubimage(_terrain, 1, 0, 3, 6);
                break;
            case "b":
                img = getSubimage(_terrain, 2, 0, 3, 6);
                break;
            case "c":
                img = getSubimage(_terrain, 1, 1, 3, 6);
                break;
            case "d":
                img = getSubimage(_terrain, 2, 1, 3, 6);
                break;
            case "1":
                img = getSubimage(_terrain, 0, 2, 3, 6);
                break;
            case "2":
                img = getSubimage(_terrain, 1, 2, 3, 6);
                break;
            case "3":
                img = getSubimage(_terrain, 2, 2, 3, 6);
                break;
            case "4":
                img = getSubimage(_terrain, 0, 3, 3, 6);
                break;
            case "5":
                img = getSubimage(_terrain, 1, 3, 3, 6);
                break;
            case "6":
                img = getSubimage(_terrain, 2, 3, 3, 6);
                break;
            case "7":
                img = getSubimage(_terrain, 0, 4, 3, 6);
                break;
            case "8":
                img = getSubimage(_terrain, 1, 4, 3, 6);
                break;
            case "9":
                img = getSubimage(_terrain, 2, 4, 3, 6);
                break;
            case "#":
                img = getSubimage(_terrain, 0, 5, 3, 6);
                break;
            case "&":
                img = getSubimage(_terrain, 1, 5, 3, 6);
                break;
            case "%":
                img = getSubimage(_terrain, 2, 5, 3, 6);
                break;
            default:
                img = null;
                break;
            }
            Tile tile = new Tile(img);
            int x = j;
            int y = i;
            tile.setLoc(x, y);
            _tileMap[i][j] = tile;
        }
    }
}

public void decOffsetX(final int i) {
    _offsetX -= i;
}

public void decOffsetY(final int i) {
    _offsetY -= i;
}

public void draw(final Graphics2D _g) {
    for (int i = 0; _tileMap.length > i; i++) {
        for (int j = 0; _tileMap[0].length > j; j++) {
            if (_tileMap[i][j].getImg() != null
                    && isVisible(_tileMap[i][j])) {
                _g.drawImage(_tileMap[i][j].getImg(), _tileMap[i][j].getX()
                        - _offsetX, _tileMap[i][j].getY() - _offsetY, null);
            } else if (_tileMap[i][j].getImg() == null
                    && isVisible(_tileMap[i][j])) {
                _g.setColor(_waterColor);
                _g.fillRect(_tileMap[i][j].getX() - _offsetX,
                        _tileMap[i][j].getY() - _offsetY, Gui.TILE_SIZE,
                        Gui.TILE_SIZE);
            }
        }
    }
}

private Image getSubimage(final BufferedImage img, final int x,
        final int y, final int w, final int h) {
    return img.getSubimage(x * Gui.TILE_SIZE, y * Gui.TILE_SIZE,
            Gui.TILE_SIZE, Gui.TILE_SIZE);

}

public void incOffsetX(final int i) {
    _offsetX += i;
}

public void incOffsetY(final int i) {
    _offsetY += i;
}

public boolean isVisible(final Tile tile) {
    if (tile.getX() - _offsetX < 0 - Gui.TILE_SIZE) {
        return false;
    } else if (tile.getX() - _offsetX > Gui.WIDTH) {
        return false;
    }
    if (tile.getY() - _offsetY < 0 - Gui.TILE_SIZE) {
        return false;
    } else if (tile.getY() - _offsetY > Gui.HEIGHT) {
        return false;
    }
    return true;
}

private BufferedImage loadImg(final String s) {
    BufferedImage img = null;
    try {
        img = ImageIO.read(new File("res/" + s));
    } catch (IOException e) {
    }
    return img;
}

private String[][] loadMap(final String s) {
    String _s = null;
    String string = null;
    int appear = 0;
    try {
        BufferedReader br;
        br = new BufferedReader(new FileReader("res/maps/" + s));
        while ((_s = br.readLine()) != null) {
            string = string + _s + ",";
            appear++;
        }
        br.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    String[] ss = string.replaceAll("null", "").split(",");
    String[][] items = new String[appear][];
    for (int i = 0; i < items.length; i++) {
        items[i] = ss[i].split(" ");
    }

    return items;
}

public void update() {
    if (_offsetX < 0) {
        _offsetX += Gui.SPEED;
    } else if (_offsetX > _map[0].length * Gui.TILE_SIZE - Gui.WIDTH
            - Gui.SPEED) {
        _offsetX -= Gui.SPEED;
    }
    if (_offsetY < 0) {
        _offsetY += Gui.SPEED;
    } else if (_offsetY > _map.length * Gui.TILE_SIZE - Gui.HEIGHT
            - Gui.SPEED) {
        _offsetY -= Gui.SPEED;
    }
}
}

I'm aware that there probably is a some bugs in the code, and everything is probably not optimal, but this project is VERY early development, and I'm a very inexperienced game developer.. I would appreciate if you would focus on the posted problem, and not my other crap code. :-D

I would really like if you would comment, if you have any suggestions :-D Thank you

EDIT: I updated the post, as the problem was unclear. So i uploaded a video, where you can view the bug. I've also cleared some other things :-)

EDIT 2: Made a new thread on gamedev.stackexchange. It may help you solve the problem: https://gamedev.stackexchange.com/questions/77998/java-game-loop-frame-jumps

SOLUTION: After a ton of research i came to the conclusion, that it had something to do with the screens refresh rate, and my loops refresh rate.. My screen was simply skipping frames, which was very noticeable!

If you want a indepth solution, google made a video, where they described the problem: https://www.youtube.com/watch?v=hAzhayTnhEI

There are two ways (according to my research) to get rid of this problem:

Either you have to enable VSync through OpenGL, as java doesn't have the option to do so..

Or you have to use a mixture of swing and awt in java (the option i chose).. Here's a thread describing how to do: Smooth Drawing using Java2d without the Opengl or Direct3d Pipelines?

Thank you for all responses!

Community
  • 1
  • 1
  • 5
    "Java is very RAM-heavy" -_- * facepalm * This isn't 2003 – Anubian Noob Jun 10 '14 at 19:13
  • You've posted too much code without accurately describing or explaining the problem. It could be as simple as jitter, or a brief stall in your game loop. It could be something more. It's unlikely anyone will wade through all that code to debug it for you though... – Tim B Jun 10 '14 at 19:17
  • 2
    Please remove code which is un-relevant to the problem itself – DAG Jun 10 '14 at 19:26
  • I'd guess the issue is in with your sleep, it seems like the longer it takes to draw the longer you sleep after? Also, I'm not sure what your img.getSubImage method does, but if this is loading from file, you should probably be caching these so they are only loaded the first time, and cached otherwise. – Zack Newsham Jun 10 '14 at 19:54
  • 3
    You're sleeping on the EDT, that's not good. – BitNinja Jun 10 '14 at 19:54

1 Answers1

0

First of all, an idea is to look into BufferStrategy as a mechanism to create a more robust and performant game loop. Secondly, your game loop is sleeping on varying values dependent on how long your update/rendering takes. This is fine, but your update method should reflect this as well, and not move things around with constant speed. You'll want your entities to move relative to the delta time elapsed since last loop cycle (lastTime - currentTime).

Some of your constants shouldn't be ints. TIME_PR_FRAME for instance, should not really be 16, but 16.67, so make this a double, or at least a float. Your if(sleep < 5) sleep = 5; code makes no sense. If rendering and updating takes very long, you shouldn't waste time sleeping, basically you are deliberately or not introducing jittering in these cases.

Did you debug the code to see what the average values of the sleep variable are, and whether it spikes or not? I'd also do that, if not.

Terje
  • 1,753
  • 10
  • 13
  • Hi!Thanks a lot for the response. I've made a couple of updates on my thread, check them out, it may help you see the problem. :-) I've never heard of BufferStrategy, and have no idea how to move the entities relative to the delta. Could you give me a code example or a tutorial that explains you're method in-depth? That would really be nice.. ;-) I'll correct the INT problem. The if(sleep < 5) sleep = 5; thing, is to avoid the game crashing, as the first frame always get a negative value on "sleep". Could be a bug too? I did debug the program and got a very stable result (sleep~12). Thanks – user3727336 Jun 14 '14 at 23:05