Slow 2D Tile Engine

Hey, guys! I’m new to this forum and I’d like to say first that I was amused by the account activation.

I decided to join here because it seems like the most likely place I can finally find the solution to a problem I’ve been having for the last week… I’ve tried help from GameDev and from the Oracle forums over the last few days, but I haven’t received the solution to it yet.

Basically, what I have is a tile engine that I’ve constructed for a side-scrolling game I’m developing. It reads in a .png image that gets converted to an array representing the level’s tiles, which then is used to calculate the view that is drawn to the screen. The user can move the little character in the middle of the screen (which right now there is no collision detection, so he/she can move the player about in any direction regardless of what tile).

The problem is that the thing is super slow! Originally, I found that I was redrawing the view every time rather than shifting it, which was thought to have been the bottleneck, but I converted it to draw the view only when the player has moved to where a new view has to be calculated and rerendered, and the slowness is still there. My computer is a beast gaming machine, so I’m not thinking it’s computer performance related at all, but I must have messed up somewhere.

I’ll post the code for the classes that compose the tile engine below. I would greatly appreciate any and all help and/or criticism and/or advice pertaining to the tile engine that anyone is willing to offer. :] I apologize that this post will be so lengthy, but since there are several components to the engine rather than just one class, I don’t want to leave a stone unturned for anybody. Thanks again! :]

This class represents the Player and manages his current state of animation:


/* This class's purpose will be to store the data of the player character for movement and appearance. It will also
 * take input. The player image will be created as a sheet of animation frames and split by the class. */
 
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import java.awt.*;
 
public class Player
{
    //
    // CONSTANTS
    //
    
    private final int TILE_SIZE = 32;
    private final int SCREEN_WIDTH = 1280;
    private final int SCREEN_HEIGHT = 768;
    
    //
    // END OF CONSTANTS
    //
    
    // image to store the player's frames of animation as a sheet/loaded first
    private BufferedImage playerSheet;
    
    // image array to store the individual frames of animation/split by program
    private BufferedImage playerFrames[];
    
    // index used to represent the current frame to draw on the screen of the player
    private int currentIndex = 0;
    
    // enum representing the player's state of movement for animation
    private enum PlayerState {STILL_LEFT, STILL_RIGHT, MOVE_LEFT, MOVE_RIGHT, JUMP}
    
    // variable used to represent the player's current animation state
    private PlayerState currentState;
    
    // variable to keep track of last direction the character was facing when going back to still
    private PlayerState lastState;
    
    // long used to represent the system time, used as a timer
    private long movementTimer;
    
    // long used to represent the previously used time as a timer
    private long stillTimer;
    
    // CONSTRUCTOR
    public Player()
    {
        playerSheet = makeColorTransparent("playersheet1.png", 0xFFFF65F6);
        playerFrames = splitImage(playerSheet, 4, 2);
        movementTimer = System.nanoTime();
        stillTimer = movementTimer;
        currentState = PlayerState.STILL_LEFT;
        lastState = PlayerState.STILL_LEFT;
    }
    
    // this method will draw to whatever graphics context is passed to the method (game window)
    public void drawPlayer(Graphics gr, int x, int y)
    {
        gr.drawImage(playerFrames[currentIndex], x, y, null);
    }
    
    // method to simply load an image from a path
    private static BufferedImage loadImage(String ref)
    {
        BufferedImage bimg = null;
           
        try
        {
           bimg = ImageIO.read(new File(ref));
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
       
        return bimg;
    }
    
    // method to create a tile array for tile sets
    private BufferedImage[] splitImage(BufferedImage img, int cols, int rows)
    {
        int w = img.getWidth() / cols;
        int h = img.getHeight() / rows;
        int num = 0;
        
        BufferedImage imgs[] = new BufferedImage[w * h];
        
        for (int y = 0; y < rows; y++)
        {
            for (int x = 0; x < cols; x++)
            {
                imgs[num] = new BufferedImage(w, h, img.getType());
                
                Graphics2D g = imgs[num].createGraphics();
                g.drawImage(img, 0, 0, w, h, w * x, h * y, w * x + w, h * y + h, null);
                g.dispose();
                num++;
            }
        }
        
        return imgs;
    }
    
    // image-loading method that will also alpha the color key for each tile
    public static BufferedImage makeColorTransparent(String ref, int color)
    {
        BufferedImage image = loadImage(ref);
        BufferedImage dimg = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = dimg.createGraphics();
        g.setComposite(AlphaComposite.Src);
        g.drawImage(image, null, 0, 0);
        g.dispose();
        
        for (int i = 0; i < dimg.getHeight(); i++)
        {
            for (int j = 0; j < dimg.getWidth(); j++)
            {
                if (dimg.getRGB(j, i) == color)
                {
                    dimg.setRGB(j, i, 0x8F1C1C);
                }
            }
        }
        
        return dimg;
    }
    
    // method to update the player based on user input
    public void updatePlayer(int input)
    {
        // update the still timer to manage when the last key press was
        stillTimer = System.nanoTime();
        
        switch (input)
        {
            // up
            case 0:
                break;
            // left
            case 1:
                if (currentState != PlayerState.MOVE_LEFT)
                {
                    movementTimer = System.nanoTime();
                }
                currentState = PlayerState.MOVE_LEFT;
                lastState = PlayerState.MOVE_LEFT;
                break;
            // right
            case 2:
                if (currentState != PlayerState.MOVE_RIGHT)
                {
                    movementTimer = System.nanoTime();
                }
                currentState = PlayerState.MOVE_RIGHT;
                lastState = PlayerState.MOVE_RIGHT;
                break;
            // down
            case 3:
                break;
            // jump
            case 4:
                break;
            
            // still left
            case 5:
                currentState = PlayerState.STILL_LEFT;
                lastState = PlayerState.STILL_LEFT;
                break;
            
            // still right
            case 6:
                currentState = PlayerState.STILL_RIGHT;
                lastState = PlayerState.STILL_RIGHT;
                break;
        }
    }
    
    // method to manage the player's animation
    public void animatePlayer()
    {   
        switch (currentState)
        {
            case STILL_LEFT:
                currentIndex = 0;
                break;
            case STILL_RIGHT:
                currentIndex = 4;
                break;
            case MOVE_LEFT:
                // if set to a still frame, set it to start running left
                if (currentIndex == 0 || currentIndex == 4)
                {
                    currentIndex = 1;
                }
                // if a 300 nanosecond gap has passed, allow the next frame
                if (System.nanoTime() - movementTimer > 100000000)
                {
                    if (currentIndex == 1)
                    {
                        currentIndex = 2;
                    }
                    else
                    {
                        currentIndex = 1;
                    }
                    
                    movementTimer = System.nanoTime();
                }
                break;
            case MOVE_RIGHT:
                // if set to a still frame, set it to start running right
                if (currentIndex == 0 || currentIndex == 4)
                {
                    currentIndex = 5;
                }
                // if a 300 nanosecond gap has passed, allow the next frame
                if (System.nanoTime() - movementTimer > 100000000)
                {
                    if (currentIndex == 5)
                    {
                        currentIndex = 6;
                    }
                    else
                    {
                        currentIndex = 5;
                    }
                    
                    movementTimer = System.nanoTime();
                }
                break;
            case JUMP:
            
                break;
        }
    }
}

This class represents the Level object that stores the .png images that will compose the drawn level and the array that represents the tiles in a more memory-efficient manner. It also manages updating the level’s view and rendering it, as well as drawing it to the screen:


/* The purpose of this class will be to load and manage a level, including its camera. */

import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import java.awt.*;

public class Level
{
    //
    // CONSTANTS
    //
    
    private final int TILE_SIZE = 32;
    private final int SCREEN_WIDTH = 1280;
    private final int SCREEN_HEIGHT = 768;
    
    //
    // END OF CONSTANTS
    //
    
    // stores the pixel image of the current level
    private BufferedImage levelImage;
    
    // stores the width and height of the level
    private int width, height;
    
    // stores the name of the level
    private String levelName;
    
    // stores collision map for level
    private LevelCollisions myCollisions;
    
    // stores the tile types in an array as assigned by colors
    private int levelTiles[][];
    
    // image used as the sheet for the level's tiles
    private BufferedImage tileSheet;
    
    // image array used to store the different tiles
    private BufferedImage[] tiles;
    
    // the image which represents the current view of the level
    private BufferedImage cameraImage;
    
    // images to represent buffers for the sides, left, right, top, bottom
    private BufferedImage sideBufferL, sideBufferR, sideBufferT, sideBufferB;
    
    // Graphics context of the camera image
    private Graphics cameraG;
    
    // variables to represent the level's offset from the top left corner while moving
    private int offsetX, offsetY;
    
    // variables to represent the level's pixel map coordinate
    private int coordX, coordY;
    
    // 
    // STATIC COLOR VARIABLES
    //
    
    private static final int SPACE_COLOR = 0xFF000000;
    private static final int WALL_COLOR = 0xFFFFFFFF;
    
    //
    // END OF STATIC COLOR VARIABLES
    //
    
    //
    // CONSTRUCTOR
    //
    public Level(String level)
    {
        // load level image and collision map
        levelName = level;
        levelImage = loadImage(level + ".png");
        myCollisions = new LevelCollisions(level + "Collision");
        levelTiles = loadLevel();   
        
        // create blank camera canvas
        cameraImage = new BufferedImage(1280, 768, BufferedImage.TYPE_INT_ARGB);
        cameraImage.createGraphics();
        cameraG = cameraImage.getGraphics();
        
        // offsets start at 0
        offsetX = offsetY = 0;
        
        // coordinate starts at bottom right
        coordX = 600;
        coordY = 383;
        
        // fill tile images
        tileSheet = loadImage("obstacletiles.png");
        tiles = splitImage(tileSheet, 2, 1);
        
        this.renderLevel();
    }
    
    // method to load the color values into an array
    public int[][] loadLevel()
    {
        height = levelImage.getHeight();
        width = levelImage.getWidth();
        
        int levelValues[][] = new int[width][height];
        
        // fill array with color values layer by layer
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                levelValues[x][y] = levelImage.getRGB(x, y);
            }   
        }
        
        return levelValues;
    }
    
    // method to get the tile color from a given tile
    public int getTile(int x, int y)
    {
        return levelTiles[x][y];
    }
    
    // method to draw the current camera view of the level on the screen
    public void drawLevel(Graphics gr, int x, int y)
    {
        gr.drawImage(cameraImage, x + offsetX, y + offsetY, null);
    }
    
    // method to render the actual image before drawing it
    public void renderLevel()
    {
        // keeps track of graphics coordinate
        int x, y;
        
        // keeps track of tile to draw
        int tileX, tileY;
        
        tileY = coordY;
        
        // draw all the tiles based on offsets, layer by layer
        for (y = 0; y < SCREEN_HEIGHT; y += TILE_SIZE)
        {
            tileX = coordX;
            for (x = 0; x < SCREEN_WIDTH; x += TILE_SIZE)
            {
                // determine which tile to draw based on tile color in array
                switch (this.getTile(tileX, tileY))
                {
                    case SPACE_COLOR:
                        cameraG.drawImage(tiles[0], x, y, null);
                        break;
                        
                    case WALL_COLOR:
                        cameraG.drawImage(tiles[1], x, y, null);
                        break;
                }
                
                tileX++;
            }
            
            tileY++;
        }
        
        // steps to take in case of an offset
        if (offsetX > 0)
        {
        
        }
        
        if (offsetX < 0)
        {
        
        }
        
        if (offsetY < 0)
        {
        
        }
        
        if (offsetY > 0)
        {
        
        }
    }
    
    // method to update the level's current position for the camera
    public void updateLevel(int input)
    {
        switch (input)
        {
            // up
            case 0:
                // update offset up if not too far up
                if (coordY > 30)
                {
                    offsetY += 2;
                }   
                
                // if a tile length has been moved, then offset becomes 0 and coordY is decreased
                if (offsetY >= TILE_SIZE)
                {
                    offsetY = 0;
                    coordY--;
                    this.renderLevel();
                }
                break;
            // left
            case 1:
                // update offset to the left if not too far left
                if (coordX > 30)
                {
                    offsetX += 2;
                }
                
                // if a tile length has been moved, then offset becomes 0 and coordX is decreased
                if (offsetX >= TILE_SIZE)
                {
                    offsetX = 0;
                    coordX--;
                    this.renderLevel();
                }
                break;
            // right
            case 2:
                // update offset to the right if not too far right
                if (coordX < width - 30)
                {
                    offsetX -= 2;
                }
                
                // if a tile length has been moved, then offset becomes 0 and coordX is increased
                if (offsetX <= -TILE_SIZE)
                {
                    offsetX = 0;
                    coordX++;
                    this.renderLevel();
                }
                break;
            // down
            case 3:
                // update offset down if not too far down
                if (coordY < height - 30)
                {
                    offsetY -= 2;
                }
                
                // if a tile legnth has been moved, then offset becomes 0 and coordY is increased
                if (offsetY <= -TILE_SIZE)
                {
                    offsetY = 0;
                    coordY++;
                    this.renderLevel();
                }
                break;
            // jump
            case 4:
                break;
        }
    }
    
    // method to simply load an image from a path
    public static BufferedImage loadImage(String ref)
    {
        BufferedImage bimg = null;
           
        try
        {
           bimg = ImageIO.read(new File(ref));
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
       
        return bimg;
    }
    
    // method to create a tile array for tile sets
    public static BufferedImage[] splitImage(BufferedImage img, int cols, int rows)
    {
        int w = img.getWidth() / cols;
        int h = img.getHeight() / rows;
        int num = 0;
        
        BufferedImage imgs[] = new BufferedImage[w * h];
        
        for (int y = 0; y < rows; y++)
        {
            for (int x = 0; x < cols; x++)
            {
                imgs[num] = new BufferedImage(w, h, img.getType());
                
                Graphics2D g = imgs[num].createGraphics();
                g.drawImage(img, 0, 0, w, h, w * x, h * y, w * x + w, h * y + h, null);
                g.dispose();
                num++;
            }
        }
        
        return imgs;
    }
    
    // image-loading method that will also alpha the color key for each tile
    public static BufferedImage makeColorTransparent(String ref, int color)
    {
        BufferedImage image = loadImage(ref);
        BufferedImage dimg = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = dimg.createGraphics();
        g.setComposite(AlphaComposite.Src);
        g.drawImage(image, null, 0, 0);
        g.dispose();
        
        for (int i = 0; i < dimg.getHeight(); i++)
        {
            for (int j = 0; j < dimg.getWidth(); j++)
            {
                if (dimg.getRGB(j, i) == color)
                {
                    dimg.setRGB(j, i, 0x8F1C1C);
                }
            }
        }
        
        return dimg;
    }
}

This is the code for the renderer itself, the class I called LevelRenderer. It calls all of the methods that perform the drawing and updating:


/* This class's job is to manage a Player and Level object and call their render and update routines. */
 
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.JFrame;
import javax.swing.*;
import java.util.Random;
import java.awt.Color;

public class LevelRenderer extends JFrame
{   
    //
    // CONSTANTS
    //
    
    private final int TILE_SIZE = 32;
    private final int SCREEN_WIDTH = 1280;
    private final int SCREEN_HEIGHT = 768;
    
    //
    // END OF CONSTANTS
    //
    
    // will be used as a buffer before everything is drawn to the screen
    private BufferedImage buffer2;
    
    // back buffer
    private BufferStrategy buffer;
    
    // character object
    private Player myPlayer;
    
    // level object
    private Level myLevel;
    
    // screen object
    private Screen s;
    
    // graphics object of the buffer
    private Graphics gr;
    
    // Graphics object for the buffer strategy
    private Graphics graphics;
    
    // boolean to determine when to end the game
    private boolean endGame;
    
    // CONSTRUCTOR
    public LevelRenderer()
    {
        setPreferredSize(new Dimension(1280, 768));
        setIgnoreRepaint( true );
        setUndecorated( true );

        setFocusable(true);
        requestFocus();
        
        setResizable(false);
        
        addKeyListener( new KeyAdapter() 
        {
            public void keyPressed(KeyEvent e)
            { 
                processKey(e);  
            }
            
            public void keyReleased(KeyEvent e)
            {
                processRelease(e);
            }
        });
        
        buffer2 = new BufferedImage(1280, 768, BufferedImage.TYPE_INT_ARGB);
        
        buffer2.createGraphics();
        gr = buffer2.getGraphics();
        
        myPlayer = new Player();
        myLevel = new Level("obstaclemap");
        
        endGame = false;
    }
    
    // method to simply load an image from a path
    public static BufferedImage loadImage(String ref)
    {
        BufferedImage bimg = null;
           
        try
        {
           bimg = ImageIO.read(new File(ref));
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
       
        return bimg;
    }
    
    // Run method for class
    public void run(DisplayMode dm)
	{	
        setBackground(Color.WHITE);
		s = new Screen();
        s.setFullScreen(dm, this);
        
        this.createBufferStrategy( 2 );
        buffer = this.getBufferStrategy();

        while (!endGame)
        {
            try
            {   
                // edit player and level
                myPlayer.animatePlayer();
                myLevel.drawLevel(gr, 0, 0);
                myPlayer.drawPlayer(gr, (SCREEN_WIDTH / 2) - TILE_SIZE / 2, (SCREEN_HEIGHT / 2) - TILE_SIZE);
                
                graphics = buffer.getDrawGraphics();
                graphics.drawImage(buffer2, 0, 0, null);
                
                if( !buffer.contentsLost() )
                {
                    buffer.show();
                }
            }
            catch (Exception ex) 
            { 
                System.err.println("Game Update Error: " + ex);
            }
            
            try
            {
                Thread.sleep(10);
            }
            catch (Exception ex)
            {
                System.out.println("Can't sleep!");
            }
        }
        
		s.restoreScreen();
	} 
    
    // method to handle inputs and adjust the player accordingly
    public void processKey(KeyEvent e)
    {
        int keyCode = e.getKeyCode();
        boolean moved = false;
        int xDisplace, yDisplace;
        
        // termination key
        if (keyCode == KeyEvent.VK_ESCAPE)
        {
            endGame = true;
        }
        
        // 0 - up
        // 1 - left
        // 2 - right
        // 3 - down
        // 4 - jump
        
        switch (keyCode)
        {
            case KeyEvent.VK_UP:
                myPlayer.updatePlayer(0);
                myLevel.updateLevel(0);
                break;
            case KeyEvent.VK_LEFT:
                myPlayer.updatePlayer(1);
                myLevel.updateLevel(1);
                break;
            case KeyEvent.VK_RIGHT:
                myPlayer.updatePlayer(2);
                myLevel.updateLevel(2);
                break;
            case KeyEvent.VK_DOWN:
                myPlayer.updatePlayer(3);
                myLevel.updateLevel(3);
                break;
            case KeyEvent.VK_SPACE:
                myPlayer.updatePlayer(4);
                myLevel.updateLevel(4);
                break;
        }
    }
    
    // method to handle inputs and adjust the player accordingly
    public void processRelease(KeyEvent e)
    {
        int keyCode = e.getKeyCode();
        boolean moved = false;
        int xDisplace, yDisplace;
        
        // 0 - up
        // 5 - left
        // 6 - right
        // 3 - down
        // 4 - jump
        
        switch (keyCode)
        {
            case KeyEvent.VK_UP:
                myPlayer.updatePlayer(0);
                break;
            case KeyEvent.VK_LEFT:
                myPlayer.updatePlayer(5);
                break;
            case KeyEvent.VK_RIGHT:
                myPlayer.updatePlayer(6);
                break;
            case KeyEvent.VK_DOWN:
                myPlayer.updatePlayer(3);
                break;
            case KeyEvent.VK_SPACE:
                myPlayer.updatePlayer(4);
                break;
        }
    }
}

Lastly, this is the file that just has the main loop in it to call the other classes:


/* This class is what controls and runs the main game loop. */

import javax.swing.JFrame;
import javax.swing.*;
import java.awt.*;

public class GameTest
{   
    public static void main(String args[])
    {
        // initialize the level, player, renderer, and display mode
        LevelRenderer myRenderer = new LevelRenderer();
        DisplayMode dm = new DisplayMode(1280, 768, 16, DisplayMode.REFRESH_RATE_UNKNOWN);
        
        myRenderer.run(dm);
    }
}

Once again, I apologize for posting so much, especially as a first post. I really hope you guys will be able to assist me in locating the problem! :] Thanks so much!

Edit: Also, I put the application through the Profiler in Netbeans and it seems that the drawLevel() method is incurring a lot of overhead, as is the run method. However, I’m still not sure as to how to go about fixing this.

Best regards,
Colton

The few improvements you can make:

  • You don’t need to draw into a buffer and then draw that buffer to the screen. Since you are using BufferStrategy, it draws it into a back buffer for you until you call show().
  • Follow BufferStrategy’s javadoc’s instructions on how to correctly put the drawing code in while loops.
  • You are drawing a 1280x768 image in drawLevel()!! That might be a big cause of the slowdown.

EDIT: Aha, I found out why the code looks familiar: been watching Bucky lately? :wink:

If you post the complete source as a .rar uploaded to somewhere I can try it out on my computer. It’s kind of time consuming to catch the flow of a program by just looking at the code. But do listen to Ra4king’s advice first and see if it solves the problem. I’m no Java2D expert.

Thanks for the responses, guys!

ra4king: I made the changes you suggested. However, it did not make the program any faster. Is there a way I can restructure or modify the code so that it will process any faster than this? It seems like the bottleneck may be the design of the method and the data’s structure. Else, how could scrollers achieve the speed that they get? Appreciate the assistance!

theagentd: I do not have a .rar compressor; however, I uploaded it as a .zip to the following URL: http://www.mediafire.com/?md5faqehgkub4z2
I would of course appreciate if you could take a look and see if there is anything that you think could be done to speed it up :] It doesn’t seem to me like a side scroller should be a troublesome program to make, but I think I just have it designed improperly.

Thank you guys very much! Looking forward to see if there is any possible solution in sight.

Colton

Have you tried using a debugger? Also try putting println()s that time how long method calls take.

ra4king,

Btw, I forgot to say, yes, I had watched the Bucky videos. :stuck_out_tongue: He actually derived much of what he used in that video from a book called Developing Games in Java, which I realized once I gave it a read; that, or the two are just uncannily similar. I like his manner and way of explaining things; he does a good job.

Anywho, I used the Profiler in Netbeans and found that the drawLevel method was taking up 21% of the program’s CPU usage, whereas nearly every other method didn’t even scratch the surface. When it has to redraw the whole level view, it usually takes up 26 ms, whereas otherwise it takes up about 0-1 to just shift the view.

Colton

Well since you are drawing a 1280x768 BufferedImage to another Image, that sounds about right.

ra4king,

Have you seen or played the game Terraria?

Some comments. Don’t know if they’ll help.

  • +1 for what ra4king said about the correct use of BufferStrategy.

  • Create ‘compatible’ images rather than instantiating BufferedImages directly. That is, use GraphicsConfiguration.createCompatibleImage(width,height,transparency). Search the forums for more details. I can’t promise it will help, but in theory it will mean that the BufferedImage can use hardware acceleration.

  • Don’t call nanoSeconds() all over the place. Calculate the time that has passed since the last update in the game loop, and pass that ‘delta time’ to the update methods. If each update method calculates its own delta time, there’s a danger of different game objects getting out of sync.

  • As I recall, the Java documentation recommends creating then disposing of Graphics objects whenever you change a BufferedImage, rather than hanging on to a reference to the same one indefinitely. I don’t know how much this affects things in practice.

  • You can cast your Graphics references to Graphics2D if you like. This will give you more functions to play with.

  • Your KeyListener methods should just record which keys are pressed or released. They shouldn’t do any real work. Currently you appear to be using the methods to redraw the entire level. Calls to the update functions should happen in the game loop itself, not in the key listeners. The problem is that the key events are processed in a different thread to the game loop, and you’re going to run into some nasty thread-safety glitches if you’re not careful.

  • Try chopping your code down so that it’s just a game loop that redraws the level tiles each frame. That’ll be easier to debug (for you and, more importantly, for us!).

Simon

I tried to run the code and got constant 60 FPS. My laptop owned your gaming beast… >_>

I get 60 fps on my computer as well. It’s not the FPS that’s the issue, it’s how fast the scrolling is on the level that’s bugging me. But thank you for taking a look at the code, in any case, theagentd.

Simon,

Thank you for all those tips… I’m going to go over them tomorrow and refactor my code to reflect them. (Gotta hit the sack soon.) All of those make sense, and since this is my first really big game project, I want to get as many things right as I can, for now and for future games. Once I’ve made the changes, I’ll post back to inform everyone of how those tips helped the code. I’m hoping there’s some way I can get this to all work without having to resort to learning OpenGL. XD

Colton


How can it be slow and have constant 60 FPS?! Your problem has absolutely nothing to do with rendering. It’s your input handling that’s the problem. Just move the input handling to the game loop and do things properly and your “slowness” will magically disappear. You should have described your problem better…

theagentd,

Forgive me for not explaining it better. I will work on fixing that soon and post regarding the performance increase. Question, though: Must I place the KeyListener in main instead of in the LevelRenderer’s run method, or is there an alternate means of accomplishing this? I appreciate your help.

Colton

Yes I have heard of Terraria. But what does that have to do with this topic?

Where KeyListener is and the way you have things set up is fine for now. If you want to get rid of the pause that comes between the first key press and all further key presses when holding down a key, use your listeners to turn boolean variables that hold the state of certain keys into true if the key is pressed and false when released.

If your game runs at 60FPS, then there is nothing wrong with rendering. What exactly is being slow?

Just keep a boolean for each key (boolean leftKey, rightKey, etc;) and in keyPressed() set the right boolean to true. In keyReleased() set it to false. Then in your gameloop (your run() method) do a check for each of the key booleans and update the player’s movement according to which keys are pressed.

You’re slowness is as I’ve said not related to rendering or computer performance at all. It’s how the logic is done.

[quote]- Don’t call nanoSeconds() all over the place. Calculate the time that has passed since the last update in the game loop, and pass that ‘delta time’ to the update methods. If each update method calculates its own delta time, there’s a danger of different game objects getting out of sync.
[/quote]
Definitely do this. When you’ve done that, move all the logic out of the key listener functions.

The player movement.

ra4king and theagentd,

I was just asking because that kind of smooth scrolling is the kind I’d like to see with my game… and the same sort of tile size. But yes, I will update the nanoSeconds() functions so that they are passed in so everything is universal. I appreciate the advice. Also, I’ve changed the logic of the whole program so that the update methods switch booleans on and off, and as a result, I’ve noticed a lot of improvement and it looks much smoother. However, the character movement is still on the slow side, much more so that what I’m going for. I’ll post the updated code below.

LevelRenderer:


/* This class's job is to manage a Player and Level object and call their render and update routines. */
 
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.JFrame;
import javax.swing.*;
import java.util.Random;
import java.awt.Color;

public class LevelRenderer extends JFrame
{   
    //
    // CONSTANTS
    //
    
    private final int TILE_SIZE = 32;
    private final int SCREEN_WIDTH = 1280;
    private final int SCREEN_HEIGHT = 768;
    
    //
    // END OF CONSTANTS
    //
    
    // back buffer
    private BufferStrategy buffer;
    
    // character object
    private Player myPlayer;
    
    // level object
    private Level myLevel;
    
    // screen object
    private Screen s;
    
    // Graphics object for the buffer strategy
    private Graphics graphics;
    
    // boolean to determine when to end the game
    private boolean endGame;
    
    // CONSTRUCTOR
    public LevelRenderer()
    {
        setPreferredSize(new Dimension(1280, 768));
        setIgnoreRepaint( true );
        setUndecorated( true );

        setFocusable(true);
        requestFocus();
        
        setResizable(false);
        
        addKeyListener( new KeyAdapter() 
        {
            public void keyPressed(KeyEvent e)
            { 
                processKey(e);  
            }
            
            public void keyReleased(KeyEvent e)
            {
                processRelease(e);
            }
        });
        
        myPlayer = new Player();
        myLevel = new Level("obstaclemap");
        
        endGame = false;
    }
    
    // method to simply load an image from a path
    public static BufferedImage loadImage(String ref)
    {
        BufferedImage bimg = null;
           
        try
        {
           bimg = ImageIO.read(new File(ref));
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
       
        return bimg;
    }
    
    // Run method for class
    public void run(DisplayMode dm)
	{	
        setBackground(Color.WHITE);
		s = new Screen();
        s.setFullScreen(dm, this);
        
        this.createBufferStrategy( 2 );
        buffer = this.getBufferStrategy();
        
        // Variables for counting frames per second
        int fps = 0;
        int frames = 0;
        long totalTime = 0;
        long curTime = System.currentTimeMillis();
        long lastTime = curTime;

        while (!endGame)
        {
            do
            {
                do
                {
                    try
                    {   
                        // count Frames per second...
                        lastTime = curTime;
                        curTime = System.currentTimeMillis();
                        totalTime += curTime - lastTime;
                        if( totalTime > 1000 ) 
                        {
                            totalTime -= 1000;
                            fps = frames;
                            frames = 0;
                        } 
                        ++frames;
                        
                        // get new draw graphics
                        graphics = buffer.getDrawGraphics();
                        
                        // edit/draw player and level
                        myPlayer.animatePlayer();
                        myLevel.updateOffsets();
                        myLevel.drawLevel(graphics, 0, 0);
                        myPlayer.drawPlayer(graphics, (SCREEN_WIDTH / 2) - TILE_SIZE / 2, (SCREEN_HEIGHT / 2) - TILE_SIZE);
                        
                        // display frames per second...
                        graphics.setFont( new Font( "Courier New", Font.PLAIN, 12 ) );
                        graphics.setColor( Color.GREEN );
                        graphics.drawString( String.format( "FPS: %s", fps ), 20, 20 );
                        
                        graphics.dispose();
                    }
                    catch (Exception ex) 
                    { 
                        System.err.println("Game Update Error: " + ex);
                    }
                    
                    try
                    {
                        Thread.sleep(2);
                    }
                    catch (Exception ex)
                    {
                        System.out.println("Can't sleep!");
                    }
                } while (buffer.contentsRestored());
                
                buffer.show();
                
            } while (buffer.contentsLost());
        }

		s.restoreScreen();
	} 
    
    // method to handle inputs and adjust the player accordingly
    public void processKey(KeyEvent e)
    {
        int keyCode = e.getKeyCode();
        boolean moved = false;
        int xDisplace, yDisplace;
        
        // termination key
        if (keyCode == KeyEvent.VK_ESCAPE)
        {
            endGame = true;
        }
        
        // 0 - up
        // 1 - left
        // 2 - right
        // 3 - down
        // 4 - jump
        
        switch (keyCode)
        {
            case KeyEvent.VK_UP:
                myPlayer.updatePlayer(0);
                myLevel.updateLevel(0);
                break;
            case KeyEvent.VK_LEFT:
                myPlayer.updatePlayer(1);
                myLevel.updateLevel(1);
                break;
            case KeyEvent.VK_RIGHT:
                myPlayer.updatePlayer(2);
                myLevel.updateLevel(2);
                break;
            case KeyEvent.VK_DOWN:
                myPlayer.updatePlayer(3);
                myLevel.updateLevel(3);
                break;
            case KeyEvent.VK_SPACE:
                myPlayer.updatePlayer(4);
                break;
        }
    }
    
    // method to handle inputs and adjust the player accordingly
    public void processRelease(KeyEvent e)
    {
        int keyCode = e.getKeyCode();
        boolean moved = false;
        int xDisplace, yDisplace;
        
        // 0 - up
        // 5 - left
        // 6 - right
        // 3 - down
        // 4 - jump
        
        switch (keyCode)
        {
            case KeyEvent.VK_UP:
                myPlayer.updatePlayer(0);
                myLevel.updateLevel(5);
                break;
            case KeyEvent.VK_LEFT:
                myPlayer.updatePlayer(5);
                myLevel.updateLevel(6);
                break;
            case KeyEvent.VK_RIGHT:
                myPlayer.updatePlayer(6);
                myLevel.updateLevel(7);
                break;
            case KeyEvent.VK_DOWN:
                myPlayer.updatePlayer(3);
                myLevel.updateLevel(8);
                break;
            case KeyEvent.VK_SPACE:
                myPlayer.updatePlayer(4);
                break;
        }
    }
}

Level:


/* The purpose of this class will be to load and manage a level, including its camera. */

import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import java.awt.*;

public class Level
{
    //
    // CONSTANTS
    //
    
    private final int TILE_SIZE = 32;
    private final int SCREEN_WIDTH = 1280;
    private final int SCREEN_HEIGHT = 768;
    
    //
    // END OF CONSTANTS
    //
    
    // stores the pixel image of the current level
    private BufferedImage levelImage;
    
    // stores the width and height of the level
    private int width, height;
    
    // stores the name of the level
    private String levelName;
    
    // stores collision map for level
    private LevelCollisions myCollisions;
    
    // stores the tile types in an array as assigned by colors
    private int levelTiles[][];
    
    // image used as the sheet for the level's tiles
    private BufferedImage tileSheet;
    
    // image array used to store the different tiles
    private BufferedImage[] tiles;
    
    // the image which represents the current view of the level
    private BufferedImage cameraImage;
    
    // images to represent buffers for the sides, left, right, top, bottom
    private BufferedImage sideBufferL, sideBufferR, sideBufferT, sideBufferB;
    
    // Graphics context of the camera image
    private Graphics cameraG;
    
    // variables to represent the level's offset from the top left corner while moving
    private int offsetX, offsetY;
    
    // variables to represent the level's pixel map coordinate
    private int coordX, coordY;
    
    // variables that represent where the level is moving
    private boolean isLeft, isUp, isDown, isRight;
    
    // 
    // STATIC COLOR VARIABLES
    //
    
    private static final int SPACE_COLOR = 0xFF000000;
    private static final int WALL_COLOR = 0xFFFFFFFF;
    
    //
    // END OF STATIC COLOR VARIABLES
    //
    
    //
    // CONSTRUCTOR
    //
    public Level(String level)
    {
        // load level image and collision map
        levelName = level;
        levelImage = loadImage(level + ".png");
        myCollisions = new LevelCollisions(level + "Collision");
        levelTiles = loadLevel();   
        
        // create blank camera canvas
        cameraImage = new BufferedImage(1280, 768, BufferedImage.TYPE_INT_ARGB);
        cameraImage.createGraphics();
        cameraG = cameraImage.getGraphics();
        
        // offsets start at 0
        offsetX = offsetY = 0;
        
        // make sure the level is set not to move
        isLeft = isRight = isUp = isDown = false;
        
        // coordinate starts at bottom right
        coordX = 600;
        coordY = 383;
        
        // fill tile images
        tileSheet = loadImage("obstacletiles.png");
        tiles = splitImage(tileSheet, 2, 1);
        
        this.redrawCamera();
    }
    
    // method to load the color values into an array
    public int[][] loadLevel()
    {
        height = levelImage.getHeight();
        width = levelImage.getWidth();
        
        int levelValues[][] = new int[width][height];
        
        // fill array with color values layer by layer
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                levelValues[x][y] = levelImage.getRGB(x, y);
            }   
        }
        
        return levelValues;
    }
    
    // method to get the tile color from a given tile
    public int getTile(int x, int y)
    {
        return levelTiles[x][y];
    }
    
    // method to draw the current camera view of the level on the screen
    public void drawLevel(Graphics gr, int x, int y)
    {
        gr.drawImage(cameraImage, x + offsetX, y + offsetY, null);
    }
    
    // method to redraw camera, needed every time the view is offset by 32
    public void redrawCamera()
    {
        // keeps track of graphics coordinate
        int x, y;
        
        // keeps track of tile to draw
        int tileX, tileY;
        
        tileY = coordY;
        
        // draw all the tiles based on offsets, layer by layer
        for (y = 0; y < SCREEN_HEIGHT; y += TILE_SIZE)
        {
            tileX = coordX;
            for (x = 0; x < SCREEN_WIDTH; x += TILE_SIZE)
            {
                // determine which tile to draw based on tile color in array
                switch (this.getTile(tileX, tileY))
                {
                    case SPACE_COLOR:
                        cameraG.drawImage(tiles[0], x, y, null);
                        break;
                        
                    case WALL_COLOR:
                        cameraG.drawImage(tiles[1], x, y, null);
                        break;
                }
                
                tileX++;
            }
            
            tileY++;
        }
        
        // steps to take in case of an offset
        if (offsetX > 0)
        {
        
        }
        
        if (offsetX < 0)
        {
        
        }
        
        if (offsetY < 0)
        {
        
        }
        
        if (offsetY > 0)
        {
        
        }
    }
    
    // method to render the actual image before drawing it
    public void updateOffsets()
    {
        // up
        if (isUp)
        {
            // update offset up if not too far up
            if (coordY > 30)
            {
                offsetY += 2;
            }   
            
            // if a tile length has been moved, then offset becomes 0 and coordY is decreased
            if (offsetY >= TILE_SIZE)
            {
                offsetY = 0;
                coordY--;
                this.redrawCamera();
            }
        }
        // left
        if (isLeft)
        {
            // update offset to the left if not too far left
            if (coordX > 30)
            {
                offsetX += 2;
            }
            
            // if a tile length has been moved, then offset becomes 0 and coordX is decreased
            if (offsetX >= TILE_SIZE)
            {
                offsetX = 0;
                coordX--;
                this.redrawCamera();
            }
        }
        // right
        if (isRight)
        {
            // update offset to the right if not too far right
            if (coordX < width - 30)
            {
                offsetX -= 2;
            }
            
            // if a tile length has been moved, then offset becomes 0 and coordX is increased
            if (offsetX <= -TILE_SIZE)
            {
                offsetX = 0;
                coordX++;
                this.redrawCamera();
            }
        }
        // down
        if (isDown)
        {
            // update offset down if not too far down
            if (coordY < height - 30)
            {
                offsetY -= 2;
            }
            
            // if a tile legnth has been moved, then offset becomes 0 and coordY is increased
            if (offsetY <= -TILE_SIZE)
            {
                offsetY = 0;
                coordY++;
                this.redrawCamera();
            }
        }
    }
    
    // method to update the level's current position for the camera
    public void updateLevel(int input)
    {
        switch (input)
        {
            // up on
            case 0:
                isUp = true;
                break;
            // left on
            case 1:
                isLeft = true;
                break;
            // right on
            case 2:
                isRight = true;
                break;
            // down on
            case 3:
                isDown = true;
                break;
            // up off
            case 5:
                isUp = false;
                break;
            // left off
            case 6:
                isLeft = false;
                break;
            // right off
            case 7:
                isRight = false;
                break;
            // down off
            case 8:
                isDown = false;
                break;
            // jump off
            case 4:
                break;
        }
    }
    
    // method to simply load an image from a path
    public static BufferedImage loadImage(String ref)
    {
        BufferedImage bimg = null;
           
        try
        {
           bimg = ImageIO.read(new File(ref));
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
       
        return bimg;
    }
    
    // method to create a tile array for tile sets
    public static BufferedImage[] splitImage(BufferedImage img, int cols, int rows)
    {
        int w = img.getWidth() / cols;
        int h = img.getHeight() / rows;
        int num = 0;
        
        BufferedImage imgs[] = new BufferedImage[w * h];
        
        for (int y = 0; y < rows; y++)
        {
            for (int x = 0; x < cols; x++)
            {
                imgs[num] = new BufferedImage(w, h, img.getType());
                
                Graphics2D g = imgs[num].createGraphics();
                g.drawImage(img, 0, 0, w, h, w * x, h * y, w * x + w, h * y + h, null);
                g.dispose();
                num++;
            }
        }
        
        return imgs;
    }
    
    // image-loading method that will also alpha the color key for each tile
    public static BufferedImage makeColorTransparent(String ref, int color)
    {
        BufferedImage image = loadImage(ref);
        BufferedImage dimg = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = dimg.createGraphics();
        g.setComposite(AlphaComposite.Src);
        g.drawImage(image, null, 0, 0);
        g.dispose();
        
        for (int i = 0; i < dimg.getHeight(); i++)
        {
            for (int j = 0; j < dimg.getWidth(); j++)
            {
                if (dimg.getRGB(j, i) == color)
                {
                    dimg.setRGB(j, i, 0x8F1C1C);
                }
            }
        }
        
        return dimg;
    }
}

I hope I’ve got things now in the right place, or at least closer to how they should be. Thank you guys very much for your help! Not that it probably matters too much, but I gave both of you appreciation. ;]

Colton

Why are you sleeping for 2 milliseconds in your loop?

ra4king,

I read in a few places that it’s good to sleep in situations like these to let the other threads process more efficiently. Is this unnecessary?

Colton

Your game loop should be following a simple flow: update -> render -> sleep.
However the sleep should be the amount you want to sleep to achieve the target FPS minus the amount of time it took to update and render, like this example.

ra4king,

Thank you very much for all the advice! I’m learning a lot. I did what you told me and created a couple bits that calculate the sleep amount using the example you showed. I also made a render() and update() function that are now called within the game loop instead, which I think is much more readable, as well. Here’s the updated code for LevelRenderer:


/* This class's job is to manage a Player and Level object and call their render and update routines. */
 
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.JFrame;
import javax.swing.*;
import java.util.Random;
import java.awt.Color;

public class LevelRenderer extends JFrame
{   
    //
    // CONSTANTS
    //
    
    private final int TILE_SIZE = 32;
    private final int SCREEN_WIDTH = 1280;
    private final int SCREEN_HEIGHT = 768;
    
    //
    // END OF CONSTANTS
    //
    
    // back buffer
    private BufferStrategy buffer;
    
    // character object
    private Player myPlayer;
    
    // level object
    private Level myLevel;
    
    // screen object
    private Screen s;
    
    // Graphics object for the buffer strategy
    private Graphics graphics;
    
    // boolean to determine when to end the game
    private boolean endGame;
    
    // Variables for counting frames per second
    private int fps = 0;
    private int frames = 0;
    private long totalTime = 0;
    private long curTime = System.currentTimeMillis();
    private long lastTime = curTime;
    
    // thread sleep count variables
    private long now = 0;
    private long rest = now;
    
    // CONSTRUCTOR
    public LevelRenderer()
    {
        setPreferredSize(new Dimension(1280, 768));
        setIgnoreRepaint( true );
        setUndecorated( true );

        setFocusable(true);
        requestFocus();
        
        setResizable(false);
        
        addKeyListener( new KeyAdapter() 
        {
            public void keyPressed(KeyEvent e)
            { 
                processKey(e);  
            }
            
            public void keyReleased(KeyEvent e)
            {
                processRelease(e);
            }
        });
        
        myPlayer = new Player();
        myLevel = new Level("obstaclemap");
        
        endGame = false;
    }
    
    // method to simply load an image from a path
    public static BufferedImage loadImage(String ref)
    {
        BufferedImage bimg = null;
           
        try
        {
           bimg = ImageIO.read(new File(ref));
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
       
        return bimg;
    }
    
    // Run method for class
    public void run(DisplayMode dm)
	{	
        setBackground(Color.WHITE);
		s = new Screen();
        s.setFullScreen(dm, this);
        
        this.createBufferStrategy( 2 );
        buffer = this.getBufferStrategy();

        while (!endGame)
        {
            do
            {
                do
                {
                    try
                    {   
                        update();
                        render();
                    }
                    catch (Exception ex) 
                    { 
                        System.err.println("Game Update Error: " + ex);
                    }
                    
                    try
                    {
                        Thread.sleep(rest);
                    }
                    catch (Exception ex)
                    {
                        System.out.println("Can't sleep!");
                    }
                } while (buffer.contentsRestored());
                
                buffer.show();
                
            } while (buffer.contentsLost());
        }

		s.restoreScreen();
	} 
    
    // method to update player and level
    private void update()
    {
        // count Frames per second...
        lastTime = curTime;
        curTime = System.currentTimeMillis();
        totalTime += curTime - lastTime;
        
        if( totalTime > 1000 ) 
        {
            totalTime -= 1000;
            fps = frames;
            frames = 0;
        } 
        ++frames;
        
        // get first reference to calculate sleep time
        now = System.currentTimeMillis();
        
        // get new draw graphics
        graphics = buffer.getDrawGraphics();
        
        // edit/draw player and level
        myPlayer.animatePlayer();
        myLevel.updateOffsets();
    }   
    
    // method to render player, level, and FPS
    private void render()
    {
        // draw level and player
        myLevel.drawLevel(graphics, 0, 0);
        myPlayer.drawPlayer(graphics, (SCREEN_WIDTH / 2) - TILE_SIZE / 2, (SCREEN_HEIGHT / 2) - TILE_SIZE);
        
        // display frames per second...
        graphics.setFont( new Font( "Courier New", Font.PLAIN, 12 ) );
        graphics.setColor( Color.GREEN );
        graphics.drawString( String.format( "FPS: %s", fps ), 20, 20 );
        
        // calculate sleep time
        rest = 1000 / 60 - (System.currentTimeMillis() - now);
                        
        if (rest < 0)
        {
            rest = 0;
        }
        
        // dispose graphics
        graphics.dispose();
    }
    
    // method to handle inputs and adjust the player accordingly
    public void processKey(KeyEvent e)
    {
        int keyCode = e.getKeyCode();
        boolean moved = false;
        int xDisplace, yDisplace;
        
        // termination key
        if (keyCode == KeyEvent.VK_ESCAPE)
        {
            endGame = true;
        }
        
        // 0 - up
        // 1 - left
        // 2 - right
        // 3 - down
        // 4 - jump
        
        switch (keyCode)
        {
            case KeyEvent.VK_UP:
                myPlayer.updatePlayer(0);
                myLevel.updateLevel(0);
                break;
            case KeyEvent.VK_LEFT:
                myPlayer.updatePlayer(1);
                myLevel.updateLevel(1);
                break;
            case KeyEvent.VK_RIGHT:
                myPlayer.updatePlayer(2);
                myLevel.updateLevel(2);
                break;
            case KeyEvent.VK_DOWN:
                myPlayer.updatePlayer(3);
                myLevel.updateLevel(3);
                break;
            case KeyEvent.VK_SPACE:
                myPlayer.updatePlayer(4);
                break;
        }
    }
    
    // method to handle inputs and adjust the player accordingly
    public void processRelease(KeyEvent e)
    {
        int keyCode = e.getKeyCode();
        boolean moved = false;
        int xDisplace, yDisplace;
        
        // 0 - up
        // 5 - left
        // 6 - right
        // 3 - down
        // 4 - jump
        
        switch (keyCode)
        {
            case KeyEvent.VK_UP:
                myPlayer.updatePlayer(0);
                myLevel.updateLevel(5);
                break;
            case KeyEvent.VK_LEFT:
                myPlayer.updatePlayer(5);
                myLevel.updateLevel(6);
                break;
            case KeyEvent.VK_RIGHT:
                myPlayer.updatePlayer(6);
                myLevel.updateLevel(7);
                break;
            case KeyEvent.VK_DOWN:
                myPlayer.updatePlayer(3);
                myLevel.updateLevel(8);
                break;
            case KeyEvent.VK_SPACE:
                myPlayer.updatePlayer(4);
                break;
        }
    }
}

Any thoughts left on what I could do make the level scroll faster? Appreciate all the assistance.

Colton