this is my first post so apologies if I have made any mistakes etc. If at the end of the post, you require more information then I'll be more then happy to post more.
I've been working on 2D tile based game for a while now and I've recently tried a different approach to rendering the tiles. But right now I have a performance issue when rendering tiles.
The game has its world split into 16*256 chunks and each chunk has its own RenderTarget2D (16*256) to hold the tile data. I've called the render target the chunks buffer.
public void updateChunks()
{
int loadMinX = (int)( ( game.camera.location.X - game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
int loadMaxX = (int)( ( game.camera.location.X + game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
int minX = (int)( game.player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) - (int)Math.Floor( chunkCount / 2f );
int maxX = (int)( game.player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) + (int)Math.Floor( chunkCount / 2f );
Chunk currentChunk;
int cursorChunk = game.cursor.chunkLocation;
for( int c = minX; c <= maxX; c++ )
{
if( chunkBuffer.ContainsKey( c ) )
{
currentChunk = chunkBuffer[c];
if( c >= loadMinX && c <= loadMaxX )
{
currentChunk.bufferBuilt = false;
}
if( currentChunk.highlighted == true )
{
currentChunk.highlighted = false;
currentChunk.bufferBuilt = false;
}
if( chunkBuffer.ContainsKey( cursorChunk ) && chunkBuffer[cursorChunk] == currentChunk )
{
currentChunk.highlighted = true;
currentChunk.bufferBuilt = false;
}
if( currentChunk.bufferBuilt == false )
{
RebuildChunk( currentChunk );
}
}
}
game.tilesLoaded = chunkBuffer.Count * Chunk.chunkWidth * Chunk.chunkHeight;
}
This method is called every tick and decides which chunks buffer needs to be rebuilt/redrawn. Currently, the chunks that are in the viewport are rebuilt every tick and also the chunk that the mouse is inside is rebuilt as well (because the tile changes to highlighted colour.)
The chunks are stored inside a dictionary and the loaded chunks are accessed via looping through minX -> minY values of the dictionary of chunks, as shown in the updateChunks() method.
public void RebuildChunk( Chunk chunk )
{
BuildChunk( chunk );
chunk.bufferBuilt = true;
}
private void BuildChunk( Chunk chunk )
{
int chunkWidth = Chunk.chunkWidth;
int chunkHeight = Chunk.chunkHeight;
int chunkLocation = chunk.id * chunkWidth;
int loadMinX = (int)( ( game.camera.location.X - game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth ) - 2;
int loadMaxX = (int)( ( game.camera.location.X + game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth ) + 2;
int loadMinY = (int)( ( game.camera.location.Y - game.GraphicsDevice.Viewport.Height / 2 ) / Tile.tileHeight ) - 2;
int loadMaxY = (int)( ( game.camera.location.Y + game.GraphicsDevice.Viewport.Height / 2 ) / Tile.tileHeight ) + 2;
game.GraphicsDevice.SetRenderTarget( chunk.buffer );
game.GraphicsDevice.Clear( Color.CornflowerBlue );
game.spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied );
for( int x = 0; x < chunkWidth; x++ )
{
for( int y = 0; y < chunkHeight; y++ )
{
if( x + chunkLocation > loadMinX && x + chunkLocation < loadMaxX && y > loadMinY && y < loadMaxY )
{
if( chunk.tiles[x, y].type == 0 && chunk.tiles[x, y].lightSource == false )
{
chunk.tiles[x, y].lightSource = true;
chunk.tiles[x, y].lightComponent = new Light( game, new Point( chunkLocation + x, y ), 1.0f, 6 );
}
if( chunk.tiles[x, y].lightSource == true )
{
if( chunk.tiles[x, y].lightComponent == null )
{
chunk.tiles[x, y].lightSource = false;
}
else
{
ApplyLighting( chunk, x, y, chunk.tiles[x, y].lightComponent );
}
}
float brightness = chunk.tiles[x, y].brightness;
if( chunk.tiles[x, y].type == 0 )
{
if( chunk.highlighted == true && game.cursor.tileLocation.X == x && game.cursor.tileLocation.Y == y )
{
game.spriteBatch.Draw( game.gameContent.airTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( 0.5f, 0.5f, 0.5f ) );
}
else
{
game.spriteBatch.Draw( game.gameContent.airTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
}
}
else if( chunk.tiles[x, y].type == 1 )
{
if( chunk.highlighted == true && game.cursor.tileLocation.X == x && game.cursor.tileLocation.Y == y )
{
game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( 0.5f, 0.5f, 0.5f ) );
}
else
{
game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
}
}
}
}
}
game.spriteBatch.End();
game.GraphicsDevice.SetRenderTarget( null );
}
RebuildChunk is responsible for building the chunk again and resetting the built flag so cpu time isn't wasted rebuilding chunks that haven't been edited or that aren't in the viewport.
BuildChunk is where the tiles are drawn to the chunks buffer. The render target is swapped to the chunks buffer and then it loops through the chunks tiles.
if( x + chunkLocation > loadMinX && x + chunkLocation < loadMaxX && y > loadMinY && y < loadMaxY )
This line checks if the tile is in the viewport. If it is, then check to see what the tiles type is and then draw the corresponding texture for that tile. There's also lighting logic in there too but it does not affect my problem.
My main rendering logic is:
protected override void Draw( GameTime gameTime )
{
frameCounter++;
drawScene( gameTime );
GraphicsDevice.Clear( Color.White );
spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied );
spriteBatch.Draw( white, new Rectangle( 0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height ), Color.CornflowerBlue );
spriteBatch.Draw( worldBuffer, new Rectangle( 0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height ), Color.White );
spriteBatch.End();
debugUI.Draw( gameTime );
base.Draw( gameTime );
}
private void drawScene( GameTime gameTime )
{
int loadMinX = (int)( ( camera.location.X - GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
int loadMaxX = (int)( ( camera.location.X + GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
int minX = (int)( player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) - (int)Math.Floor( worldManager.chunkCount / 2f );
int maxX = (int)( player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) + (int)Math.Floor( worldManager.chunkCount / 2f );
GraphicsDevice.SetRenderTarget( worldBuffer );
GraphicsDevice.Clear( Color.CornflowerBlue );
spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, null, camera.GetViewTransformation( GraphicsDevice ) );
for( int c = minX; c <= maxX; c++ )
{
if( c >= loadMinX && c <= loadMaxX )
{
if( worldManager.chunkBuffer.ContainsKey( c ) )
{
Chunk currentChunk = worldManager.chunkBuffer[c];
if( currentChunk.bufferBuilt == true )
{
spriteBatch.Draw( currentChunk.buffer, new Rectangle( c * Chunk.chunkWidth * Tile.tileWidth, 0, Chunk.chunkWidth * Tile.tileWidth, Chunk.chunkHeight * Tile.tileHeight ), Color.White );
}
}
}
}
spriteBatch.Draw( player.material, new Rectangle( (int)player.location.X, (int)player.location.Y, 16, 32 ), Color.White );
spriteBatch.End();
GraphicsDevice.SetRenderTarget( null );
}
Finally, inside my input manager I have the following:
if( mouseState.LeftButton == ButtonState.Pressed )
{
game.worldManager.RemoveTile( game.cursor.chunkLocation, game.cursor.tileLocation );
}
which calls:
public void RemoveTile( int chunkLocation, Point location )
{
if( chunkBuffer.ContainsKey( chunkLocation ) && location.X >= 0 && location.X < Chunk.chunkWidth && location.Y >= 0 && location.Y < Chunk.chunkHeight )
{
if( chunkBuffer[chunkLocation].tiles[location.X, location.Y].type != 0 )
chunkBuffer[chunkLocation].tiles[location.X, location.Y].type = 0;
}
}
This is just my test method for changing the state of different tiles, in this case it changes the type form dirt to air to simulate removing a tile.
Now for the problem. When the game first runs it's fine and stays at 60fps. I move my character to the right to fill the screens width with tiles and then I dig down to fill the screens height with tiles.
http://i.imgur.com/NBJ8qRj.png
http://i.imgur.com/N9i8asW.png (note: lighting is turned off in the second image)
The game is still running fine in the first image, but when I start to remove tiles surrounding the player in the second image, after a while the framerate drops from 60 to below 10 and falls to around 2-3 fps and it stays at that rate forever and doesn't recover even though I've stopped clicking/doing anything to the game.
The problem occurs at full-screen resolutions and the performance analysis says a lot of cpu time is used during the draw calls inside the BuildChunk() method:
else
{
game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
}
So could it be caused by the constant swapping of textures to draw? ie; swapping from drawing an airTexture to a dirtTexture etc? Because I've found that if I changed the airTexture to the dirtTexture as well, the issue goes away.
If I dig further down below all of the tiles I removed, the fps recovers back to 60 so my guess is it has something to do with drawing the textures in the rebuild phase.
If someone could look over this code and maybe point out any flaws with it that would be appreciated and possible solutions would be great.
Also, if someone has experience with this sort of situation with tiles and rendering your advice would be invaluable!