Results 1 to 2 of 2

Thread: [Vb.Net] Managed Game Loop

  1. #1

    Thread Starter
    Super Moderator dday9's Avatar
    Join Date
    Mar 2011
    Location
    South Louisiana
    Posts
    11,698

    [Vb.Net] Managed Game Loop

    In this thread I will discuss how to create a managed game loop in a proper manner. First I will identify ways that are most commonly used and explain why they should not be used. Then I will explain how to set up the proper managed game loop as well as why it works the way it does.

    In game programming, one of the biggest and most basic concepts that should be applied is the game loop. A game loop is simply something that keeps the game in perpetual motion. Think of a normal windows form application for a second, in most scenarios the program remains idle until a user invokes some sort of an event such as clicking a button. In game programming, the game is constantly moving. In other words the game assumes that something is always happening and doesn't wait for user interaction for the program to continue.

    Something that most beginners use as their game loop is the System.Windows.Forms.Timer class. This is OK in the sense that the timer does provide a simple means of raising an event(the Tick event) at user-defined intervals, however these timers are notoriously unreliable at lower intervals. The Interval property of the Timer gets or sets the time measured in milliseconds that invokes the Tick event. Therefore the lower the Interval, the less time between ticks. While theoretically the timer's Interval can be at one, realistically the timer starts to become inaccurate when it's Interval is set at about 50. The reason for this lies in the fact that this timer is not a high precision timer. This translates to over a period of time being off x amount of milliseconds(which is sufficient for most applications) after each Tick, there starts to be greater period of gaps. In games, those gaps are what are known as lag.

    So if using the System.Windows.Forms.Timer is not a suitable solution, then why not set up a loop that runs as fast as the CPU will allow it to run. Running this type of loop is what is known as running a "Busy Wait" or "Busy Loop". The technique is often used(incorrectly) in normal windows form applications to just kill time until something has happened. One of the reasons why this is a terrible solution is because it kills the performance of the computer and can raise the CPU usage to almost %100! The other reason for this not being a suitable solution is the concept of a locked frame rate. The frame rate is the speed at which the game is refreshed measured in frames-per-second(FPS). Having a locked frame rate allows for a consistent animation of the game. Using a busy loop does not produce a precise and predictable frame rates which result in choppy animation.

    The busy loop and the System.Windows.Forms.Timer is out of the question for our game loop. So what do we use? From my experience there are two solutions. One, which is the simpler of the two(but less accurate), is using a high precision timer. In the .NET framework you can find a high precision timer in the System.Diagnostics.Stopwatch class. This class, unlike the System.Windows.Forms.Timer class, does not provide a Tick event or some equivalent; so it is our job to create a method that will keep track of the frame rate and lock in the frames-per-second. This should be done in a separate thread to achieve maximum accuracy. The second solution is to use the QueryPerformance APIs. The QueryPerformance API's consist of two APIs: the QueryPerformanceCounter function and the QueryPerformanceFrequency function. Using both of these functions will return a high-resolution time stamp that cannot be surpassed.

    Here is an example of using the System.Diagnostics.Stopwatch:
    Code:
    Public Class GameLoop
        Inherits ComponentModel.Component
    
        Private _enabled As Boolean
        Public Property Enabled As Boolean
            Get
                Return _enabled
            End Get
            Set(ByVal value As Boolean)
                If Not _enabled.Equals(value) Then
                    _enabled = value
                    Me.OnEnabledChanged()
                End If
            End Set
        End Property
    
        Private _interval As Single
        Public Property Interval As Single
            Get
                Return _interval
            End Get
            Set(ByVal value As Single)
                If Not _interval.Equals(value) Then
                    _interval = value
                    Me.OnIntervalChanged()
                End If
            End Set
        End Property
    
        Protected Overridable Sub OnEnabledChanged()
            RaiseEvent EnabledChanged(Me, EventArgs.Empty)
        End Sub
    
        Protected Overridable Sub OnIntervalChanged()
            RaiseEvent IntervalChanged(Me, EventArgs.Empty)
        End Sub
    
        Public Event EnabledChanged(ByVal sender As Object, ByVal e As EventArgs)
    
        Public Event IntervalChanged(ByVal sender As Object, ByVal e As EventArgs)
    
        Public Event Tick(ByVal sender As Object, ByVal e As GameLoopEventArgs)
    
        Private Sub GameLoop_EnabledChanged(sender As Object, e As EventArgs) Handles Me.EnabledChanged
            If _enabled Then
                Dim game_thread As Threading.Thread = New Threading.Thread(AddressOf ThreadedGameLoop)
                game_thread.Start()
            End If
        End Sub
    
        Private Sub GameLoop_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed
            If _enabled Then
                Me.Enabled = Not _enabled
            End If
        End Sub
    
        Private Sub ThreadedGameLoop()
            Dim s As New Stopwatch
            Do
                s.Restart()
    
                While _enabled AndAlso s.ElapsedMilliseconds < _interval
                End While
                s.Stop()
    
                RaiseEvent Tick(Me, New GameLoopEventArgs() With {.ActualFPS = s.ElapsedMilliseconds, .TargetFPS = _interval})
            Loop Until Not _enabled
        End Sub
    
        Public Sub Start()
            If Not _enabled Then
                Me.Enabled = Not _enabled
            End If
        End Sub
    
        Public Sub [Stop]()
            If _enabled Then
                Me.Enabled = Not _enabled
            End If
        End Sub
    
        Sub New()
            _interval = 16.6
        End Sub
    
        Sub New(ByVal interval As Single)
            _interval = interval
        End Sub
    
    End Class
    
    Public Class GameLoopEventArgs
        Inherits EventArgs
    
        Private _actualFPS As Single
        Public Property ActualFPS As Single
            Get
                Return _actualFPS
            End Get
            Set(ByVal value As Single)
                If Not _actualFPS.Equals(value) Then
                    _actualFPS = value
                    Me.OnActualFPSChanged()
                End If
            End Set
        End Property
    
        Private _targetFPS As Single
        Public Property TargetFPS As Single
            Get
                Return _targetFPS
            End Get
            Set(ByVal value As Single)
                If Not _targetFPS.Equals(value) Then
                    _targetFPS = value
                    Me.OnTargetFPSChanged()
                End If
            End Set
        End Property
    
        Protected Overridable Sub OnActualFPSChanged()
            RaiseEvent ActualFPSChanged(Me, EventArgs.Empty)
        End Sub
    
        Protected Overridable Sub OnTargetFPSChanged()
            RaiseEvent TargetFPSChanged(Me, EventArgs.Empty)
        End Sub
    
        Public Event ActualFPSChanged(ByVal sender As Object, ByVal e As EventArgs)
    
        Public Event TargetFPSChanged(ByVal sender As Object, ByVal e As EventArgs)
    
    End Class
    In this example, I create a new component which is similar to a System.Timers.Timer class. Whenever the game loop is meant to be running a new thread is created and will not exit the thread until the game loop is set to stop. Inside of the loop I reset the stopwatch(which tracks how much time has elapsed) and do nothing until the time that has elapsed is equal to or greater than the desired interval. Once I reach the desired elapse time I stop the stopwatch and raise an event to signal that the user needs to update/draw/render the game and I also report the Actual vs. Target elapsed milliseconds.

    The frame rate will vary depending on what device you are targeting. In this example, you will see that the frames-per-second is locked in at 60FPS(interval = 16.6) if the programmer simply creates a new GameLoop without passing any arguments. This is because the example targets computers that have a screen refresh rate of 60Hz. So how do you determine what the frame rate should be set at?

    The simplest way is to check what the maximum refresh rate will be and set the frame rate equal to or less than that refresh rate. In the United States, the refresh rate for most monitors is 60Hz while in Europe the refresh rate for most monitors is 50Hz. If you have a television that has a refresh rate of 120Hz and you want to develop specifically for that TV, then lock your FPS at or below 120. You can get the exact refresh rate by using the ManagementObjectSearcher class.

    My last bit of advice is that if you want to target multiple screens then the general rule of thumb is to have the FPS set between 30 and 60.
    Last edited by dday9; May 26th, 2016 at 10:41 AM.
    "Code is like humor. When you have to explain it, it is bad." - Cory House
    VbLessons | Code Tags | Sword of Fury - Jameram

  2. #2
    Member
    Join Date
    Oct 2016
    Posts
    32

    Re: [Vb.Net] Managed Game Loop

    But isn't this essentially just a:

    Code:
    Public Event Tick()
    
    Dim Interval as Long = CLng(stopwatch.Frequency / 60)
    Dim NextTime as Long = 0
    
    Public Sub Start()
       NextTime = Stopwatch.GetTimeStamp + Interval
       While true
          If Stopwatch.GetTimeStamp >= NextTime Then
             RaiseEvent Tick
             NextTime += Interval
          End If
       End While
    End Sub
    Just with some threading and event sugar?
    This still uses essentially the same CPU as using the while loop by itself (which is a lot).

    Edit: Here is my solution and performance difference on my PC.
    http://pastebin.com/NnvJQCmK

    @60FPS or (1000 / 60) Milliseconds
    While Loop = ~14.5%
    GameLoop = ~14.5%
    MicroTimer = ~00.7%

    Specs:
    CPU = Core i7 2600K
    GPU = GTX 680
    Ram = 8GB DDR3 1333
    Last edited by TizzyT; Oct 18th, 2016 at 08:43 AM. Reason: Added link to my MicroTimer, Added my PC specs

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width