3
\$\begingroup\$

I'd like to find the average input value from a controller over the last n seconds (say the last 0.1 seconds) and do this every frame.

I can see how to do this for the past n frames by storing a list of values for the last n frames and averaging that total every frame, but I want to account for a variable frame rate as the resulting output is driving a character controller and should feel precise and consistent. I tried storing a list of value pairs containing the input value and delta time value for every frame, then counting back through the list of delta time values until they sum to n seconds, then using the total of those input values to work out the average, but it seems a bit clunky and means I have to guess how big the list should be to store enough values, is there a better way?

\$\endgroup\$
0

3 Answers 3

2
\$\begingroup\$

Two options:

1. Arithmetic average (worse)

Have a 2-column table that has sufficiently many rows to be able to store all values for your chosen time (0.1 s in your question) at the potentially highest possible, supported frame rate - say 300 fps (unless you limit it to for example 60). 300 fps would mean 30 rows, in order to store all values for the last 0.1 seconds. During each frame, (over)write the oldest one row in the table, in a circular fashion; ie. have an index counter that wraps back to 1 after 30 (or whatever the number of rows is).

Each row holds a time stamp (a time, not a delta time!) and the controller value at that time stamp.

When calc'in the average (sum of values / number of values), per frame apparently, use only rows with a timestamp that is greater than Tnow - 0.1. Ignore other rows, but if all rows are to be ignored, ensure you use at least the most recent row.

Weakness: Potentially much data and calculation, result not necessarily better than [below].

2. Quasi-average (better)

NewValue = [read from controller]
NewShare = min(1, FrameDeltaTime / 0.1)
OldShare = 1 - NewShare
ThisValue = (OldValue * OldShare) + (NewValue * NewShare)
OldValue = ThisValue // Use ThisValue for whatever needed

This gives a nice "rubber band affect", removes some stutter from unstable controllers etc., and above all, it is a minor calculation that produces an equally good result as in /1/ above; additionally itäs easy to trim the 0.1 s into whatever is the good value, by trial and error.

  • if FrameDeltaTime is 0.0001, it takes 0.999 of the old and 0.001 of the new
  • if FrameDeltaTime is 0.0167 (60 fps), it takes 0.83 of the old and 0.167 of the new. Doing that repeatedly "seeks" towards whatever the controller outputs.
  • if FrameDeltaTime is 0.1 it takes 0 of the old and 1 of the new (ie. 0.1 s is the "break point")
  • if FrameDeltaTime is 5 (eg. a long blocking load delay) it takes 0 of the old and 1 of the new
\$\endgroup\$
1
  • \$\begingroup\$ #2 works great, feels great, thank you! And thanks for pointing out I should having been using time stamps for #1 \$\endgroup\$
    – Sea Scout
    May 19, 2017 at 22:49
5
\$\begingroup\$

Expanding on Stormwind's lead, I'd be curious whether your application needs a windowed average with equal weights per unit of time, or if an exponential moving average may suffice. The latter is very simple to implement, and can be adapted to a variable framerate like so:

 float weight = 1 - pow(1 - responsiveness, dT * referenceFPS);
 smoothedValue += (currentValue - smoothedValue) * weight;

Similar to a windowed average, this smooths-out fluctuations in an input signal, and gives the value a degree of "memory" or "inertia," while being much simpler to calculate. The differences is that it weighs the newest samples much more heavily than old samples, and has no absolute cutoff age like a windowed average.

Here's an example of how this compares against a 0.1s windowed average for a signal with a variable sampling rate. Here I'm using responsiveness = 0.5 and referenceFPS = 30

enter image description here

You can see the exponential moving average gives a fairly similar profile to the windowed average, especially when the value changes continuously. For sharp changes in the input value, you can see the exponential average has a somewhat sharper attack, and then closes in on a new sustained value asymptotically (where the moving average is guaranteed to reach a sustained input value by the end of the window duration).

In practice, we can usually tune the responsiveness value to get the desired behaviour.

If your application really needs a windowed average, let us know and we can show you how to implement it efficiently with a ring buffer, albeit with more code complexity than the exponential example above.

\$\endgroup\$
2
  • \$\begingroup\$ A great further approach! A 3rd would be making it adaptive, ie. resolve equation constants along the road, depending on actual controller behaviour. I had to do this for the most unstable controller i've met so far, a Saitek Pro Flight (2 of them, actually), which had a huge over +-10 % on multiple axis, even when untouched (while the same code was to handle good controllers as well). Making it adaptive seeks suitable damping values depending on controller behaviour, but then also the nature of the action (eg. steering, throttle, rapidly-changing, set-to-a-level etc) come in as parameters. \$\endgroup\$
    – Stormwind
    May 20, 2017 at 14:53
  • \$\begingroup\$ This is great too thanks @dmgregory. The reason I was asking this question in the first place was to detect user input over time to set a "Move" condition for the character, as an alternative to using a threshold on an input value. This is how the Aloy character controller in Horizon Zero Dawn is set up, it feels very responsive. I had a play using exponential moving average to smooth input and it feels good, thanks! \$\endgroup\$
    – Sea Scout
    May 25, 2017 at 3:19
0
\$\begingroup\$

The answer by stormwind was exactly what I needed to smooth out a calculation that was getting jittery when the delta time wasn't perfectly the same.

I discovered that UnrealEngine has the quasi-average solution in his post already in its library as FMath::WeightedMovingAverage(float CurrentSample, float PreviousSample, float Weight)

For the Weight parameter, you would pass in FrameDeltaTime / SamplingTime and use 0.1 for SamplingTime based on what the original poster wanted for this question.

\$\endgroup\$

You must log in to answer this question.

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