Hello,
i am currently trying to develop a 2d game.
The map is composed with tiles, which so far works great as long as the map is not too big. I only draw the tiles that the player can see. That is, with a window size of 1280*720, about 920 tiles maximum.
I already know that it is caused by the draw method but idk how to fix it. Can someone pls help to optimize it?
Update method code:
public void Update(GameTime gameTime)
{
_actionTime += gameTime.ElapsedGameTime.TotalMilliseconds;
if (_actionTime > 20)
{
var cameraMoved = MoveCamera();
if (cameraMoved)
{
_tilesToDraw = _map.Tiles.Where(x => CheckIfTileNeedsToDraw(x));
}
}
}
My draw method looks like this:
public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
foreach (var tile in _tilesToDraw)
{
spriteBatch.Draw(_tileSet, new Rectangle(new Point(tile.X * _tileSize - _camera.Position.X, tile.Y * _tileSize - _camera.Position.Y), new Point(_tileSize, _tileSize)), new Rectangle(new Point(tile.TileSetPosX * Shared.TileSize, tile.TileSetPosY * Shared.TileSize), new Point(Shared.TileSize, Shared.TileSize)), Color.White, 0f, Vector2.Zero, SpriteEffects.None, (float)tile.Layer / 10);
}
}
FPS i get depending on the size of the map:
128x128 ~60fps
1024x1024 ~30fps
4096x4096 ~2fps
I think your problem is here. You’re using a LINQ where statement, which is still going to iterate over every single item in your map, so even though you only draw what’s on the screen, you still process the whole array in your Update loop. I suspect if you profiled, this would be your hotspot.
If your _map
data type allows you to query by index, just map screen entries to indicies and draw directly. Like, if your tile map was a 2D array you could just draw from a starting corner to an ending corner and you don’t need to process anything else.
I think in the update logic, going through all the tiles to check if those are visible or not takes too much time.
Since you are only using X to check you can create a list or array of X values and put all the tiles for a given X in a list, queue or array , then you only need to calculate once and draw all the ones that you need for screen until you reach the width of your screen, this easily will allow you to render way more than what you need.
You can happily render 1000s of tiles per frame (I even did that on mobile 10 yrs ago … in java … : tiles + particles), I would rather assume, that LINQ is just painfully slow.
What you should do instead is iterate over a flat array (using for, in case of list<> you can still use for and spare the enumerator) and do the culling checks in place (they are extremely cheap when using AABB testing and continue early). Could also be your CheckIfTileNeedsToDraw is slow …
You can check if it’s a hardware limit by just rendering the same first item in the list 1000 times without doing any other checks, I am pretty sure you will find it will render just fine - except you are on some very slow hardware like no GPU or something
Thank you guys for you help!
I actually managed to fix it!
What I did:
1.) I replaced the List with a two dimensional array of Tile[mapWidth, mapHeight]
2.) On initializing my element which draws the map I calculate how many tiles are maximum visible per row/column (Viewport.Width / TileSize and Viewport.Height / TileSize)
3.) On camera update I calculate which tile is the first one I need
private int _xTileToDrawFrom, _yTileToDrawFrom;
private void UpdateTilesToDraw()
{
var camPos = _camera.Position;
_xTileToDrawFrom = camPos.X / _tileSize;
_yTileToDrawFrom = camPos.Y / _tileSize;
}
4.) In the draw function I use two for loops to iterate through my tiles starting from my caluclated x and y index
for (var x = _xTileToDrawFrom ; x <_xTileToDrawFrom + _maxTilesX; x++)
{
if (x > _map.Tiles.GetLength(0))
break;
if (x < 0)
continue;
for (var y = _yTileToDrawFrom; y<_yTileToDrawFrom + _maxTilesY; y++)
{
if (y > _map.Tiles.GetLength(1))
break;
if (y < 0)
continue;
var tile = _map.Tiles[x, y];
spriteBatch.Draw(_tileSet, new Rectangle(new Point(tile.X * _tileSize - _camera.Position.X, tile.Y * _tileSize - _camera.Position.Y), new Point(_tileSize, _tileSize)), new Rectangle(new Point(tile.TileSetPosX * Shared.TileSize, tile.TileSetPosY * Shared.TileSize), new Point(Shared.TileSize, Shared.TileSize)), Color.White, 0f, Vector2.Zero, SpriteEffects.None, (float)tile.Layer / 10);
}
}
1 Like
If you wanted, you probably don’t need the to sanity checks in your for loops. Just make sure that, prior to entering them, the start and end values for both X and Y are within the array bounds. This is probably true anyway and those checks are superfluous.
Your code should more or less be…
for (int x = startX; x < endX; x++)
for (int y = startY; y < endY; y++)
// draw tile at (x, y)
Those checks don’t add a lot to your overall draw loop, but you also shouldn’t really need them.
Past that, nice work! I’m glad you got it sorted out