Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: VB6: Pelin FPS:n hallinta: tapahtumien moniajo

Merri [05.01.2006 12:55:53]

#

Tämä koodi antaa mahdollisuuden laittaa pelikoodin pyörimään tiettyjä kertoja sekunnissa, joka on erittäin hyödyllinen peleissä. Koodi on joustava ja tapahtumia voi laittaa pyörimään useita yhtäaikaisesti, esimerkkikoodissa on kolme tapahtumaa. Kontrolli on todella tarkka ja koodi pitää hyvin huolen siitä, että pelikoodit pyörivät sen tietyn määrän sekunnissa. Samalla koodi pitää automaattisesti huolen siitä, että prosessorikäyttö pysyy minimissä (tämä esimerkki näyttää kokoajan 0% huolimatta siitä, että ohjelma on jatkuvassa loopissa).

Esimerkkikoodissa on siis kolme eri tapahtumaa:

1. Sisäinen koodinkäsittely (240 kertaa sekunnissa, TICK_INTERNAL)
2. Ruudunpäivitysnopeus (60 kertaa sekunnissa, TICK_DISPLAY)
3. Sekunttipäivitys (kerran sekunnissa, TICK_SECOND)

Nämä tapahtumat on määritelty TICK_INDEX enumiin modTicks.bas-tiedostossa. Voit lisätä tähän omia tapahtumiasi tai muuttaa vanhoja, tyylillä TICK_OMA = 3 ja sitten lisätä päivitystahdin Init_Ticks-proseduuriin ja myös koodinpätkän Main-proseduuriin. Suosittelen lisäämään arvoa varten oman constantin esimerkin mukaisesti, jotta arvoa on helppo muuttaa suoraan moduulin koodin alusta.

Esimerkkikoodi ei tee mitään muuta kuin sen, että se kertoo sekunnin välein Form1.Captioniin kuinka monesti koodit on suoritettu ja kuinka kauan koodi on ollut toiminnassa.

Näin saat koodin toimintaan:
- Aloita uusi projekti.
- Luo uusi moduuli, nimeä se nimelle modTicks ja kopioi ensimmäinen koodilistaus siihen.
- Luo toinen moduuli, nimeä se nimelle modMain ja kopioi toinen koodilistaus siihen.
- Kopioi kolmas koodilistaus Form1:een.
- Tärkeää! Projekti pysyy käynnissä Main-proseduurin kautta. Katso siis valikosta Project > Project1 Properties ja aseta Startup Objectiksi Sub Main.

Loppusanoiksi vielä se, että kannattaa pitää suoritettava koodi mahdollisimman kevyenä. Nopeudelle on itsemurhaa alkaa esimerkiksi pelleilemään variant-muuttujilla, joten määrittele Dimeillä tarkasti mitä muuttujia haluat käyttää. Kaikkein eniten luuppaavissa tapahtumissa kannattaa välttää myös string-muuttujan käyttöä, koska muuten prosessorikäyttö ampaisee helposti korkealle. On myös hyvä pitää Main-proseduurin koodi lyhyenä, siispä tee uusiin moduuleihin proseduureja tai peräti class moduuleja, joissa suoritetaan pääkoodia.

Tapahtumien moniajo vaatii myös uudenlaista ajattelutapaa: on mahdollista hajauttaa usein kutsuttujen proseduurien loopit toimimaan useamman kutsun aikana sen sijaan, että joka kutsulla looppi suoritettaisiin kokonaisena (joka olisi hyvin tappavaa suorituskyvylle, varsinkin jos kyse on esimerkiksi useamman vihollisen tekoälystä). Tämä on kuitenkin jo aivan eri aihe, tässä pääasia on näyttää tarkasti aikataulussa pysyvä tapahtumien hallinta ja itse rakennettu moniajo. Todellinen Windowsin tarjoama moniajo mahdollistaisi sen, että peli ei pysähtyisi esimerkiksi formia liikuteltaessa kuten nyt tapahtuu, mutta sekin on oma aiheensa.

modTicks.bas

Option Explicit

Private Const FRAMES_PER_SECOND = 60
Private Const MOVES_PER_SECOND = 240

Public Enum TICK_INDEX
    TICK_INTERNAL = 0   ' for internal processing (moving objects, checking keyboard state etc.)
    TICK_DISPLAY = 1    ' for updating the screen
    TICK_SECOND = 2     ' for displaying FPS information once a second
End Enum

Private Type TICKS
    Count As Long       ' number of ticks done
    Ending As Currency  ' the next ending tick
    Freq As Currency    ' tick frequency
End Type

Private dblFreqToMS As Double   ' to convert frequency to millisecond
Private curLateTick As Currency ' start skipping when more late than this value
Private udtTicks(2) As TICKS     ' the array to hold tick information

' the API declarations
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
' this function will wait for the next tick and returns the index that is about to happen
Public Function GetNextTickIndex() As TICK_INDEX
    Dim lngA As Long, curDifference As Currency, enmIndex As TICK_INDEX
    Dim curTick As Currency
    ' we skip the first item, because we default for it
    For lngA = 1 To UBound(udtTicks)
        ' check if this tick will be processed next
        If udtTicks(lngA).Ending < udtTicks(enmIndex).Ending Then enmIndex = lngA
    Next lngA
    ' increase tick index
    udtTicks(enmIndex).Count = udtTicks(enmIndex).Count + 1
    ' get current tick
    QueryPerformanceCounter curTick
    ' because of the falling behind prevention, we need to set this to correct value
    If udtTicks(enmIndex).Ending = 0 Then udtTicks(enmIndex).Ending = curTick
    ' then wait for the tick
    curDifference = udtTicks(enmIndex).Ending - curTick
    If curDifference >= 0 Then
        lngA = CLng(CDbl(curDifference) * dblFreqToMS)
        ' do we need to sleep? (this prevents 100% processor usage!)
        If lngA > 0 Then
            Sleep lngA
            ' prevent being late
            QueryPerformanceCounter curTick
            curDifference = udtTicks(enmIndex).Ending - curTick
            ' set the next tick
            SetNextTick enmIndex, -curDifference
        Else
            ' set the next tick
            SetNextTick enmIndex
        End If
    Else
        If curDifference > curLateTick Then
            ' WE ARE LATE, but not too badly
            SetNextTick enmIndex, -curDifference
        Else
            ' WE ARE BADLY LATE, so we have to skip processing :/
            ' otherwise on slow computers the things just get worse and worse...
            SetNextTick enmIndex
        End If
    End If
    ' return the tick index
    GetNextTickIndex = enmIndex
End Function
Public Function GetTickCount(ByVal Index As TICK_INDEX) As Long
    ' return the current tick count
    GetTickCount = udtTicks(Index).Count
End Function
Public Sub Init_Ticks()
    Dim curFreq As Currency, curTick As Currency
    ' we need a good timer: use queryperformancecounter
    QueryPerformanceFrequency curFreq
    ' calculate frequency to second value
    dblFreqToMS = 1000 / CDbl(curFreq)
    ' when we skip stuff?
    curLateTick = -(curFreq / 20)
    ' set frequencies
    udtTicks(TICK_INTERNAL).Freq = curFreq / MOVES_PER_SECOND
    udtTicks(TICK_DISPLAY).Freq = curFreq / FRAMES_PER_SECOND
    udtTicks(TICK_SECOND).Freq = curFreq
End Sub
Public Sub ResetTickCount(ByVal Index As TICK_INDEX)
    udtTicks(Index).Count = 0
End Sub
Private Sub SetNextTick(ByVal Index As TICK_INDEX, Optional ByVal curFallBehind As Currency = 0)
    Dim curTick As Currency
    ' get current tick
    QueryPerformanceCounter curTick
    ' set next event to future
    udtTicks(Index).Ending = curTick + udtTicks(Index).Freq - curFallBehind
End Sub

modMain.bas

Option Explicit

' nämä kutsut ovat parempaa DoEventsin hallintaa varten
' mitä sitä turhaan hidasta DoEventsiä kutsumaan, jos sille ei ole tarvetta...
Public Const QS_KEY = &H1&
Public Const QS_MOUSEMOVE = &H2&
Public Const QS_MOUSEBUTTON = &H4&
Public Const QS_POSTMESSAGE = &H8&
Public Const QS_TIMER = &H10&
Public Const QS_PAINT = &H20&
Public Const QS_SENDMESSAGE = &H40&
Public Const QS_HOTKEY = &H80&
Public Const QS_ALLINPUT = (QS_SENDMESSAGE Or QS_PAINT Or QS_TIMER Or QS_POSTMESSAGE Or QS_MOUSEBUTTON Or QS_MOUSEMOVE Or QS_HOTKEY Or QS_KEY)
Public Const QS_MOUSE = (QS_MOUSEMOVE Or QS_MOUSEBUTTON)
Public Const QS_INPUT = (QS_MOUSE Or QS_KEY)
Public Const QS_ALLEVENTS = (QS_INPUT Or QS_POSTMESSAGE Or QS_TIMER Or QS_PAINT Or QS_HOTKEY)

Public Declare Function GetQueueStatus Lib "user32" (ByVal qsFlags As Long) As Long

' kun tämän muuttaa arvoon True, ohjelma sulkeutuu
Public Quit As Boolean

Private Sub Main()
    ' alusta päivityksenhallinta
    Init_Ticks
    ' näytä pääformi
    Form1.Show
    ' jatketaan kunnes Quit on asetettu arvoon True
    Do While Not Quit
        ' odota kunnes seuraava tapahtuma tapahtuu
        ' GetNextTickIndex löytyy modTicks.bas-tiedostosta
        ' ohjelma on pysähtynyt GetNextTickIndexiin kunnes jotain pitäisi tapahtua
        ' funktio palauttaa arvon, joka kertoo mikä koodi pitäisi nyt suorittaa
        Select Case GetNextTickIndex

        ' sisäisen koodin hallinta
            Case TICK_INTERNAL
                ' tänne voi kirjoittaa esim.:
                ' - ammusten ja pelaajien liikuttelun
                ' - törmäysten tunnistamisen
                ' - näppäimistön ja hiiren lukemisen
        ' ruudunpäivityksen hallinta
            Case TICK_DISPLAY
                ' tänne voi kirjoittaa ruudunpäivityksen, esim.:
                ' - hae kopio taustabufferista (jossa ei ole objekteja yms.)
                ' - piirrä ruudulla näkyvät ammukset, pelaajat ja viholliset kopiobufferiin
                ' - piirrä bufferi ruudulle näkyviin
        ' sekunttipäivitys
            Case TICK_SECOND
                ' näytä formin otsikossa sekuntin välein kuinka paljon on tapahtumia tapahtunut
                Form1.Caption = _
                    "FPS: " & Str$(GetTickCount(TICK_DISPLAY)) & _
                    " | MPS: " & Str$(GetTickCount(TICK_INTERNAL)) & _
                    " | " & Format$(TimeSerial(0, 0, CInt(GetTickCount(TICK_SECOND) And &HFFFF)), "n:ss")
                ' nollaa laskurit
                ResetTickCount TICK_DISPLAY
                ResetTickCount TICK_INTERNAL
        End Select

        ' estä ohjelmaa jäämästä jumiin antamalla Windowsille aikaa suorittaa moniajoa
        ' tämä pitää huolen siitä, että DoEventsiä kutsutaan vain silloin kun sille on tarvetta
        ' tämä on tärkeää, koska DoEvents on hidas ja tätä koodirykelmää suoritetaan todella monesti sekuntia kohden
        If GetQueueStatus(QS_SENDMESSAGE Or QS_ALLEVENTS) <> 0 Then DoEvents
    Loop
    ' poista Form1 muistista
    Set Form1 = Nothing
End Sub

Form1

Option Explicit

Private Sub Form_Unload(Cancel As Integer)
    ' kertoo että nyt lopetetaan
    Quit = True
End Sub

Merri [05.01.2006 21:39:33]

#

Kaipaako kukaan jotakin laajennettua esimerkkiä siitä, miten tuota voi käyttää hyväksi?

JoreSoft [06.01.2006 00:33:36]

#

Tossa osoitteessa on tuolla koodilla tehtynä yksi Sprite juttu :)
http://koti.mbnet.fi/joresoft/Download/Esim/Spriten%20sis%E4%E4n.zip
Kuva => http://koti.mbnet.fi/joresoft/images/Kuva-SpritenSisaan.png

nopeni hieman, kun muutin timerin tuohon koodiin :)

Koodissa on nyt se Quit = True :) muutin ennen kuin meen nukkumaan :)

Antti Laaksonen [06.01.2006 01:00:35]

#

Hieno koodinpätkä. DoEvents-kikka on aika ovela. GetTickCount on vähän hämäävä nimi samannimisen API-funktion takia.

Merri [06.01.2006 01:22:43]

#

Jeps, se sattui vahingossa, mutta en sitten jaksanut keksiä sille muutakaan nimeä kun koodi oli jo levityksessä. Eikä GetTickCountille ole käsittääkseni tarvetta tätä käyttävässä ohjelmassa :)

Ja kiitokset Jorelle graafisen esimerkin väsäämisestä!

Merri [07.01.2006 12:35:26]

#

Lisäkorjausta tuohon Joren uuteen koodiin: Main-proseduurin loppuun pitää lisätä

Unload f1
Unload f2

Muuten ohjelma jää muistiin kykkimään, koska kaikkia formeja ei ladata pois muistista.

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta