Animação básica do jogo em 2D em Java stutter

Então, eu estou trabalhando em um rpg 2D há algum tempo e não consigo resolver esse problema. Os gráficos parecem "pular" ou gaguejar a cada poucos segundos por um motivo desconhecido. Isso está ficando muito chato, porque eu não sei o que está causando isso.

Aqui está um programa muito básico que escrevi que tem apenas um quadrado vermelho que se move de um lado da tela para o outro lado. Mesmo neste programa muito básico, a praça ainda gagueja a cada poucas atualizações e eu realmente não consigo descobrir isso por toda a minha vida.

public class Main extends JPanel {

int x=0, y=0;

public JFrame window = new JFrame("Window");

public Main(){
    window.setSize(1000, 500);
    window.setVisible(true);
    window.add(this);
}

public void paintComponent(Graphics g){
    super.paintComponent(g);
    g.setColor(Color.red);
    g.fillRect(x, y, 500, 500);
    x+=3;
    if(x>900){
        x=0;
    }
}

public void start(){
    while(true){
        repaint();
        try {
            Thread.sleep(16);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public static void main(String[] args){
    Main game = new Main();

    game.start();
}

}

Se você executar a classe, verá qual é o problema graficamente. Obviamente, meu jogo é composto de muito mais classes e é muito mais complexo que isso, mas o mesmo princípio se aplica. Se alguém tiver alguma idéia do meu problema, eu adoraria ouvi-lo. Desde já, obrigado.

Atualizada

Aqui estão minhas duas classes principais:

Classe principal:
pacote com.ultimatum.Main;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.JPanel;

import com.ultimatum.Mangers.ImageLoader;
import com.ultimatum.Mangers.KeyStates;
import com.ultimatum.Mangers.ScreenUpdater;
import com.ultimatum.Mangers.UserInput;

@SuppressWarnings("serial")
public class Ultimatum extends JPanel {

    /**
     * @param x This is the start width of the screen and can be adjusted manually, but will not go any lower than this integer
     * @param y This is the start height of the screen and can be adjusted manually, but will not go any lower than this integer
     * @param contentPlaneX This is how much the width of the content plane is (Frame-Borders)
     * @param contentPlaneY This is how much the height of the content plane is (Frame-Borders)
     */
    public int x=850, y=610, contentPlaneX, contentPlaneY, middleX, middleY, tileSize=90;

    public Dimension minimumSize = new Dimension(x, y);

    public JFrame window = new JFrame("Ultimatum");//This makes the JFrame for the game
    public KeyStates keyStates = new KeyStates();
    public UserInput input = new UserInput(keyStates);
    public ImageLoader imageLoader = new ImageLoader();
    public static Ultimatum ultimatum;//Makes the object of this class
    public static ScreenUpdater su;//This is creating the object that is going to be making changes to the screen. For example, the animation.
    private BufferedImage screenImage;

    public boolean isWindowInFullscreenMode;

    private boolean imagesLoaded;

    public void initializeUltimatum(){
        toWindowedMode();

        addMouseListener(input);
        addMouseMotionListener(input);

        contentPlaneX=window.getContentPane().getWidth();
        contentPlaneY=window.getContentPane().getHeight();
        middleX=(int)contentPlaneX/2;
        middleY=(int)contentPlaneY/2;
        su = new ScreenUpdater(ultimatum, keyStates, imageLoader, "Test", tileSize);

        imageLoader.loadImages();
    }

    public void toFullscreenMode(){
        window.dispose();
        window.setUndecorated(true);
        window.setVisible(true);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setBounds(0,0,Toolkit.getDefaultToolkit().getScreenSize().width,Toolkit.getDefaultToolkit().getScreenSize().height);
        addListenersAndClassToWindow();
        isWindowInFullscreenMode=true;
    }

    public void toWindowedMode(){
        window.dispose();
        window.setUndecorated(false);
        window.setSize(x,y);
        window.setMinimumSize(minimumSize);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setVisible(true);
        window.setLocationRelativeTo(null);
        addListenersAndClassToWindow();
        isWindowInFullscreenMode=false;
    }

    public void addListenersAndClassToWindow(){
        window.add(ultimatum);//This connects paintComponent and the frame to this class
        window.addKeyListener(input);
    }

    public void paintComponent(Graphics g){
        if(imagesLoaded){
            super.paintComponent(g);
            //su.updateScreen(g);
            g.drawImage(screenImage, 0, 0, contentPlaneX, contentPlaneY, null);
        }else imagesLoaded = true;
    }

    public void update(){
        screenImage = su.updateScreen();
    }

    /**
     * This main class sets up the program. The while loop that keeps the game running is also contained inside this class. Most of this class is easily
     * readable so i'm not going to comment that much.
     */
    public static void main(String[] args){
        ultimatum = new Ultimatum();
        ultimatum.initializeUltimatum();

        final int FPS=60, TARGET_TIME=1000/FPS;

        long start, elapsed, wait;

        while(true){//This loops purpose is to keep the game running smooth on all computers 
            start = System.nanoTime();

            ultimatum.update();
            ultimatum.repaint();//This calls the paintComponent method

            elapsed = System.nanoTime() - start;

            wait = TARGET_TIME-elapsed/1000000;
            if(wait<0) wait = TARGET_TIME;


        try{//Catches the error in case the tries to give an error (which it won't)
            Thread.sleep(wait);//This is how long it waits it till the screen gets repainted
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
}

Atualizador de tela:

package com.ultimatum.Mangers;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;

import com.ultimatum.Engine.BuildingGenerator;
import com.ultimatum.Engine.TextBoxGenerator;
import com.ultimatum.Entities.Character.Player;
import com.ultimatum.Gamestates.Buildings.HealingCenter;
import com.ultimatum.Gamestates.Menus.EscapeScreen;
import com.ultimatum.Gamestates.Menus.StartScreen;
import com.ultimatum.Gamestates.Menus.TitleScreen;
import com.ultimatum.Gamestates.Routes.RouteSuperClass;
import com.ultimatum.Gamestates.Towns.TownSuperClass;
import com.ultimatum.Main.Ultimatum;

public class ScreenUpdater {

    public Ultimatum ul;
    public Resizer rs;//This is the object that captures the resize in two integers
    public KeyStates ks;
    public ImageLoader loader;
    public Fader fader;
    public TextBoxGenerator textBox;
    public Initializer initer;
    public TileMap tm;
    public Player p;
    public BuildingGenerator bg;

    //Menus
    public TitleScreen titleScreen;
    public StartScreen startScreen;
    public EscapeScreen escScreen;

    //Towns
    public TownSuperClass towns;

    //Routes
    public RouteSuperClass routes;

    //Buildings
    public HealingCenter healingCenter;

    public final int TITLE_SCREEN=0, START_SCREEN=TITLE_SCREEN+1, LOAD=START_SCREEN+1, TOWN_ONE=LOAD+1, 
            ROUTE_ONE=TOWN_ONE+1, HEALING_CENTER=ROUTE_ONE+1, ESC_MENU=HEALING_CENTER+1;

    public int screenNo = TITLE_SCREEN;

    public int prevScreen=0;
    public boolean prevMenuState, menuState;//These variables are for the checkEsc method
    public boolean isMouseVisible=true, prevIsMouseVisible;//Simple boolean for setting the mouse from visible to invisible and vice versa

    public ScreenUpdater(Ultimatum ultimatum, KeyStates keyStates, ImageLoader imageloader, String location, 
            int tileSize){

        ul = ultimatum;
        ks = keyStates;
        loader = imageloader;
        fader = new Fader(ul, this);
        textBox = new TextBoxGenerator(loader, ks, ul);
        initer = new Initializer(fader, textBox);
        fader.sendIniterData(initer);

        p = new Player(ul, fader, loader, ks, initer, this);
        fader.sendPlayerData(p);

        tm = new TileMap(tileSize, loader, p);
        fader.sendTileMapData(tm);

        rs = new Resizer(ul, p);

        bg = new BuildingGenerator(ul, p, loader, tm);

        //Below are the game states being loaded

        //Menus
        titleScreen = new TitleScreen(ul, this, loader, ks, fader);
        startScreen = new StartScreen(ul, this, fader, loader, ks, textBox);
        escScreen = new EscapeScreen(ul, fader, loader, ks);
        rs.sendEscapeScreenData(escScreen);

        //Towns
        towns = new TownSuperClass(p, fader, bg, tm, this);

        //Routes
        routes = new RouteSuperClass(p, fader, bg, tm, this);

        //Buildings
        healingCenter = new HealingCenter(ul, fader, loader, ks, textBox);
    }

    public void clearScreen(Graphics g){
        g.setColor(Color.black);
        g.fillRect(0, 0, ul.contentPlaneX, ul.contentPlaneY);
    }

    public void checkEsc(Graphics g){
        if(ks.escReleased&&screenNo>LOAD&&!fader.fadingOut&&fader.fadingIn){
            if(screenNo<HEALING_CENTER&&!p.isMoving){


        menuState=true;
            prevScreen=screenNo;
        }
        else if(screenNo==ESC_MENU) menuState=false;
    }

    if(prevMenuState!=menuState){
        int toScreen;
        boolean mouseVisiblity;
        if(menuState){
            toScreen=ESC_MENU;
            mouseVisiblity=true;
        }
        else{
            toScreen=prevScreen;
            mouseVisiblity=false;
        }

        fader.FadeOut(g, 255, toScreen, false, "", 0, 0, false, 0, mouseVisiblity);//The zeros don't matter because the boolean is set to false
        if(!fader.fadingOut){
            prevMenuState=menuState;
            initer.initFader();
        }
    }
}

public void checkForF11(){
    if(ks.isF11PressedThenReleased){
        if(ul.isWindowInFullscreenMode) ul.toWindowedMode();
        else ul.toFullscreenMode();
    }
}

public void setMouseVisible(){
    ul.window.setCursor(ul.window.getToolkit().createCustomCursor(loader.cursor, new Point(0, 0),"Visible"));
}

public void setMouseInvisble(){
    ul.window.setCursor(ul.window.getToolkit().createCustomCursor(new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB), new Point(0, 0),"Clear"));
}

public void checkMouseState(){
    if(isMouseVisible!=prevIsMouseVisible){
        if(isMouseVisible) setMouseVisible();
        else setMouseInvisble();
        prevIsMouseVisible=isMouseVisible;
    }
}

public BufferedImage updateScreen(){
    BufferedImage screenImage = new BufferedImage(ul.contentPlaneX, ul.contentPlaneY, BufferedImage.TYPE_INT_ARGB);
    Graphics2D screenGraphics = screenImage.createGraphics();
    Color oldColor = screenGraphics.getColor();
    screenGraphics.setPaint(Color.white);
    screenGraphics.fillRect(0, 0, ul.contentPlaneX, ul.contentPlaneY);
    screenGraphics.setColor(oldColor);

    checkForF11();
    clearScreen(screenGraphics);
    switch(screenNo){
        case TITLE_SCREEN:
            titleScreen.titleScreen(screenGraphics);
            break;
        case START_SCREEN:
            startScreen.startScreen(screenGraphics);
            break;
        case TOWN_ONE:
            towns.townOne(screenGraphics);
            break;
        case ROUTE_ONE:
            routes.routeOne(screenGraphics);
            break;
        case HEALING_CENTER:
            healingCenter.healingCenter(screenGraphics);
            break;
        case ESC_MENU:
            escScreen.escapeScreen(screenGraphics);
            break;
    }
    checkEsc(screenGraphics);
    rs.checkForResize();
    ks.update();
    checkMouseState();

    //g.drawImage(screenImage, 0, 0, ul.contentPlaneX, ul.contentPlaneY, null);
    //screenGraphics.dispose();
    return screenImage;
}
}

questionAnswers(1)

yourAnswerToTheQuestion