[SOLVED] How to normalize force dependent on device fps

@will Any ideas on this as well?

@yaustar you are correct, it is impulse that is the problem force * change in time which is drive by this code

this.entity.rigidbody.applyImpulse(this.force);

I think with high refresh rate the impulse is applied more frequently generating higher velocity.

Oooooh, I missed that you are using impulse, not force. In which case that does make sense that instantly giving a rigidbody energy more times per second would make it be faster.

The only thing I can think off right now is to rate limit the impulse so that you are only applying it 15fps /30fps by keeping track of the frame times

Could you provide an example of how I would do this?

I believe I found a solution but I am open to anything that may be better.

update = function(dt)
 var fps = this.app.stats.frame.fps;

if (this.app.keyboard.isPressed(pc.KEY_LEFT)) {

        forceX = -this.speed * (60/fps);

        forceZ += this.speed * (60/fps);

      

    } else if  (this.app.keyboard.isPressed(pc.KEY_RIGHT)) {

        forceX += this.speed * (60/fps);

        forceZ = -this.speed * (60/fps);

        

     

    }

This should normalize force dependent on device fps to what the force should be based on original coding of game using 60 hz refresh rate.

I was thinking something like:

Script.prototype.update = function(dt) {
    this._secsTillLastUpdate += dt;

    if (this._secsTillLastUpdate >= (1/30)) {
        this._secsTillLastUpdate -= (1/30);

        // Apply impluse
    }
}

Would this work with different refresh rates? My game works on mobile as well where refresh rates on some devices can be as low as 30 hz. I was thinking of trying to get it to dynamically adjust the speed based on the current refresh rate within the game so that performance is similar across devices.

Should do as it’s applying impulse at the same rate across different framerates. It would fail if the framerate drops below 30 fps though

1 Like

The issue is that the force and impulse application is cumulative. If you apply force 2 times in a row:

rigidbody.applyForce(0, 1, 0);
rigidbody.applyForce(0, 2, 0);

The total force applied on Y axis on rigidbody will be 3, not 2. On higher refresh rate monitors the result would be that you will apply force more times. Since the force accumulates during the simulation step, then it will be stronger.

Two ways to mitigate it:

  1. Integrate delta time into your force calculation. On higher refresh rates the force will receive smaller gains.
  2. In my games I implemented a fixedUpdate method and apply any forces on dynamic bodies there. The default update method is used for moving kinematic and non-physical entities.

This article has a good description on how to implement a fixed update method. It does also explain how to interpolate leftover delta time, which is not performed in PlayCanvas. Perhaps could be a good first PR.

3 Likes

@yaustar @LeXXik thanks a lot for your responses. I have seen that fixed update method article and will have to dig through how to perhaps implement in my game. One thing that is unique to my game, than perhaps others but I may be wrong, is that I collect avatar position and acceleration data so I think I will test each method and see how the real time adjustments made by each approach facilitate similar game “feel” dependent on refresh rate. I use my game as part of my academic research so unity between game feel and data quality are two things I need to consider. I’ll try and post results here which hopefully will be helpful for the community.

Thanks again!

1 Like


Hello @yaustar and @LeXXik
I have given my first pass at trying to normalize game performance dependent on screen refresh rate. I have had some success using the below method. As you can see in the graph the position traces of my game avatar are more similar than if no control method is used. I will provide a graph of the difference with no control method later.

However, I still find that this method does not generate a similar enough performance between a standard and high refresh rate. I have tried modeling the data based on the code below and they should be similar in theory. This led me to thinking that perhaps a change in refresh rate also has some effect on how reactive the game may be to button presses. I began to think this because the rate at which the avatar builds up speed seems higher than that at a lower refresh rate. I began to look at alternative explanations as to why this may be and I came across this video

This person found that refresh rate correlated to his reaction time using the website human bench mark reaction time. The results of his tests demonstrate some validity to the time at which a person reacts to visual stimuli based on refresh rate, but my thinking is that refresh rate has less to do with human performance versus machine performance. Is it possible that a game running at a lower refresh rate would also receive inputs and initiate commands at a slower rate as well? This may explain why the feel of the game even after trying to control for refresh rate still has the high refresh rate game performing better but I am not sure if this is the case and if it is would have the effects I am still seeing now. Any input would be great! Thanks!

update = function(dt)
 var fps = this.app.stats.frame.fps;

if (this.app.keyboard.isPressed(pc.KEY_LEFT)) {

        forceX = -this.speed * (60/fps);

        forceZ += this.speed * (60/fps);

      

    } else if  (this.app.keyboard.isPressed(pc.KEY_RIGHT)) {

        forceX += this.speed * (60/fps);

        forceZ = -this.speed * (60/fps);

        

     

    }
1 Like

Have you tried using a fixed time update using rhe method above? How to normalize force dependent on device fps - #10 by yaustar

2 Likes

Here is an example of using a fixed timestep:
https://playcanvas.com/project/914980/overview/physics-fixed-update

3 Likes

Hello again @LeXXik and @yaustar,

I already owe you a huge debt of gratitude for sticking with me on this issue. @LeXXik thank you specifically for making the fixed updated script. I have performed a type of pilot test on it however, and it does not appear to remedy the issue. I recorded said test here. As you can see even with the fixed update method the box still falls at a different rate when refresh rate is changed. Additionally, if the fixed update timestep is greater than the refresh rate the box does not fall at all.

This video is more for how game “feel” may vary.

In the above video I varied the refresh rate and kept the fixed update constant. Below is data that demonstrates how varying refresh rate and time step impact distance the box fell after 3 seconds.

Data comparing falls with varying parameters.

fixed step at 1/10
refresh rate at 60 Hz

time 3.0000000000000013
position-35.51

fixed step at 1/10
refresh rate at 50 Hz

time 3.056000000000002
position -30.16

fixed step at 1/20
refresh rate at 60 Hz

time 3.033999999999999
position -26.20

fixed step at 1/20
refresh rate at 50 Hz

time 3.060000000000002
position -20.81

fixed step at 1/10
refresh rate at 240 Hz

time 3.056000000000002
position -38.13

fixed step at 1/20
refresh rate at 240 Hz

time 3.033999999999999
position -28.31

Graph of above data

It appears that change in timestep generates a change in intercept independent of screen refresh rate. Change in distance based on screen refresh rate changes at what appears to be at some negative logarithm, i.e. changes in physics are greater at differences at lower screen refresh than higher screen refresh. However, I don’t have 120 hz screen refresh to support this. The good news each of these could be controlled for by varying how force is applied based on screen refresh rate. However, the change in distance is not consistent across different fixed time steps. At 1/30 time step with 60 hz screen the distance is -20. As I showed in the video as the time step gets closer to the refresh rate the slower the box will fall.

I do concede that this issue is not hugely significant when you consider that screen refresh rates do not vary that much. My own data from a sample of 273 individuals who have played my game supports this. 211 out of 273 played with a refresh rate between 55 and 65 hz. Although about 10% of players did play the game at a refresh rate below 49 Hz. With about 4% even playing at below 20 Hz. Yikes. So overall the vast majority play around 60 Hz and I would suspect that give or take 5 Hz, game feel doesn’t considerably vary.

However, my data does demonstrate a linear relationship between a participant’s refresh rate and their game high score (see below). This is more of an issue as a researcher, as I want to measure a person’s performance without too much interference or noise from the device with which they play. Overall, I can either remove people with extreme refresh rates from my dataset or control for refresh rate in a statistical model. However, screen refresh rates, especially for mobile, may have 120 hz become the new standard.

Thanks again for all your help and sorry for the very long reply. I hope these types of tests and posts can be informative to others in the community.

UPDATE: using applyimpulse has different effects compared to applyforce.

Additionally if there is no fixed updated method and you just let the box fall in the default environment this is how far it falls within 3 seconds

60 Hz screen refresh
time 3.000124
position-42.36

50 Hz screen refresh
time 3.009767
position-42.54

This is confusing as now there is no difference in fall between screen refresh rate…

There is no difference if I use apply impulse instead of apply force. Note this is only when force is set to 0.

This is the code I used on the box within the original app with the box.js and fixed-update.js script turned off.

 var time = 0;
// update code called every frame
BoxFall.prototype.update = function(dt) {
   
        this.entity.rigidbody.applyForce(0,0,0);
        time += dt;
        console.log("time" + time);
        console.log("position" + this.entity.localPosition.y.toFixed(2).toString());

};

However, once I change the y parameter using apply impulse things get weird

CODE with apply 1 unit of force upward

var time = 0;
// update code called every frame
BoxFall.prototype.update = function(dt) {
        //var force = new pc.Vec3(0,1,0);
   
        this.entity.rigidbody.applyImpulse(0,1,0);
        time += dt;
        console.log("time" + time);
        console.log("position" + this.entity.localPosition.y.toFixed(2).toString());

};

50 Hz
time 3.00304600001192
position 186.05

60 Hz
time 3.0043130000119183
position 229.73

So it appears with no fixed update method that apply impulse is what causes the issue. Give the equation for impulse this is understandable. When I do the same thing with apply force there is no difference. Thus, the root cause is with the apply impulse function. Makes sense.

Interesting when I change @LeXXik code from apply force to apply impulse I get the intended result, similar distance traveled with varying refresh rate.

@LeXXik method at 1/30 fixed timestep
50 Hz
time3.0000000000000013
position 86.41

time3.015999999999996
position 87.85

Although when I increase the timestep to 1/60 things get weird again and are similar to if no method is being used suggesting the timestep picked is of importance. However, when I used a timestep above the refresh rate the box still moves which is good and different from what I found in my original video.

50 Hz
time 3.0000000000000018
position 170.36

60 Hz
time 3.0129999999999937
position 214.27

For comparison here is how my previously posted method compares

My method - normalize force to 60 fps

50 Hz
time 3.007188000023842
position 77.60

60 Hz
time 3.002173
position 77.35

Of note: My method requires detecting if yf = infinity because when the app runs the fps counter is 0 for several samples to it logs infinity which shoots the box into oblivion. I included an if statement to set yf to 0 first so the box falls momentarily until the fps counter picks up. This is why its distance is smaller than the other methods. This isn’t an issue for my current game because there is a login screen and the fps counter is running fine once they get past the login screen.

CODE

var time = 0;
var yf = 1;

// update code called every frame
BoxFall.prototype.update = function(dt) {
        var fps = this.app.stats.frame.fps;
        yf = 1*60/fps;
        if(yf===Infinity){
            yf=0;
        }
        console.log(yf);
        this.entity.rigidbody.applyImpulse(0,yf,0);
        time += dt;
        console.log("time" + time);
        console.log("position" + this.entity.localPosition.y.toFixed(2).toString());

};

If I change the if statement from 0 to 1 then the result is not quire the same

50 Hz
time 3.007188000023842
position 206

60 Hz
time 3.002173
position 229

This is probably because during that delay while the counter ramps up more impulse is applied during the 60 hz than the 50 hz.

I will need to conduct another test with the very high refresh rate to see how things diverge.

Finished some high resolution tests.

@LeXXik method with 1/30 time step works great
50 Hz
87

60 Hz
85

240 Hz
88

My method
50 Hz
79

60 Hz
74

240 Hz
78

Again, mine is lower because the box is in free fall before the the fps counter kicks in. However, the box starts in the same position in each test because I had previously established that when the argument is 0 there is no difference in distance dependent on refresh rate.

It gives me pause to think that the fixed update method may resolve the issue I see in my game. I hope this has been informative on the discrepancies in game performance based on apply impulse and screen refresh rate.

Hey, @hooymana ! Thank you for a detailed response! I will look closely at it tonight. Just a couple of notes I noticed after having a quick glance:

  1. The example has a world gravity of [0, -9.8, 0] (the negative sign means downwards). The box has a script with a positive 9.8, effectively making box float in zero-g. If the box is falling - it means there is not enough force to keep it in place.

  2. Impulse affects the body velocity, while the force affects acceleration. In other words impulse is an instantaneous change in velocity. Right there on that simulation step a new value is used, regardless of what it was before. It doesn’t care how much time passed from last simulation step. A force on the other hand depends on delta time, accelerating or decelerating the body.

I wonder if this could be related to the engine doing a physics system update each animation frame and therefore differs on 60/120/240 Hz?

So I ran a little experiment to see how position of the box changes over time based upon method used to account for the applyimpulse function based on screen frame rate. In all I had 5 conditions. A control which uses no method and is just the playcanvas engine “out of the box”. Three levels of the fixed update method where each level varies based upon its fixed time step. Time step for the first level was 1/30, second was 1/60, and third was 1/90. The last method was the normalized to FPS method. I then recorded the position of the box for each condition at every second across a 6 second interval. I ran a linear model to determine if any interactions between refresh rate and position given time.

Results can be visualized below.

In the first box, the control, we see that there are indeed performance differences dependent on fps. The higher the fps the greater the position and the increase in position based on frame rate occurs in a non-linear way. This suggests that impulse is applied to the box at every frame so a higher fresh rate will apply more impulses per second compared to a lower refresh rate.

The second box demonstrates the fixed update method with a timestep of 1/30. Huzzah! The differences we observed in the control condition have been remedied.

The third box is fixed update with a 1/60 timestep. Sort of huzzah! With the timestep now above the 50 hz fps we see divergence in position over time. However, the performance between the 60 and 240 hz fps is still equivalent.

The third box is fixed update with 1/90 timestep. Huz-ahhh! Here we see that the 240 hz rate is performing more like the control condition and the differences between 50 and 60 hz are now the same as what we observed in the 1/60 time step and somewhat similar to the differences we see during the control condition.

The fifth box is the normalized fps technique, where impulse applied is modified based upon the current screen fps. There is a con to this technique which I describe above, however, it can be worked around. Here we see the same performance as the 1/30 fixed update technique where are fps conditions are equivalent in performance.

method_compare

Overall, the fixed update method is effective but the selection of timestep is important as too high a time step may create differences in performance for players with lower screen fps relative to the selected timestep.

This results are encouraging and offer a couple of different methods to account for the screen fps to applyimpulse issue. However, a major limitation of this experiment is that it was done in a controlled environment where impulse is applied by the script and not by the player. In my original test of the normalized method within my game I still observed differences between different fps. This experiment confirms that the difference I observed was not due to issues in the method. Potentially, there is another issue when it comes to how user input is handled dependent upon screen fps. Perhaps when I apply the impulse in the 240 hz condition the impulse is applied sooner than that in the 60 Hz condition and this allows for the performance difference I see. I haven’t thought this all the way out yet but remember that reaction time video I posted previously? Maybe the differences he saw in his reaction time were more related to how well the machine detects the mouse click than the speed at which the screen changes color. At any rate this was a worthwhile exercise and hopefully it helps others who may run into this problem.

Supplemental material

All the code I used to create these graphs. Done in R

library(ggplot2)

data50=data.frame(t=c(1,2,3,4,5,6),p=c(21,80,178,311,484,696))
data60=data.frame(t=c(1,2,3,4,5,6),p=c(27,103,230,408,635,913))
data240=data.frame(t=c(1,2,3,4,5,6),p=c(105,436,996,1784,2803,4050))

dataall=rbind(data50,data60,data240)
dataall$Hz=rep(c('50','60','240'),each=6)

summary(lm(p~t+I(t^2),data50))
summary(lm(p~t+I(t^2),data60))
summary(lm(p~t+I(t^2),data240))
summary(lm(p~t*Hz+I(t^2)*Hz,dataall))

ggplot(dataall,aes(t,p,color=Hz))+
  geom_point()+
  geom_smooth(method = 'lm',formula = 'y~poly(x,2)')+
  xlab('time seconds')+
  ylab('box position')+
  labs(color="Screen\nFPS (Hz)")+
  ggtitle("ApplyImpulse to Box (Control)")

data50f=data.frame(t=c(1,2,3,4,5,6),p=c(-3,10,74,189,356,575))
data60f=data.frame(t=c(1,2,3,4,5,6),p=c(-3,10,74,189,355,573))
data240f=data.frame(t=c(1,2,3,4,5,6),p=c(-3,11,75,191,359,577))

dataallf=rbind(data50f,data60f,data240f)
dataallf$Hz=rep(c('50','60','240'),each=6)

summary(lm(p~t*Hz+I(t^2)*Hz,dataallf))

ggplot(dataallf,aes(t,p,color=Hz))+
  geom_point()+
  geom_smooth(method = 'lm',formula = 'y~poly(x,2)')+
  xlab('time seconds')+
  ylab('box position')+
  labs(color="Screen\nFPS (Hz)")+
  ggtitle("ApplyImpulse to Box\n(Normalize to 60 FPS)")

data50fi=data.frame(t=c(1,2,3,4,5,6),p=c(9,38,87,157,246,356))
data60fi=data.frame(t=c(1,2,3,4,5,6),p=c(9,37,86,155,244,353))
data240fi=data.frame(t=c(1,2,3,4,5,6),p=c(9,39,88,157,247,353))

dataallfi=rbind(data50f,data60f,data240f)
dataallfi$Hz=rep(c('50','60','240'),each=6)

summary(lm(p~t*Hz+I(t^2)*Hz,dataallfi))

ggplot(dataallfi,aes(t,p,color=Hz))+
  geom_point()+
  geom_smooth(method = 'lm',formula = 'y~poly(x,2)')+
  xlab('time seconds')+
  ylab('box position')+
  labs(color="Screen\nFPS (Hz)")+
  ggtitle("ApplyImpulse to Box\n(Fixed Update 1/30)")

data50fi6=data.frame(t=c(1,2,3,4,5,6),p=c(16,71,161,291,459,665))
data60fi6=data.frame(t=c(1,2,3,4,5,6),p=c(21,91,212,383,604,876))
data240fi6=data.frame(t=c(1,2,3,4,5,6),p=c(22,93,215,386,608,881))

dataallfi6=rbind(data50fi6,data60fi6,data240fi6)
dataallfi6$Hz=rep(c('50','60','240'),each=6)

summary(lm(p~t*Hz+I(t^2)*Hz,dataallfi6))

ggplot(dataallfi6,aes(t,p,color=Hz))+
  geom_point()+
  geom_smooth(method = 'lm',formula = 'y~poly(x,2)')+
  xlab('time seconds')+
  ylab('box position')+
  labs(color="Screen\nFPS (Hz)")+
  ggtitle("ApplyImpulse to Box\n(Fixed Update 1/60)")

data50fi9=data.frame(t=c(1,2,3,4,5,6),p=c(16,71,164,294,461,670))
data60fi9=data.frame(t=c(1,2,3,4,5,6),p=c(21,91,212,383,604,876))
data240fi9=data.frame(t=c(1,2,3,4,5,6),p=c(34,148,342,616,971,1405))

dataallfi9=rbind(data50fi9,data60fi9,data240fi9)
dataallfi9$Hz=rep(c('50','60','240'),each=6)

summary(lm(p~t*Hz+I(t^2)*Hz,dataallfi9))

ggplot(dataallfi9,aes(t,p,color=Hz))+
  geom_point()+
  geom_smooth(method = 'lm',formula = 'y~poly(x,2)')+
  xlab('time seconds')+
  ylab('box position')+
  labs(color="Screen\nFPS (Hz)")+
  ggtitle("ApplyImpulse to Box\n(Fixed Update 1/90)")

dataallm=rbind(dataall,dataallf,dataallfi,dataallfi6,dataallfi9)
dataallm$method=rep(c("Control","Normalized to FPS","Fixed 1/30","Fixed 1/60","Fixed 1/90"),each=18)

ggplot(dataallm,aes(t,p,color=Hz))+
  geom_point()+
  geom_smooth(method = 'lm',formula = 'y~poly(x,2)')+
  xlab('time seconds')+
  ylab('box position')+
  labs(color="Screen\nFPS (Hz)")+
  ggtitle("ApplyImpulse to Box\n(Fixed Update 1/60)")+
  facet_wrap(~method,scales = "free")

Hey, @hooymana

I had another look at this - and found out I had a bug in my code. Sorry for that :slight_smile: I’ve updated the example.

The reason the speed is different is due to the way the example is set up. On the very first frame there is no force applied on the box, so it just free falls. On a large fixed step, it will gain more acceleration during that step because of that. On the second step we apply the reverse force. One way to fix it for this example, would probably be to reset the current velocity of the body, or apply the force on the same frame we create the box, etc.

2 Likes