7
\$\begingroup\$

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.

enter image description here

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.

\$\endgroup\$
  • 3
    \$\begingroup\$ Code stubs aren't appropriate for this site. Flesh it out to make it a complete code with the minimal amount required to compile and run it. \$\endgroup\$ – tinstaafl Jan 1 at 17:17
  • \$\begingroup\$ I will add a fully functional example tomorrow, thank your for the advice \$\endgroup\$ – Tremah Jan 2 at 0:24
  • \$\begingroup\$ the code in my question is now a fully functional minimal example \$\endgroup\$ – Tremah Jan 2 at 18:12
2
\$\begingroup\$

If it is immaterial whether minY or maxY is used in case of angleRad == controlAngleRad, you can avoid duplication in the final position for y-handling:

//determine final position for y
mouseTileYPos = (mouseRectangleSector[0] == 'S')
    == (angleRad > controlAngleRad) ? maxY : minY;
\$\endgroup\$
  • \$\begingroup\$ As for number of lines, I prefer block delimiters in-line with control structures, and more often than not use them when mandatory, only. IDEs and tools like indent (the flags *do me*.) are good at bulk-changing such conventions to see whether one prefers one convention over the other. \$\endgroup\$ – greybeard Jan 1 at 19:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.