Klik Programming Gems: Smooth rotation, Vector Movements, and Jump heights
So there are already plenty of good articles on this site, but after a brief (first 2 pages) search, I was unable to find anything about smooth rotations or using vectors in movements. (The easy way to precalc a jump height was a bonus)
So I decided to write a short article on these three things!
This is one that Ive tried (failed) to solve so many times: So you have a gun turret, and you dont want it to 'snap' to its target (what EVERY other mmf/tgf game out there does), you want to be different, you wanna give it some style.
Brilliant idea! Lets make it smoothly rotate towards the destination direction!
But how to do that you say? Well, any of you who tried knows that the first thing that always comes to mind is always:
(assuming angleToTarget is the angle you want it to point in and curDirection is the current angle)
+If curDirection(object) < angleToTarget
-set curDirection(object) to curDirection(object) + 1
+If curDirection(object) > angleToTarget
-set curDirection(object) to curDirection(object) - 1
Now theres nothing wrong with this approach, it seems to work. That is, until the object your tracking crosses the origin (degree 0) and your turret turns all the way around in the OPPOSITE DIRECTION to reach it. Clearly this origin presents a problem. And to make it worse, no amount of googleing will provide an answer to the problem? You might just say your screwed.
But your not! Thats why you have this article!
Heres the secret: you have to first place the direction you want to face in terms of the current direction you are facing. Finally, you clamp this result to values between +180 and -180.
Kinda like this:
-temp(object) = angleToTarget(object) - curDirection(object)
+if temp(object) > 180
-temp(object) = -360+temp(object)
+if temp(object) < -180
-temp(object) = 360+temp(object)
Perfect! Now temp(object) will always hold the shortest route around the circle to its move target! But you cant use it like this in its pure raw form, if you were to add temp(object) to the object's angle then it would snap directly to angleToTarget(object). To give it the smoothness, you further clamp this number to a turn speed. Like this:
(Where X represents the turn speed)
-curDirection(object) = curDirection(object) + max(min(temp(object),X),-1*X)
And see, that wasnt anywhere near as complicated as you might have thought it coudve been huh?
(Credits: I found this little gem while sorting through some source code on the microsoft XNA website)
===Vector based moving===
Okay, so theres this guy named Cameron, and he's tired of playing games with stupid AI as your friends. After playing enough of these, I decided to make AI THE main focal point of all of my games. And after searching this sites articles, I noticed AI theory for platform games was discussed a whole TON, but that there were no implementations, and not even a single article about top-down-shooter AI. So let me be the first.
(Lets say your making a top-down battle game with tanks)
In principle, there are two jobs an AI friend has to do:
As a general tip, NEVER combine the two into one FSM! NEVER! It sets the groundwork for VERY stupid things (ex: AI that will charge into enemy lines alone trying to gun down one renegade target). I always keep the two as separate bots.
Why do you keep them separate? Mainly, because it allows you to do away with FSM's in general. You see, when you separate these you remove the tendency to use ridiculous states such as Shoot at enemy, Chase enemy, Move to destination. But what you replace it with is fluid: You have a driver in control of where the tank is and keeping it away from obstacles, and you have a gunner responsible to shoot at any targets that the driver happens to place in range.
I will save gunner AI for my next Article.
So my focus here is on the driving AI. Now, our goals are:
1) produce an 'aware' driver, one that actively avoids targets
2) produce as human a driver as possible, one that anticipates coming within enemies attack ranges and smoothly maneuvers around these, while still providing targets to the gunner
This may sound like a very complicated goal, and it is, but the solution really is not: Vector based moving.
Heres the idea:
There are four types of weighted vectors each with a purpose:
1) Enemy push vectors
2) Wall push vectors
3) Tank avoidance vectors
4) Tank move vector
Each enemy has a push vector proportional to the difference between the distances of your attack range and thiers. (This allows enemies to present themselves as targets without the tank presenting one itself)
Each wall will have a push vector that keeps the tank from trying to run off the map.
Each friendly tank will have a vector that ensures that the tanks keep some distance between each other.
Each tank will have a move vector that ensures that the general direction it moves in is towards the target.
All these vectors will be computed into an average direction for the tank to move in. Because the direction is an average, the vectors can (and will) have weights. These can be in the form of a multiplier as follows:
Enemy push (x1)
Wall push (x5)
Tank avoidance (x1) [reason this is low is because its not like getting too close to a friendly tank will hurt you]
Tank move (xtotalVectors) [this has to be at least the total of all the other vectors. Otherwise the tank could be put in situations where the vectors pushing away from the destination are stronger than the vector pushing toward it, causing the tank to move AWAY from the destination]
Note: all of these can a should be tweaked specifically for your game. Especially the last one, making it larger than the vector total will make the tank more 'reckless' when heading toward its target.
After adding up all these vectors, we then compute an average, find the direction of this new vector, and this will become our tanks direction. This affords alot of flexibility:
*the move speed is controlled separately, meaning you can cruise as slowly or as fast as you want while maintaining the same path.
*you can set the multiplier for the Tank Move vector to zero, meaning the tank can now find the shortest route away from trouble. (this allows the enemy push vectors to completely control the tanks direction)
Now, to further complicate matters, all of these vectors will be proportional to distance. Why? It makes things smooth. Instead of jerking the tank away if it starts getting to close to an enemy, we instead gradually and fluidly move the tank away as it approaches.
Short notice: not for the novice klicker, as vectors, fastloops, and 360 directions can be hard enough for an experienced one.
Another: This article assumes you know how to place things inside of a fastloop that parses through an objects alterable values, if you try to apply this article without first placing in such a loop, youll be forced to only have one friendly tank or have many that all behave like first instance.
Now, lets begin!
We will address the vectors in the order they are listed above.
So, the first thing you need to do is parse through see if theres any enemies inside of your attack range. Why your attack range? Why not theirs? Well, (assuming your attack range is greater than theirs) this allows the tank to maneuver around them, keeping them in your attack range without actually entering theirs.
Because the normal distance equation d = sqrt(a^2 + b^2) is slow (computers are not good at factoring, and factoring is exactly what a square root is), we will instead square all the distances to be compared against (d^2 = a^2 + b^2). Not sure if this is anything of a big speed increase, considering the speed overhead already inherent in coding in TGF/MMF, but any little bit helps.
Now, a few assumptions:
'Tanks' is the name of the fastloop that parses tanks
'Add enemy push vectors' is the name of the loop that parses through enemies, adding thier vectors to its
'Add wall push vectors' is the name of the loop that will check the tank against all four sides of the screen, applying vectors as necessary
'Add tank avoidance vectors' is the name of the loop that parses through the tanks, adding other applicable tank's vectors to its
'Add tank push vector' is the name of the loop that will add in the tanks vector, and will finalize and compute the average.
plyrAtckRng = player attack range. (175 would be good for this)
enmyAtckRng = enemy attack range. (125 might be good.)
AtckRngBuffer = buffer before tank begins maneuvering away from enemies (try 4)
TankRngBuffer = buffer before tank begins maneuvering away from other tanks (try 1.25)
addX = temporary variable that will be used to add all vector X components into
addY = temporary variable that will be used to add all vector Y components into, this and addY will be used to find the tanks final direction
numVectors = temporary varaible that keeps track of how many vectors have already been added
wallR = range from wall before it begins pushing away. (try 100)
tankR = range from tank before it begins pushing away. (try 70)
wallM = wall vector multiplier. (try 5)
tankM = tank vector multiplier. (try 1)
tankPushM = tank push vector, in percent of all vectors. (try nothing below 1.0)
destX = destination X coordinate
destY = destination Y coordinate
'tank' is the name of the tank
'enemy' is the name of the enemy
//All comments will be presented in c++ form, proceeded by two forward slashes
<pc> will precede and pseudocode
</pc> will conclude any pseudocode
Now, assuming that 'Tanks' is the main loop for all the tank AI, then we will begin parsing all enemies on every occurance of this loop.
//Start the loops that will compute all the push vectors
//We also reset addX and addY here
->addX = 0
->addY = 0
->Start loop('Add enemy push vectors') NObjects(enemy) times
->Start loop('Add wall push vectors') 1 times
->tmpX = X(tank) //Why we need to do this will be evident later
->tmpY = Y(tank)
->Start loop('Add tank avoidance vectors') NObjects(tank) times
->Start loop('Add tank push vector') 1 times
//Check to see if the enemy is within our bounding box
+On loop('Add enemy push vectors')
+Index(enemy) = loopindex('Add enemy push vectors')
+Index(tank) = loopindex('Tanks')
//This is the distance equation with the distance squared
+(X(tank) - X(enemy))*(X(tank) - X(enemy)) + (Y(tank) - Y(enemy))*(Y(tank) - Y(enemy)) < plyrAtckRng*plyrAtckRng
//If this is true, add the push vector for the enemy into addX and addY
->addX = addX + Sin(AnglePoints(Clickteam Movement Controller,X(enemy), Y(enemy), X(tank), Y(tank))+90)*(((plyrAtckRng-enmyAtckRng)/AtckRngBuffer+enmyAtckRng)*((plyrAtckRng-enmyAtckRng)/AtckRngBuffer+enmyAtckRng)-((X( enemy )-X( tank ))*(X( enemy )-X( tank ))+(Y( enemy )-Y( tank ))*(Y( enemy )-Y( tank ))))/(enmyAtckRng*enmyAtckRng)
->addY = addY + Cos(AnglePoints(Clickteam Movement Controller,X(enemy), Y(enemy), X(tank), Y(tank))+90)*(((plyrAtckRng-enmyAtckRng)/AtckRngBuffer+enmyAtckRng)*((plyrAtckRng-enmyAtckRng)/AtckRngBuffer+enmyAtckRng)-((X( enemy )-X( tank ))*(X( enemy )-X( tank ))+(Y( enemy )-Y( tank ))*(Y( enemy )-Y( tank ))))/(enmyAtckRng*enmyAtckRng)
//Add one to the number of vectors we have
->numVectors = numVectors + 1
Whoa! Those last two actions seemed like alot didnt they? Well, its really simple, all we do is figure out the push vectors heading, and then we multiply it by the magnitude. Like:
Except, where B is a proportion! Heres a visual explanation of what B achieves:
* enemy attack range
- your attack range
= range before the tank starts meneuvering
the size of AtckRngBuffer determines how big the '====' will be.
...Set it to one and:
(the vector begins applying force as soon as the enemy enters your attack range)
...Set it higher (like twenty) and:
(the vector begins applying force at the very last second before you enter the enemy's attack range)
Note: In case it wasnt obvious, the '====' portion of the range is the part where the force is a proportion, the '----' portion applies a force of 0 and the '****' portion applies a force of 1.
Enough with the explainer, back to the code:
So now we need to add in the wall push vectors. This is extremely simple stuff, so I will only show the first two walls as examples:
//Add vector for the leftmost wall
+On loop('Add wall push vectors')
+Index(tank) = loopindex('Tanks')
+X(Tank) < wallR
//This will be another proportion, but hopefully much less complicated
->addX = addX + (wallR-X(tank))/(wallR*1.0)*wallM
//Note that we dont add one, we add the multiplier to our vector count
->numVectors = numVectors + wallM
//Add vector for the rightmost wall
+On loop('Add wall push vectors')
+Index(tank) = loopindex('Tanks')
+X(Tank) > Frame Width-wallR
//We simply invert this to apply to the other wall
->addX = addX + (X(tank)-(Frame Width-wallR))/(wallR*1.0)*wallM
//Note again that we dont add one, we add the multiplier to our vector count
->numVectors = numVectors + wallM
No explainer here, nothing there should have been too complicated. All that is left would be to apply these to the Y walls, and anyone should be able to substitute an X for a Y right?
Now the next part should be straight forward enough, we essentially copy and paste the code from adding the enemy push vectors and adapt it for the tank object. (For simplicity you could leave this out, it shouldnt damage the 'smart' effect too badly, unless of course friendly bullets cant pass through other friendly tanks in which case it should be left in.)
+On loop('Add tank avoidance vectors')
+Index(tank) = loopindex('Add tank avoidance vectors')
//This is the same distance equation from above,just adapted for tanks
+(X(tank) - tmpX)*(X(tank) - tmpX) + (Y(tank) - tmpY)*(Y(tank) - tmpY) < tankR*tankR
Did you figure out why earlier we had to set tmpX and tmpY before we ran this loop? Yes, because TGF cannot focus on two tanks at once, and if it does, theres no way to distinguish between the two. So we set the original tanks X and Y coordinates to temporary varaibles so that we can distinguish between thier positions anyway.
Now, back to the actions:
//This is the exact same thing you did before, again, just adapted
->addX = addX + (Sin(AnglePoints( Clickteam Movement Controller, X(tank), Y(tank), X( Tank body ), Y( Tank body ))+90)*((tankR *TankRngBuffer)*(tankR *TankRngBuffer)-((X(tank)-tmpX)*(X(tank)-tmpX)+(Y(tank)-tmpY)*(Y(tank)-tmpY)))/(tankR *tankR ))*tankM
->addY = addY + (Cos(AnglePoints( Clickteam Movement Controller, X(tank), Y(tank), X( Tank body ), Y( Tank body ))+90)*((tankR *TankRngBuffer)*(tankR *TankRngBuffer)-((X(tank)-tmpX)*(X(tank)-tmpX)+(Y(tank)-tmpY)*(Y(tank)-tmpY)))/(tankR *tankR ))*tankM
//Add to the number of vectors we have
->numVectors = numVectors + tankM
The only major difference is that TankRngBuffer is multiplied, where as AtckRngBuffer was devided. This was done to avoid the extra complexity of weird situations with improper fractions (1.25 = 5/4).
If youve followed along this far, then I congratulate you: were almost done
The last thing we have to do is add in the tanks push vector and compute the average! And these parts are easy
The tanks move vector is simply one huge vector pointing in the direction the tank wants to move. In the same fastloop that we add this, we will compute the average, to keep things quick.
+On loop('Add tank push vector')
+Index(tank) = loopindex('Tanks')
//Add in one huge vector towards the destination equal to the sum of all vectors
->addX = addX + Sin(AnglePoints(Clickteam Movement Controller, X(tank), Y(tank), destX, destY)+90)*tankPushM*numVectors
->addY = addX + Sin(AnglePoints(Clickteam Movement Controller, X(tank), Y(tank), destX, destY)+90)*tankPushM*numVectors
->numVectors = numVectors + Max(numVectors+tankPushM*numVectors, 1)
//The reason for the Max() there is to prevent a division by zero error in the event that there are no enemies, friends, or walls that added vectors.
//Reason we do it now? I mean cmon, we are about to try to calculate an average here...
->addX = addX / numVectors
->addY = addY / numVectors
Now heres the trick: because we have been keeping track of the vectors we have been adding as multiples of unit vectors, we can just divide the multiples out to produce a unit vector! (Exactly what we just did)
And now, we can find the direction our tank wants to travel in like so:
//Compute the angle from the origin to our unit vector
angle(tank) = AnglePoints(Clickteam Movement Controller, 0, 0, addX, addY)
And weve done it! Now, just make the tank move in that direction and you will be set to go! (no trick here, just basic moving in 360 directions)
I wont explain that here though because theres plenty of other articles that will. (Probably better than I can too)
Another fun tip, if you give it a speed (so that the tank is moving) but set tankPushM to zero, it will find a 'safe spot' away from enemies, walls, and other tanks. Cool shit
Note: if anyone has trouble understanding this, check this next example out:
Its nothing but an annotated version of my own implementation (annotated comments in yellow)
(Credits: a movement engine of my own design)
===Precalcing a jump height===
This is for everyone who has to do something like this to calculate a jump height:
(where A is the initial jump speed)
+Start of frame
->Start loop(precalc) A times
+On loop (precalc)
->jumpHeight(Player) = jumpHeight(Player) + loopindex(precalc)+1
Ive figured out that you can just use this formula:
jumpHeight(Player) = A*A/2+A/2
What this will do is return the maximum height that you will reach if you had the initial jumping speed A. So if A is 5:
5+4+3+2+1 = 15
5*5/2+5/2 = 15
I completely found this formula by chance one day (dont get me wrong, I was trying to figure out a formula for this, but I had no idea it'd be so simple) and I dont know why it works, it just simply does.
Try it for yourself. This would go good, for example, in a platformer's AI where the enemy will jump over an obstacle if it can jump high enough, and leaves if it cant.
Feel free to pm me for any extra explainers, or an example .mfa or two.
EDIT 6/29/09 : All quotation marks were removed, replaced with single quotes
EDIT 6/30/09 : Example file added, edits for clarity/readability