I´m having some problems with a small game I´m developing. although I´m using double buffer and was able to get rid of flickering, the movement still looks somewhat jittery and not fluid.
I know it can be caused by increasing the movement in big steps and/or low framerate, but I still have the same problem using increments of 1 and 50+ fps. It´s somewhat difficult to explain, but the sprites move strangely (correctly, but not in a fluid motion)
I would appreciate if someone could point me in the right direction.
public class Gameplay extends javax.swing.JPanel {
GUI gui;
Canvas canvas = new Canvas();
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gd.getDefaultConfiguration();
BufferedImage bi;
BufferStrategy buffer;
public Gameplay(GUI gui) {
this.gui = gui;
initComponents();
}
// Used to start the gameplay. Called by GUI
public void start() {
new RefreshScreen().start();
}
// ==============================================================================================
// DOUBLE BUFFER AND PAINTING ===================================================================
// ==============================================================================================
Date date = new Date(); // time
int lastSecond = 0; // seconds controll - for FPS calculation
int fpsCount = 0; // Count frames rendered
int showFps = 0; // The total FPS text that will appear on screen
int MAX_FPS = 30; // targeted Max FPS
int MIN_FPS = 24; // targeted Min FPS
int sleepTimeBetweenRefresh = 20; // Delay before new refresh
Color fpsColor = Color.yellow; // color of the FPS information on screen
String fpsInfo = ""; // Aditional info on FPS (increasing, decreasing, etc)
class RefreshScreen extends Thread {
public void run() {
Graphics graphics = null;
Graphics2D bufferGraphics = null;
add(canvas, BorderLayout.CENTER);
canvas.setIgnoreRepaint(true);
canvas.setVisible(true);
canvas.setSize(gui.getWidth(), gui.getHeight());
canvas.createBufferStrategy(2);
buffer = canvas.getBufferStrategy();
bi = gc.createCompatibleImage(gui.getWidth(), gui.getHeight());
bufferGraphics = bi.createGraphics();
while (true) {
try {
//FrameRate count
date = null;
date = new Date();
if (lastSecond != date.getSeconds()) {
lastSecond = date.getSeconds();
showFps = fpsCount;
fpsCount = 0;
if (showFps > MAX_FPS) {
sleepTimeBetweenRefresh++;
fpsInfo = "(--)";
fpsColor = Color.blue;
}
if ((showFps < MIN_FPS) && (sleepTimeBetweenRefresh > 5)) {
sleepTimeBetweenRefresh--;
fpsInfo = "(++)";
}
if (showFps < MIN_FPS) {
fpsColor = Color.red;
}
if ((showFps > MIN_FPS) && (showFps <= MAX_FPS)) {
fpsColor = Color.green;
fpsInfo = "(ok)";
}
}
fpsCount++;
//Clear canvas =============================
bufferGraphics.setColor(Color.black);
bufferGraphics.clearRect(0, 0, gui.getWidth(), gui.getHeight());
//FPS =============================
bufferGraphics.setColor(fpsColor);
bufferGraphics.drawString("FPS: " + showFps, 3, 15);
bufferGraphics.setColor(Color.black);
//SPRITES =============================
try {
for (int count = 0; count < Sprites.getSprites().size(); count++) {
bufferGraphics.drawImage(Sprites.getSprite(count).getImage(), Sprites.getSprite(count).getX(), Sprites.getSprite(count).getY(), null);
}
} catch (Exception e) { }
//HERO =============================
try {
bufferGraphics.drawImage(Sprites.getHero().getImage(), Sprites.getHero().getX(), Sprites.getHero().getY(), null);
} catch (Exception e) { }
// PAINT BUFFER =================================
try {
graphics = buffer.getDrawGraphics();
graphics.drawImage(bi, 0, 0, null);
if (!buffer.contentsLost()) {
buffer.show();
}
} catch (Exception e) { }
// SLEEP =================================
sleep(sleepTimeBetweenRefresh);
} catch (Exception e) { }
}//while
}//run
}//inner class
I would also like to point out that the X and Y of the sprite are being handled so that it doesn´t go right and then down - for instance - when it is supposed to go diagonally. I don´t suppose that´s the problem anyways since the problems occurs even on a straight line.
You might try timing how long it takes to draw everything then subtracting that from your sleep time.
long beforeTime = System.currentTimeMillis();
// do all drawing
sleepTimeBetweenRefresh -= System.currentTimeMillis() - beforeTime;
if(sleepTimeBetweenRefresh < 0) sleepTimeBetweenRefresh = 0;
This helps ensure that your thread is firing every 50ms (or whatever sleepTimeBetweenRefresh is). Say you want it to fire ever 50ms, your drawing takes 10ms. Without the code above after the code ran one time you would actually be painting 60ms after the last paint because you painted for 10 and slept for 50. By subtracting the time it took to paint the components you can keep your FPS stable.
Related
I am trying to create a little game for a project in university. Since I need to update the screen permanently I am looking for a good render loop implementation. It should be full screen. I found this on gamedev.net - Java Games: Active Rendering:
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.swing.JFrame;
public class SimpleFullScreenGame {
static boolean running;
public static void main( String[] args ) {
// Create game window...
JFrame app = new JFrame();
app.setIgnoreRepaint( true );
app.setUndecorated( true );
// Add ESC listener to quit...
app.addKeyListener( new KeyAdapter() {
public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE )
running = false;
}
});
// Get graphics configuration...
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gd.getDefaultConfiguration();
// Change to full screen
gd.setFullScreenWindow( app );
if( gd.isDisplayChangeSupported() ) {
gd.setDisplayMode(
new DisplayMode( 640, 480, 32, DisplayMode.REFRESH_RATE_UNKNOWN )
);
}
// Create BackBuffer...
app.createBufferStrategy( 2 );
BufferStrategy buffer = app.getBufferStrategy();
// Create off-screen drawing surface
BufferedImage bi = gc.createCompatibleImage( 640, 480 );
// Objects needed for rendering...
Graphics graphics = null;
Graphics2D g2d = null;
Color background = Color.BLACK;
Random rand = new Random();
// Variables for counting frames per seconds
int fps = 0;
int frames = 0;
long totalTime = 0;
long curTime = System.currentTimeMillis();
long lastTime = curTime;
running = true;
while( running ) {
try {
// count Frames per second...
lastTime = curTime;
curTime = System.currentTimeMillis();
totalTime += curTime - lastTime;
if( totalTime > 1000 ) {
totalTime -= 1000;
fps = frames;
frames = 0;
}
++frames;
// clear back buffer...
g2d = bi.createGraphics();
g2d.setColor( background );
g2d.fillRect( 0, 0, 639, 479 );
// draw some rectangles...
for( int i = 0; i < 20; ++i ) {
int r = rand.nextInt(256);
int g = rand.nextInt(256);
int b = rand.nextInt(256);
g2d.setColor( new Color(r,g,b) );
int x = rand.nextInt( 640/2 );
int y = rand.nextInt( 480/2 );
int w = rand.nextInt( 640/2 );
int h = rand.nextInt( 480/2 );
g2d.fillRect( x, y, w, h );
}
// display frames per second...
g2d.setFont( new Font( "Courier New", Font.PLAIN, 12 ) );
g2d.setColor( Color.GREEN );
g2d.drawString( String.format( "FPS: %s", fps ), 20, 20 );
// Blit image and flip...
graphics = buffer.getDrawGraphics();
graphics.drawImage( bi, 0, 0, null );
if( !buffer.contentsLost() )
buffer.show();
} finally {
// release resources
if( graphics != null )
graphics.dispose();
if( g2d != null )
g2d.dispose();
}
}
gd.setFullScreenWindow( null );
System.exit(0);
}
}
I executed the code and there are two things I am wondering about:
My display resolution is 1920x1080. The code is using 640x480. Therefore the rendered rectangles look like a 640x480 picture upscaled to 1920x1080 meaning you have huge pixels and the code doesn't take avantage of the higher posible resolution of the display. How can I adapt the code to actually use the native display resolution of the user?
Second, usually what you see when you take a look on games code is a rendering loop living in its own thread to not block the main thread. However thats not the case here. Why? I assume because the JFrame already lives in its own thread created by swing?
Thanks for the help.
There are lots of issues with this code.
First of all, when you are checking (polling) a flag variable in the main thread, that will be updated by a key listener, which will be called in the event dispatch thread, the minimum you have to do, is to declare that variable volatile.
Then, there is no point in using JFrame when you don’t use the Swing framework at all. Further, it’s nonsensical to request double buffering from the AWT and then, use a BufferedImage for another buffering atop the already buffered operation.
Using the native resolution is as easy as removing the setDisplayMode(…) call. After turning the window to full screen, you can simply use getWidth() and getHeight() on it to get the actual dimensions for the operations (it’s not needed for the buffered image, as that was obsolete anyway).
public class SimpleFullScreenGame {
static volatile boolean running = true;
public static void main(String[] args) {
// Create game window...
Frame app = new Frame();
app.setIgnoreRepaint(true);
app.setUndecorated(true);
// Add ESC listener to quit...
app.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_ESCAPE) running = false;
}
});
GraphicsDevice dev = app.getGraphicsConfiguration().getDevice();
// Change to full screen
dev.setFullScreenWindow(app);
int width = app.getWidth(), height = app.getHeight();
// Create BackBuffer...
app.createBufferStrategy(2);
BufferStrategy buffer = app.getBufferStrategy();
// Objects needed for rendering...
Color background = Color.BLACK, textColor = Color.GREEN;
Font font = new Font("Courier New", Font.PLAIN, 12);
ThreadLocalRandom rand = ThreadLocalRandom.current();
// Variables for counting frames per seconds
int fps = 0, frames = 0, totalTime = 0;
long currTime = System.nanoTime(), lastTime;
while(running) {
// count Frames per second...
lastTime = currTime;
currTime = System.nanoTime();
totalTime += currTime - lastTime;
if(totalTime > 1_000_000_000) {
totalTime -= 1_000_000_000;
fps = frames;
frames = 0;
}
frames++;
Graphics gfx = buffer.getDrawGraphics();
gfx.setColor(background);
gfx.fillRect(0, 0, width, height);
// draw some rectangles...
for(int i = 0; i < 20; ++i) {
gfx.setColor(new Color(rand.nextInt(0x1000000)));
int x = rand.nextInt(width/2), y = rand.nextInt(height/2);
int w = rand.nextInt(width/2), h = rand.nextInt(height/2);
gfx.fillRect(x, y, w, h);
}
// display frames per second...
gfx.setFont(font);
gfx.setColor(textColor);
gfx.drawString("FPS: " + fps, 20, 20);
gfx.dispose();
if(!buffer.contentsLost()) buffer.show();
}
dev.setFullScreenWindow(null);
System.exit(0);
}
}
I made some other small improvements.
I'm playing around with graphics in Java. At the moment I have a rectangle that moves from left to right. I want it to start moving left once it hits the right side of the Canvas and left when it hits the right side, i have included a game loop as this will eventually turn into my first very basic game. Thanks.
P.S - I followed some tutorials for different parts of this code hence why it might be a bit messy, I'm working on it :)
Main Class:
public class Game extends JFrame implements Runnable {
private Canvas canvas = new Canvas();
private RenderHandler renderer;
private boolean running = true;
public static int WIDTH = 1200, HEIGHT = WIDTH / 12*9;
public static int moveX =WIDTH/2;
public Game() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(WIDTH, HEIGHT);
setLocationRelativeTo(null);
setLocationRelativeTo(null);
add(canvas);
setVisible(true);
canvas.createBufferStrategy(3);
renderer = new RenderHandler(getWidth(), getHeight());
}
public void update() {
}
public void render() {
BufferStrategy bufferStrategy = canvas.getBufferStrategy();
Graphics graphics = bufferStrategy.getDrawGraphics();
super.paint(graphics);
renderer.render(graphics);
graphics.dispose();
bufferStrategy.show();
}
public void run() {
BufferStrategy bufferStrategy = canvas.getBufferStrategy();
int FRAMES = 0;
int TICKS = 0;
long lastTime = System.nanoTime();
double unprocessed = 0;
double nsPerSecs = 1000000000 /60.0;
long Timer = System.currentTimeMillis();
while(running) {
long now = System.nanoTime();
unprocessed += (now - lastTime) / nsPerSecs;
lastTime = now;
if(unprocessed >= 1) {
TICKS ++;
update();
unprocessed -= 1;
}
try
{
Thread.sleep(3);
}catch (InterruptedException e) {
e.printStackTrace();
}
FRAMES++;
render();
if(System.currentTimeMillis() - Timer > 1000) {
System.out.println("Ticks: " + TICKS + " FPS: " + FRAMES);
TICKS = 0;
FRAMES = 0;
Timer += 1000;
}
}
}
public static void main(String[] args) {
Game game = new Game();
Thread gameThread = new Thread(game);
gameThread.start();
}
}
Class drawing the graphics:
public class RenderHandler {
public RenderHandler(int width, int height) {
}
public void render(Graphics g) {
g.setColor(Color.BLACK);
g.fillRect(0, 0, Game.WIDTH, Game.HEIGHT);
g.setColor(Color.RED);
g.fillRect(Game.moveX, Game.HEIGHT/2, 50, 50);
if (Game.moveX >= Game.WIDTH) {
Game.moveX ++;
} else if (Game.moveX <= 0) {
Game.moveX --;
}else { Game.moveX++;
}
}
}
If you know how to draw on the screen and how stuffs work, I would think that this is more about getting the logic down.
This code slice I so brutally tore from your question is right next to where the rendering takes place (a problem because I view it as rather unorganized; I would recommend game logic and rendering to take place in two different functions). It basically says that it will move right if it is beyond the right of the screen, if not, it will move left if it is beyond the left of the screen, and finally, if not, it will just move left.
if (Game.moveX >= Game.WIDTH) {
Game.moveX ++;
} else if (Game.moveX <= 0) {
Game.moveX --;
}else { Game.moveX++;
}
If you want it to bounce, you will have to use a boolean to keep track of its moving state, or, if you want more versatility, use a pair of floats or doubles (floats are typically used in Java game design) to keep track of its position, and another for its velocity. I'm in a tight squeeze right now, I will return.
Add this to render handler instead of the current if statement in render
bool rol = true; // initialize this outside the method
If(Game.movex + 50 >= Game.width)
rol = false;
Else if(Game.movex <= 0)
rol = true;
If(rol)
Game.movex++;
Else
Game.movex--;
You need to store current moving direction somewhere, so add this to Game class:
public static int deltaX = 1;
And replace condition in render() with
if (Game.moveX >= Game.WIDTH-50) {
Game.deltaX =-1;
} else if (Game.moveX <= 0) {
Game.deltaX =1;
}
Game.moveX += Game.deltaX;
first post here so forgive me if this is the absolute wrong thing but I'm in a tiny bit of a pickle. - My computer science class has us making our own game/program from scratch, but our teacher specifically ignored the existence of swing and said that it was out of his realm to teach it so from my readings it seems as if a game is almost out of the question, with what I've written so far my main sprite and debugging information flickers violently on key input. Is there a way around this?
I know JPanel/JFrame/Swing would fix this, but we weren't taught it and all I've read so far is confusing me to no end, if someone would be able to help me convert my code to use it that would be amazing as I learn easier from dissecting it.. Which may seem a bit dodgy but it really is the easiest way to learn from. My code so far is;
public class GameThing extends Applet implements KeyListener {
Image rightsprite, picture;
Image leftsprite, picture2;
Image spritedead, picture3;
Image background, picture4;
boolean left;
boolean dead;
boolean wall;
boolean menu;
int widthX, heightY;
int bgx, bgy;
String sr = " ";
char ch = '*';
Graphics bufferGraphics;
Image offscreen;
Dimension dim;
#Override
public void init() {
rightsprite = getImage(getDocumentBase(), "SpriteRight.png");
leftsprite = getImage(getDocumentBase(), "SpriteLeft.png");
spritedead = getImage(getDocumentBase(), "Sprite.png");
background = getImage(getDocumentBase(), "Background.png");
requestFocus();
addKeyListener(this);
left = false;
menu = true;
setSize(600, 380);
dim = getSize();
offscreen = createImage(dim.width, dim.height);
bufferGraphics = offscreen.getGraphics();
bgx = 0;
bgy = 0;
}
public void pause(final int delay) { // PAUSE COMMAND LIFTED FROM RAY
// waits a while
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
return;
}
}
public GameThing() { // SPRITE INITIAL SPAWN LOCATION
widthX = 100;
heightY = 300;
}
#Override
public void paint(final Graphics g) { // DRAW MAIN CHARACTER SPRITE AND BACKGROUND
bufferGraphics.setColor(Color.black); // DRAW GROUND (REPLACE WITH BLOCKS SOON)
bufferGraphics.drawRect(0, 334, 500, 50);
bufferGraphics.setColor(Color.green);
bufferGraphics.fillRect(0, 335, 500, 50);
g.drawImage(offscreen, 0, 0, this);
if (menu == true) {
pause(500);
g.drawString("Side Scroller Test.", 180, 250);
pause(1500);
menu = false;
}
if (left == false) { // LEFT KEY MOVEMENT COMMANDS
g.drawImage(rightsprite, widthX, heightY, this);
g.drawString("Sprites Current X Location is: " + widthX, 20, 30);
g.drawString("Sprites Current Y Location is: " + heightY, 20, 50);
g.drawString("Background X location is: " + bgx, 20, 90);
g.drawString("Currently: " + sr, 20, 70);
}
if (left == true) { // RIGHT KEY MOVEMENT COMMANDS
g.drawImage(leftsprite, widthX, heightY, this);
g.drawString("Sprites Current X Location is: " + widthX, 20, 30);
g.drawString("Sprites Current Y Location is: " + heightY, 20, 50);
g.drawString("Background X location is: " + bgx, 20, 90);
g.drawString("Currently: " + sr, 20, 70);
}
if (dead == true) { // COMMANDS TO EXECUTE WHEN MAIN CHARACTER DIES
widthX = 506;
heightY = 314;
g.drawImage(spritedead, widthX, heightY, this);
pause(500);
g.setColor(Color.white);
g.drawRect(widthX, heightY, widthX, heightY);
g.fillRect(widthX, heightY, widthX, heightY);
pause(500);
widthX = 100;
heightY = 300;
bgx = 0;
left = true;
dead = false;
}
}
#Override
public void update(final Graphics g) { // KEEPS BACKGROUND STATIC
paint(g);
}
public void keyPressed(final KeyEvent e) {
sr = "blank!";
ch = '1';
if (e.getKeyCode() == 39) {
sr = "Moving Right!";
widthX = widthX + 7;
if (widthX <= 121) {
bgx = bgx;
} else {
bgx = bgx - 7;
}
left = false;
if ((widthX > 490) && (heightY > 300)) { // FALL DEATH
sr = "You Died!";
widthX = 900;
heightY = 900;
dead = true;
}
if (widthX == 499) {
heightY = heightY + 7;
}
}
if (widthX == 2) {
wall = true;
}
if (e.getKeyCode() == 37) {
if (wall == true) {
sr = "Wall!";
if (widthX > 2) {
wall = false;
}
}
if (wall == false) {
sr = "Moving Left!";
widthX = widthX - 7;
if (bgx >= 0) {
bgx = bgx;
} else {
bgx = bgx + 7;
}
left = true;
}
}
if (e.getKeyCode() == 38) {
sr = "Jumping!";
}
repaint();
}
public void keyTyped(final KeyEvent e) {
ch = e.getKeyChar();
repaint();
}
public void keyReleased(final KeyEvent e) {
} // key released
}
ps: Jumping with the up key has been a problem too, I can't simply add to the Y variable and then subtract as it just goes so fast it's as if it never moved.. The basics of a sidescroller are a lot more than I ever expected..
I edited your code:
public class GameThing extends Applet {
// External resources
BufferedImage rightSprite, leftSprite, spriteDead, backgroundImg;
// Game data
int state = 1; //0 = left, 1 = right, 2 = dead
// Geometry
int locX = 100, locY = 300;
int bgX = 0, bgY = 0;
int groundX = 0, groundY = 334, groundW = 500, groundH = 50;
int appletW = 600, appletH = 480;
int wallW = 20, wallH = 40, wallX = 20, wallY = groundY - wallH;
final int STEP_SIZE = 7;
// Information
final String X_LOC_STR = "Sprites Current X Location is: ";
final String Y_LOC_STR = "Sprites Current Y Location is: ";
final String STATE_STR = "Currently: ";
final String X_BG_STR = "Background X location is: ";
String stateString = "";
// Double buffering
Image offscreen;
Graphics bufferGraphics;
Dimension dim;
// GUI components
Panel gamePanel;
Panel statusPanel = new Panel();
Label xLocLabel = new Label();
Label yLocLabel = new Label();
Label stateLabel = new Label();
Label xBGLocLabel = new Label();
#Override
public void init() {
// Load images
try {
rightSprite = ImageIO.read(new File(getClass().getResource("SpriteRIght.png").getPath()));
leftSprite = ImageIO.read(new File(getClass().getResource("SpriteLeft.png").getPath()));
spriteDead = ImageIO.read(new File(getClass().getResource("SpriteDead.png").getPath()));
backgroundImg = ImageIO.read(new File(getClass().getResource("Background.png").getPath()));
} catch (IOException e) {
e.printStackTrace();
}
// Set the panel that displays data
statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.Y_AXIS));
statusPanel.add(xLocLabel);
statusPanel.add(yLocLabel);
statusPanel.add(stateLabel);
statusPanel.add(xBGLocLabel);
// Create the panel that draws the game and specify its behavior
gamePanel = new Panel() {
// Reduces flickering
#Override
public void update(Graphics g) {
paint(g);
}
// DRAW MAIN CHARACTER SPRITE AND BACKGROUND
#Override
public void paint(Graphics g) {
bufferGraphics.clearRect(0, 0, dim.width, dim.width);
bufferGraphics.drawImage(backgroundImg, bgX, bgY, this);
bufferGraphics.setColor(Color.BLACK); // DRAW GROUND (REPLACE WITH BLOCKS SOON)
bufferGraphics.drawRect(groundX, groundY, groundW, groundH);
bufferGraphics.setColor(Color.GREEN);
bufferGraphics.fillRect(groundX, groundY + 1, groundW, groundH);
bufferGraphics.setColor(Color.RED);
bufferGraphics.fillRect(wallX, wallY, wallW, wallH);
switch (state) {
case 0: bufferGraphics.drawImage(leftSprite, locX, locY, this);
break;
case 1: bufferGraphics.drawImage(rightSprite, locX, locY, this);
break;
case 2: bufferGraphics.drawImage(spriteDead, locX, locY, this);
// After death wait a bit and reset the game
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
pause(2000);
reset();
}
});
break;
}
g.drawImage(offscreen, 0, 0, this);
}
};
// Set the applet window
setLayout(new BorderLayout());
add(statusPanel, BorderLayout.PAGE_START);
add(gamePanel);
// Set double buffering
setSize(appletW, appletH);
dim = getSize();
offscreen = createImage(dim.width, dim.height);
bufferGraphics = offscreen.getGraphics();
// Set the panel that draws the game
gamePanel.addKeyListener(new Controls());
gamePanel.requestFocusInWindow();
updateLabels();
}
// PAUSE COMMAND LIFTED FROM RAY (who is Ray?)
// waits a while
public void pause(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Set to start parameters
public void reset() {
locX = 100; locY = 300;
bgX = 0;
state = 1;
gamePanel.repaint(); // These 2 lines automatically restart the game
updateLabels(); // Remove if you want to stay on the death screen until next press
}
public void updateLabels() {
xLocLabel.setText(X_LOC_STR + locX);
yLocLabel.setText(Y_LOC_STR + locY);
stateLabel.setText(STATE_STR + stateString);
xBGLocLabel.setText(X_BG_STR + bgX);
}
private class Controls extends KeyAdapter {
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_RIGHT:
stateString = "Moving Right!";
locX += STEP_SIZE;
state = 1;
if (locX > 121)
bgX -= STEP_SIZE;
// FALL DEATH
if ((locX > 490) && (locY > 300)) {
stateString = "You Died!";
state = 2;
}
// Start fall
if (locX == 499)
locY += STEP_SIZE;
// Check wall collision
if (locX >= wallX && locX <= wallX + wallW) {
locX -= STEP_SIZE;
stateString = "Wall!";
}
break;
case KeyEvent.VK_LEFT:
stateString = "Moving Left!";
locX -= STEP_SIZE;
state = 0;
if (bgX < 0)
bgX += STEP_SIZE;
// Check wall collision
if (locX >= wallX && locX <= wallX + wallW) {
locX += STEP_SIZE;
stateString = "Wall!";
}
break;
case KeyEvent.VK_UP:
// Implement jumping
stateString = "Jumping!";
break;
}
// Repaint the game panel and update the data panel
gamePanel.repaint();
updateLabels();
}
}
}
Start by testing and understanding the changes:
I created a separate panel for the current data display and one for the game (which gets repainted).
I added a few help functions.
I made a lot of syntax changes (e.g. multiple if else replaced by switch, VK_UP instead of 37 and such).
Better to have all values in one place and read them from there instead of spreading them in various places (still needs a bit of work but it will change when advancing with the code).
Make constant values (values that don't change) final.
In Java conventions, constants are named with uppercase and underscore (_) (so Color.RED, not Color.red).
Better to use #Override when overriding methods even when the compiler understands it.
Do not block the event dispatch thread (EDT) with sleep or expensive computations. Methods like paint key events are meant to execute quickly. I used invokeLater to pause only after the EDT finished processing all queued events. (I understand that this threading subject comes as a bomb from nowhere, just bear with me).
Looking forward for the current state of the code (no new features):
state should probably be an enum or something close to it instead of random numbers.
Using Shape for drawn objects instead of specifying their parameters during painting.
Double buffering implementation notes:
You create an off-screen Image object offscreen and a Graphics object to draw on it - bufferGraphics. Insidepaint, you use only bufferGraphics for the drawings. You in fact draw on offscreen even though the paint belongs to gamePanel (or any other object and its paint), slightly confusing. When you finish drawing everything off-screen, you throw it all in one go onto the screen with g.drawImage(offscreen...) which uses gamePanel's Graphics object g.
The idea is like drawing everything on a paper and then putting it in front of the camera instead of drawing bit by bit on the camera, or something like that...
Currently, the off-screen image is the size of the whole applet and not just the game panel, but it makes little difference.
Relevant differences from a Swing approach:
Replace Label with JLabel, Panel with JPanel etc.
Swing components automatically implement double buffering (see JComponent's setDoubleBuffered and isDoubleBuffered). Remove all of those off-screen shenanigans and draw directly with Graphics g. No need to override update also.
Painting in Swing is done in paintComponent and not in paint.
Swing runs from its own thread and not from the EDT (not automatically, but easily done). There are some benefits to this I won't get into right now.
Key events are better handled by key bindings (Swing only) rather than the key listener. This manifests in responsiveness, native environment independence and more (e.g. concurrent key events).
#user1803551 Okay, so it's all sorted back to how it was again. - The scrolling background is in (Just a couple things around the place needed some tweaking from your post), and I understand that the final line "gamePanel.repaint(); is what's refreshing everything on the screen, since the background needs to be updated with every key input, it shouldn't be limited to the tiny square from your code, right? This now refreshes the whole screen, but I assume this isn't what is wanted because it just results in the flickering, the buffergraphics of the green rectangle at the bottom however don't flicker. So my assumption is to convert
g.drawImage(backgroundImg, bgx, bgy,this);
into buffergraphics?
-EDIT-
Moved the main Sprite into bufferGraphics aswell as the background image. Flickering is now almost gone, but after holding down an arrow key for a little bit the screen will flicker just for a moment and then return to normal.
public class GameTest extends Applet {
/**
*
*/
private static final long serialVersionUID = -1533028791433336859L;
Image rightsprite, leftsprite, spritedead, backgroundImg, offscreen;
boolean left = false, dead = false, wall = false, menu = true;
int locX = 100, locY = 300, width, height, bgx = 0, bgy = 0;
final int STEP_SIZE = 7;
final String X_LOC_STR= "Sprites Current X Location is: ";
final String Y_LOC_STR= "Sprites Current Y Location is: ";
final String STATE_STR= "Currently: ";
final String X_BG_STR= "Background X location is: ";
String sr = " ";
Graphics bufferGraphics;
Dimension dim;
Panel gamePanel;
Panel statusPanel = new Panel();
Label xLoc = new Label(X_LOC_STR);
Label yLoc = new Label(Y_LOC_STR);
Label state = new Label(STATE_STR);
Label xBGLoc = new Label(X_BG_STR);
public void init() {
try {
rightsprite = ImageIO.read(new File(getClass().getResource("SpriteRight.png").getPath()));
leftsprite = ImageIO.read(new File(getClass().getResource("SpriteLeft.png").getPath()));
spritedead = ImageIO.read(new File(getClass().getResource("SpriteDead.png").getPath()));
backgroundImg = ImageIO.read(new File(getClass().getResource("Background.png").getPath()));
} catch (IOException e) {
e.printStackTrace();
}
height = rightsprite.getHeight(this);
width = rightsprite.getWidth(this);
statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.Y_AXIS));
statusPanel.add(xLoc);
statusPanel.add(yLoc);
statusPanel.add(state);
statusPanel.add(xBGLoc);
gamePanel = new Panel() {
/**
*
*/
private static final long serialVersionUID = 1L;
// DRAW MAIN CHARACTER SPRITE AND BACKGROUND
#Override
public void paint(Graphics g) {
bufferGraphics.drawImage(backgroundImg,bgx, bgy, this);
bufferGraphics.setColor(Color.black); // DRAW GROUND (REPLACE WITH BLOCKS SOON)
bufferGraphics.drawRect(0, 334, 500, 50);
bufferGraphics.setColor(Color.green);
bufferGraphics.fillRect(0, 335, 500, 50);
if (menu == true) {
pause(500);
g.drawString("Side Scroller Test.", 180, 250);
pause(1500);
menu = false;
}
// LEFT KEY MOVEMENT COMMANDS
if (left == false)
bufferGraphics.drawImage(rightsprite, locX, locY, this);
// RIGHT KEY MOVEMENT COMMANDS
else
bufferGraphics.drawImage(leftsprite, locX, locY, this);
// COMMANDS TO EXECUTE WHEN MAIN CHARACTER DIES
if (dead == true) {
locX = 506;
locY = 314;
bufferGraphics.drawImage(spritedead, locX, locY, this);
pause(500);
bufferGraphics.setColor(Color.white);
bufferGraphics.drawRect(locX, locY, width, height);
bufferGraphics.fillRect(locX, locY, width, height);
pause(500);
locX = 100;
locY = 300;
bgx = 0;
bgy = 0;
bufferGraphics.clearRect(locX, locY, width, height);
left = true;
dead = false;
}
g.drawImage(offscreen, 0, 0, this);
super.paint(g);
}
};
setLayout(new BorderLayout());
add(statusPanel, BorderLayout.PAGE_START);
add(gamePanel);
setSize(600, 480);
dim = getSize();
offscreen = createImage(dim.width, dim.height);
bufferGraphics = offscreen.getGraphics();
gamePanel.addKeyListener(new Controls());
gamePanel.requestFocusInWindow();
}
// waits a while
public void pause(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private class Controls extends KeyAdapter {
public void keyPressed(KeyEvent e) {
sr = "blank!";
if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
sr = "Moving Right!";
locX += STEP_SIZE;
if (locX > 121)
bgx = bgx - STEP_SIZE;
left = false;
// FALL DEATH
if ((locX > 490) && (locY > 300)) {
sr = "You Died!";
locX = 900;
locY = 900;
dead = true;
gamePanel.repaint();
}
if (locX == 499) {
locY += STEP_SIZE;
}
}
if (locX == 2) {
wall = true;
}
else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
if (wall == true) {
sr = "Wall!";
if (locX > 2) {
wall = false;
}
}
else {
sr = "Moving Left!";
locX -= STEP_SIZE;
if (bgx < 0)
bgx += STEP_SIZE;
left = true;
}
}
if (e.getKeyCode() == KeyEvent.VK_UP) {
sr = "Jumping!";
}
xLoc.setText(X_LOC_STR + locX);
yLoc.setText(Y_LOC_STR + locY);
state.setText(STATE_STR + sr);
xBGLoc.setText(X_BG_STR + bgx);
gamePanel.repaint();
}
}
}
At the moment I'm trying to make a simple videogame in Java, just for fun. But there seems to be lag, and I'm not sure why it's happening. I'll give the lowdown:
The way it draws is using JFrame, and the actual drawing happens in the ImagePanel class. In ImagePanel, this is how I draw. It includes some things about debugging to show FPS and a timer to show length of run, but I'm not sure if that's important. It goes through multiple ArrayLists to show all the objects on the JFrame.
//Painting
public void paintComponent(Graphics g)
{
//Paint the background with its upper left corner at the upper left corner of the panel
g.drawImage(background, 0, 0, null);
//Paint each image in the foreground where it should go
for(MovingImage img : backgrounds)
{
g.drawImage(img.getImage(), (int)(img.getX()), (int)(img.getY()), null);
}
for(MovingImage img : foreground)
{
g.drawImage(img.getImage(), (int)(img.getX()), (int)(img.getY()), null);
}
if(g instanceof Graphics2D)
{
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.scale(2, 2);
g2.setColor(Color.WHITE);
String milSpace;
if(timer%100 < 10)
milSpace = "0";
else
milSpace = "";
String secSpace;
if(timer/100 < 10)
secSpace = "0";
else
secSpace = "";
g2.drawString(secSpace + timer/100 + ":" + milSpace + timer%100, 10, 20);
//Debug
if(debug)
{
long currentTime = System.currentTimeMillis();
if (currentTime > nextSecond)
{
nextSecond += 1000;
frameInLastSecond = framesInCurrentSecond;
framesInCurrentSecond = 0;
}
framesInCurrentSecond++;
//g2.drawString("LagMS:" + (-frameRate - 10) + " FPS:" + frameInLastSecond, 20, 40); <-includes "lag"
g2.drawString("FPS:" + frameInLastSecond, 20, 40);
}
}
}
//Replaces the list of foreground images with the one given, and repaints the panel
public void updateImages(ArrayList<MovingImage> newForeground, ArrayList<MovingImage> newBackgrounds)
{
foreground = newForeground;
backgrounds = newBackgrounds;
//time checking
long time = System.currentTimeMillis();
lastTime = time;
repaint(); //This repaints stuff... you don't need to know how it works
}
Inside the primary class I made that includes a tick system, which causes it to be painted in the first place.
public void tick()
{
long lastTime = System.currentTimeMillis();
int place = 0;
boolean go = true;
while(go)
{
long time = System.currentTimeMillis(); //The current time
if(time - 10 > lastTime) //Update every .01 seconds, or 1 TICK (if time - 10 > lastTime)
{
lastTime = time;
//Reset the last time
place++;
imagePanel.incTime();
for(MovingImage object : movingObjects)
{
if(object instanceof Building)
{
object.incrementPosition(); //Augment position by velocity
if(place%500 == 0)//If 5 seconds have passed...
{
((Building) object).speedUp();//make it go a little faster!
}
}
if(object instanceof Player)
{
if(jumpKeyOn)
((Player) object).jump();//Initiate jump class assuming key is pressed
object.incrementPosition();
((Player) object).constrainPlayerToObjects(movingObjects, yMax);
if(object.getY()>yMax + 1000)
{
go = false;
}
}
}
//Repaint all the things in their new positions, possibly faster
for(MovingImage bg : backgrounds)
{
bg.incrementPosition();
if(place%500 == 0)
bg.setVelocity(bg.getXvel() - 0.1, 0);
}
/*
* Acceleration
*/
//Removes buildings once left screen
int i = 0;
while(i < movingObjects.size())
{
if(movingObjects.get(i) instanceof Building)
{
if(movingObjects.get(i).getX() + movingObjects.get(i).getImage().getWidth(null) < 0)
movingObjects.remove(i);
else
i++;
}
else
i++;
}
imagePanel.updateImages(movingObjects, backgrounds);
}
}
gameOver();
}
It's an endless loop that essentially runs the program. I used multiple ArrayLists in order to put different layers down. What am I doing that's causing it to lag? I'm still fairly new, but I'll answer any questions about the code or provide more details. I couldn't find any other questions that helped.
EDIT: There are some odd things I should mention. Occasionally it runs at nearly the full FPS, but most of the time not. I also noticed that when I ran another java program at the same time, it ran at nearly full speed.
EDIT 2: Should I include the entire primary class code and ImagePanel?
So I followed a free manual instructing how to write Asteroids in Java, and it runs great. I would like to add it into a program so I can call it, run it for a time, and then have it return an int. The program I'm making is an application, and I've been searching for a while trying and failing, figuring out how to make make it run as an application. The stuff I've read here says I should do this:
public class Main extends JFrame {
public static void main(String[] args) {
AsteroidsGame game = new AsteroidsGame();
JFrame frame = new JFrame();
frame.setLayout(new GridLayout (1,1));
frame.add(game);
frame.setSize(500,500);
game.init();
game.start();
frame.setVisible(true);
}
}
But then I get an error
Exception in thread "main" java.lang.NullPointerException
at AsteroidsGame.init(AsteroidsGame.java:49)
at Main.main(Main.java:15)
which points me to the below for line 49, and my game.init() for line 15.
g = img.getGraphics();
Here's the code for the AsteroidsGame, I'm too tired to think anymore. I just need to get it to run as an application, any other code changes are irrelevant at this point. I know it's a lot of code but I'm hoping it's a simple something I've missed. Any help is greatly appreciated. Thank you!
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Random;
public class AsteroidsGame extends Applet implements Runnable, KeyListener {
Thread thread;
Dimension dim;
Image img;
Graphics g;
long endTime, startTime, framePeriod;
Ship ship;
boolean paused; // True if the game is paused. Enter is the pause key
Shot[] shots;
int numShots;
boolean shooting;
Asteroid[] asteroids; // the array of asteroids
int numAsteroids; // the number of asteroids currently in the array
double astRadius, minAstVel, maxAstVel; // values used to create
// asteroids
int astNumHits, astNumSplit;
int level; // the current level number
int money;
double fuel;
Random rand = new Random();
public void init() {
resize(500, 500);
shots = new Shot[41]; // 41 is a shot's life period plus one.
// since at most one shot can be fired per frame,
// there will never be more than 41 shots if each one only
// lives for 40 frames.
numAsteroids = 0;
level = rand.nextInt(25)+1; // will be incremented to 1 when first level is set up
astRadius = 10; // values used to create the asteroids
minAstVel = 2;
maxAstVel = 8;
astNumHits = 3;
astNumSplit = 2;
money = 0;
fuel = 100;
endTime = 0;
startTime = 0;
framePeriod = 25;
addKeyListener(this); // tell it to listen for KeyEvents
dim = getSize();
img = createImage(dim.width, dim.height);
g = img.getGraphics();
thread = new Thread(this);
thread.start();
}
public void setUpNextLevel() { // start new level with one more asteroid
level++;
// create a new, inactive ship centered on the screen
// I like .35 for acceleration, .98 for velocityDecay, and
// .1 for rotationalSpeed. They give the controls a nice feel.
ship = new Ship(250, 250, 0, .35, .98, .1, 12);
numShots = 0; // no shots on the screen at beginning of level
paused = false;
shooting = false;
// create an array large enough to hold the biggest number
// of asteroids possible on this level (plus one because
// the split asteroids are created first, then the original
// one is deleted). The level number is equal to the
// number of asteroids at it's start.
asteroids = new Asteroid[level
* (int) Math.pow(astNumSplit, astNumHits - 1) + 1];
numAsteroids = level;
// create asteroids in random spots on the screen
for (int i = 0; i < numAsteroids; i++)
asteroids[i] = new Asteroid(Math.random() * dim.width,
Math.random() * dim.height, astRadius, minAstVel,
maxAstVel, astNumHits, astNumSplit);
}
public void paint(Graphics gfx) {
g.setColor(Color.black);
g.fillRect(0, 0, 500, 500);
for (int i = 0; i < numShots; i++)
// draw all the shots on the screen
shots[i].draw(g);
for (int i = 0; i < numAsteroids; i++)
asteroids[i].draw(g);
ship.draw(g); // draw the ship
g.setColor(Color.cyan); // Display level number in top left corner
g.drawString("Level " + level, 20, 20);
g.drawString("Money " + money, 80, 20);
g.drawString("Fuel " + fuel, 20, 50);
gfx.drawImage(img, 0, 0, this);
}
public void update(Graphics gfx) {
paint(gfx);
}
public void run() {
for (;;) {
startTime = System.currentTimeMillis();
// start next level when all asteroids are destroyed
if (numAsteroids <= 0)
setUpNextLevel();
if (!paused) {
ship.move(dim.width, dim.height); // move the ship
// move shots and remove dead shots
for (int i = 0; i < numShots; i++) {
shots[i].move(dim.width, dim.height);
// removes shot if it has gone for too long
// without hitting anything
if (shots[i].getLifeLeft() <= 0) {
// shifts all the next shots up one
// space in the array
deleteShot(i);
i--; // move the outer loop back one so
// the shot shifted up is not skipped
}
}
if(ship.accelerating && fuel>0){
fuel -= 1.5;
}
if(ship.accelerating && fuel == 0)
{
ship.accelerating = false;
}
// move asteroids and check for collisions
updateAsteroids();
if (shooting && ship.canShoot()) {
// add a shot on to the array
shots[numShots] = ship.shoot();
numShots++;
}
}
repaint();
try {
endTime = System.currentTimeMillis();
if (framePeriod - (endTime - startTime) > 0)
Thread.sleep(framePeriod - (endTime - startTime));
} catch (InterruptedException e) {
}
}
}
private void deleteShot(int index) {
// delete shot and move all shots after it up in the array
numShots--;
for (int i = index; i < numShots; i++)
shots[i] = shots[i + 1];
shots[numShots] = null;
}
private void deleteAsteroid(int index) {
// delete asteroid and shift ones after it up in the array
numAsteroids--;
for (int i = index; i < numAsteroids; i++)
asteroids[i] = asteroids[i + 1];
asteroids[numAsteroids] = null;
}
private void addAsteroid(Asteroid ast) {
// adds the asteroid passed in to the end of the array
asteroids[numAsteroids] = ast;
numAsteroids++;
}
private void updateAsteroids() {
for (int i = 0; i < numAsteroids; i++) {
// move each asteroid
asteroids[i].move(dim.width, dim.height);
// check for collisions with the ship, restart the
// level if the ship gets hit
if (asteroids[i].shipCollision(ship)) {
money+= Math.random()*1000;
deleteAsteroid(i);
//level--; // restart this level
//numAsteroids = 0;
return;
}
// check for collisions with any of the shots
for (int j = 0; j < numShots; j++) {
if (asteroids[i].shotCollision(shots[j])) {
// if the shot hit an asteroid, delete the shot
deleteShot(j);
// split the asteroid up if needed
if (asteroids[i].getHitsLeft() > 1) {
for (int k = 0; k < asteroids[i].getNumSplit(); k++)
addAsteroid(asteroids[i].createSplitAsteroid(
minAstVel, maxAstVel));
}
// delete the original asteroid
deleteAsteroid(i);
j = numShots; // break out of inner loop - it has
// already been hit, don't need to check
// for collision with other shots
i--; // don't skip asteroid shifted back into
// the deleted asteroid's position
}
}
}
}
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
// These first two lines allow the asteroids to move
// while the player chooses when to enter the game.
// This happens when the player is starting a new life.
if (!ship.isActive() && !paused)
ship.setActive(true);
else {
paused = !paused; // enter is the pause button
if (paused) // grays out the ship if paused
ship.setActive(false);
else
ship.setActive(true);
}
} else if (paused || !ship.isActive()) // if the game is
return; // paused or ship is inactive, do not respond
// to the controls except for enter to unpause
else if (e.getKeyCode() == KeyEvent.VK_UP)
{if(fuel>0){
ship.setAccelerating(true);
//fuel -= 1.5;
}
else{
fuel = 0;
}
}
else if (e.getKeyCode() == KeyEvent.VK_LEFT)
{
ship.setTurningLeft(true);
}
else if (e.getKeyCode() == KeyEvent.VK_RIGHT)
ship.setTurningRight(true);
//else if (e.getKeyCode() == KeyEvent.VK_CONTROL)
//shooting = true; // Start shooting if ctrl is pushed
}
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_UP)
ship.setAccelerating(false);
else if (e.getKeyCode() == KeyEvent.VK_LEFT)
ship.setTurningLeft(false);
else if (e.getKeyCode() == KeyEvent.VK_RIGHT)
ship.setTurningRight(false);
//else if (e.getKeyCode() == KeyEvent.VK_CONTROL)
//shooting = false;
}
public void keyTyped(KeyEvent e) {
}
}
I believe that createImage() is going to return null until the component is actually on-screen, wihch i believe is after the thread has started.
Instead of this:
img = createImage(dim.width, dim.height);
g = img.getGraphics();
thread = new Thread(this);
thread.start();
try:
new Thread(new Runnable() {
public void run() {
do {
try {
Thread.sleep(25);
} catch(InterruptedException e) {
System.out.println("wait interrupted");
break;
}
} while(!isDisplayable());
// Now it should be safe to make these calls.
img= createImage(dim.width, dim.height);
g = img.getGraphics();
start();
}
}).start();