Game Main Loop Timing (desiring feedback) (solved)

Game development specific discussions.
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Game Main Loop Timing (desiring feedback)

Post by paul doe »

leopardpm wrote:First, I must say that I REALLY appreciate how well you comment your code and how clean and straightforward it is - this makes it so amateurs like me have a chance of understanding the inner workings of whats occurring - Thank you!
You're welcome. Commenting code as if I were explaining it to somebody else who has NO idea of what the code is doing is something some of my mentors taught me a long while ago. Because, three months from when you wrote the code, the one that won't have any idea of what the code does will be yourself =D
leopardpm wrote:Basically, each animation bitmap is only stored once:

And every 'agent' which uses that animation just maintains an index to which frame that particular agent is currently showing.

This way you could have 100 elves walking around, each having their individual frames advancing at different rates, yet still only uses one animation bitmap

is that what you are showing in your animation class demo?
Yes, that's one of the things shown there, but it's not the most important bit. The real gist of the demo is how you can control multiple agents, all with their own timings, and how to maintain them. If you press 'p' in the demo, you'll see that the elf pauses both its movement AND its timings, which are restored upon resuming the processing of the animation.

What you call 'timed events' is usually called a process in a cooperatively multitasked model. Implementing one isn't particularly hard, and will give you the flexibility to code anything you can dream of without much hassle (the very first OSses were laid out this way).

Some other links that can help you:
Perfectly accurate game timing. If you positively, absolutely have to have control over the last tick of your game.
Entity-Component-System. This is how modern games (for desktops at least) are laid out. Check it out because you can implement this model without resorting to OOP (although I'd recomend to do so).

I have some code to illustrate this, but I'm afraid that's all object-oriented so it's of no use to you.
Last edited by paul doe on Dec 09, 2018 21:18, edited 1 time in total.
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

badidea...
your test of the sleep time:

Code: Select all

dim as double t = timer
sleep 1
print (timer - t) * 1000; " ms"
sleep
end
gives me a different answer each time I run it - anywhere from 1.3ms to up to 8ms! I definitely am not liking having to use the SLEEP function to release time to the system....
I am going to draw horses of 33 x 33 pixels, that should be fun!
I found a good horse animation... colored it Blue for FreeBasic, but hard to come up with a good way to insert it into the game I want to make... was thinking the user would control the horse... but not enough things for them to actually 'do' but run around spreading the good news of FB....
badidea
Posts: 2586
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: Game Main Loop Timing (desiring feedback)

Post by badidea »

Yeah, it isn't the best test code. Probably adding a sleep before the measure sleep already improves it:

Code: Select all

sleep 1
dim as double t = timer
sleep 1
print (timer - t) * 1000; " ms"
sleep
end
An average of e.g. 100 runs would be better, but I was lazy.

I have good use for a horse, but I think I stick will simpler graphics for now. Reason: Image
Last edited by badidea on Dec 09, 2018 21:31, edited 2 times in total.
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

paul doe wrote:Because, three months from when you wrote the code, the one that won't have any idea of what the code does will be yourself =D
yup, that always happens to me... except the '3 months' timeframe is usually measured in days....cuz I am gettin' too old...

The real gist of the demo is how you can control multiple agents, all with their own timings, and how to maintain them. If you press 'p' in the demo, you'll see that the elf pauses both its movement AND its timings, which are restored upon resuming the processing of the animation.
hmmm... will have to look into the code closer

looked at your other links...interesting -Thank You!
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

So, is SLEEP the only way to give the OS some time? it is very annoying....
dodicat
Posts: 7976
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Game Main Loop Timing (desiring feedback)

Post by dodicat »

My simple fps regulator is governed by the timer, not the sleep 1 resolution.
Windows test
lines 74 and 76
timebeginperiod
..
timeendperiod
ensures 1 millisesond for sleep 1

At the first (60 fps) rate, the pendulum has a period of about 2.1 (let it settle).
If you comment out
timebeginperiod
and
timeendperiod
then the period remains the same
You can tell if your computer reverts to 15 ms by asking for a framerate of maybe 200, if it sticks about 60 then you are on the longer 15(ish) sleep.
but the period remains the same.

Code: Select all

Declare Function timeBeginPeriod       Alias "timeBeginPeriod"(As Ulong=1) As Long
Declare Function timeEndPeriod         Alias "timeEndPeriod"  (As Ulong=1) As Long

Sub readstring(st As String,message As String)
    Static As String j,blink',i
    Static As Double t,lt
    Static As Long lft
    If lt-t>.5 Then blink="_"
    If lt-t>1 Then t=Timer:blink=" "
    Dim As String i=Inkey
    lft=Iif(Len(i),i[0],-1)
    If Lft=08 Then j=Mid(j,1,Len(j)-1)'backspace
    If lft>=0 Andalso lft<=254 Andalso lft<>8 Then j=j+Chr(Lft)
    If Lft=27 Then j=""               'esc clears the line
    If lft<>13 Then
        Print st & j & blink
    Else
        j=Rtrim(j,Chr(13))
        message=j
        j=""
    End If
    lt=Timer
End Sub

Sub drawline(x As Long,y As Long,angle As Single,length As Long,col As Ulong,Byref period As Double)
    Static As Double lastt
    Static As Long flag
    Dim As Double x2=x+length*Cos(angle)
    Dim As Double y2=y-length*Sin(angle)
    Line(x,y)-(x2,y2)
    If x2<550 Then flag=0
    If x2>588 And flag=0  Then 
        flag=1
        period = Timer-lastt
        lastt=Timer
    End If
    Circle(x2,y2),10,col,,,,f
End Sub

Function Regulate(Byval MyFps As Long,Byref fps As Long) As Long
    Static As Double timervalue,_lastsleeptime,t3,frames
    frames+=1
    If (Timer-t3)>=1 Then t3=Timer:fps=frames:frames=0
    Var sleeptime=_lastsleeptime+((1/myfps)-Timer+timervalue)*1000
    If sleeptime<1 Then sleeptime=1
    _lastsleeptime=sleeptime
    timervalue=Timer
    Return sleeptime
End Function

Screen 19,32
Dim As String msg="60"
Dim As Single angle,pi=4*Atn(1)
Dim As Double period
Dim As Long myfps,fps

Do
    angle+=.05
    Screenlock
    Cls
    Locate 4
    Print "FPS ";fps
    Print "Set  ";msg
    lbl:
    Locate 9
    readstring ("Enter a new a framerate ---> ",msg)
    If Valint(msg)=0 Then msg=Str(fps)
    drawline(500,200,.3*Sin(angle)-pi/2,300,Rgb(0,200,0),period)
    Locate 23,15
    Print "Period ";period
    Screenunlock
    If Multikey(&h01) Then Exit Do '<esc>
    timebeginperiod
    Sleep regulate(Abs(Valint(msg)),fps),1
    timeendperiod
Loop 
Sleep


  
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

dodi,
Maybe I am confused, but, why does the pendulum swing faster or slower based on the framerate? I would think that it should move at the same speed to show its independence from the frame rate... right?
dodicat
Posts: 7976
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Game Main Loop Timing (desiring feedback)

Post by dodicat »

I have deliberately made the pendulum swing according to the framerate as visual proof.
Otherwise you would have to believe the numbers only.
The idea is set the game framerate at say 60 fps, it will be 60 fps on all computers.
I think about 25 fps the human eye sees a smooth motion (The old movies used 25).
So 60 fps seems good enough.
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

oh ok

I don't have multiple computers to verify
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

wait... just re-read your post:
timebeginperiod
..
timeendperiod
ensures 1 millisesond for sleep 1
so those functions force Sleep 1 to be 1 ms?

if so, then THAT is what I am looking for! This will allow me to give the computer whatever time that I have free (cycles during which no Events are being executed...) - will try to incorporate now...
badidea
Posts: 2586
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: Game Main Loop Timing (desiring feedback)

Post by badidea »

Info on timeBeginPeriod: https://docs.microsoft.com/en-us/window ... eginperiod
These calls won't work on linux, but I think linux uses a shorter sleep period anyway.
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

seems to work correctly....

I can get a max of 3000 FPS with 10% CPU use

At 60 FPS I am at 2% CPU use

Ryzen 5 1600, 6 core, 16gb, 64bit OS (Win10 home)

Code: Select all

const TILEW = 5
const TILEH = 5

const SCRW = 1280
const SCRH = 600

    dim shared as integer map(100,100,5)
    dim shared as double starttime, endtime
    dim shared as long rfps

    ' PQ_TE = Sorted Array/Stack for Timer Event Stack
    dim shared as double PQ_TE(1000,1)
    ' TE_pntr = Timer Event stack pointer
    dim shared as integer TE_pntr

declare sub Refreshscreen
declare sub DrawGrid
declare function PQ_TE_Add(ByVal newtime as double, byval event as double) as integer
declare function PQ_TE_Del(ByVal thisOne as integer) as integer

Declare Function timeBeginPeriod       Alias "timeBeginPeriod"(As Ulong=1) As Long
Declare Function timeEndPeriod         Alias "timeEndPeriod"  (As Ulong=1) As Long

    screenres SCRW,SCRH,32,2 'set up 2 video pages (#0 visible, #1 working)
    
    dim shared as integer FPS, LPS, desiredFPS, Loops, Frames, SleepTImes, NoSleepTimes
    dim shared as double TimeStart, OldTime, RefreshTime, EstRefreshdelay, Lastframe, SleepDelay

' Initialize Main Loop
    locate 50,20 : input "Desired FPS :";desiredFPS
    RefreshTime = 1/desiredFPS
    SleepTImes = 0
    
    TE_pntr = 0 ' no events in stack
    Loops = 0 : Frames = 0
    ScreenSet 1, 0 ' working page #1, visible page #0
    DrawGrid
    PQ_TE_Add(timer+RefreshTime,100) ' add the next screen refresh to Timer Event Stack (100 = refresh screen event)

' Main Loop
do
    Loops = Loops + 1
    
    if timer > PQ_TE(TE_pntr,0) then 'check timer against the next event time
        select case PQ_TE(TE_pntr,1) ' figure out what Event needs to be dealt with
            case 100 ' this is the code for RefreshScreen Event
                Refreshscreen
            case 200 ' could be an action or world update or even animation frame change...
                
        end select
    end if
    
    ' if there is enough time for a minimal sleep (1.5ms) then sleep
    if PQ_TE(TE_pntr,0) > (timer + .0015) then
        timebeginperiod
        sleep 1
        timeendperiod
        SleepTImes += 1
    else
        NoSleepTimes +=1
    end if
    
loop until inkey = "q"

end


' some subroutines....

sub Refreshscreen
    dim as double t1

    t1 = timer
    
    'PQ_TE_Del(TE_pntr) ' remove the Event
    TE_pntr -= 1 ' Quick n Dirty remove this event
    PQ_TE_Add(t1+RefreshTime,100) ' add a new Scren Refresh Event to Timer Event Stack

    ' FPS stuff
    FPS = 1/(t1 - OldTime) ' time between frames
    OldTime = t1 'reset OldTime

    locate 3,70 : print "Main Loops per Frame =";Loops;"    "
    locate 5,70 : print "Display FPS =";FPS;"       "
    locate 7,70 : print "Timer =";t1;"      "
    locate 9,70 : print "Refresh Time = ";RefreshTime
    locate 11,70 : print "# of times Slept =";SleepTImes
    locate 13,70 : print "# of loops with No Sleep =";NoSleepTimes
    locate 15,70 : print "SleepDelay =";SleepDelay
    ScreenCopy
    Loops = 0 'reset Loop counter
end sub

sub DrawGrid
        dim as integer x1, y1
        dim as ulong c1

    cls
    ' draw grid
    for i as integer = 1 to 100
        x1 = i*TILEW
        for j as integer = 1 to 100
            y1 = j*TILEH
            c1 = rgb(200,200,200)
            select case map(i,j,0)
                case 1
                    c1 = rgb(0,255,0)
                case 2
                    c1 = rgb(255,0,0)
                case 3
                    c1 = rgb(0,0,255)
                case 4
                    c1 = rgb(255,255,0)
            end select
            line(x1,y1)- step(TILEW-2,TILEH-2),c1,BF
        next j
    next i
end sub

'
'   this is a Sorted Array which maintains an array in sorted order
'
'
function PQ_TE_Add(ByVal newtime as double, byval event as double) as integer
    ' Adds an event to the Timer Event Stack Sorted Array
    ' this function uses and alters the shared variables: PQ_TE() & TE_pntr

' ... add it to the end then bubble sort it down...
    dim as integer bub, addHere
    TE_pntr = TE_pntr + 1
    addHere = TE_pntr
    PQ_TE(TE_pntr,0) = newtime
    PQ_TE(TE_pntr,1) = event
    if TE_pntr > 1 then
        bub = TE_pntr
        do
            if PQ_TE(bub,0) > PQ_TE(bub-1,0) then
                swap PQ_TE(bub,0) , PQ_TE(bub-1,0)
                swap PQ_TE(bub,1) , PQ_TE(bub-1,1)
                addHere = bub - 1
            else
                bub = 2 ' early exit
            end if
            bub = bub - 1
        loop until bub < 2
    end if
    return addHere
end function

function PQ_TE_Del(ByVal thisOne as integer) as integer
    ' Deletes an event from the Timer Event Stack Sorted Array
    select case thisOne
        case is < TE_pntr
            for i as integer = thisOne to (TE_pntr-1)
                PQ_TE(i,0) = PQ_TE(i+1,0)
                PQ_TE(i,1) = PQ_TE(i+1,1)
            next i
            TE_pntr = TE_pntr - 1
        case is = TE_pntr
            TE_pntr = TE_pntr - 1
    end select
    return thisOne
end function
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

badidea wrote:Info on timeBeginPeriod: https://docs.microsoft.com/en-us/window ... eginperiod
These calls won't work on linux, but I think linux uses a shorter sleep period anyway.
that makes me sad... I would rather having my programs work well on all OSes....

Is there a compiler check to determine the OS type so I can turn on/off these calls depending on the OS?
badidea
Posts: 2586
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: Game Main Loop Timing (desiring feedback)

Post by badidea »

leopardpm wrote:Is there a compiler check to determine the OS type so I can turn on/off these calls depending on the OS?
This might do the trick (a __FB_WIN64__ does not seem exist):

Code: Select all

#ifdef __FB_WIN32__
	Declare Function timeBeginPeriod       Alias "timeBeginPeriod"(As Ulong=1) As Long
	Declare Function timeEndPeriod         Alias "timeEndPeriod"  (As Ulong=1) As Long
#else
	#define timeBeginPeriod
	#define timeEndPeriod
#endif
Maybe the timing will be a little bit different, but hopefully not noticeable.
leopardpm
Posts: 1795
Joined: Feb 28, 2009 20:58

Re: Game Main Loop Timing (desiring feedback)

Post by leopardpm »

cool! will implement - how does it work on yours with this code?

This code also calcs the % of time 'sleeping' (if I did it right...)depending on FPS - try diff fps to test...

I get around sleeping 46% of the time at 60 fps - max out at about 520 fps with 0% sleep time (actually sleeps 1/sec about)

Code: Select all

#ifdef __FB_WIN32__
   Declare Function timeBeginPeriod       Alias "timeBeginPeriod"(As Ulong=1) As Long
   Declare Function timeEndPeriod         Alias "timeEndPeriod"  (As Ulong=1) As Long
#else
   #define timeBeginPeriod
   #define timeEndPeriod
#endif

const TILEW = 5
const TILEH = 5

const SCRW = 1280
const SCRH = 600

    dim shared as integer map(100,100,5)
    dim shared as double starttime, endtime
    dim shared as long rfps

    ' PQ_TE = Sorted Array/Stack for Timer Event Stack
    dim shared as double PQ_TE(1000,1)
    ' TE_pntr = Timer Event stack pointer
    dim shared as integer TE_pntr

declare sub Refreshscreen
declare sub DrawGrid
declare function PQ_TE_Add(ByVal newtime as double, byval event as double) as integer
declare function PQ_TE_Del(ByVal thisOne as integer) as integer

'Declare Function timeBeginPeriod       Alias "timeBeginPeriod"(As Ulong=1) As Long
'Declare Function timeEndPeriod         Alias "timeEndPeriod"  (As Ulong=1) As Long
'
    screenres SCRW,SCRH,32,2 'set up 2 video pages (#0 visible, #1 working)
    
    dim shared as integer FPS, LPS, desiredFPS, Loops, Frames, SleepTImes, NoSleepTimes
    dim shared as double TimeStart, OldTime, RefreshTime, EstRefreshdelay, Lastframe, SleepDelay

' Initialize Main Loop
    locate 50,20 : input "Desired FPS :";desiredFPS
    RefreshTime = 1/desiredFPS
    SleepTImes = 0
    TimeStart = timer
    TE_pntr = 0 ' no events in stack
    Loops = 0 : Frames = 0
    ScreenSet 1, 0 ' working page #1, visible page #0
    DrawGrid
    PQ_TE_Add(timer+RefreshTime,100) ' add the next screen refresh to Timer Event Stack (100 = refresh screen event)

' Main Loop
do
    Loops = Loops + 1
    
    if timer > PQ_TE(TE_pntr,0) then 'check timer against the next event time
        select case PQ_TE(TE_pntr,1) ' figure out what Event needs to be dealt with
            case 100 ' this is the code for RefreshScreen Event
                Refreshscreen
            case 200 ' could be an action or world update or even animation frame change...
                
        end select
    end if
    
    ' if there is enough time for a minimal sleep (1.5ms) then sleep
    if PQ_TE(TE_pntr,0) > (timer + .0015) then
        timebeginperiod
        sleep 1
        timeendperiod
        SleepTImes += 1
    else
        NoSleepTimes +=1
    end if
    
loop until inkey = "q"

end


' some subroutines....

sub Refreshscreen
    dim as double t1

    t1 = timer
    
    'PQ_TE_Del(TE_pntr) ' remove the Event
    TE_pntr -= 1 ' Quick n Dirty remove this event
    PQ_TE_Add(t1+RefreshTime,100) ' add a new Scren Refresh Event to Timer Event Stack

    ' FPS stuff
    FPS = 1/(t1 - OldTime) ' time between frames
    OldTime = t1 'reset OldTime

    locate 3,70 : print "Main Loops per Frame =";Loops;"    "
    locate 5,70 : print "Display FPS =";FPS;"       "
    locate 7,70 : print "Timer =";t1;"      "
    locate 9,70 : print "Refresh Time = ";RefreshTime
    locate 11,70 : print "# of times Slept =";SleepTImes
    locate 13,70 : print "# of loops with No Sleep =";NoSleepTimes
    locate 17,70 : print "% of time sleeping ";(SleepTImes*.001)/(t1-TimeStart)*100
    ScreenCopy
    Loops = 0 'reset Loop counter
end sub

sub DrawGrid
        dim as integer x1, y1
        dim as ulong c1

    cls
    ' draw grid
    for i as integer = 1 to 100
        x1 = i*TILEW
        for j as integer = 1 to 100
            y1 = j*TILEH
            c1 = rgb(200,200,200)
            select case map(i,j,0)
                case 1
                    c1 = rgb(0,255,0)
                case 2
                    c1 = rgb(255,0,0)
                case 3
                    c1 = rgb(0,0,255)
                case 4
                    c1 = rgb(255,255,0)
            end select
            line(x1,y1)- step(TILEW-2,TILEH-2),c1,BF
        next j
    next i
end sub

'
'   this is a Sorted Array which maintains an array in sorted order
'
'
function PQ_TE_Add(ByVal newtime as double, byval event as double) as integer
    ' Adds an event to the Timer Event Stack Sorted Array
    ' this function uses and alters the shared variables: PQ_TE() & TE_pntr

' ... add it to the end then bubble sort it down...
    dim as integer bub, addHere
    TE_pntr = TE_pntr + 1
    addHere = TE_pntr
    PQ_TE(TE_pntr,0) = newtime
    PQ_TE(TE_pntr,1) = event
    if TE_pntr > 1 then
        bub = TE_pntr
        do
            if PQ_TE(bub,0) > PQ_TE(bub-1,0) then
                swap PQ_TE(bub,0) , PQ_TE(bub-1,0)
                swap PQ_TE(bub,1) , PQ_TE(bub-1,1)
                addHere = bub - 1
            else
                bub = 2 ' early exit
            end if
            bub = bub - 1
        loop until bub < 2
    end if
    return addHere
end function

function PQ_TE_Del(ByVal thisOne as integer) as integer
    ' Deletes an event from the Timer Event Stack Sorted Array
    select case thisOne
        case is < TE_pntr
            for i as integer = thisOne to (TE_pntr-1)
                PQ_TE(i,0) = PQ_TE(i+1,0)
                PQ_TE(i,1) = PQ_TE(i+1,1)
            next i
            TE_pntr = TE_pntr - 1
        case is = TE_pntr
            TE_pntr = TE_pntr - 1
    end select
    return thisOne
end function
Post Reply