Simple acceleration/deceleration code!

Discussion in 'Tutorials' started by Roderick, Oct 3, 2018.

  1. Roderick

    Roderick Member

    Joined:
    Jun 22, 2016
    Posts:
    565
    I've seen a few tutorials that have commented that systems to make a character smoothly accelerate or decelerate were "too complicated to go into here", or have included large blocks of code to handle it. Well, I'm here to tell you that this is not the case! A nice smooth acceleration and deceleration can both be handled in a single line of code!

    Code:
    speed = (speed + acceleration) * deceleration
    A couple caveats:
    1. speed and acceleration are both in pixels per frame.
    2. deceleration is a percentage, ranging from 0 to 1.
    3. Because deceleration is being applied to acceleration immediately, your actual speed gain is acceleration * deceleration. For example, if acceleration is 10 and deceleration is 0.8, you only gain 8 speed in the first frame (and less in each subsequent frame, because you're losing 20% of speed's current value).
    4. Setting deceleration to 0 will prevent your character from moving.
    5. Setting deceleration to 1 will make your character accelerate linearly, with no cap.
    6. Setting deceleration over 1 will make the character accelerate faster than the acceleration value, and will make them continue to accelerate, even after they stop moving.
    7. This applies acceleration and deceleration every frame! As such, if you're working with very small values (such as pixel movement in a low-res game), you will hit the cap super fast, and there won't be any visible smooth acceleration/deceleration. We'll discuss how to deal with that a bit further down.
    So, what does this formula do for us? First, look at the following graphs:

    Untitled.png

    On the Accelerating chart, the object is starting at 0 and adding 5, then decelerating by 0.70 each frame. On the Decelerating chart, it's starting just below 12 (where the previous chart maxed out), adding 0 per frame, and still decelerating by 0.70. If you were to accelerate in the opposite direction, it would do the exact same thing, just in negative numbers.
    Technically, it will never reach 12, instead becoming infinitely close. Likewise, the speed will never drop to 0 (unless you apply negative acceleration), but will again get infinitely close; so close that it would take years to move a single pixel. Due to the limits of computer storage and the resulting rounding errors, you will eventually get a result of those maximum or minimum numbers.

    If you don't want sub-pixel movements, you could apply a round() to the formula. It would alter the curve, but still give a similar final effect. Do not use floor() or ceil() though, as those round down or up, not towards or away from 0 as many people expect (ie, floor(3.5) is 3, but floor(-3.5) is -4, not -3).
    Be aware that this has it's own risk: In the charts above, since deceleration is 0.7, you get the following results:
    12 * 0.70 = 8.40 = 8
    8 * 0.70 = 5.60 = 6
    6 * 0.70 = 4.20 = 4
    4 * 0.70 = 2.80 = 3
    3 * 0.70 = 2.10 = 2
    2 * 0.70 = 1.40 = 1
    1 * 0.70 = 0.70 = 1
    1 * 0.70 = 0.70 = 1
    Without some sort of error checking, the character never stops once they start moving. Personally, I'd recommend sticking with the unrounded, sub-pixel movement.


    If you are using delta time, the calculations become a bit more complicated, but not untenable.

    The first thing you have to do is reduce acceleration from its per-second value to its per-frame value. This is easy:
    Code:
    acceleration * delta
    So, if your acceleration is 60 pixels per second and delta is exactly 1/60 of a second this frame, you get an acceleration of 60/60 or 1 pixel.

    However, if you do the same thing to deceleration, everything falls apart. If you take a deceleration of 0.60 and multiply it by the same 1/60 above, you get 0.60/60 = 0.01, which, over the course of 1 second results in the retention of 0.000000000000000000000000000000000000000000000000000000000001% of a pixel per second, which is so small that internal rounding would make it so that there was no movement at all.

    What went wrong? It's because the value of deceleration is what's left over after we slow down, and we don't want to break that up into it's per-frame value, we want the loss (in the above example 100% - 60% = 40% or 0.40) to be broken up. So the per-frame for deceleration is:
    Code:
    (1 - ((1 - deceleration) * delta))
    What's happening there? First, we're subtracting deceleration from 1 to give us the speed loss as a percentage. Then, we're multiplying that by delta to figure out how much speed is lost per frame. Then, we're subtracting the per-frame speed loss from 1, so that we again have the percentage of speed retained. Taken together, it's:
    Code:
    speed = (speed + (acceleration * delta)) * (1 - ((1 - deceleration)  * delta))
    Of course, you could get the same result by just using the first formula and the per-second values adjusted to their per-frame values, but this method lets you set acceleration and deceleration as per-second values, and have them stay accurate, even in the case of lag or frame skips.

    The one drawback to this system is that the maximum speed is not easily calculated based on your acceleration and deceleration values. Looking at the original chart, how do we get a max speed of 12 from acceleration 5 and deceleration 0.70?
    Each frame, we take the starting value, add acceleration, and multiply the total by deceleration. This is the same as taking the starting speed and multiplying it by deceleration, and then adding acceleration * deceleration. Each frame, the starting value of speed is the ending value of the previous frame.
    Frame 1: (5 * 0.70)
    Frame 2: (5 * 0.70) + (5 * 0.70 * 0.70)
    Frame 3: (5 * 0.70) + (5 * 0.70 * 0.70) + (5 * 0.70 * 0.70 * 0.70)

    And so on. Eventually, the difference between two frames becomes immeasurably small. If you know calculus, there should be some way to calculate that limit based on the known information. However, that's beyond my mathematical skills. For me, the easiest way is to plug the numbers into a spreadsheet like the one I used to generate those graphs, and tweak acceleration and deceleration until I get the numbers I want.

    Hopefully this helps someone out there. Questions, comments or corrections? Post 'em below. And if you're a math whiz and want to fill in that final formula for me, please do so!

    Thanks all for reading!
     
    Last edited: Oct 3, 2018
    computercoder, NeZvers, Pyxus and 2 others like this.
  2. Roderick

    Roderick Member

    Joined:
    Jun 22, 2016
    Posts:
    565
    In addition to applying the smooth acceleration, by applying a built-in cap, you can also remove a couple extra lines of code:

    You no longer need a max_run_speed variable, and you don't need to check against it to clamp your speed.

    You don't need a maximum fall speed; just apply air resistance each time you move vertically. Eventually the air resistance will offset the acceleration due to gravity, creating a terminal velocity just like in real life. Remember that the air resistance (deceleration) will apply in both directions, so you'll need more powerful jumps to offset it.

    Want ice? Create a function that checks what you're standing on for its friction variable, and use that as your deceleration value instead of a global constant. Make sure all your tiles have one, and set the ice tile to have a higher value then normal items. Remember that deceleration is actually how much speed is retained, so slipperier ground is a higher deceleration value.

    This can apply to any movement, whether you're making a platformer, top-down, or even 3d game.
     
    Last edited: Oct 3, 2018
  3. Pyxus

    Pyxus Member

    Joined:
    Nov 6, 2016
    Posts:
    185
    Im observing some weird behavior in my game and figured you might be able to steer me in the right direction (apologies if im just overlooking something you already mentioned). For whatever the reason the deceleration only appears to work when i'm "on the ground"; Basically on a collision object. I am using my own speed variable not gamemaker's so maybe that is a factor but the logic should still be same and it does actually work if the character is on the ground. If there is no collision with the ground, however, the object will move at a constant speed.
    Code:
    on_ground = place_meeting(x, y+1, _obj);
    in_air = !on_ground;
    
    // Player movement
    var _hacc, _vacc;
    _hacc = (input.right - input.left)*3;
    _vacc = (input.jump)*5;
    if (on_ground) phy_accel(0, -_vacc);
    phy_accel(_hacc, 0);
    
    // Gravity
    if (in_air) phy_accel(g[X], g[Y]);
    
    // Get Current Velocity
    vel[X] = (vel[X] + accel[X])*fric[X];
    vel[Y] = (vel[Y] + accel[Y])*fric[Y];
    
    // REMOVED COLLISION CHECKS FOR SIMPLICITY
    x+=vel[X];
    y+=vel[Y];
    
    phy_accel just adds to the accel variable. I also want to mention that I say it only works "on_ground" but it specifically only works if I dont do a if (in_air) on my gravity.
     
    Last edited: Oct 5, 2018
  4. Roderick

    Roderick Member

    Joined:
    Jun 22, 2016
    Posts:
    565
    For clarity, everything in my post is pseudo-code; there is no reliance on any of Game Maker's internal variables. In fact, I haven't used GMS2 at all, and I don't use 1.x very often any more.

    Nothing in your code is popping out at me as wrong, but the phy_accel calls are definitely doing something I'm not following. All I can suggest is double checking the numbers it's generatimg when in the air versus on the ground. It's possible that you're using too low of an acceleration, and the deceleration is automatically capping you, or too low of a deceleration, and it's instantly offsetting your acceleration.

    If your deceleration is below 0.7, definitely try increasing it, at least for testing.
     
  5. Pyxus

    Pyxus Member

    Joined:
    Nov 6, 2016
    Posts:
    185
    Thanks for the reply, this is all phy_accel does btw.
    Code:
    var inst = id;
    if (argument_count = 3) inst = argument[2];
    inst.accel[X] += argument[0];
    inst.accel[Y] += argument[1];
    
    Anyway, so long as I know I didn't just fail to understand and implement your tutorial then its reasonable to assume that this issue is due to something else in my project. When I get home ill start troubleshooting to see what's causing the behavior, great tutorial btw I never thought to emulate friction by multiplying the acceleration and speed by a fraction; the solution is 10x more elegant than what I was trying to do before.

    edit: so after thinking about it logically, the real issue is im an idiot. Still no idea why it works with continuous gravity but my problem is I was using a constant acceleration. I may as well have been complaining that my character keeps moving when I hold the move input.
     
    Last edited: Oct 6, 2018
  6. Jack McRip

    Jack McRip Member

    Joined:
    Dec 25, 2018
    Posts:
    8
    Thank you :)
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice