Search code examples
pythonmathgame-physicstrigonometryangle

How can I limit the X and Y offsets of a car with a max_speed value while taking its angle into account?


Context :

I have this game where I control a car in a top-down view. I use multiple variables to move it.

  • The MAX_SPEED is 5. (meaning it can move 5px per tick at most)
  • The FORWARD_ACC (for acceleration) is 0.25 (meaning the speed goes up by a quarter of a px per tick when accelerating).
  • The speed of the car (which is a vector [0,0] : [x offset, y offset]).
  • The acceleration of the car (which is a vector too [x acc, y acc]).
  • Finally, the angle of the car (which is in radians).

Those variables allow me to assign an acceleration to the car according to its angle in top-down view.

I calculate the acceleration value with trigonometry : acceleration = [cos(angle)*FORWARD_ACC, sin(angle)*FORWARD_ACC].

So, when accelerating (0.25px) at an angle of pi/2 radians (which is going down, so 90 degrees in pygame), my acceleration vector will be [0, 0.25]. when going at an angle of pi radians (which is going left, so 180 degrees in pygame), my acceleration vector will be [-0.25, 0].

I then add the acceleration to the speed with this function :

def add_acc_to_speed(speed, acceleration):
    returned_speed = [speed[0]+acceleration[0], speed[1]+acceleration[1]]
    return(returned_speed if abs(returned_speed[0])+abs(returned_speed[1])<MAX_SPEED else speed)

This function was supposed to make it so that the sum of |x offset| + |y offset| could not exceed MAX_SPEED. This way the car cannot go faster than 5px.

What's wrong :

The issue is that this function, as you can see, returns the passed speed parameter if the sum is higher than 5. A side effect of this is that the new acceleration, which is passed as a parameter and depends on the angle of the car, is not taken into account.

Basically, let's say your car goes right for a while and rapidly reaches max speed. if you try to turn to the down direction, your car won't turn and rather go kind of sideways as long as your Y offset from the acceleration variable is not higher than the x offset. so for example if I have [0.19, 0.16] I'd still go right for a good 30 seconds before the 0.16 starts being taken into account (and only because the 0.19 is not 0.25 so the x offset is slowly reducing, allowing more "space" in the window I allowed, which is limited by max_speed.

Now let's say you turn a little more and your acceleration value is now [0.17,0.18]. Since the Y offset of your acceleration is now higher than the X offset, the car will make a turn as intended.

This leads to strange, sharp turns and a car mostly going up, down, left or right : sharp turns.

TL;DR :

the limit I put on my speed vector makes it so that one of the offsets needs to lower in order for the other one to go up, which won't happen as long as the one that needs to lower has a higher acceleration value.

What did I already try ?

  • Setting an individual limit of MAX_SPEED on each of the offsets (this leads to both offsets being able to go up to 5 ([5,5]) and the car going WAY TOO FAST when going diagonally).
  • Setting an individual limit of MAX_SPEED//2 on each of the offsets (this solves the diagonal issue but the car now goes only 2.5 even when facing up, down, left or right ([2.5,0] for example).
  • I did not try that yet but maybe dynamically modifying the MAX_SPEED value so that I have a middle ground between both those attempts ?

I know that it's more complicated than just setting an integer as a limit, and that my mistake that leads to those sharp turns is returning the passed speed parameter and thus not taking the new acceleration and angle into account.

I hope I explained myself clearly ! feel free to ask further questions or for code snippets :)

EDIT :

Following @SimonGoater's comment, my car can now go in circles, but it seems the issue of the car not actually turning until the lower offset's acceleration value takes over the other one's is still affecting my game.

Here you can see the acceleration values on top, then the speed value underneath it, in the brackets. As you can see, the car is facing the bottom right corner of the screen, and still won't turn because of the function returning the passed speed parameter (and thus not taking the angle of the car into account) when the returned speed exceeds MAX_SPEED.

car going almost completely right even though it's facing the bottom right corner of the picture (note that if the speed says 0.05 where the acceleration is 0.17, it's because there is also friction in my game, which can be ignored in this question.)


Solution

  • You are calculating what is know as Manhattan Distance abs(x) + abs(y) for the length of your speed vector. You should be using the normal Cartesian Distance sqrt(x^2 + y^2). Since your maximum doesn't change, it is more efficient to compare the squares directly since square root calculations are relatively slow. I think for a naive solution, you just need to change the return line

    return(returned_speed if returned_speed[0]*returned_speed[0]
      +returned_speed[1]*returned_speed[1]
      <MAX_SPEED*MAX_SPEED else speed)
    

    Better still use a constant for MAX_SPEED*MAX_SPEED. It might be better if the length exceeds MAX_SPEED, that the vector is scaled to the maximum length rather than just returning the previous vector. This can be achieved by multiplying both components by

    MAX_SPEED / sqrt(returned_speed[0]*returned_speed[0]
      +returned_speed[1]*returned_speed[1])
    

    if the denominator exceeds MAX_SPEED.

    Something to consider, is that velocity is the term usually used for the rate of change of position, and 'speed' is normally the scalar length of that vector.