[java] Optimized 2D Drawing

Started by
8 comments, last by Tanyss 20 years ago
As of now, I''m making a fairly complex 2D tile engine, ala Mario or Zelda. I won''t bore you with reasons as to why I''m using Java or why I''m making this. But the problem that I''m having is a large one. I''m having issues getting decent framerates on anything less than my home system. I''m running an Athlon XP 2600+ overclocked to 2.3 with a gig of ram. As it is, my theoretical frame count is around 78-80. When I run my code on systems that have lower specs than mine, I get severe lag. This ranges from different processors from Intel to AMD at varying speeds. My method for drawing is simple. I have a linked list of all the tiles on screen. I iterate through that list, checking to see which ones are off screen. If they are, delete them from the list. If they aren''t draw them. Then, I retrieve the new rows and cols that have been introduced onto the screen from player movement, draw those, and then add those to the list. To me, this seems like a relatively fast method. But, I''m getting incredible lag, probably around 10 fps on any machine other than mine. Does anyone know what the issue is or a faster way to draw the tiles?
Advertisement
Sounds like a big "O" problem. I don't know how you have your linked list set up but lets assume you have a singly linkedlist. If you iterate over a linked list looking for new tiles to draw and your tilemap is 100x100 tiles then that is 10,000 elements you access every frame. On the other hand if you store your tile map in an array then you go straight to the tile you want by using the x,y coordinates. Its a no brainer to see an array would be faster in this case.

If your using java2d and your testers have jre 1.4.2+ then you should see 100+ fps consistantly. Java2d with 'managed images' is directdraw fast.

[edited by - nonnus29 on April 20, 2004 9:20:01 PM]
And this brings up the (rather obvious) question: Are you using Java 2D? Or are you using the basic Graphics class provided in AWT?

I certainly hope that this is a stupid question to ask. Otherwise, you should be using Java 2D... Other than that, nonnus makes some excellent suggestions.

http://chaos.webhop.org
I'm using Graphics2D.

As it is, I'm essentially overriding the automatic painting functions in Java and getting a graphics context to the JFrame that I'm using. I'm also double-buffering, which takes up some resources.

For the actual tile map, I have that stored in a matrix. The draw class has a linked list of the screen tiles, removing and adding them as they leave and enter the screen. If they are not removed, they are drawn.

[edited by - Tanyss on April 21, 2004 2:10:34 PM]
As a note, I realize that I'm being sort of vague and nebulous by just saying how I do it. So here's the code to my ScrollingTileMapRenderer. This is how the tiles are drawn on the screen.

//v 3.0 - 4-9-04 - reimplementation of a 1D list to draw the tiles.package Rain.Game.Tile;import java.awt.Graphics2D;import java.util.LinkedList;import java.util.ListIterator;public class ScrollingTileMapRenderer extends TileMapRenderer{	//member list	LinkedList list;	double playerX;	double playerY;	int screenWidth;	int screenHeight;	int unitSize;	int oldStartRow;	int oldStartCol;	//constructor	public ScrollingTileMapRenderer(TileMap tileMap, double playerX, double playerY, int screenWidth, int screenHeight)	{		//init the values		this.playerX = playerX;		this.playerY = playerY;		this.screenWidth = screenWidth;		this.screenHeight = screenHeight;		unitSize = tileMap.getUnitSize();		oldStartRow = ((int) playerY - (screenWidth / 2)) / unitSize;		oldStartCol = ((int) playerX - (screenHeight / 2)) / unitSize;		//init the list		list = tileMap.getTileList(oldStartRow, oldStartCol, oldStartRow + (screenHeight / unitSize), oldStartCol + (screenWidth / unitSize));	}	public void drawTiles(TileMap tileMap, double newPlayerX, double newPlayerY, Graphics2D g)	{		playerX = newPlayerX;		playerY = newPlayerY;		int startX = (int) playerX - (screenWidth / 2);		int startY = (int) playerY - (screenHeight / 2);		int startRow = ((int) playerY - (screenHeight / 2)) / unitSize;		int startCol = ((int) playerX - (screenWidth / 2)) / unitSize;		int endRow = startRow + (screenHeight / unitSize);		int endCol = startCol + (screenWidth / unitSize);				//for use if the player traverses more than a row or col per frame		int deltaRows = startRow - oldStartRow;		int deltaCols = startCol - oldStartCol;				//draw the background image		g.drawImage(tileMap.getBackgroundAnimation().getCurrentImage(), - ((int) playerX - (screenWidth / 2)), (int) - playerY + screenHeight / 2, (tileMap.getCols() * unitSize), (tileMap.getRows() * unitSize), null);				ListIterator i;		Tile tempTile;				//if the screen hasn't changed, draw the tiles and exit		if (startRow == oldStartRow && startCol == oldStartCol)		{			i = list.listIterator();						while (i.hasNext())			{				tempTile = (Tile) i.next();				g.drawImage(tempTile.getCurrentImage(), (tempTile.getCol() * unitSize) - startX, (tempTile.getRow() * unitSize) - startY, unitSize, unitSize, null);			}						return;		}				//remove off-screen tiles; draw those that are on screen		i = list.listIterator();				while (i.hasNext())		{			tempTile = (Tile) i.next();						//if tile isn't on the screen			if (tempTile.getRow() < startRow || tempTile.getRow() > endRow || tempTile.getCol() < startCol || tempTile.getCol() > endCol)			{				i.remove(); //remove				continue; //skip to the next loop iteration			}						g.drawImage(tempTile.getCurrentImage(), (tempTile.getCol() * unitSize) - startX, (tempTile.getRow() * unitSize) - startY, unitSize, unitSize, null);		}		LinkedList newTilesList = new LinkedList();		int j = 0;				//add the rows		if (deltaRows < 0)		{			for (j = 0; j > deltaRows; j--)			{				newTilesList.addAll(tileMap.getRow(startRow + j,startCol,endCol));			}		}				else if (deltaRows > 0)		{			for (j = 0; j < deltaRows; j++)			{				newTilesList.addAll(tileMap.getRow(endRow + j,startCol,endCol));			}		}				//add the cols		if (deltaCols < 0)		{			for (j = 0; j > deltaCols; j--)			{				newTilesList.addAll(tileMap.getCol(startCol + j, startRow, endRow));			}		}				else if (deltaCols > 0)		{			for (j = 0; j < deltaCols; j++)			{				newTilesList.addAll(tileMap.getCol(endCol + j, startRow, endRow));			}		}		i = newTilesList.listIterator();				while (i.hasNext())		{			tempTile = (Tile) i.next();						g.drawImage(tempTile.getCurrentImage(), (tempTile.getCol() * unitSize) - startX, (tempTile.getRow() * unitSize) - startY, unitSize, unitSize, null);		}				//add the new tiles to the old ones		list.addAll(newTilesList);				//update the record of the bounds		oldStartRow = startRow;		oldStartCol = startCol;	}}  


I know this is a bit long and tedious, but I appreciate any help. Thanks.

[edited by - Tanyss on April 21, 2004 1:51:10 PM]
Use 'source' tags, not 'code'

I'm not really grokking your code but I see you are iterating over a list, allocating a new list and populating it; like I said, if you have a 100x100 tilemap thats 10,000 x 2 = 20,000 comparisons.

So compare to my tile map drawing code:

public void paint(Graphics g)  {    	    	//We use the 'camera' x,y from the update method to determine    	//the tiles to draw.  The offset tells how far right to draw the    	//tiles on the oversized backbuffer.  We will draw only part of the     	//backbuffer.    			tilex=camx/TILE_W;		tiley=camy/TILE_H;		offsetx=TILE_W-camx%TILE_W;		offsety=TILE_H-camy%TILE_H;		for(int i=0;i<SCREEN_H/TILE_H+2;i++)		   for(int j=0;j<SCREEN_W/TILE_W+2;j++)	    	  g.drawImage(tiles[leveldata[i+tiley][j+tilex]-1], // -1 because of the						offsetx+j*TILE_W,// mapeditor I used						offsety+i*TILE_H,// didn't start at zero						applet);  	} 


Maybe you should rethink you design?

[edited by - nonnus29 on April 21, 2004 7:16:56 PM]
Yeah, a redesign is most definately what I need.

Ok, thanks. The comparisons bit I didn''t really consider. The way I had originally planned this was to have a list that represented the screen in the TileMapRenderer and mathematically add and remove the tiles. Essentially, have them ordered. But that didn''t work too well.

Ok, I''ll work on a redesign. Thanks.

Oh, and thanks for the source info. Haha, didn''t quite think about it.
Also, since you are creating and destroying an object every frame, then you will get that annoying garbage collector slowing you down. An alternate way of doing it would be to draw all your tiles to a large image, then display the part of the image that would be on screen. This way there is no loop or iteration for drawing, only a single set of offsets.


First make it work, then make it fast. --Brian Kernighan

The problems of this world cannot possibly be solved by skeptics or cynics whose horizons are limited by the obvious realities. We need men and women who can dream of things that never were. - John Fitzgerald Kennedy(35th US President)

Do not interrupt your enemy when he is making a mistake. - Napolean Bonaparte
"None of us learn in a vacuum; we all stand on the shoulders of giants such as Wirth and Knuth and thousands of others. Lend your shoulders to building the future!" - Michael Abrash[JavaGaming.org][The Java Tutorial][Slick][LWJGL][LWJGL Tutorials for NeHe][LWJGL Wiki][jMonkey Engine]
The thing is, every time that a tile updates in its animation, I''d have to redraw the image. This is also very memory intensive, I think. The execution time for that would be pretty bad, if I have my math done correctly.

As of right now, I''m working on a Camera class, but it doesn''t seem to be working properly. Additionally, with my current frame times, they seem to have dropped by about 20 fps. I find this odd.
Are your images stored in the video memory? If they are not this would probably slow you down alot. I hear in Java 1.5 BufferedImage will also be accelerated but I haven''t tried it myself though.

To get high performance java2d images you can read more in this weblog.

This topic is closed to new replies.

Advertisement