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
Kaipaako kukaan jotakin laajennettua esimerkkiä siitä, miten tuota voi käyttää hyväksi?
Tossa osoitteessa on tuolla koodilla tehtynä yksi Sprite juttu :)
http://koti.mbnet.fi/joresoft/Download/Esim/
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 :)
Hieno koodinpätkä. DoEvents-kikka on aika ovela. GetTickCount on vähän hämäävä nimi samannimisen API-funktion takia.
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ä!
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.
Aihe on jo aika vanha, joten et voi enää vastata siihen.