4
votes

Scenario

I am working on a top-down view game in which enemies move towards certain positions. The destination being moved towards will often change drastically - sometimes while the enemy is still in motion towards the previous destination...

I want to achieve movement which is more realistic than linear movement, so there should be some acceleration and deceleration as enemies switch between the destinations.

Steering (direction) is not a factor. You may assume the sprites will move much like a hovercraft, drifting between the destinations as quickly as it can manage with respect to acceleration and deceleration.

For simplicity - lets assume there is only 1 dimension (Y) instead of X and Y... the movement should emulate a car which can only move north or south.

Since we are considering realistic movement in this scenario, you might not be surprised that a maximum speed over time is also a consideration. The enemy should never exceed it's own maximum speed; enemies store their own maximum speed in a variable.

One final consideration is that enemies will not only have a 'maximum speed' value, but it will also have a 'maximum acceleration' value - this would be the thing which guides how quickly each enemy can respond to moving in the opposite direction.

For simplicity, assume that the enemy does not have any movement friction... when it stops accelerating, it just keeps cruising at the current velocity forever.

Example

For context, lets elaborate on the car example. A particular car has:

  • Maximum speed: 10 meters per second
  • Maximum acceleration: can reach top speed in 2 seconds
  • (Other factors like destination y-pos, position y-pos, current velocity, etc)

Just like when I'm driving a car, I imagine all of these values are present, but I can't change them. All I can really change is how much I'm putting my foot on the acceleration/brake. Let's call this the 'throttle'. Like the acceleration pedal in a car, I can change this value to any amount at any time, in no time at all.

I can plant my foot down (throttle=1), let go of the pedal immediately (throttle=0), and even change into reverse and plant my foot down again (throttle=-1). Lets assume these changes in throttle are INSTANT (unlike speed or acceleration, which grow/shrink over TIME)

All that said, I imagine the only value that I really need to calculate is what the throttle should be, since thats the only thing I can control in the vehicle.

So, how then do I know how much throttle to use at any given moment, to arrive as quickly as possible to a destination (like some traffic lights) without overshooting my destination? I will need to know how much to push down the accelerator, briefly not accelerate at all, and then how hard decelerate as I'm closing in on the destination.

Preempted movement

This game will likely have an online component. That said, players will be transmitting their positions over a socket connection... however even the best connection could never achieve sending positions frequently enough to achieve smooth movement - you need interpolation. You need to send the 'velocity' (speed) along with the co-ordinates so that I can assume future positions in the time between packets being received.

For that reason, Tweens are a no-go. There would be no way to send a tween, then accurately inform the other parties at what point along each tween each entity currently is (I mean, I guess it is possible, but horrendously overcomplicated and probably involves rather large packet sends, and is probably also very exploitable in terms of the online component), then you need to consider aborting Tweens as the destinations change, etc.

Don't get me wrong, I could probably already model some very realistic movement with the ease-in/ease-out functionality of Tweens, and it would look great, but in an online setting that would be.. very messy.

How's it look so far?

So, essentially I have established that the primary aspect which needs to be calculated at any time is how much throttle to use. Let's work through my logic...

Imagine a very basic, linear movement over time formula... it would look like this:

Example 1 - position over time

currentDY = 5;               // Current 'velocity' (also called Delta Y or 'DY')
currentY += currentDY * time // Current Y pos is increased by movement speed over time.

As you can see, at any given moment, the Y position increases over time due to the 'velocity' or DY (over time). Time is only a factor ONCE, so once we reach the destination we just set the DY to zero. Very sharp unrealistic movement. To smoothen the movement, the velocity ALSO needs to change over time...

Example 2 - velocity over time

throttle = -1
currentDY += throttle * time;
currentY += (currentDY * time);
//Throttle being -1 eventually decelerates the DY over time...

Here, the throttle is '-1' (maximum reverse!), so over time this will reduce the velocity. This works great for realistic acceleration, but provides no realistic anticipation or deceleration.

If we reach the destination this time, we can set the throttle to '0'... but it's too late to brake, so the resulting enemy will just keep moving past the target forever. We could throttle = '1' to go back, but we'll end up swinging back and forth forever.

(Also note that the maximum acceleration and speed isn't even a factor yet - it definately needs to be! The enemy cannot keep ramping up their speed forever; velocity delta AND ALSO acceleration delta need to have limits).

All that said, it's not enough to simply change velocity over time, but we also need be able to anticipate how much to decelerate (i.e. 'backwards throttle') BEFORE IT HAPPENS. Here's what I've got so far, and I'm practically certain this is the wrong approach...

Example 3 - throttle over time?? (I'm stuck)

guideY = currentY + (currentDY * (timeScale * 3000));
dist = endY - guideY;
throttle = Math.max(-1, Math.min(1, dist / 200));
currentDY += throttle * time;
currentY += (currentDY * time);

As you can see, this time we attempt to anticipate how much throttle to use by guessing where the enemies position will be in an arbitrary time in the future (i.e. 3 seconds). If the guideY went past the destination, we know that we have to start BRAKING (i.e. reducing speed to stop on top of the destination). By how much - depends on how far away the enemies future position is (i.e. throttle = dist / 200;)

Here's is where I gave up. Testing this code and changing the values to see if they scale properly, the enemy always swings way over the destination, or takes far too long to 'close in' on the destination.

I feel like this is the wrong approach, and I need something more accurate. It feels like I need an intersection to correctly anticipate future positions.

Am I simply using the wrong values for the 3 seconds and dist / 200, or am I not implementing a fully working solution here?

Presently, compared to the linear movement, it always seems to take 8 times longer to arrive at the target position. I haven't even reached the point of implementing maximum values for DeltaVelocity or DeltaAcceleration yet - the accepted solution MUST consider these values despite not being present in my JSFiddle below...

Test my logic

I've put all my examples in a working JSFiddle.

Screenshot of JS fiddle and code (for context)

JSFiddle working testbed (Click the 'ms' buttons below the canvas to simulate the passing of time. Click a button then press+hold Return for very fast repetition)

The sprite is initially moving in the 'wrong' direction - this is intended for testing robustness - It assumes an imaginary scenario where we just finished moving as fast as possible toward an old destination far lower on the screen, and now we need to suddenly start moving up...

As you can see, my third example (see the update function), the time for the sprite to 'settle' at the desired location takes far longer than it should. My math is wrong. I can't get my head around what is needed here.

What should the throttle be at any given time? Is using throttle even a correct approach? Your assistance is very appreciated.

Tiebreaker

Alright, I've been stuck on this for days. This question is going up for some phat bounty.

If a tiebreaker is required, the winner will need to prove the math is legit enough to be tested in reverse. Here's why:

Since the game also comprises of a multiplayer component, enemies will be transmitting their positions and velocities.

As hacking protection, I will eventually need a way to remotely 'check' if the velocity and position at between any two sample times is possible

If the movement was too fast based on maximum velocity and acceleration, the account will be investigated etc. You may assume that the game will know the true maximum acceleration and velocity values of the enemies ahead of time.

So, as well as bounty, you can also take satisfaction in knowing your answer will contribute to ruining the lives of filthy video game cheaters!

1
What is your primary goal? Having starting coordinate and velocity, to stop in the given point as soon as possible? To stop in that point in predetermined time? Formulate a task clearly.MBo
Good feedback, will make some edits (actually the reason I didn't do that sooner is because the question was already wordy enough, but it definitely needs some context...)1owk3y
Isn't acceleration by definition over time?Anthony

1 Answers

3
votes

Edit 2: Fiddle added by answer author; adding into his answer in case anyone else finds this question: http://jsfiddle.net/Inexorably/cstxLjqf/. Usage/math explained further below.

Edit 1: Rewritten for clarification received in comments.

You should really change your implementation style.

Lets say that we have the following variables: currentX, currentY, currentVX, currentVY, currentAX, currentAY, jerk. currentVX is what would have been your currentDX. Likewise, currentAX is the x component of your delta velocity value (accel is derivative of velocity).

Now, following your style, we're going to have a guideX and a guideY. However, there is another problem with how you're doing this: You are finding guideY by predicting the target's position in three seconds. While this is a good idea, you're using three seconds no matter how close you are to the target (no matter how small dist is). So when the sprite is 0.5 seconds from the target, it's going to still be moving towards the target's estimated position (three seconds into the future). This means it won't actually be able to hit the target, which seems to be the problem that you implied.

Moving on, recall the previous variables that I mentioned. Those are the current variables -- ie, they will be updated at every call after some seconds have been passed (like you have been doing before). You also mentioned a desire to have maxV, and maxA.

Note that if you have a currentVX of 5 and a currentVY of 7, the velocity is (5^2+7^2)^0.5. So, what you're going to want to do each time you're updating the 'current' archetype of variables is before updating the value, see if the magnitude (so sqrt(x^2+y^2) of those variables like I showed with velocity) will exceed the respective maxV, maxA, or jmax values that you have set as constants.

I'd also like to improve how you generate your guide values. I'm going to assume that the guide can be moving. In this case, the target will have the values listed above: x, y, vx, vy, ax, ay, jx, jy. You can name these however you'd like, I'm going to use targetX, targetY... etc to better illustrate my point.

From here you should be finding your guide values. While the sprite is more than three seconds away from the target, you may use the target's position in three seconds (note: I recommend setting this as a variable so it is simple to modify). For this case:

 predictionTime = 3000*timescale; //You can set this to however many seconds you want to be predicting the position from.

If you really wanted to, you could smooth the curve using integration functions or loops for the guide values to get a more accurate guide value from the target values. However, this is not a good idea because if you ever implement multiple targets / etc, it could have a negative impact on performance. Thus, we will use a very simple estimation that is pretty accurate for being such low cost.

 if (sprite is more than predictionTime seconds away from the target){
      guideX = targetX + predictionTime * targetVX;
      guideY = targetY + predictionTime * targetVY;
 }

Note that we didn't account for the acceleration and jerk of the target in this, it's not needed for such a simple approximation.

However, what if the sprite is lese than predictionTime seconds away from the target? In this case we want to start increasingly lessening our predictionTime values.

 else{
      guideX = targetX + remainingTime * targetVX;
      guideY = targetY + remainingTime * targetVY;.
 }

Here you have three choices on finding the value of remaining time. You can set remainingTime to zero, making the guide coordinates the same as the targets. You can set remainingTime to be sqrt((targetX-currentX)^2+(targetY-currentY))/(sqrt(currentVX)^2+(currentVY)^2), which is basically distance / time in 2d, a cheap and decent approximation. Or you can use for loops as mentioned before to simulate an integral to account for the changing velocity values (if they are deviating from maxV). However, generally you will be remaining at or close to maxV, so this is not really worth the expense. Edit: Also, I'd also recommend setting remainingTime to be 0 if it is less than some value (perhaps about 0.5 or so. This is because you don't want hitbox issues because your sprite is following guide coordinates that have a slight offset (as well as the target moving in circles would give it a larger velocity value while changing direction / essentially a strong evasion tactic. Maybe you should add something in specifically for that.

We now have the guideX and guideY values, and account for getting very close to a moving target having an effect on how far from the target the guide coordinates should be placed. We will now do the 'current' value archetype.

We will update the lowest derivative values first, check to see if they are within bounds of our maximum values, and then update the next lowest and etc. Note that JX and JY are as mentioned before to allow for non constant acceleration.

 //You will have to choose the jerk factor -- the rate at which acceleration changes with respect to time.    

 //We need to figure out what direction we're going in first.  Note that the arc tangent function could be atan or whatever your library uses.
 dir = arctan((guideY-currentY)/(guideX-currentX));

This will return the direction as an angle, either in radians or degree depending on your trig library. This is the angle that your sprite needs to take to go in the direction of guide.

 t = time; //For ease of writing.
 newAX = currentAX + jerk*t*cos(dir);
 newAY = currentAY + jerk*t*sin(dir);

You may be wondering how the newAx value will ever decrease. If so, note that cos(dir) will return negative if guide is to the left of the target, and likewise sin(dir) will return negative if the sprite needs to go down. Thus, also note that if the guide is directly below the sprite, then newAx will be 0 and newAY will be a negative value because it's going down, but the magnitude of acceleration, in other words what you compare to maxA, will be positive -- as even if the sprite is moving downwards, it's not moving at negative speed.

Note that because cos and sin are of the same library as atan presumably, so the units will be the same (all degrees or all radians). We have a maximum acceleration value. So, we will check to make sure we haven't exceeded that.

 magnitudeA = sqrt(newAX^2+newAY^2);
 if (magnitudeA > maxA){
      currentAX = maxA * cos(dir);
      currentAY = maxA * sin(dir);
 }

So at this point, we have either capped our acceleration or have satisfactory acceleration components with a magnitude less than maxA. Let us do the same for velocity.

 newVX = currentVX + currentAX*t;
 newVY = currentVY + magnitudeA*t*sin(dir);

Note that I have included two ways to find the velocity components here. Either one works, I'd recommend choosing one and using it for both x and y velocity values for simplicity. I just wanted to highlight the concept of magnitude of acceleration.

 magnitudeV = sqrt(newVX^2+newVY^2);
 if (magnitudeV > maxV){
      currentVX = maxV * cos(dir);
      currentVY = maxV * sin(dir);
 }

We'd also like to stop boomeranging around our target. However, we don't want to just say slow down alot like you did in your JSFiddle, because then if the target is moving it will get away (lol). Thus, I suggest checking how close you are, and if you are in a certain proximity, reduce your speed linearly with distance with an offset of the target's speed. So set closeTime to something small like 0.3 or what ever feels good in your game.

 if (remainingTime < closeTime){
      //We are very close to the target and will stop the boomerang effect.  Note we add the target velocity so we don't stall while it's moving.
      //Edit: We should have a min speed for the enemy so that it doesn't slow down too much as it gets close.  Lets call this min speed, relative to the target.
      currentVX = targetVX + currentVX * (closeTime - remainingTime);
      currentVY = targetVY + currentVY * (closeTime - remainingTime);
      if (minSpeed > sqrt(currentVX^2+currentVY^2) - sqqrt(targetVX^2-targetVY^2)){
           currentVX = minSpeed * cos(dir);
           currentVY = minSpeed * sin(dir);
      }

 }
 magnitudeV = sqrt(currentVX^2+currentVY^2);

At this point we have good values for velocity too. If you going to put in a speedometer or check the speed, you're interested in magnitudeV.

Now we do the same for position. Note you should include checks that the position is good.

 newX = currentX + currentVX*t;
 newY = currentY + currentVY*t;

 //Check if x, y values are good.
 current X = newX; currentY = newY;

Now everything has been updated with the good values, and you may write to the screen.