I agree that its a big optimization to avoid attempting to draw tiles that aren't visible to the client. I suggest drawing the client screen area using a staggered approach. Refer to the image you've provided in your question to make sense of the following algorithm. It has a nicely visible grid to figure out the row and column movements in the server tilemap data structure. I assume you want as much of the client screen area covered as possible.
Here are some output examples for the code I've provided.

- The top-left image shows how the top left corner of the server's tilemap gets drawn in a staggered fashion without adequate border padding tiles. The black spaces can be seen along the outer edges of the client screen area.
- The bottom-left shows the same inadequate padding near the center of the server's tilemap data.
- The top-right and bottom-right images show staggered isometric drawing with betting border padding tiles to allow for the illusion of a more continuous area.
Note that I removed the grass tiles from the "insufficient padding" example on the left to highlight the missing tiles against the back background. The orange square is my programmer art for my player sprite.
Here is the general algorithm:
- Get the top left point of the visible window, meaning the position of the camera in server space.
- Convert this point to Cartesian coordinates on the assumption that its already an isometric coordinate, even if you never explicitly converted the camera movement or position to isometric. Here is a useful link for coordinate conversion and isometric tilemaps in general
- Calculate the tilemap cell (aka tile) using that Cartesian point, meaning the index in your tilemap array. I use a 2D array, so I directly index tiles by row and column.
- Decrease the row by 1 or 2 to find a cell that will start drawing at a wider point beyond the client screen area.
- Continue from there by querying and drawing tiles "diagonally" through the server tilemap data structure. This will result in a visibly horizontal line of tiles across the client screen area. Meaning, from the staring [row][column] go to [row++][column--].
- The number of cells to draw across is just the ratio of isometric cell width to client screen area width. EG:
screenWidth == 800
, isometric cell width == 64
, so the number of cells to draw horizontally is (int)(800 / 64) == 12
loops. You'll probably have to add some extra loops, say + 5, depending on whatever offset your floor tile images have.
- Once a line is complete, toggle an odd/even line boolean, meaning if its false make it true, and vis versa.
- If odd/even boolean is true, increment the starting column, then set your indexing row and column back the the starting row and column (using the modified starting column).
- If odd/even boolean is false, increment the starting row, then set your indexing row and column back the the starting row and column (using the modified starting row).
- Continue in this odd/even staggered movement down the client screen area according to twice the ratio of isometric cell height to client screen area height. EG:
screenHeight == 600
, isometric cell height == 32
, so the number of horizontal lines to draw is (2 * (int)(600 / 32)) == 37
loops. Again, you'll probably have to add some extra loops, say + 5, to ensure the client screen area is fully covered.
My Solution
I am using SDL and C++ but the principles are the same for Java. Note that my tile origins have been pre-calculated and saved as isometric values. I am aware that the following code may be difficult to understand without knowing what all the function calls I'm making are; however, the idea here is that you should look into staggered isometric drawing instead of diamond isometric drawing because it maps much better to client screen area (even if it is more confusing than diamond drawing methods) and the relative draw order is preserved.
void eMap::Draw() {
// DEBUG: these constants assume a cell is square, and that its isometric projection
// is twice as wide as it is tall, the invIsoCellHeight is halved to account for the
// staggered isometric cell alignment
static const float invIsoCellWidth = 1.0f / (float)(tileMap.CellWidth() << 1);
static const float invIsoCellHeight = 1.0f / (float)(tileMap.CellHeight() >> 1);
// DEBUG: the extra '+ 5' is on both of these to ensure enough tiles are drawn to cover the screen
int maxHorizCells = (int)(game.GetRenderer().ViewArea().w * invIsoCellWidth) + 5;
int maxVertCells = (int)(game.GetRenderer().ViewArea().h * invIsoCellHeight) + 5;
eVec2 camTopLeft = eVec2(game.GetCamera().GetAbsBounds().x, game.GetCamera().GetAbsBounds().y);
eMath::IsometricToCartesian(camTopLeft.x, camTopLeft.y);
int startRow, startCol;
int row, column;
tileMap.Index(camTopLeft, startRow, startCol); // DEBUG: this tile has isometric coordinates
// DEBUG: also note that the startRow and startCol may be negative if the top left corner of the camera is negative.
startRow -= 2; // DEBUG: ensure enough rows cover the screen area
row = startRow;
column = startCol;
bool oddLine = false;
for (int vertCount = 0; vertCount < maxVertCells; vertCount++) {
for (int horizCount = 0; horizCount < maxHorizCells; horizCount++) {
// DEBUG: ensure the tilemap row and column about to be queried are valid
if (row < tileMap.Rows() && row >= 0 && column < tileMap.Columns() && column >= 0) {
eTile & tile = tileMap.Index(row, column);
eVec2 screenPoint = eVec2(
eMath::NearestFloat(tile.Origin().x - game.GetCamera().GetAbsBounds().x),
eMath::NearestFloat(tile.Origin().y - game.GetCamera().GetAbsBounds().y)
);
game.GetRenderer().AddToRenderQueue(renderImage_t(screenPoint, tile.Image(), tile.Layer()));
}
row++; column--;
}
oddLine = !oddLine;
oddLine ? startCol++ : startRow++;
row = startRow;
column = startCol;
}
}
I hope all this gets you thinking and ideally helps solve some problems. Here's another useful answer to staggered drawing