[PicWin] Animation and FPS synchronization by Epsilon


#1

Version: 1.0

Required level: beginner/intermediate

Credits: special thanks to ToKeN for his great ideas.

Author: Epsilon

1) Intro

If you ever tried to animate an object with picture windows (a moving text, a boucing ball, …), you certainly used an alias called a lot of time with a millisecond timer.
for example: .timeranim -m 0 200 /game_anim
And you certainly incremented the coordinates of the object with a fixed number in this alias.
for example: /inc %ball.x 10

If you did, then FORGET THAT VERY BAD METHOD . But why ?
Because setting a constant millisecond value doesn’t mean that the alias will get executed every 200 milliseconds.
There is multiple reasons to that:

  • /timer -m are not accurate. (sometimes it will get launched after 210ms, sometimes after 204ms, …)
  • The method doesn’t take in account the time for the drawing alias to get executed. The speed will greatly vary depending of the CPU and the video card of the user.
  • The fixed value of the delta (like the number of pixel to increment per frames) will produce different results on different CPU. If you increment a value by ‘10’ in every frames, at 60 FPS the value will be incremeted by 600 every seconds, and at 20 FPS only by 200px. That means that the speed will depend of the FPS. And the FPS will vary depending of a lot of events (other running programs, a subit increase of the CPU load, etc.), so you cannot define an average speed.
  • Something that runs fine on your PC might runs too fast on the next generation of PC (and too slow on old PCs). You must ensure that the animation will run fine on old PC, and will take advantage of the future CPUs.

With this tutorial, you will learn how to calculate the speed in pixel per seconds instead pixel per frames .

Whatever the machine, the CPU load and the current FPS are, your animation must always run at the same speed .
Yes it is possible. The only thing that will vary is the fluidity of the animation, not the speed .

Important note: in this tutorial I’ll use $ticks, but you can forget about that identifier, because it is not accurate enough for FPS synchronization. High FPS calc. needs a high accuracy (±1ms), and $ticks is limited to an accuracy of 10ms on WinXP/2k, and 55ms (!) on Win9x. If you want to test the accuracy:

alias test.tick { var %i = 0 | while (%i <= 100) { echo $ticks | inc %i } }

So you need to use a DLL with an accurate ‘ticks’ function, like Game Tools , or you won’t be able to synchronize the FPS above 60 FPS (and even below with Win9x OS).

2) FPS calculation.

First we need an initialisation alias, and a ‘loop’ alias (the drawing part).

alias init {
%fps = 0
%frame.counter = 0
%fps.tick = $ticks
main.loop ; launches the animation
}

It sets the currents FPS and the frame counter to 0, and set the current ticks (remember not to use $ticks in your game but a DLL to get accurate ticks).

Then, the “loop” alias, that wil get executed [FPS] times per seconds.

alias main.loop {
(...) drawing here (...)
if ($calc($ticks - %fps.tick) >= 1000) {
%fps = %frame.counter ; the FPS = number of frames during that second
%frame.counter = 0 ; reset
%fps.tick = $ticks ; set the current time
}
(...)
}

That will count the FPS (the number of time the alias got executed). Never use a second timer with one second delay to count the FPS.

Now we need to trigger the loop alias. The best non CPU consuming method is to call the alias again with a /timer -h 1 0 /main.loop.
ToKeN found that method It triggers the alias immediatly, but without consumming CPU like while() does. It only happens one time, do not use timer -h 0 0, to loop continuously, because this is not the best method.
Place this line at the end of the main.loop:

.timeranim -ho 1 0 /main.loop

Tip: You can use this ‘one time’ timer to delay the next call of the loop, thus limitating the maximum FPS. The delay is (1000 / [maxFPS]).
ex: .timeranim -ho 1 $calc(1000/20) /main.loop to limit the animation to 20 FPS, whatever the PC is. (But that doesn’t means that it will always run at 20 FPS, it can still be slower).
But there is a variable that you must take in account to ensure a proper fixed FPS value, when delaying the timer : the time of the alias to process. So put a %start_ticks = $ticks at the beginning of your code, and substract the difference of the start ticks and the end ticks from the delay value:

.timerdot -ho 1 (1000/ %desiredFPS)) - ($ticks - %start_ticks)) /main.loop

There is no advantages in limitating FPS, since you’ll learn how to adapt the speed, whatever the FPS is, in this tutorial. But it is a nice tip to know if you want to limit the FPS for debugging purposes.

3) Adjusting the speed of the moving objects

Now the most interesting part : speed adjustment.

As I said, you should never use fixed values for deltas. Instead you must calculate the time elasped since the last frame, and then determine the new delta value based on this time.

Ex: if your animation runs at 20 FPS, and you want a ball to move left at the speed of 100 pixels/seconds, you should increase the x coord. of the ball by 5 pixels every frames.
But suddenly a software eats 80% of the CPU, and the FPS drops down to 10. Then the ball should move from 10 pixels every frame, to maintain the speed of 100px/sec.

But the FPS is only determined once every seconds. And a second is far too much time, and will lead to desynchronization of the speed.

So you must adjust the speed delta of the objects everytime that you draw a frame !

How ? Well you first need to calculate the time elapsed since the last frame.

Just store a $tick in the end of the loop alias.
%old_ticks = $ticks

Then in the next call, we will make a simple calc to get the number of milliseconds between the stored ticks and the actual.
var %loop_time = $mticks - %old_ticks

Now you must calc the speed’s delta. We will use an integer value. But the result of the calc will be a floating point value. So keep in mind that you must store the decimal remainder part somewhere, to use it in the next frame. Because imagine that the calculated delta is 10.9 . The delta in pixel will be 10. But the 0.9 part is lost. After 500 frames, the error will be 500 * 0.9 = 450 !! (if the delta is always the same). So we will add those 0.9 to the next delta value: 10.9 + 0.9 = 11.8, so we store the 0.8, and add 11 pixels to the x coord of the ball, and so on…

To get a delta value, use this method: %new_delta = $calc(%speed /1000 * %loop_time)

The new delta value is the speed in pixel/sec divided by 1000, and the result is multiplicated by the time elapsed since the last frame (the last frame could have been delayed by the timer, so its not the time of the alias execution, it’s the time since the last frame).

In the next frame, add %new_delta to the the x coord of the ball. And that’s it, the ball will always move at the desired speed, whatever the FPS is.

code:

; time elapsed since the last frame draw
var %loop_time = $mticks - %old_ticks
; new delta, based on the time and the speed of 200px/sec + the decimal part of the previous delta
var %new_delta_tmp = $round($calc(200 /1000 * %loop_time),2) + %old_delta_decimal_part
; the new delta is the integer part of the value
%new_delta = $int(%new_delta_tmp)
; store the decimal part for later use (in the next frame's delta calc) Its the (float value - int value). Ex: 10.9 - 10 = 0.9
%old_delta_decimal_part = $calc(%new_delta_tmp - %new_delta)
; now store the actual tick for the next frame's delta calc
%old_ticks = $ticks
; launches the timer to loop again.
.timerloop -ho 1 0 main.loop

4) Example demo

Check out my “moving dot” demo for an implementation of this tutorial. It is fully documented.

5) Conclusion

I know that it can be a bit hard to understand, but if you failed to, read this tutorial again and again, because this method is the best method to ensure that you’re animation (or your game) will run at the same speed on a PentiumII 266Mhz as on a Pentium 4 3 Ghz. This is critical in multiplayer games.

This method can (and should) be applied to every moving objects.


The previous tutorial was written by Epsilon for picwin.tk. Because the “moving dot” demo was lost, here is my small demo:

alias demo {
  window -hpfB +d @dbbg 0 0 1000 500
  drawrect -rf @dbbg 65793 1 0 0 1000 500
  var %i = 255
  var %y = $null
  var %od = 1000 $rand(190,200)
  var %sd = %od
  var %nd = $null
  while (%i) {
    %y = $calc(%i / 255 * 150)
    %nd = $calc(6.7 * %y) $rand(190,200)
    drawdot -r @dbbg $rgb(0,0,%i) 1 0 %y
    drawline -r @dbbg 0 1 %nd %od
    %od = %nd
    dec %i 1.7
  }
  %nd = 0 $gettok(%sd,2,32)
  drawline -r @dbbg 0 1 %nd %od
  drawcopy @dbbg 0 0 1 150 @dbbg 0 0 1000 150
  drawdot -r @dbbg 16777215 1 $(,$str($!rand(1,999) $!rand(0,149) $chr(32),10))
  drawdot -r @dbbg 16777215 1 $(,$str($!rand(1,999) $!rand(0,149) $chr(32),10))
  drawdot -r @dbbg 16777215 1 $(,$str($!rand(1,999) $!rand(0,149) $chr(32),10))
  drawdot -r @dbbg 16777215 1 $(,$str($!rand(1,999) $!rand(0,149) $chr(32),10))
  drawdot -r @dbbg 16777215 1 $(,$str($!rand(1,999) $!rand(0,149) $chr(32),10))
  drawline -r @dbbg 0 1 0 249 1000 249
  drawfill -r @dbbg 12800 0 1 225
  drawrect -ref @dbbg 9868950 1 0 255 30 30
  drawrect -ref @dbbg 65793 1 -10 253 30 30
  drawcopy -t @dbbg 65793 0 255 30 30 @dbbg 900 5
  drawrect -rf @dbbg 65793 1 0 255 30 30
  var %t = This is a small demo inspired by "da beat machine" demo by visionz. - You want your text here? - Use: /demo My cool text -
  drawtext -r @dbbg 6579300 tahoma 12 0 255 $iif($1-,$v1,%t)
  drawrect -ref @dbbg 0 1 10 300 5 5
  drawline -r @dbbg 0 1 12 300 6 318
  drawcopy @dbbg 0 300 15 25 @dbbg 15 300
  drawline -r @dbbg 0 1 5 315 5 310 10 305 15 310 15 305
  drawline -r @dbbg 0 1 0 325 6 318 10 322 12 325
  drawline -r @dbbg 0 1 25 305 25 310 30 310
  drawline -r @dbbg 0 1 21 318 21 325
  drawline -r @dbbg 0 1 21 318 24 322 19 322
  window -hpfB +d @dbs 0 0 400 280
  window -CkpfB @demo 0 0 400 280
  %demo_fps = 0
  %demo_frame_counter = 0
  %demo_fps_ticks = $ticks
  %demo_old_ticks = $ticks
  %demo_sky_pos = 0
  %demo_mountains_pos = 0
  %demo_man_pos = 1
  %demo_text_pos = 0
  demo_loop
}
on *:close:@demo: {
  unset %demo_*
  window -c @dbbg
  window -c @dbs
}
alias -l demo_loop {
  if ($window(@demo)) {
    var %demo_loop_time = $ticks - %demo_old_ticks
    var %new_delta_tmp = $calc($round($calc(200/1000 * %demo_loop_time),2) + %demo_old_delta_decimal)
    var %new_delta = $int(%new_delta_tmp)
    drawrect -rfn @dbs 0 1 0 0 400 300
    inc %demo_sky_pos $calc($int($calc(%new_delta / 3)) * .2)
    if (%demo_sky_pos >= 1000) {
      %demo_sky_pos = 0
    }
    if (%demo_sky_pos > 600) {
      var %sky_width = $calc(1000 - %demo_sky_pos)
      drawcopy -tn @dbbg 65793 0 0 400 150 @dbs %sky_width 0
    }
    drawcopy -tn @dbbg 65793 %demo_sky_pos 0 $iif(%demo_sky_pos > 600,%sky_width,400) 150 @dbs 0 0
    inc %demo_mountains_pos $int($calc(%new_delta / 3))
    if (%demo_mountains_pos >= 1000) {
      %demo_mountains_pos = 0
    }
    if (%demo_mountains_pos > 600) {
      var %mountains_width = $calc(1000 - %demo_mountains_pos)
      drawcopy -tn @dbbg 65793 0 150 400 100 @dbs %mountains_width  100
    }
    drawcopy -tn @dbbg 65793 %demo_mountains_pos 150 $iif(%demo_mountains_pos > 600,%mountains_width,400) 100 @dbs 0 100
    if (%new_delta_tmp >= 4) {
      %demo_man_pos = $iif(%demo_man_pos,0,1)
    }
    drawcopy -tn @dbbg 65793 $calc(%demo_man_pos * 15 + %demo_man_pos) 300 16 25 @dbs 5 175
    drawcopy -n @dbs 0 0 400 200 @dbs 0 280 400 -81
    inc %demo_text_pos $calc($int($calc(%new_delta / 3)) * .4)
    if (%demo_text_pos >= 1000) {
      %demo_text_pos = 0
    }
    if (%demo_text_pos > 600) {
      var %text_width = $calc(1000 - %demo_text_pos)
      drawcopy -tin @dbbg 65793 0 255 400 14 @dbs %text_width 5
    }
    drawcopy -tin @dbbg 65793 %demo_text_pos 255 $iif(%demo_text_pos > 600,%text_width,400) 14 @dbs 0 5
    if ($calc($ticks - %demo_fps_ticks) >= 1000) {
      %demo_fps = %demo_frame_counter
      %demo_frame_counter = 0
      %demo_fps_ticks = $ticks
      titlebar @demo - FPS: %demo_fps
    }
    drawcopy @dbs 0 0 400 300 @demo 0 0
    %demo_old_delta_decimal = $calc(%new_delta_tmp - %new_delta)
    inc %demo_frame_counter
    %demo_old_ticks = $ticks
    .timerdemo1 -ho 1 0 demo_loop
  }
}