I wrote an algorithm to transform mouse screen coordinates within an isometric map to corresponding coordinates of an underlying 2D-square grid. The algorithm works and I tested it successfully. I wanted to post it here so interested people could maybe check the algorithm and see if I can make any optimizations beyond of what I did already. My goal is to make the algorithm as compact as possible. There maybe are some smart mathematical solutions I didn't think of to save some lines of code or make it faster.
The input data for the rendering of the map is a simple 2D-array like:
0,0,0,0
0,0,0,0
0,0,0,0
...
Here is a screenshot of what the map looks like when rendered. It is not really a map right now but more of an outlining of the tiles I will render. The red coordinates are x and y values of the underlying source grid.
It is rendered with a zig-zag approach with an origin outside of the screen (top left). It is rendered row by row from left to right whereby every uneven row is inset by half a tiles width. This is a common approach to avoid having to fill the source 2D-grid with "zombie-data" if one would render it in a diamond approach.
What the algorithm does is:
- the screen is logically divided into square rectangles with a width of my tile width and height of my tile height, i.e. 128px by 64px
- the mouse coordinates are scaled by tile width and tile height and then floored. This determines in which screen rectangle the user clicked
- then the mouse coordinates are compared against the center coordinates of this rectangle
- regarding this result, a right angled triangle is calculated to check if the user clicked within a certain area of the rectangle. I am using the cosine of this triangle to compare against my initial rotation angle of 26,565 (atan of 0.5). This is the angle in which the sides of the rendered rhombi are rotated to achieve a 2:1 ratio of width and height (classic isometric projection for video games).
- regarding on where the user clicked
- the y value is determined from a lower and upper boundary
- the x value is determined based on y being even or uneven
To run this code, you will need SDL2-2.0.9 and boost 1.67.0. to run. I am using MS Visual Studio Community 2017 as an IDE.
#include <iostream>
#include <vector>
#include <SDL.h>
#include <SDL_Image.h>
#include <boost/cast.hpp>
struct Point
{
std::size_t x;
std::size_t y;
};
Point mouseToGrid(int pMouseXPos, int pMouseYPos, std::size_t pTileWidth, std::size_t pTileHeight, const double PI, const int SCREEN_WIDTH, const int SCREEN_HEIGHT)
{
double mouseXPos = boost::numeric_cast<double>(pMouseXPos);
double mouseYPos = boost::numeric_cast<double>(pMouseYPos);
double tileWidth = boost::numeric_cast<double>(pTileWidth);
double tileHeight = boost::numeric_cast<double>(pTileHeight);
double tileWidthHalf = tileWidth / 2;
double tileHeightHalf = tileHeight / 2;
double mouseTileYPos = mouseYPos / tileHeight;
mouseTileYPos = std::floor(mouseTileYPos);
int screenRectCenterX = ((std::floor((mouseXPos / tileWidth))) * tileWidth) + tileWidthHalf;
int screenRectCenterY = (mouseTileYPos * tileHeight) + tileHeightHalf;
//determine lower and upper boundary for y
int minY = boost::numeric_cast<int>(2 * mouseTileYPos);
int maxY = boost::numeric_cast<int>((2 * mouseTileYPos) + 1);
if (mouseYPos >= screenRectCenterY)
{
minY = maxY;
maxY++;
}
//calc triangle sides in pixels
char mouseRectangleSector[2]{};
double opposite;
double adjacent;
if (mouseYPos >= screenRectCenterY)
{
mouseRectangleSector[0] = 'S';
opposite = mouseYPos - screenRectCenterY;
}
else
{
mouseRectangleSector[0] = 'N';
opposite = screenRectCenterY - mouseYPos;
}
if (mouseXPos >= screenRectCenterX)
{
mouseRectangleSector[1] = 'E';
adjacent = (screenRectCenterX + tileWidthHalf) - mouseXPos;
}
else{
mouseRectangleSector[1] = 'W';
adjacent = tileWidthHalf - (screenRectCenterX - mouseXPos);
}
double hypothenuse = std::sqrt(std::pow(opposite, 2) + std::pow(adjacent, 2));
//calculate cos and corresponding angle in rad and deg
double cos = adjacent / hypothenuse;
double angleRad = std::acos(cos);
double angleDeg = angleRad * 180 / PI;
//calculate initial rotation angle in rad and deg
double controlAtan = 0.5;
double controlAngleRad = std::atan(controlAtan);
double controlAngleDeg = controlAngleRad * 180 / PI;
//determine final position for y
if (mouseRectangleSector[0] == 'S')
{
if (angleRad > controlAngleRad)
{
mouseTileYPos = maxY;
}
else
{
mouseTileYPos = minY;
}
}
else
{
if (angleRad < controlAngleRad)
{
mouseTileYPos = maxY;
}
else
{
mouseTileYPos = minY;
}
}
//determine position for x
double mouseTileXPos;
if ((boost::numeric_cast<int>(mouseTileYPos)) % 2 == 0)
{
mouseTileXPos = (mouseXPos + tileWidthHalf) / tileWidth;
}
else
{
mouseTileXPos = mouseXPos / tileWidth;
}
mouseTileXPos = std::floor(mouseTileXPos);
Point gridXY{(std::size_t)mouseTileXPos, (std::size_t)mouseTileYPos};
return gridXY;
}
int main(int argc, char *args[])
{
const double PI = 3.1415926535897932384626433832795;
const int SCREEN_WIDTH = 1600;
const int SCREEN_HEIGHT = 900;
//init SDL Components
if (SDL_Init(SDL_INIT_EVERYTHING) != 0)
{
return 1;
}
SDL_Window *sdlWindow = SDL_CreateWindow("A New Era", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
SDL_Renderer *sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (sdlWindow == nullptr)
{
SDL_DestroyWindow(sdlWindow);
SDL_Quit();
return 1;
}
if (sdlRenderer == nullptr)
{
SDL_DestroyWindow(sdlWindow);
SDL_DestroyRenderer(sdlRenderer);
SDL_Quit();
return 1;
}
if (!(IMG_Init(IMG_INIT_PNG)))
{
SDL_DestroyWindow(sdlWindow);
SDL_DestroyRenderer(sdlRenderer);
SDL_Quit();
return 1;
}
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, 255);
SDL_RenderClear(sdlRenderer);
SDL_SetRenderDrawColor(sdlRenderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
//tile dimensions
std::size_t tileWidth = 128;
std::size_t tileHeight = 64;
std::size_t tileWidthHalf = tileWidth / 2;
std::size_t tileHeightHalf = tileHeight / 2;
int originX = 0;
int originY = 0;
int xScreenOffset;
int yScreenOffset = 0 - tileHeightHalf;
//add pseudo data points to tile vector
std::vector<Point> mapTiles{};
for (std::size_t y = 0; y < 10; y++)
{
for (std::size_t x = 0; x < 10; x++)
{
Point point{x, y};
mapTiles.push_back(point);
}
}
for (std::vector<Point>::iterator itPoint = mapTiles.begin(); itPoint < mapTiles.end(); itPoint++)
{
if (itPoint->y % 2 == 0)
{
xScreenOffset = 0;
}
else
{
xScreenOffset = tileWidthHalf;
}
//draw 2:1 rombus
std::size_t tileOriginX = itPoint->x * tileWidth;
std::size_t tileOriginY = itPoint->y * tileHeightHalf;
std::size_t x1 = tileOriginX + tileWidthHalf;
std::size_t y1 = tileOriginY + tileHeightHalf;
SDL_RenderDrawLine(sdlRenderer, tileOriginX + originX + xScreenOffset, tileOriginY + originY + yScreenOffset, x1 + originX + xScreenOffset, y1 + originY + yScreenOffset);
std::size_t x = x1;
std::size_t y = y1;
x1 = x - tileWidthHalf;
y1 = y + tileHeightHalf;
SDL_RenderDrawLine(sdlRenderer, x + originX + xScreenOffset, y + originY + yScreenOffset, x1 + originX + xScreenOffset, y1 + originY + yScreenOffset);
x = x1;
y = y1;
x1 = x - tileWidthHalf;
y1 = y - tileHeightHalf;
SDL_RenderDrawLine(sdlRenderer, x + originX + xScreenOffset, y + originY + yScreenOffset, x1 + originX + xScreenOffset, y1 + originY + yScreenOffset);
x = x1;
y = y1;
x1 = tileOriginX;
y1 = tileOriginY;
SDL_RenderDrawLine(sdlRenderer, x + originX + xScreenOffset, y + originY + yScreenOffset, x1 + originX + xScreenOffset, y1 + originY + yScreenOffset);
}
SDL_RenderPresent(sdlRenderer);
//game loop
//control variables
SDL_Event event;
bool quit = false;
//originX = originX - tileWidthHalf;
while (!quit)
{
while (SDL_PollEvent(&event) != 0)
{
if (event.type == SDL_QUIT)
{
quit = true;
}
else if (event.type == SDL_MOUSEBUTTONDOWN)
{
if (event.button.button == SDL_BUTTON_LEFT)
{
int mouseXPos;
int mouseYPos;
SDL_GetMouseState(&mouseXPos, &mouseYPos);
Point gridCoordinates = mouseToGrid(mouseXPos, mouseYPos, tileWidth, tileHeight, PI, SCREEN_WIDTH, SCREEN_HEIGHT);
std::cout << "x,y : " << gridCoordinates.x << "," << gridCoordinates.y << std::endl;
}
}
}
}
SDL_DestroyWindow(sdlWindow);
SDL_DestroyRenderer(sdlRenderer);
SDL_Quit();
return 0;
}
As I said, the algorithm works so I don't need help getting it to run. I am looking for help or advice on how to make it more compact (reduce lines of code). Maybe I can get rid of a couple of if statements by doing some more maths. This is really the first time after my graduation I used this kind of maths in programming so there might be some tweaks that can be done.