Probably your issue is that you’re incrementing the scroll from within the key update, which means your scrolling is updating whenever the EDT is updating - the result is that it will look choppy and crap.
The solution is to store buttons that have been pressed in a list, then remove them from the list when they’ve been released. Factor all buttons pressed every update. This is good practice in general - you want absolutely everything that actually updates your entities to happen within the update function. Just like you don’t draw anything outside of the draw function, don’t update anything outside of the update function.
I made a quick scrolling demo for you. It’s just my fixed timestep demo with some scrolling thrown on top. Press Start to have the ball bounce around, then scroll with the keyboard keys. The ball’s position never changes from the scrolling - things are merely drawn offset from the scroll amount. If you’re confused from the interpolation, that’s just what to do to get an FPS higher than your game hertz.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
public class ScrollTest extends JFrame implements ActionListener, KeyListener
{
private GamePanel gamePanel = new GamePanel();
private JButton startButton = new JButton("Start");
private JButton quitButton = new JButton("Quit");
private boolean running = false;
private int fps = 60;
private int frameCount = 0;
private ArrayList<Integer> keysPressed;
public ScrollTest()
{
super("Fixed Timestep Game Loop Test");
Container cp = getContentPane();
cp.setLayout(new BorderLayout());
JPanel p = new JPanel();
p.setLayout(new GridLayout(1,2));
p.add(startButton);
p.add(quitButton);
cp.add(gamePanel, BorderLayout.CENTER);
cp.add(p, BorderLayout.SOUTH);
setSize(500, 500);
keysPressed = new ArrayList<Integer>();
startButton.addActionListener(this);
quitButton.addActionListener(this);
gamePanel.addKeyListener(this);
}
public static void main(String[] args)
{
ScrollTest st = new ScrollTest();
st.setVisible(true);
}
public void actionPerformed(ActionEvent e)
{
Object s = e.getSource();
if (s == startButton)
{
running = !running;
if (running)
{
startButton.setText("Stop");
runGameLoop();
}
else
{
startButton.setText("Start");
}
}
else if (s == quitButton)
{
System.exit(0);
}
}
public void keyPressed(KeyEvent e)
{
if (!keysPressed.contains(new Integer(e.getKeyCode())))
{
keysPressed.add(new Integer(e.getKeyCode()));
}
}
public void keyReleased(KeyEvent e)
{
keysPressed.remove(new Integer(e.getKeyCode()));
}
public void keyTyped(KeyEvent e)
{
//Do nothing.
}
//Starts a new thread and runs the game loop in it.
public void runGameLoop()
{
Thread loop = new Thread()
{
public void run()
{
gameLoop();
}
};
loop.start();
}
//Only run this in another Thread!
private void gameLoop()
{
//This value would probably be stored elsewhere.
final double GAME_HERTZ = 30.0;
//Calculate how many NS each frame should take for our target game hertz.
final double TIME_BETWEEN_UPDATES = 1000000000 / GAME_HERTZ;
//At the very most we will update the game this many times before a new render.
final int MAX_UPDATES_BEFORE_RENDER = 5;
//We will need the last update time.
double lastUpdateTime = System.nanoTime();
//Store the last time we rendered.
double lastRenderTime = System.nanoTime();
//If we are able to get as high as this FPS, don't render again.
final double TARGET_FPS = 60;
final double TARGET_TIME_BETWEEN_RENDERS = 1000000000 / TARGET_FPS;
//Simple way of finding FPS.
int lastSecondTime = (int) (lastUpdateTime / 1000000000);
while (running)
{
double now = System.nanoTime();
int updateCount = 0;
//Do as many game updates as we need to, potentially playing catchup.
while( now - lastUpdateTime > TIME_BETWEEN_UPDATES && updateCount < MAX_UPDATES_BEFORE_RENDER )
{
updateGame();
lastUpdateTime += TIME_BETWEEN_UPDATES;
updateCount++;
}
//If for some reason an update takes forever, we don't want to do an insane number of catchups.
//If you were doing some sort of game that needed to keep EXACT time, you would get rid of this.
if (lastUpdateTime - now > TIME_BETWEEN_UPDATES)
{
lastUpdateTime = now - TIME_BETWEEN_UPDATES;
}
//Render. To do so, we need to calculate interpolation for a smooth render.
float interpolation = Math.min(1.0f, (float) ((now - lastUpdateTime) / TIME_BETWEEN_UPDATES) );
drawGame(interpolation);
lastRenderTime = now;
//Update the frames we got.
int thisSecond = (int) (lastUpdateTime / 1000000000);
if (thisSecond > lastSecondTime)
{
fps = frameCount;
frameCount = 0;
lastSecondTime = thisSecond;
}
//Yield until it has been at least the target time between renders. This saves the CPU from hogging.
while ( now - lastRenderTime < TARGET_TIME_BETWEEN_RENDERS && now - lastUpdateTime < TIME_BETWEEN_UPDATES)
{
Thread.yield();
now = System.nanoTime();
}
}
}
private void updateGame()
{
gamePanel.factorKeyPresses(keysPressed);
gamePanel.update();
}
private void drawGame(float interpolation)
{
gamePanel.setInterpolation(interpolation);
gamePanel.repaint();
}
private class GamePanel extends JPanel
{
float interpolation;
float ballX, ballY, lastBallX, lastBallY;
int ballWidth, ballHeight;
float ballXVel, ballYVel;
float ballSpeed;
float scrollX, scrollY, lastScrollX, lastScrollY;
int lastDrawX, lastDrawY;
public GamePanel()
{
ballX = lastBallX = 100;
ballY = lastBallY = 100;
ballWidth = 25;
ballHeight = 25;
ballSpeed = 25;
ballXVel = (float) Math.random() * ballSpeed*2 - ballSpeed;
ballYVel = (float) Math.random() * ballSpeed*2 - ballSpeed;
}
public void setInterpolation(float interp)
{
interpolation = interp;
}
public void update()
{
requestFocus();
lastBallX = ballX;
lastBallY = ballY;
ballX += ballXVel;
ballY += ballYVel;
if (ballX + ballWidth/2 >= getWidth())
{
ballXVel *= -1;
ballX = getWidth() - ballWidth/2;
ballYVel = (float) Math.random() * ballSpeed*2 - ballSpeed;
}
else if (ballX - ballWidth/2 <= 0)
{
ballXVel *= -1;
ballX = ballWidth/2;
}
if (ballY + ballHeight/2 >= getHeight())
{
ballYVel *= -1;
ballY = getHeight() - ballHeight/2;
ballXVel = (float) Math.random() * ballSpeed*2 - ballSpeed;
}
else if (ballY - ballHeight/2 <= 0)
{
ballYVel *= -1;
ballY = ballHeight/2;
}
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
//Draw a box around the area so we can see scrolling.
float thisXScroll = (scrollX - lastScrollX) * interpolation + lastScrollX;
float thisYScroll = (scrollY - lastScrollY) * interpolation + lastScrollY;
g.setColor(Color.BLACK);
g.drawRect((int)scrollX, (int)scrollY, getWidth(), getHeight());
//Draw the ball.
g.setColor(Color.RED);
int drawX = (int) ((ballX - lastBallX) * interpolation + lastBallX - ballWidth/2 + thisXScroll);
int drawY = (int) ((ballY - lastBallY) * interpolation + lastBallY - ballHeight/2 + thisYScroll);
g.fillOval(drawX, drawY, ballWidth, ballHeight);
lastDrawX = drawX;
lastDrawY = drawY;
g.setColor(Color.BLACK);
g.drawString("FPS: " + fps, 5, 10);
frameCount++;
}
public void factorKeyPresses(ArrayList<Integer> presses)
{
lastScrollX = scrollX;
lastScrollY = scrollY;
for (int i = 0; i < presses.size(); i++)
{
int key = presses.get(i).intValue();
if (key == KeyEvent.VK_LEFT)
{
scrollX -= 5;
}
else if (key == KeyEvent.VK_RIGHT)
{
scrollX += 5;
}
else if (key == KeyEvent.VK_UP)
{
scrollY -= 5;
}
else if (key == KeyEvent.VK_DOWN)
{
scrollY += 5;
}
}
}
}
}