I'm working on a side-scrolling game in Java. The issue at hand is that I've created the needed classes for scrolling and rendering the character and the level, only it is very slow. I'm not sure if this is a Java limitation or what, but I would be greatly appreciative if someone could perhaps tell me what it is either I'm doing wrong or what needs to be added to make the scrolling and rendering faster. I would think my machine (quad-core 3.4 GHz processor, 8 GB of ram, superpower graphics card, etc.) would run this program way faster and more efficiently, but I don't know if maybe there are things under the hood that I am missing. I'll post the classes that comprise the engine below.
Here's a general view of what I've made: I have a Player class, a Level class, and a LevelRenderer class. The Level loads a black and white pixel map that represents the level's tiles and converts it to an array, loads and holds the tile images to blit, stores the current coordinates of the camera view, and has a method to draw its current view. The Player class can draw the Player sprite always in the middle of the screen, and the LevelRenderer instantiates one of each of the Player and Level and handles input, being an extended JFrame. I have another class called GameTest that has a main method to create a LevelRenderer object, but that's probably irrelevant, as it only calls the run() method of the LevelRenderer and the constructor pretty much and lets the other classes do all the work. In the end, it's a simple scrolling engine that keeps the character in the middle of the screen at all times and just updates the level.
This is my first side-scrolling engine, so if you have any constructive criticism to offer, I would be greatly appreciative of that, as well. I mostly tried to piece together what information I could find about them and tried designing part of it myself. I've found it's not usually right the first time when I undertake something fairly complex like this, but it's certainly rewarding.
Thanks again for taking a look! And I apologize of the post gets a little bit lengthy
Here's the Level class:
/* 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 = 14;
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;
// 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, y, 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 = offsetY; y < SCREEN_HEIGHT + offsetY; y += TILE_SIZE)
{
tileX = coordX;
for (x = offsetX; x < SCREEN_WIDTH + offsetX; 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--;
}
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--;
}
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++;
}
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++;
}
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;
}
}
Here's the Player class:
/* 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 = 14;
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;
}
}
}
And here's the LevelRenderer class:
/* 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 = 14;
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 buffer;
// character object
private Player myPlayer;
// level object
private Level myLevel;
// screen object
private Screen s;
// graphics object of the buffer
private Graphics gr;
// boolean to determine when to end the game
private boolean endGame;
// CONSTRUCTOR
public LevelRenderer()
{
setPreferredSize(new Dimension(1280, 768));
setFocusable(true);
requestFocus();
setResizable(false);
addKeyListener( new KeyAdapter()
{
public void keyPressed(KeyEvent e)
{
processKey(e);
}
public void keyReleased(KeyEvent e)
{
processRelease(e);
}
});
buffer = new BufferedImage(1280, 768, BufferedImage.TYPE_INT_ARGB);
buffer.createGraphics();
gr = buffer.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();
try
{
s.setFullScreen(dm, this);
}
catch (Exception ex)
{
System.out.println("Error creating rendered tiles!");
}
while (!endGame)
{
try
{
Thread.sleep(2);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
try
{
myLevel.renderLevel();
myLevel.drawLevel(gr, 0, 0);
myPlayer.animatePlayer();
myPlayer.drawPlayer(gr, (SCREEN_WIDTH / 2) - TILE_SIZE / 2, (SCREEN_HEIGHT / 2) - TILE_SIZE);
}
catch (NullPointerException ex)
{
System.err.println("Game Update Error: " + ex);
}
try
{
repaint();
}
catch (Exception ex)
{
System.err.println("Repaint Error: " + ex);
}
}
s.restoreScreen();
}
// method to draw the tiles on the screen
public void paint(Graphics g)
{
g.drawImage(buffer, 0, 0, null);
}
// 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;
}
// 1 - up
// 2 - left
// 3 - right
// 4 - down
// 5 - jump
if (keyCode == KeyEvent.VK_UP)
{
try
{
myPlayer.updatePlayer(0);
myLevel.updateLevel(0);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
if (keyCode == KeyEvent.VK_LEFT)
{
try
{
myPlayer.updatePlayer(1);
myLevel.updateLevel(1);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
if (keyCode == KeyEvent.VK_RIGHT)
{
try
{
myPlayer.updatePlayer(2);
myLevel.updateLevel(2);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
if (keyCode == KeyEvent.VK_DOWN)
{
try
{
myPlayer.updatePlayer(3);
myLevel.updateLevel(3);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
if (keyCode == KeyEvent.VK_SPACE)
{
try
{
myPlayer.updatePlayer(4);
myLevel.updateLevel(4);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
}
// method to handle inputs and adjust the player accordingly
public void processRelease(KeyEvent e)
{
int keyCode = e.getKeyCode();
boolean moved = false;
int xDisplace, yDisplace;
// 1 - up
// 2 - left
// 3 - right
// 4 - down
// 5 - jump
if (keyCode == KeyEvent.VK_UP)
{
try
{
myPlayer.updatePlayer(0);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
if (keyCode == KeyEvent.VK_LEFT)
{
try
{
myPlayer.updatePlayer(5);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
if (keyCode == KeyEvent.VK_RIGHT)
{
try
{
myPlayer.updatePlayer(6);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
if (keyCode == KeyEvent.VK_DOWN)
{
try
{
myPlayer.updatePlayer(3);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
if (keyCode == KeyEvent.VK_SPACE)
{
try
{
myPlayer.updatePlayer(4);
}
catch (Exception ex)
{
System.err.println("Error: " + ex);
}
}
}
}
If anything else is needed, please let me know and I will gladly post or elaborate. The LevelCollisions class is irrelevant at this point, as I haven't implemented the functions that will make it work. Eventually, it will be what determines whether the player can move or not. Right now, you can pretty much move the character in any direction and the level will scroll, regardless of terrain.