Monday, January 26, 2009

Moving in an RTS

In this post I will be discussing the theory behind the code in making a unit move from one location to another. This is not meant to be a complete tutorial, nor ground breaking. In fact, it won't even cover basic path finding. However, if you are having problems making a unit move between two points (namely, they move diagonally and then straight along) read on.

Such a seemingly simple thing, moving a unit between two points in a straight line (ignoring path finding as FK will not have path finding) However,I struggled with it for quite some time.

At first, I thought it would be really simple. See if the X co-ordinate of the object is bigger or smaller than the target X co-ordinate. Then, add/subtract the desired speed depending on whether the number is bigger or smaller (with bigger numbers being down and to the right). This works fine if the destination is as much above as it is to the right. However, if the target is not, the unit moves at 45 degrees toward the thing until either the X or Y co-ordinate are in line with the destrination (which ever is sooner) at which point, it moves horizontally/vertically. Apart from looking ridiculous, it is not the quickest way between the two points and so will be incredibly frustrating for the player. But then, if you are the target audience for this post, you knew this already.

[Note: Another problem that you may encounter is that as the unit reaches it's destination, it jumps around it. This is because it is unlikely the player will click on a pixel that your units movement speed goes in to exactly. As a result, you unit will switch between being to the left and to the right of the target. I will explain my solution to this at the end of the post]

I tried a couple of other things that, in hind sight, are long winded amd complicated; I won't bore you with the the details. They ended up working to a point, except when the unit had to move either vertically or horizontally, it would accelerate to near infinite speeds.

The solution I finally settled on, at first seemed too complicated (and for all I know, there might be a better one) It relies on creating a right angled triangle with the destination point, finding the accute angle and creating proportional x/y speeds that add up to make a total (limiting the maximum speed) (See diagram)



Using trigonometry (Soh Cah Toa!) we can find the angle using xDist and yDist (which can be calculated by subtracting the larger x/y co-ordinate from the smaller of the unit and it's destination)

Tan(Angle) = yDist/xDist

Or...

Angle = atan(yDist/xDist) //atan is the inverse of tan, called atan in most programming langauges.

In most programming languages, this will actually give you the answer in radians (a way of numbering angles where pi represents 180 degrees). However, for the sake of simplicity (no PI key on my keyboard!) I will use degrees. You can either convert Angle in to degrees (*180 and then divide by PI) or when I say 90, use PI/2.

Next, we work out how steep the hypotenuse (longest side of the trianlge) needs to be. By doing the angle/90 we can work out what proportions the 2 speeds need to be. If the angle is 90, then we know we want the whole speed to be vertical, whereas 0 needs to give the whole speed as horizontal.

The way I did this was first calculate the Y speed.

ySpeed = (Angle/90)*Speed //where speed is the distance in pixels that you want your unit to cover in one frame.

In this, if the angle is 45, (ie. as far up as it is across) then you get (1/2)*speed resulting in half of your total speed to vertical.

The xSpeed is then calculated by taking the angle from 90 and putting that over 90. In the end, you will get two angles that add up to 90. Therefore, when you put them over 90 as two seperate fractions, they will add up to 1. So, when the two fractions are times by the speed, the two fractions of speed will add up to speed.

The problem with this is it will only work when both values are increasing (ie, the unit is moving right and down) To get around this (and deal with a second problem which I mentioned earlier) when the destination is selected, define two boolean variables to store whether the target is left/right and up/down. Then, when your regular function to move the unit is called, if movingRight == true, if != true, then subtract the number and so on.

I realise that this is a little confusing, so here is a quick summary of what I mean

Summary: By finding out the angle that the destination is from the current position, you can find the preportion that the two speeds need to be.

The final issue is checking when a unit is arriving. By using your movingRight boolean variable, you can simply see whether the unit has passed the X co-ordinate. If movingRight==true then if curX >= targetX then it has arrived. Likewise, if movingRight != true, then curX <= targetX for it to have arrived. And you do not need to check y, as they should happen at the same time.

Here is my C#/XNA code if it is any help: (distASec is a Vector2 which stores the speed, curPosition is a Vector2 which has the units location and currentTarget is a Vector2 which is where the unit is heading. Vector2 is a XNA data type which stores X and Y co-ordinates(as floating point numbers, if your in to that sort of thing!).

if (currentPosition != newTarget)
{
Vector2 totDist;
float refAngle;
currentTarget = newTarget;

//Calcuates total distance and direction
if (currentPosition.position.X < movingright =" true;" x =" currentTarget.position.X">


}
else
{
movingRight = false;
totDist.X = currentPosition.position.X - currentTarget.position.X;


}

if (currentPosition.position.Y < movingdown =" true;" y =" currentTarget.position.Y">


}
else
{
movingDown = false;
totDist.Y = currentPosition.position.Y - currentTarget.position.Y;
}
refAngle = (((float)(Math.Atan((totDist.Y / totDist.X))))*180)/(float)Math.PI;


distASec.Y = (refAngle / 90)*speed;
distASec.X = ((90 - refAngle)/90)*speed;

}

Finally, to check if the unit has arrived.

if((movingRight && currentPosition.position.X >= currentTarget.position.X)(!movingRight && currentPosition.position.X <= currentTarget.position.X))
{

//Code to be excuted upon arrival

}

No comments: