Help with game timing
BlitzMax Forums/BlitzMax Beginners Area/Help with game timing
| ||
I am developing a game and I have the following code which is derived from the Digesteroids example that comes with Blitzmax. I am struggling with the timing logic. What I want is a fixed rate timer that is able to determine if updates are lagging so that something can be done to ensure updates occur at the desired time. I also want my game to 'feel' the same if updates aren't occurring at the desired rate (or if the user chooses a different update rate by setting the targetFPS variable to something else E.G. 60) Here is my code so far: SuperStrict AppTitle = "Test Game" Graphics 800, 600 SetClsColor(100, 149, 237) New TGame.Run() Type TGame Field targetFPS:Int = 30 Field isRunning:Int = True Field isRunningSlow:Int = False Field x:Float = 30 Field y:Float = 60 Method Update(delta:Float) If KeyHit(KEY_ESCAPE) Then isRunning = False If KeyDown(KEY_UP) Then y:- 60 * delta If KeyDown(KEY_DOWN) Then y:+ 60 * delta If KeyDown(KEY_LEFT) Then x:- 60 * delta If KeyDown(KEY_RIGHT) Then x:+ 60 * delta End Method Method Draw() Cls DrawRect(x, y, 20, 20) End Method Method Run() ' --------------------------------------------------------------- ' This is the amount of time (in milliseconds) that should occur ' between each frame before it is drawn. ' --------------------------------------------------------------- Local targetElapseTime:Int = 1000 / TargetFPS ' --------------------------------------------------------------- ' Store the initial start time and calculate how long to wait ' before the first frame is drawn. ' These values will be recalculated after all required updates ' have been made but before the game is drawn. ' --------------------------------------------------------------- Local frameStartTime:Int = MilliSecs() Local frameDelay:Int = MilliSecs() + targetElapseTime ' --------------------------------------------------------------- ' These variables will be used to calculate how many updates need ' to be made. ' --------------------------------------------------------------- Local timeMissed:Int Local updatesRequired:Float While isRunning ' --------------------------------------------------------------- ' If the game is running faster than it needs to, the game will ' continuously loop until it is time to perform an update. ' --------------------------------------------------------------- Repeat Until MilliSecs() > frameDelay ' --------------------------------------------------------------- ' Next we calculate if we are running behind; this can occur ' on subsequent iterations (E.G. if Draw() has taken a long time) ' so we calculate how much time has been missed to determine how ' many updates need to be made in order to catch up. ' --------------------------------------------------------------- timeMissed = (MilliSecs() - frameStartTime) updatesRequired = Float(timeMissed) / Float(targetElapseTime) ' --------------------------------------------------------------- ' This 'caps' the updates if too many are required and provides ' notification so that you can do something about it. ' --------------------------------------------------------------- If updatesRequired > targetElapseTime Then updatesRequired = targetElapseTime / 2 isRunningSlow = True Else isRunningSlow = False End If Local delta:Float = (MilliSecs() - Float(frameStartTime)) / 1000 If updatesRequired > 1.0 Then For Local Count:Int = 1 To Int(updatesRequired) Self.Update(delta) Next End If frameStartTime = MilliSecs() frameDelay = MilliSecs() + targetElapseTime Self.Draw() Flip End While End Method End Type My code appears to do what I want it to do but something doesn't seem right and I can't quite put my finger on it. I'm hoping someone who has a better understanding of this stuff can review my code and let me know if I am on the right lines. I also have a few questions that have occurred to me whilst learning about game timing: 1. When people talk about game timing, when they say 'updates per second' do they really mean 'updates and render' per second or just updates? 2. Which operation would you usually expect to take the longest (Update or render; I am guessing rendering would usually take longer). 3. The main addition I have made to the Digesteroids timing code is the inclusion of a cap on the amount of updates that can be made. My thinking behind this was that if the game is running slow, it will try and catch up by calling more updates. If the updates are also running slow then this can cause the game to try and catch up on the next loop and so on causing the game to potentially grind to a halt. When I cap the number of updates I set a variable that says 'hey, the game is running slow, you best do something about it'. Is my thinking behind this correct? If so, does my implementation make sense? Last edited 2012 Last edited 2012 |
| ||
Answers to your questions: 1) I don't. Updates should happen at fixed times, rendering as often as possible. The trick is to de-couple logic updates and rendering. 2) Depends on your game, but most of the time it's rendering. Which is why it makes sense to renders as often as possible. 3) Your thinking is correct, but your solution to the problem will cause stuttering and slowed down gameplay, as you mention. A better solution IMO is to keep the updates going at a steady rate, and render less when needed but use tweening to smooth out movement. That is why separating logic and rendering is important. I'm working on a small game engine, which uses fixed updates and render tweening for situations like this. It will use a lot more but the engine itself is perfect as an example for you. code: http://code.google.com/p/noisycode/source/browse?repo=game2d wiki: http://code.google.com/p/noisycode/wiki/game2d Take a look at the code and examples and see if it makes sense to you. It's not that complicated. |
| ||
As you put your question into "beginners forum", I will give you a easy to understand solution: This is never a good solution: Repeat Until MilliSecs() > frameDelay better is to give back control to your system: FPS=CreateTimer(60) Repeat ..... Game.Run() WaitTimer FPS Until KeyHit(Key_Escape) In the game.Run you could take care about the timing with using individual timers. The following code shows a simple system, where the Update() is called every 10msec. Also when the computer interupts the game for a longer time (f.e. 200msec) the code will prefer the updating of the game and catch up the lost updates: This is the Run()-Methode: ..... If UpdateTimer<Millisecs()-1000 then ' for the case, when game was interupted for a long timer (f.e. menu screen) UpdateTimer=Millisecs()-1 Endif If UpdateTimer<Millisecs() UpdateTimer=UpdateTimer+10 Update() ElseIf DrawTimer<Millisecs() DrawTimer=Millisecs()+16 Draw() Endif This code need no think about "delta factor" in the game code. The Run() is called max. 200 times a second. The Update() is called 100 times a second with guarantee of 100%. The Draw() is called less or max 60 times a second. |
| ||
Thank you both for the help so far. @wiebo - The information I have read so far led me to believe that it was a waste of time to render more than once per frame as you could potentially be re-rendering exactly the same thing multiple times which is a waste. Also, if rendering is taking longer than it should (i.e. longer than the desired frame time) then multiple renders are just going to make this worse or am I misunderstanding something here? I understand my solution to point 3 could cause slowdown but the point is to flag this up to the programmer via the 'isRunningSlow' variable so that they can do something abut it to help the game recover (remove the number of entities from the screen, update every other frame etc.) I had considered render tweening instead but this added another layer of complexity onto something I already do not fully understand. @Midimaster - I had originally considered using a timer but reasoned that this was not a good solution as I was unable to find any code that used this method (all code I have seen so far manually calculates the frame tick times). The main problem I am having is everyone seems to have a different opinion on how game timing should be done. This stuff is also difficult to figure out due to the small time scales being used I.E. stepping through the code doesn't really show you what is going on; I also tried simulating this using an Excel spreadsheet to see what was happening but think I have just confused myself further. |
| ||
The Update() is called 100 times a second with guarantee of 100% drag your windowed app in windows ... (we had that topic already). for your case superstrict ?Threaded Import Brl.threads ? 'class for smooth framerates Type TDeltaTimer field newTime:int = 0 field oldTime:int = 0.0 field loopTime:float = 0.1 field deltaTime:float = 0.1 '10 updates per second field accumulator:float = 0.0 field tweenValue:float = 0.0 'between 0..1 (0 = no tween, 1 = full tween) field fps:int = 0 field ups:int = 0 field deltas:float = 0.0 field timesDrawn:int = 0 field timesUpdated:int = 0 field secondGone:float = 0.0 field totalTime:float = 0.0 ?Threaded Global UpdateThread:TThread Global drawMutex:TMutex = CreateMutex() Global useDeltaTimer:TDeltaTimer= null ? Function Create:TDeltaTimer(physicsFps:int = 60) local obj:TDeltaTimer = new TDeltaTimer obj.deltaTime = 1.0 / float(physicsFps) obj.newTime = MilliSecs() obj.oldTime = 0.0 return obj End Function ?Threaded Function RunUpdateThread:Object(Input:Object) repeat useDeltaTimer.newTime = MilliSecs() if useDeltaTimer.oldTime = 0.0 then useDeltaTimer.oldTime = useDeltaTimer.newTime - 1 useDeltaTimer.secondGone :+ (useDeltaTimer.newTime - useDeltaTimer.oldTime) useDeltaTimer.loopTime = (useDeltaTimer.newTime - useDeltaTimer.oldTime) / 1000.0 useDeltaTimer.oldTime = useDeltaTimer.newTime if useDeltaTimer.secondGone >= 1000.0 'in ms useDeltaTimer.secondGone = 0.0 useDeltaTimer.fps = useDeltaTimer.timesDrawn useDeltaTimer.ups = useDeltaTimer.timesUpdated useDeltaTimer.deltas = 0.0 useDeltaTimer.timesDrawn = 0 useDeltaTimer.timesUpdated = 0 endif 'fill time available for this loop useDeltaTimer.loopTime = Min(0.25, useDeltaTimer.loopTime) 'min 4 updates per seconds 1/4 useDeltaTimer.accumulator :+ useDeltaTimer.loopTime if useDeltaTimer.accumulator >= useDeltaTimer.deltaTime 'force lock as physical updates are crucial LockMutex(drawMutex) While useDeltaTimer.accumulator >= useDeltaTimer.deltaTime useDeltaTimer.totalTime :+ useDeltaTimer.deltaTime useDeltaTimer.accumulator :- useDeltaTimer.deltaTime useDeltaTimer.timesUpdated :+ 1 EventManager.triggerEvent( "App.onUpdate", TEventSimple.Create("App.onUpdate",null)) Wend UnLockMutex(drawMutex) else delay( floor(Max(1, 1000.0 * (useDeltaTimer.deltaTime - useDeltaTimer.accumulator) - 1)) ) endif forever End Function Method Loop() 'init update thread if not self.UpdateThread OR not ThreadRunning(self.UpdateThread) useDeltaTimer = self print " - - - - - - - - - - - - " print "Start Updatethread: create thread" print " - - - - - - - - - - - - " self.UpdateThread = CreateThread(self.RunUpdateThread, Null) endif 'if we get the mutex (not updating now) -> send draw event if TryLockMutex(drawMutex) 'how many % of ONE update are left - 1.0 would mean: 1 update missing self.tweenValue = self.accumulator / self.deltaTime 'draw gets tweenvalue (0..1) self.timesDrawn :+1 EventManager.triggerEvent( "App.onDraw", TEventSimple.Create("App.onDraw", string(self.tweenValue) ) ) UnlockMutex(drawMutex) endif delay(2) End Method ? ?not Threaded Method Loop() self.newTime = MilliSecs() if self.oldTime = 0.0 then self.oldTime = self.newTime - 1 self.secondGone :+ (self.newTime - self.oldTime) self.loopTime = (self.newTime - self.oldTime) / 1000.0 self.oldTime = self.newTime if self.secondGone >= 1000.0 'in ms self.secondGone = 0.0 self.fps = self.timesDrawn self.ups = self.timesUpdated self.deltas = 0.0 self.timesDrawn = 0 self.timesUpdated = 0 endif 'fill time available for this loop self.loopTime = Min(0.25, self.loopTime) 'min 4 updates per seconds 1/4 self.accumulator :+ self.loopTime 'update gets deltatime - fraction of a second (speed = pixels per second) While self.accumulator >= self.deltaTime self.totalTime :+ self.deltaTime self.accumulator :- self.deltaTime self.timesUpdated :+ 1 EventManager.triggerEvent( TEventSimple.Create("App.onUpdate",null) ) Wend 'how many % of ONE update are left - 1.0 would mean: 1 update missing self.tweenValue = self.accumulator / self.deltaTime 'draw gets tweenvalue (0..1) self.timesDrawn :+1 EventManager.triggerEvent( TEventSimple.Create("App.onDraw", string(self.tweenValue) ) ) 'Delay(1) End Method ? 'tween value = oldposition*tween + (1-tween)*newPosition 'so its 1 if no movement, 0 for full movement to new position 'each drawing function has to take care of it by itself Method getTween:float() return self.tweenValue End Method 'time between physical updates as fraction to a second 'used to be able to give "velocity" in pixels per second (dx = 100 means 100px per second) Method getDeltaTime:float() return self.deltaTime End Method End Type local timer:TDeltaTimer = TDeltaTimer.Create(60) '60 update calls per second / "physical updates" repeat timer.loop() 'will call the update and draw functions until AppTerminate OR KeyHit(KEY_ESCAPE) Replace the "EventManager..."-lines with the corresponding "Update"/"Draw"-functions (eg from your game object, screen manager, ...). What it does: it runs updates at the interval set in the TDeltaTimer.Create() it runs draw functions as often as possible (between the update cycles). if compiled in threaded mode, the loop will be different and updates are running in different threads (drawing has to be done in main thread). Benefit from using the threaded one ? - you can move the window of your app in windows and the updates are still processed instead of being freezed (eg. when using Millisecs() to see if an object was visible long enough or a network message is to old and should be send to trash bin). It is easily edited in a way to use a similar style of accumulator/deltaTiming to limit the fps to a specific value (if you want less than the one flip 1/monitor refreshrate provides) edit: the getDeltaTime() method can be used for movement within your update-function. eg. to move a object 10px per second ... object.x :+ 10*timer.getDeltaTime() as the time is only a fraction of a second and after 60 updates the second is gone (60 times the update is 60*(0.0...*10)). bye Ron Last edited 2012 |
| ||
@Festay: What I mean with 'rendering as many times as possible' is this: the less time your update loop is taking, the more times you can render. BUT: You update and then render, and never do update,render,render (for instance) I can choose to update my game 30 times a second, and still render at 60 fps. That is what de-coupling means. Read this article: http://gafferongames.com/game-physics/fix-your-timestep/ |
| ||
the article is nice. it describes the main problem you will have with a delta timing: in a case of a game "delay", a jumping of 10pix in one step is not the same as 10 times call a function, which jumps 1pix. the second solution is more save, when you think about collisions, etc.... |
| ||
I re-implementing my game loop based on the code here. My code now looks like this: SuperStrict Graphics 800, 600 SetClsColor(100, 149, 237) Const TICKS_PER_SECOND:Int = 25 Const SKIP_TICKS:Int = 1000 / TICKS_PER_SECOND Const MAX_FRAMESKIP:Int = 5; Global x:Float Global y:Float Global xpos:Int = 20 Global ypos:Int = 20 New tgame.run() Type TGame Field isRunning:Int = True Field framesPerSecond:Int Method Run() Final Local nextGameTick:Int = MilliSecs() Local loops:Int Local tween:Float Local frameStartTime:Float While isRunning loops = 0 frameStartTime = MilliSecs() While (MilliSecs() > nextGameTick And loops < MAX_FRAMESKIP) update() nextGameTick:+ SKIP_TICKS loops:+ 1 Wend tween = Float(MilliSecs() + SKIP_TICKS - nextGameTick) / Float(SKIP_TICKS) draw(tween) Print 1000 / (MilliSecs() - framestartTime) Wend End Method Method Draw(tween:Float) Cls xpos:+ x + (x * tween) ypos:+ y + (y * tween) DrawRect(xpos,ypos,20,20) Flip End Method Method Update() If KeyHit(KEY_ESCAPE) Then isRunning = False If KeyDown(KEY_UP) Then y = -1 If KeyDown(KEY_DOWN) Then y = 1 If KeyDown(KEY_LEFT) Then x = -1 If KeyDown(KEY_RIGHT) Then x = 1 If Not KeyDown(KEY_LEFT) And Not KeyDown(KEY_RIGHT) Then x = 0 If Not KeyDown(KEY_UP) And Not KeyDown(KEY_DOWN) Then y = 0 End Method End Type Last edited 2012 |
| ||
This is not constant on different computers! Test it: With a FLIP 0 you will see, that your moving speed rises. Is this really, what you want? To add values to the xpos inside the drawing is never a good solution. The only way to update the values is inside the update() function. With your model you add +1 to the xpos every time the draw() is called... additional to the tween-related x-value. with a higher vsync you will add it more often than on a slow computer. this could be a more constant way: Method Draw(tween:Float) Cls xpos:+ (x * tween) ypos:+ (y * tween) DrawRect(xpos,ypos,20,20) Flip 0 End Method Method Update() Xpos:+X Ypos:+Y X=0 Y=0 If KeyHit(KEY_ESCAPE) Then isRunning = False ..... but I keep on saying, that my way is a solution with a high quality: SuperStrict Graphics 800, 600 SetClsColor(100, 149, 237) Const SKIP_TICKS% = 7 Const DRAW_TICKS% = 15 Global x:Float Global y:Float Global xpos:Int = 20 Global ypos:Int = 20 Global xpos_B:Double = 20 Global xpos_C:Double = 20 New tgame.run() Type TGame Field isRunning:Int = True Field Timer:TTimer=CreateTimer(500) Method Run() Final Local nextGameTick:Int,nextDrawTick:Int While isRunning If MilliSecs() > nextGameTick +1000 nextGameTick= MilliSecs()-1 EndIf If MilliSecs() > nextGameTick update() nextGameTick:+ SKIP_TICKS Print "Update" + nextgametick ElseIf MilliSecs() > nextDrawTick nextDrawTick= MilliSecs()+DRAW_TICKS draw (1) Print "Draw"+ nextDrawtick EndIf Print "Timer" +MilliSecs() WaitTimer Timer Wend End Method Method Draw(tween:Float) Cls DrawRect(xpos,ypos,20,20) DrawRect(xpos_B,200,20,20) DrawRect(xpos_C,300,20,20) Flip 0 End Method Method Update() If KeyHit(KEY_ESCAPE) Then isRunning = False If KeyDown(KEY_UP) Then y = -1 If KeyDown(KEY_DOWN) Then y = 1 If KeyDown(KEY_LEFT) Then x = -1 If KeyDown(KEY_RIGHT) Then x = 1 If Not KeyDown(KEY_LEFT) And Not KeyDown(KEY_RIGHT) Then x = 0 If Not KeyDown(KEY_UP) And Not KeyDown(KEY_DOWN) Then y = 0 xpos:+ x ypos:+ y xpos_B:+ 1.1 xpos_C:+ 1.3 End Method End Type If You really want to add tweening to your code, you have to take care about, that you have two variables for each time related value. One, who will represent the "past" status, and one who will show the "future" status. With this the Draw() methode would be able to interpolate between this values. and alway when the "future" is reached, the future values have to become the "past" values for the next cylce. |
| ||
@Midimaster: thank you for explaining the flaws in my code, I really appreciate your feedback. Could you please explain with some more detail what your example is doing please to help me understand it better?Field Timer:TTimer=CreateTimer(500) I think this will 'tick' every 2 milliseconds? Why has this been chosen over 60 which you used in your original example? If MilliSecs() > nextGameTick +1000 nextGameTick = MilliSecs() - 1 EndIf Could you explain what this line of code is for (why Millisecs() - 1)? Is there a reason why updateGameTick is updated AFTER the update but drawGameTick is updated BEFORE the draw? Also, what happens if the update takes longer than usual (or draw, or both)? Does this protect against this scenario or will it just spiral out of control? Finally, do you have any tips for learning this sort of code? I noticed you have included print statements and this shows (on my machine anyway) that the game is updating 3 times and then rendering but it doesn't explain to me why. I have tried stepping through the code in the debugger to see what happens but due to the small time-scales involved stepping through the code doesn't give an accurate reflection of what would actually happen (e.g. if I step through the code you have supplied, draw never gets called). Sorry for asking a lot of questions but I'm really keen to understand this; it's tempting to just use some pre-existing code and get on with my game but I don't like using other peoples code unless I understand it first. Last edited 2012 |
| ||
for my music programs i often need a more accurate timer than 60 ticks a second. so why not using a tick of 200, which is 5msec or 500, which is 2msec? the effect is, that the system is calling your function every 5msec to check, if one of the "internal" timers is necessary for ticking. in 80% of this it imediately returns without doing anything. this is very performant. but if there is to do something it will be done. the update-timer steps forward in relation to his past time. the draw-timer always in relation to now. this causes, that the update-timer will catch up the lost updates. f.e.: the update timer normaly ticks with 10msec, but the computer stopps your program for 60msec. after this my code will prefer the updates and will run 6 times the update function, but only one timer the drawing. if now the game stopps for a much longer time, f.e. 3 seconds you do not really want to catch up all 300 lost updates, because this looks or sounds very strange. for this cases you should have a function, that forgets the 300 updates and continue with a new setting of the timer. Millisecs()-1 because I want to start immediately with a update()-call. The update is only called, when Millisecs() is greater than the timer. The PRINT statements are exactly for displaying, how the timing works. You cannot do that in step mode. Only the printing shows when which function was called. With this I can see, whether my timing is working as expected. by the way... even PRINT already disturbes the timing. so (later) without the statements it will run still better (accurate) there is nothing more to learn. The Update() is called exactly with a fixed rate on each computer. All movements (addings) should be placed here. you only have to find out how fast your "actors" should move in one tick. I suggest not to start with a timer of 500. It may be enough accurate, if you use 200. I only switched it in my example, that you can see, that there is no need to use a certain value. the game speed depends on the Skip_Ticks value f.e. you could use Skip_Ticks=15 in level 1 and Skip_Ticks=10 in level 2, which makes the game running 50% faster without changing any game code. move the mousex() to see the effect: SuperStrict Graphics 800, 600 SetClsColor(100, 149, 237) Global SKIP_TICKS% = 10 Const DRAW_TICKS% = 15 Global xpos_B:Double = 20 Global xpos_C:Double = 20 New tgame.run() Type TGame Field isRunning:Int = True Field Timer:TTimer=CreateTimer(500) Method Run() Final Local nextGameTick:Int,nextDrawTick:Int While isRunning If MouseX()>10 And MouseX()<500 Skip_Ticks=Sqr(MouseX()) EndIf If MilliSecs() > nextGameTick +1000 nextGameTick= MilliSecs()-1 EndIf If MilliSecs() > nextGameTick update() nextGameTick:+ SKIP_TICKS Print "Update" + nextgametick + " " ElseIf MilliSecs() > nextDrawTick nextDrawTick= MilliSecs()+DRAW_TICKS draw (1) Print "Draw"+ nextDrawtick EndIf 'Print "Timer" +MilliSecs() WaitTimer Timer Wend End Method Method Draw(tween:Float) Cls DrawRect(xpos_B,200,20,20) DrawRect(xpos_C,300,20,20) Flip 0 End Method Method Update() If KeyHit(KEY_ESCAPE) Then isRunning = False xpos_B:+ 1.1 xpos_C:+ 1.3 If xpos_B > 800 Then Xpos_B=0 If xpos_C > 800 Then Xpos_C=0 End Method End Type |
| ||
Don't use the timer mode (with a low resolution) if checking optimizations in graphical operations1 second ---------------- - UpdatesPerSecond = Maximum FPS timer resolution (elseif...) Blitzmax-Timers are not working as expected with higher resolution. So take this into consideration when working on engine parts of your app. First I thought that hickups in the update would make the loop skipping updates but the timer seems to get rid of that. The most important problem of the loop midimaster posted: what is the speed of the X-coord-changes within the update loop? It is pixels per "(Second minus FPS)/timerRes) -- see code snippet at the beginning of my post. That is why people are using deltaTime and tweenValue: deltaTime ... is the fraction of a second this update is able to "spend". All updates within one second could sum up their deltaTime to the result of ~1.00000x seconds. tweenValue ... if your objects keep the last coordinates they used as a backup you can use that value. tweenValue indicates how many percents of the next step are reached. So tweenValues of 0.8 indicate: newPosX = oldPosX + 0.8 * (newPosX - oldPosX) (or newPosX = 0.8*newPosX - 0.2*oldPosX) What is the benefit? Animations may look not smooth when jumping between positions with x2-x1 > 1.0 (jumping more than one pixel). Exceptions are fast moving objects with a dX/dY of 10,20...px/second. But like always: all methods have their benefits. (simplicity VS exact predictions, easy "slowMo"-functionality ...) bye Ron |