0
\$\begingroup\$

am trying to do something that should be quite common in games: Applying a factor over a certain amount of time. For example let's say a number should become a quarter of itself every second.

How do we do this with variable frame rate?

I have written the following code:

using System;
                    
public class Program
{
    static double NthRoot(double x, double n)
    {
        return Math.Pow(x, 1.0f / n);
    }
    
    public static void Main()
    {
    double input = 10;
    double factor = 0.25;
    
    Console.WriteLine("Direct result: " + (input * factor));
    Random rnd = new Random();

    // simulate everything five times
    for (int i = 0; i < 5; i++)
    {
        double val = input;
        
        double total_time = 0;
        // simulate the frames that accumulate to one second
        while(total_time < 1.0)
        {
            // simulate a delta time for the current frame
            double delta = rnd.Next(50, 100)/1000.0;

            total_time += delta;
            double current_frames = 1.0/delta;  
            
            // apply the current factor
            val *= Program.NthRoot(factor, current_frames);
        }

        Console.WriteLine("Result over one second: " + val);
    }
}

It produces a random delta (as if frame rate were fluctuating) and calculates a special factor for every frame based on the framerate derived by this delta time.

However the results are inaccurate and tend to be too small:

Direct result: 2.5
Result over one second: 2.37171078785113
Result over one second: 2.36842517649817
Result over one second: 2.4655817612334
Result over one second: 2.34555299125168
Result over one second: 2.38820734095609

Here a Fiddle for the code: https://dotnetfiddle.net/1tEXba

How does one do this more accurately?

Huge thanks!

\$\endgroup\$
3
  • \$\begingroup\$ If you're using a game engine, you might want to add it as a tag and in the description, since different engines might manage ticks/frames differently. \$\endgroup\$
    – Sacha
    Commented Nov 1, 2021 at 18:46
  • \$\begingroup\$ You aren't running your simulation for a full second, as total_time<1.0 when your loop quits. If you add val *= Program.NthRoot(factor, 1.0 / (1.0 - total_time)); before printing the result, you ensure that you run the simulation for a full second and always get the 2.5 you wanted: ideone.com/72yH0S \$\endgroup\$
    – user35344
    Commented Nov 1, 2021 at 18:47
  • 1
    \$\begingroup\$ @Tyyppi_77 well, total_time<1.0 when the loop enters for the last time. Then we add a random value 0.050-0.099 to it. So we might simulate a total of between 1.000 seconds (if our last random amount was just exactly enough to meet the exit condition) and 1.098 seconds (if our total had been 0.999 before the final loop and we rolled the max). So usually we end up with more than a full second of simulation by the time the loop exits. The code addition you have there adds some "negative time" to rewind the spillover back to 1.0. \$\endgroup\$
    – DMGregory
    Commented Nov 1, 2021 at 20:06

1 Answer 1

2
\$\begingroup\$
currentValue *= Math.Pow(ratioRemainingAfter1Second, secondsElapsedThisFrame);

If your time delta were exactly 1 second, then this power function would spit our your original ratio unchanged.

If your time delta were exactly 2 seconds, then this power would spit out your ratio squared - reducing the value by the ratio once for the first second, and again for the second second.

If your time delta were exactly 0.5s second, then this power would spit out the square root of your ratio. Multiplying by this square root twice in a row (two 0.5 second frames) gives the same outcome as multiplying by the original ratio once (one full second).

And so on. This adjusts the ratio correctly to whatever amount of time has passed (up to the limits of floating point precision and rounding error - so if you need exact determinism you'll want a different mechanism).


The error in your code is that you were simulating different time intervals. Depending on the random rolls, you'd simulate between 1.0 seconds and 1.098 seconds - and that makes a difference between runs! Fix up your final loop so that you always end at exactly 1.0 seconds and you'll get the desired outcome:

while(total_time < 1.0)
{
    // simulate a delta time for the current frame
    double delta = rnd.Next(50, 100)/1000.0;

    if (total_time + delta > 1) {
        // For the final loop, ensure we finish exactly at 1.0 seconds.
        delta = 1.0 - total_time;
        total_time = 1;                 
    } else {
        // For other loops, advance by our random delta.
        total_time += delta;
    }       
    
    double current_frames = 1.0/delta;  
    
    // apply the current factor
    val *= Math.Pow(factor, delta);
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .