Sunday, March 3, 2013

RTS Input

Just wanted to point out a little strange thing I ran into, which is what I consider "syntactic" (although I suppose it is technically not). I wanted to make a std::hash_map using an std::pair. In C++ that most people are using, it seems that if you want to use std::pair then you'll run into a type cast error because it tries to hash a pair object but it can't! You'll have to define your own hash function and use it with the std::hash_map. Poop!

Here is the code if you are interested. This is as simplified as I could make and thus you should replace it with what you need yourself. (Such as a more complicated hash function)

#include <hash_map>

struct hash_int_pair : public std::hash_compare< std::pair<int,int>>
 {
  const size_t operator() ( const std::pair<int,int> &p ) const
  {
    return p.first * 10000 + p.second;
  }

 bool operator() ( const std::pair<int,int> &a, const std::pair<int,int> &b) const
 {
  if (a.first == b.first)
  {
    return a.second < b.second;
  }
  else
  {
    return a.first < b.first;
  }
 }
};

typedef std::hash_map<std::pair<int,int>,std::vector<CUnit>, hash_int_pair> UnitMap;

Anyhow, we move into the world of the basic RTS UI and forget about isometric engine performance right now. We can go back to that when it becomes too slow again. Right now we want to be able to move the mouse around, click, capture the click and translate it into a unit selection box. Nothing fancy. Anything inside the box gets selected. But, there is the standard RTS selection algorithm.
  • Select your own units first
  • Select enemy units if any, but only the first
  • Select animals if any, but only the first
  • Select a piece of terrain if it was just a click
Drawing a box around the units is a bit confuzzling due to the rotated nature of the cartesian xy plane. Here is a little excerpt of the mash up code I wrote to just do it. I convert the coordinates into something "rotated" and then convert back and forth as necessary to get the right numbers. This is a bit inaccurate but close enough for now.

Quickly explained, the code basically selects a box along a zig-zagging top, iterate downward to reach the bottom zig-zagging row and then move to the next column toward the right.  I'm quite sure there's lots of problems with this right now but I'm just trying to get mouse picking working initially.

void CFaction::getUnitsByArea(std::pair<int,int> firstCorner, std::pair<int,int> secondCorner, std::vector<CUnit*> &unitsInArea, bool getFirstUnitOnly)
{
//convert coordinates into (x+y),(y-x)
std::pair<int,int> convertedFirstCorner = std::pair<int,int>(firstCorner.first + firstCorner.second, firstCorner.second - firstCorner.first);
std::pair<int,int> convertedSecondCorner = std::pair<int,int>(secondCorner.first + secondCorner.second, secondCorner.second - secondCorner.first);

int leftX = convertedFirstCorner.first < convertedSecondCorner.first ? firstCorner.first : secondCorner.first;
int rightX = convertedFirstCorner.first < convertedSecondCorner.first ? secondCorner.first : firstCorner.first;
int topY = convertedFirstCorner.second < convertedSecondCorner.second ? secondCorner.second : firstCorner.second;
int bottomY = convertedFirstCorner.second < convertedSecondCorner.second ? firstCorner.second : secondCorner.second;

//this is actually too fine grain
int currColumnX = leftX;
int currColumnY = topY;
bool moveDownRight = true;
for (int i = 0; i < (Ogre::Math::Abs(convertedFirstCorner.first - convertedSecondCorner.first) + 1); i++)
{
int jMax = Ogre::Math::Abs(float(convertedFirstCorner.second) - float(convertedSecondCorner.second)) / 2.0f;
for (int j = 0; j <= jMax; j++)
{
//shift down
int x = currColumnX + j;
int y = currColumnY - j;

std::pair<int,int> currLocation = std::pair<int,int>(x,y);

for (int i = 0; i < m_unitsByLocation[currLocation].size(); i++)
{
unitsInArea.push_back(&m_unitsByLocation[currLocation][i]);

if (getFirstUnitOnly)
{
return;
}
}
}

if (moveDownRight)
{
currColumnX++;
moveDownRight = false;
}
else
{
currColumnY++;
moveDownRight = true;
}
}
}

What do in Ogre3d land?  We create a ray based on the mouse position.  Ogre provides an API to do so.  We then calculate it's intersection with the XY plane.  Ogre also has code to do this.  Then we translate the x, y coordinates to a game coordinate (I have a constant called UNIT_DISTANCE in my code).


if (btn == OIS::MB_Left)
{
Ogre::Ray mouseRay = m_nCamera->getCameraToViewportRay(evt.state.X.abs/float(evt.state.width), evt.state.Y.abs/float(evt.state.height));
std::pair<bool, Ogre::Real> intersectionValues = mouseRay.intersects(Ogre::Plane(Ogre::Vector3::UNIT_Z, 0));

Ogre::Vector3 intersectionPoint;
if (intersectionValues.first)
{
intersectionPoint = mouseRay.getPoint(intersectionValues.second);
//translate the intersection point into gameworld coordinates
m_firstCorner = std::pair<int,int>(intersectionPoint.x / UNIT_DISTANCE, intersectionPoint.y / UNIT_DISTANCE);
}
}


And now let's see it!

Okay, so a static image doesn't really help show it. :P

In any case, it will have to be improved with several features.  First, I need to draw the selection box.  Second, make sure the box works no matter how you define the two corners.  Third, add in the ability to use shift and alt clicking which is a pretty standard UI interface.  The selection is somewhat inaccurate due to the weird isometric layout, but I'll just leave it alone for now (also because I box based on actual grid squares instead of something more fine).

2 comments:

  1. Side note: std::unordered_map should probably be used in favor of std::hash_map since it's part of the standard and the implementations of std::hash_map are typically built as compiler-specific extensions.

    ReplyDelete
    Replies
    1. Thankfully that change could be made without doing anything to my code. I was curious as to why I needed to include hash_map if it was part of std but I suppose I mistook the msdn article as it being part of std lib when it was not.

      Delete