Fix Your Timestep!
BlitzMax Forums/BlitzMax Programming/Fix Your Timestep!
I have been trying to convert
this article to Bmax because I have never used Fixed Logic timing before.
Unfortunately, I'm finding some of the article incoherent so might be missing something. My code doesn't run very smoothly.
First, here's what I understand the example code to be doing with my own comments
float t = 0.0f; * Guess this is gametime but don't quite know what it's used for.
const float dt = 0.01f; * Timestep in seconds?
float currentTime = 0.0f; * OK.
float accumulator = 0.0f; * OK.
State previous; * Guess in Bmax you have to keep the old positions.
State current; * and this is the new position.
while (!quit)
{
float newTime = time(); *Guess time() is millisecs()
float deltaTime = newTime - currentTime; * OK with this..
*** Needed some clamp on deltatime here I assume.
currentTime = newTime; * OK.
accumulator += deltaTime; * OK
while (accumulator>=dt)
{
previousState = currentState; * old_xpos=new_xpos etc?
integrate(currentState, t, dt); * no idea
what this really means but assumed you pass
your update methods/functions the timestep value.
Not sure what you use 't' for.
t += dt; * Keep a count of 't' but not sure why.
accumulator -= dt; * OK.
}
const float alpha = accumulator / dt; * OK
State state = currentState*alpha + previousState*(1.0f-alpha); * OK.
render(state); *OK
}
Here is the code I came up with :
SuperStrict
Type ttest1
Global list:tlist=CreateList()
Field x:Float = 0.0
Field y:Float = 100.0
Field xvel:Float=1.0
Field previous_x:Float = 0.0
Method New()
ListAddLast list , Self
End Method
Function Draw(alpha:Float)
'standard draw without interpolation for comparison.
For Local all:ttest1 = EachIn list
DrawOval all.x , all.y , 8 , 8
Next
End Function
Function update()
For Local all:ttest1=EachIn list
all.previous_x=all.x
all.x=(all.x+all.xvel) Mod GraphicsWidth()
Next
End Function
End Type
Type ttest2
Global list:tlist=CreateList()
Field x:Float = 0.0
Field y:Float = 200.0
Field xvel:Float=1.0
Field previous_x:Float = 0.0
Method New()
ListAddLast list , Self
End Method
Function Draw(alpha:Float)
'interpolated draw
For Local all:ttest2 = EachIn list
Local xpos:Float = (all.x * alpha) + (all.previous_x * (1.0 - alpha))
DrawOval xpos , all.y , 8 , 8
Next
End Function
Function update(timestep:Float)
For Local all:ttest2=EachIn list
' saving previous x for interpolation.
all.previous_x=all.x
all.x=(all.x+all.xvel) Mod GraphicsWidth()
Next
End Function
End Type
Graphics 800 , 600
Local cycletime:Float = 0.0
Const timestep:Float = 10.0 ' run 10ms timesteps?
Local accumulator:Float = 0.0
Local t1:ttest1 = New ttest1
Local t2:ttest2 = New ttest2
Local Current_Time:Float = MilliSecs()
While Not KeyHit(KEY_ESCAPE)
Local newtime:Float = MilliSecs()
Local deltatime:Float = newtime - current_time
'clamp deltatime.
If deltatime > 300.0 deltatime = 300.0
current_time = newtime
accumulator:+ deltatime
While accumulator >= timestep
update_game(timestep)
cycletime:+ timestep
accumulator:- timestep
Wend
'I have called it cycletime but this is 't' in the article.
'Anybody know what it's for? I zero it to stop it incrementing
' cycletime=0.0
Local alpha:Float = accumulator / timestep
render(alpha)
Wend
Function render(alpha:Float)
Cls
ttest1.draw(alpha)
ttest2.draw(alpha)
Flip 0
End Function
Function update_game(timestep : Float)
ttest1.update()
ttest2.update(timestep)
End Function
Have I missed something?
I realise there are other timing methods available but this is purely to check how fixed rate logic behaves for myself.
copypaste and crippled a bit from my code (left out some specific things)
UpdateMain uses Game.intDeltaTime to calc movement and so on
'--- Variables for compensated timing
Const dt:Float=1.0/60.0
Global CurrTime:Int=0
Global Kompensator:Float=0.0
Global newTime:Int = 0
Global deltaTime:Float = 0.0
'-------------
Repeat
If CurrTime = 0 Then CurrTime = MilliSecs()
newTime = MilliSecs()
deltaTime = (newTime - CurrTime) / 1000.0
CurrTime = newTime
Kompensator:+ deltaTime
While(Kompensator >= dt)
Game.intDeltaTime = (MilliSecs() - Game.IntTimer) / 1000.0
Game.IntTimer = MilliSecs()
UpdateMain()
Kompensator :- dt
Wend
DrawMain()
Until AppTerminate() Or ExitGame = 1
So "previousState" is missing - no tweening
bye
MB
I'm not good at reading other people's code at the best of times, but your understanding of the article seems right to me, and I can't immediately spot anything wrong in your implementation. Have you tried using something more accurate than milliseconds to see if that makes the difference?
Thanks Michael/Gab,
I was going to try and use
this for a high resolution timer.
However, checking Michael's code (which still wasn't too smooth for me although I might have implemented it wrong) led me to
this For some reason this seems to work much better when (if I have read it right) it should be less 'blended'.
SuperStrict
Type ttest1
Global list:tlist=CreateList()
Field x:Float = 0.0
Field y:Float = 100.0
Field xvel:Float=8.0
Field pre_x:Float = 0.0
Method New()
ListAddLast list , Self
End Method
Function Draw(alpha:Float)
For Local all:ttest1 = EachIn list
' DrawOval all.x+(all.xvel*alpha),all.y,8,8
DrawOval all.x,all.y,8,8
Next
End Function
Function update()
For Local all:ttest1=EachIn list
all.pre_x=all.x
all.x=(all.x+all.xvel) Mod GraphicsWidth()
Next
End Function
End Type
Type ttest2
Global list:tlist=CreateList()
Field x:Float = 0.0
Field y:Float = 200.0
Field xvel:Float=8.0
Field pre_x:Float = 0.0
Method New()
ListAddLast list , Self
End Method
Function Draw(alpha:Float)
For Local all:ttest2 = EachIn list
DrawOval all.x+(all.xvel*alpha),all.y,8,8
Next
End Function
Function update()
For Local all:ttest2 = EachIn list
all.pre_x=all.x
all.x=(all.x+all.xvel) Mod GraphicsWidth()
Next
End Function
End Type
Graphics 800 , 600
Local t1:ttest1 = New ttest1
Local t2:ttest2 = New ttest2
Const TICKS_PER_SECOND:Int = 25
Const SKIP_TICKS:Int = 1000 / TICKS_PER_SECOND
Const MAX_FRAMESKIP:Int = 5
Local NEXT_GAME_TICK:Double = MilliSecs()
Local LOOPS:Int
Local INTERPOLATION:Float
While Not KeyHit(KEY_ESCAPE)
LOOPS = 0
While (MilliSecs()>NEXT_GAME_TICK And loops < MAX_FRAMESKIP)
update_game()
next_game_tick:+ skip_ticks
loops:+ 1
Wend
interpolation=Float(MilliSecs()+SKIP_TICKS-NEXT_GAME_TICK)/Float(skip_ticks)
render(INTERPOLATION)
Wend
Function render(alpha:Float)
Cls
ttest1.draw(alpha)
ttest2.draw(alpha)
Flip 0
End Function
Function update_game()
ttest1.update()
ttest2.update()
End Function
Also found Beakers interval system which looks funky with things done by events although I've yet to find how to get the remainder for an interpolation.
If anybody else uses, or has checked, fixed logic timesteps then I'd be interested how your code differs.
tonyg,
my version is based on the article in your first post. in the example below two of the three rects are moved by fixed logic timesteps. all move using the same amount.
http://www.winblitz3d.co.uk/FixYourTimestep.zipkev
Kev, does your code run smoothly?
Is this something you're using which works OK?
How does your code compare to the code I have posted?
TonyG: Hmm, yeah that first one isn't so hot but the last one works well on my system. I used that article to make my framework timer. It uses the fixed rate logic but without the interpolation. BUT what it does is run the fixed rate logic a certain number of times with Delta at 1, then the last loop is run with Delta at an appropriate fraction. Then my game logic used Delta for movements and timers etc and it works really well. Best of both worlds. I don't know if you've seen my framework in action but it's pretty smooth ;-)
To some degree I think all these methods use the 'remainder' whether they call it interpolation, blending or delta. I was just expecting more from the first method as it uses the extra information from the last step.
GA, I've seen the demos from your framework and it does look impressive. If I was interested in writing games I would probably consider getting it but as I simply mess about it's a bit of a waste to have somebody else do the 'messing' for me.
.
Anybody seen anything I've done wrong? Anybody explain what 't' is for in the Fix Your Timestep article?
Anybody got fully working source they're prepared to share so I can check what I'm missing?
I'd like to share my implementation of this but A) Since I'm still running my game engine in a window and only for the editors at the moment, I've no idea if it even works. and B) It's so tightly integrated within my game engine, it would be hard to rip out and even harder to read. When I get a little further along the line and I actually have the game doing something ( ANYTHING! ) then I'd be more than happy to share the implementation ( assuming it works. ) However, that's not likely to be for a while yet.
I haven't looked at Kev's so cannot comment on that.
The problem with Grey's implementation is that his timestep is not fixed, defeating the purpose of the article. In a simple game which doesn't use any physics, this won't matter, but if you do use any physics, it will matter, and it will explode your simulation just as the article describes. I haven't looked at Grey's implementation in any detail, but if you're not going to fix your timestep, I don't see the purpose in doing anything more complicated than delta time anyway. This may simply be shortsightedness on my part, however.
Since T is implemented in exactly the same way in each type of timing Glenn uses, it won't be having an impact on your results. As far as I can tell, and without digging through all my rather complicated code, I'm pretty sure I assumed that T was the total simulation time, as you have.
I'll try to find some time on Monday or Tuesday to have a proper look at your version and see if I can compare it to my own. A perfect game timing system is very necessary, so it would be good for all of us if we can all have a good understanding of it.
TonyG: Fair enough :-)
Gabriel: My code runs at a fixed rate of 200 FPS (obvisouly that's modifiable) and just uses Delta time for a bit of extra smoothing (the remainder). It's based on that article. Maybe you misunderstood the way it works, or I've misunderstood something ;-)
TonyG: Just looked at that link you posted, "The Game Loop", and I agree with why the first 3 are not great, but I also don't like the 4th one (it does have some merits of course). The 4th one uses render tweening and could result in objects being drawn within other objects before collisions are detected - this seems messy to me. Also if you are trying to make an object fall with gravity, the change in y velocity will only occur at 25FPS and the interpolation will NOT use gravity (unless you coded it that way I guess) in between - so I'm not sure it would look that good...but maybe it woudn't be that noticeable.
Probably my 200FPS fixed rate loop is overkill but it does look nice. I've also built in a threshold so that it won't go crazy on slow machines. It's probably not ideal either, I'm not sure there is an IDEAL game loop. Each has pros and cons and maybe it's down to developer preference e.g. do you want it to run on slow PCs, do you want it to look good on normal/fast PCs, how easy is the game to code etc.?
If a PC get's too slow, well maybe it's not suitable to run your game anyway...
tonyg,
does your code run smoothly?
Is this something you're using which works OK?
this is still to be tested. basicly its a wrapped version from the article, so it should function correctly.
How does your code compare to the code I have posted?
it does not, as before mine is a wrapped .cpp and your .bmx
if your intreasted in the code email me and i will send it your way.
*EDIT, this example now includes a simple example of firing a buttet that collides with 2 images. im intrested in the smoothness of this example.
http://www.winblitz3d.co.uk/FixYourTimestep.zipkev
Gabriel: My code runs at a fixed rate of 200 FPS (obvisouly that's modifiable) and just uses Delta time for a bit of extra smoothing (the remainder). It's based on that article. Maybe you misunderstood the way it works, or I've misunderstood something ;-)
Well the article is not about fixing your frames per second, it's about fixing your timestep. The purpose is so that your physics updates are always fed the same timestep, because doing anything else results in broken, unpredictable physics. In your description above, you said you do a final timestep which is different from the rest, thus you're not using a fixed timestep at all.
So I'm not sure which of us has misunderstood either, but I thought you were describing a system of timing involving a variable timestep, because you do a small timestep instead of a tween.
The 4th one uses render tweening and could result in objects being drawn within other objects before collisions are detected - this seems messy to me.
No it couldn't. You cannot tween forwards, only backwards. So you're tweening back through a physics update you already did, and you didn't hit a collision in your physics update, then you won't hit one by not doing x% of that update.
Also if you are trying to make an object fall with gravity, the change in y velocity will only occur at 25FPS and the interpolation will NOT use gravity (unless you coded it that way I guess) in between - so I'm not sure it would look that good...but maybe it woudn't be that noticeable.
This, however, is quite correct. Your tweening is linear, and will not correspond perfectly with dynamics of whatever kind. I've engaged in long debates on this subject in IRC, and we never did reach complete agreement, but I feel that it won't be noticeable as long as you keep your FPS up. I'm not sure what you mean about only occurring at 25FPS, it will occur as often as you update your physics, but if you were updating your physics at 25FPS, then it might indeed be noticeable that your linear tweening was making things less than smooth. I wouldn't recommend going below 60.
@Kev, that is a lot smoother.
@GA, I understood you were using the delta only in the render phase (e.g. as a type of tween) but it you're using it in the update phase then I can see why you Gabe asked why you don't just use Deltatime.
Anyway, I replaced millisecs() with
QueryPerformanceCounter() which *might* have made the first code look a bit better (my eyes are fried by looking at sodding ovals move across a screen).
SuperStrict
Type ttest1
Global list:tlist=CreateList()
Field x:Float = 0.0
Field y:Float = 100.0
Field xvel:Float=1.0
Field previous_x:Float = 0.0
Method New()
ListAddLast list , Self
End Method
Function Draw(alpha:Float)
'standard draw without interpolation for comparison.
For Local all:ttest1 = EachIn list
DrawOval all.x , all.y , 8 , 8
Next
End Function
Function update()
For Local all:ttest1=EachIn list
all.previous_x=all.x
all.x=(all.x+all.xvel) Mod GraphicsWidth()
Next
End Function
End Type
Type ttest2
Global list:tlist=CreateList()
Field x:Float = 0.0
Field y:Float = 200.0
Field xvel:Float = 0.5
Field previous_x:Float = 0.0
Method New()
ListAddLast list , Self
End Method
Function Draw(alpha:Float)
'interpolated draw
For Local all:ttest2 = EachIn list
Local xpos:Float = (all.x * alpha) + (all.previous_x * (1.0 - alpha) )
For Local loopy:Int = 0 To 200 Step 10
DrawOval xpos , all.y + loopy, 8 , 8
Next
Next
End Function
Function update(timestep:Float)
For Local all:ttest2=EachIn list
' saving previous x for interpolation.
all.previous_x=all.x
all.x=(all.x+all.xvel) Mod GraphicsWidth()
Next
End Function
End Type
Graphics 800 , 600
TCodeTimer.Initialise()
Local timer:TCodeTimer = TCodeTimer.Create()
Local cycletime:Float = 0.0
Const timestep:Float = 5.0 ' run 10ms timesteps?
Local accumulator:Float = 0.0
Local t1:ttest1 = New ttest1
Local t2:ttest2 = New ttest2
timer.StartWatch()
Local Current_Time:Double = timer.watchtime()
While Not KeyHit(KEY_ESCAPE)
' Local newtime:Float = MilliSecs()
Local newtime:Double = timer.watchtime()
Local deltatime:Float = newtime - current_time
'clamp deltatime.
If deltatime > 50.0 deltatime = 50.0
current_time = newtime
accumulator:+ deltatime
While accumulator >= timestep
update_game(timestep)
cycletime:+ timestep
accumulator:- timestep
Wend
'I have called it cycletime but this is 't' in the article.
'Anybody know what it's for? I zero it to stop it incrementing
' cycletime=0.0
Local alpha:Float = accumulator / timestep
render(alpha)
Wend
Function render(alpha:Float)
Cls
ttest1.draw(alpha)
ttest2.draw(alpha)
' Flip -1 works nicely
Flip 0
End Function
Function update_game(timestep : Float)
ttest1.update()
ttest2.update(timestep)
End Function
' classes
Extern "win32"
Function QueryPerformanceCounter(count:Long Var)
Function QueryPerformanceFrequency(freq:Long Var)
End Extern
Type TCodeTimer
Field _start:Long
Field _stop:Long
Field _iterations:Int
Field _totaltime:Double
Method ResetTimer()
_start = 0
_stop = 0
_iterations = 0
_totaltime = 0
End Method
Method StartTimer()
QueryPerformanceCounter(_start)
End Method
Method StopTimer()
QueryPerformanceCounter(_stop)
_totaltime :+ (Double(_stop - _start) / Double(_freq))
_iterations :+ 1
End Method
''' Stopwatch functions
Field _watchstart:Double
Method StartWatch()
self._watchstart = QueryCounter()
End Method
''' returns the number of milliseconds since calling StartWatch()
Method WatchTime:Double()
Return QueryCounter() - _watchstart
End Method
''' Globals
''' Initialize should be called before first use, to determine timer class overhead
Global _overhead:Double
Global _freq:Long
Function Create:TCodeTimer()
Return New TCodeTimer
End Function
Function Initialise()
QueryPerformanceFrequency(_freq)
_freq :/ 1000
End Function
Function QueryCounter:Double()
Local counter:Long
QueryPerformanceCounter(counter)
Return Double(counter) / _freq
End Function
End Type
Timing code *IS* bizarre. There must be lots of people happy with whatever method they used.
If there are pros/cons for different types of game then maybe there should be a 'suite' of timing solutions
e.g. set_timing_mode=FIXED_RATE
from simple 'waittimers' to those more difficult to implement.
<edit> Hold on. Other than (probably) a programming glitch causing some extra ovals to be drawn when x reaches graphicswidth() the code I just posted is smooth when I used Flip -1.
I was using Flip 0 to flip as quick as possible.
<edit2> AND it seems to be OK with normal millisecs() counter.
<edit3> Ignore the dodgy ovals drawn. It's simply when x loops t 0 so (x*alpha)+(prev_x*(1.0-alpha)) is not correct.
...until, of course, I try it on another PC and flip -1 causes terrible tearing. Back to the drawing board.
Gabriel: Ah yes I see now. My timestep is ixed for all but the last iteration which is based on delta as you have observed. The article actually says that the objects may be drawn in other objects but I see what you mean. As for 25FPS I was just referring to the timing that the article used.
TonyG: I'm not just using Delta because I wanted a high resolution game logic. Higher than the FPS, and one that remains constant so that collisions etc are always accurate. It really is the same method as fix your timestep except I'm not render tweening I'm using a variable Delta for the last game loop interation. I integrated sswifts sprite and physics code with my framework and that worked fine.
Just have to say a big thankyou to Tonyg - I've had my game on hold for ages thinking it would be impossible to plug in some kind of timing code...
First tried delta timing, but it broke the predictability of my platformer (jumping etc)... However your code worked from the outset with very little work to add it in... Thanks so much!!!!
One thing I noted was you use:
all.x=(all.x+all.xvel)
Wheras I used:
all.x=all.x+( ( all.x - all.pre_x ) * alpha )
Wondered if there was a reason for that (I realise it's functionally the same in your code... But for something more complex?)
Hey that's great news you finally got it sorted.
Got a question though ...
When I introduce random fluctuations in the draw time (simply using delay() ), the smoothness factor drops quite a bit. The logic still feels the same, but the interpolation feels like it's not quite where it should be. In other words, I'm running logic at 60Hz, so sometimes the redraw is occurring only 30 times / second when the delay is introduced, however it should potentially run pretty smooth even in 30fps... So I'm wondering...
Is there something broken with your implementation Tony? Or would it just need some kind of averaging-out or 'anti-jitter'? If so I'm not quite sure where in the code I would implement that.
Lol Grey, are you ever not in the forums? So do you have any idea what I'm talking about? You seem to be really good at this sort of thing...
I didn't do any more work on it but
this might help
nm
Hmm... This is what I'm using, based on your second example.
Const FPS:Int = 60
Const FRAMELENGTH:Int = 1000 / FPS
Const MAX_FRAMESKIP:Int = 5
Local nextframetime:Double = MilliSecs()
Local loops:Int
Local tween:Float
Repeat
' THE BIG BIT
loops = 0
Local ms:Double=MilliSecs()
While (ms>nextframetime And loops < MAX_FRAMESKIP)
update()
nextframetime:+ FRAMELENGTH
loops:+ 1
Wend
tween=Float(ms+FRAMELENGTH-nextframetime)/Float(FRAMELENGTH)
Cls
draw(tween)
flip (1)
Until 0
So I figure it happily flies when there's more draw() than update() called (the tween definitely works)...
But even when I force it to 30 fps draw() by using delay 17 (just over the length of one frame) it seems jerky. I know it's possible to be smooth in 30fps, so I'm wondering what could be causing it.
Hmm... The never-ending quest for smoothness continues.
BTW How do I post a codebox?
You use [] with the word CODE in it. You can also use QUOTE like I did here. Finish it off with another [] with /CODE or /QUOTE
[quote ]This is a quote[/quote ]
[code ]Some code.[/code ]
I added spaces between the text and the ]
otherwise it would be interpreted.
can you post some demo code of the jittering problem? Probably you just need some jitter correction code. Thing is if you start using timing code, it will never lopk as good as when you had none and locked to 60Hz BUT timing code is a neccessity for any pro game.
And yeah, I probably check the forum too often. I only check a couple of areas of interest tho so it's pretty quick.
Hehe Jake I was only teasing... I think it's great! You've helped me out plenty, and you probably learn a lot yourself.
Anyway, the code I posted above is taken exactly from my game. The thing I'm not sure about is how Tony came from this:
float newTime = time();
float deltaTime = newTime - currentTime;
currentTime = newTime; * OK.
accumulator += deltaTime; * OK
while (accumulator>=dt)
{
previousState = currentState;
integrate(currentState, t, dt);
t += dt;
accumulator -= dt; * OK.
}
to this:
loops = 0
Local ms:Double=MilliSecs()
While (ms>nextframetime And loops < MAX_FRAMESKIP)
update()
nextframetime:+ FRAMELENGTH
loops:+ 1
Wend
The original author seems to be doing something extra in the update() phase - some kind of integration? Or am I being stupid?
the original author was calling their update function Integrate, that's all. They were passing in more variables because it's a physics sim.
Does making FRAMELENGTH a float and doing 1000.0/FPS help?
@Sonic
The second post I made was with code taken from a different article from the first.
First code taken from
here Second code taken from
here
So with the interest in my game on the forum, I thought I'd revive this thread to see if anyone can help me with my last stumbling block.
As I mentioned before I'm happily using the frameskip + tweening code at frame rates higher than the 60hz my logic runs at. However, when I introduce a delay of 17ms to ensure that it runs at under 60hz (and the nearest would be 30hz) it seems to jerk nastily. This is my code:
Const FPS:Int = 60
Const FRAMELENGTH:Int = 1000 / FPS
Const MAX_FRAMESKIP:Int = 5
Local nextframetime:Double = MilliSecs()
Local loops:Int
Local tween:Float
Repeat
' THE BIG BIT
loops = 0
Local ms:Double=MilliSecs()
While (ms>nextframetime And loops < MAX_FRAMESKIP)
all.update()
nextframetime:+ FRAMELENGTH
loops:+ 1
Wend
tween=Float(ms+FRAMELENGTH-nextframetime)/Float(FRAMELENGTH)
Cls
all.draw(tween)
Until 0
Why do you want to limit it to 30FPS, it will look better at 60...?
No that's not it Grey - I'd like it to be runnable on machines that can only render at 30fps, hence introducing an artificial delay to test it.
Ah I see, OK got it. Hmm I'm not really an expert on this sort of tweening code. Hope you can sort it.
Sonic - Could you explain your main loop please? I've never seen a loop like that before? I can see the update being called as needed, but the tween value you pass to the draw routine confuses me, because the draw routine should just draw objects at their current position unless your draw routine is also moving objects based on the tween value passed? Thanks!
That's what tweening does, it's a drawing thing. It doesn't move them, it interpolates between current and past (or maybe future) positions based on the tween and draws it there. It complicates drawing routines though which is why I didn't bother with tweening.
I can see the update being called as needed, but the tween value you pass to the draw routine confuses me, because the draw routine should just draw objects at their current position unless your draw routine is also moving objects based on the tween value passed?
That's right. The tween value is between 0.0 and 1.0 and represents how much of the last update you *should* have done. You only ever update in full steps, so your objects get ahead of themelves. The tween value is used in the draw function to linear interpolate between the position, scale, orientation, color, etc before the last update and the value after the last update.
It complicates drawing routines though which is why I didn't bother with tweening.
a little but it's well worth it! I tweened all my Blitz3d stuff (using Marks code) and you could really tell the difference. My Invaders from Space looked 10 times better on a larger range of machines after I started tweening it. Most recommended. press the function keys in that demo to really see it in it's glory.
[edit] slowmo is a great tool, it allows you to see the animation defects that need to be fixed like with the little guy...
Yeah but I did logic tweening instead so same difference and I have a slow mo mode too, I use it ALL the time to check out the effects I'm programming.
oh I know... and you get slowmo with deltatime too. I just was mentioning it because I think the tweened slowmo looks smoother than any of the other methods.
Yeah actually the beauty is it doesn't complicate drawing at all. All that's needed is an additional 'draw_x= ( current_x * tween ) + ( new_x * (1.0 - tween) )' etc. All my graphics derive from a bass class anyway, so it only needed to change one routine, rather than add delta time to all logic.
Still... something is slightly broken!
I will have a look at that code, Tony, hopefully I can spot a difference.
I converted the whole Gaffer post about 4 months ago, including the RK4 (integrator function..etc) and it works silky smooth. If anyone is interested I can post it.
Here's part of it (non-RK4). Supply your own ball.png.
Press the Space Bar to see the Tween in action.
Part 1 (main.bmx):
'FRLRT
SuperStrict
Import "Vec3.bmx"
AppTitle$ = "Fixed Rate Logic with Render Tweening"
Graphics 800,600
Local newTime:Int = MilliSecs()
Local oldtime:Int = newTime
Local dt# = 1.0/100.00 '100 FPS logic
Local deltaTime#, accumulator#, alpha#, t#
Local ball:TBody = CreateBody()
ball.current.vPos.x = 400
ball.current.vPos.y = 500
ball.current.vVel.Set(500,-500,0)
SetBlend ALPHABLEND
Global imgBall:TImage = LoadImage("ball.png")
MidHandleImage imgBall
Global RenderTween:Int = False
While Not KeyHit(KEY_ESCAPE)
Cls
newTime = MilliSecs()
deltaTime# = (newTime - oldTime) * 0.001
oldTime = newTime
accumulator :+ deltaTime
While accumulator >= dt
'Apply some gravity
ApplyLinearForce ball, 0, ball.fMass * 981.0, 0
ball.Logic(dt)
accumulator :- dt
't :+ dt
Wend
alpha = accumulator / dt
ball.Render(alpha)
If KeyHit(KEY_SPACE) Then RenderTween = 1 - RenderTween
DrawText "Press the SPACE BAR to turn on render tweening mode.",5,5
DrawText "RenderTween: "+RenderTween,5,50
Flip 1 'Eliminates Tearing
Wend
End
Type TBody
Field fMass:Float = 1.0
Field current:TState = New TState
Field previous:TState = New TState
Method Logic(dt#)
'Basic Linear Physics
self.previous = self.current.copy()
self.current.vAcc = self.current.vForces.DividedByScalar(self.fMass)
self.current.vVel = self.current.vVel.Plus(self.current.vAcc.TimesScalar(dt))
self.current.vPos = self.current.vPos.Plus(self.current.vVel.TimesScalar(dt))
self.current.vForces.Zero()
'Simple Collision
If self.current.vPos.y <= 0 Or self.current.vPos.y >= 600
self.current.vVel.y = -self.current.vVel.y
EndIf
If self.current.vPos.x <= 0 Or self.current.vPos.x >= 800
self.current.vVel.x = -self.current.vVel.x
EndIf
End Method
Method Render(alpha#)
Local x#, y#
If RenderTween
x# = self.current.vPos.x * alpha + self.previous.vPos.x * (1.0 - alpha)
y# = self.current.vPos.y * alpha + self.previous.vPos.y * (1.0 - alpha)
Else
x = self.current.vPos.x
y = self.current.vPos.y
EndIf
DrawImage imgBall, x, y
End Method
End Type
Function CreateBody:TBody()
Return New TBody
End Function
Type TState
Field vPos:TVec3 = Vec3()
Field vVel:TVec3 = Vec3()
Field vAcc:TVec3 = Vec3()
Field vForces:TVec3 = Vec3()
Field vAngAcc:TVec3 = Vec3()
Field vAngVel:TVec3 = Vec3()
Field vMoments:TVec3 = Vec3()
Method Copy:TState()
Local t:TState = New TState
t.vPos = self.vPos.Copy()
t.vVel = self.vVel.Copy()
t.vAcc = self.vAcc.Copy()
Return t
End Method
End Type
Function ApplyLinearForce(body:TBody, xf#, yf#, zf#)
Local vF:TVec3 = Vec3(xf, yf, zf)
body.current.vForces = body.current.vForces.Plus( vF )
End Function
Part 2 (Vec3.bmx):
Rem
bbdoc:
EndRem
Type TVec3
Field x:Float, y:Float, z:Float
Method Copy:TVec3()
Return Vec3(self.x, self.y, self.z)
End Method
Method CrossProduct:TVec3(v:TVec3)
Local res:TVec3 = New TVec3
res.x = self.y * v.z - self.z * v.y
res.y = -self.x * v.z + self.z * v.x
res.z = self.x * v.y - self.y * v.x
Return res
End Method
Method DividedBy:TVec3(v:TVec3)
Local res:TVec3 = New TVec3
res.x = self.x / v.x
res.y = self.y / v.y
res.z = self.z / v.z
Return res
End Method
Method DotProduct:Float(v:TVec3)
Return self.x * v.x + self.y * v.y + self.z * v.z
End Method
Method DividedByScalar:TVec3(divisor#)
Local res:TVec3 = New TVec3
res.x = self.x / divisor
res.y = self.y / divisor
res.z = self.z / divisor
Return res
End Method
Method Inverse:TVec3()
Local v:TVec3 = New TVec3
v.x = -self.x
v.y = -self.y
v.z = -self.z
Return v
End Method
Method Magnitude:Float()
Return Sqr(self.x * self.x + self.y * self.y + self.z * self.z)
End Method
Method Minus:TVec3(v:TVec3)
Local res:TVec3 = New TVec3
res.x = self.x - v.x
res.y = self.y - v.y
res.z = self.z - v.z
Return res
End Method
Method Normalize()
Local m# = self.Magnitude()
self.x :/ m
self.y :/ m
self.z :/ m
End Method
Method Plus:TVec3(v:TVec3)
Local res:TVec3 = New TVec3
res.x = self.x + v.x
res.y = self.y + v.y
res.z = self.z + v.z
Return res
End Method
Method Set(x#, y#, z#)
self.x = x
self.y = y
self.z = z
End Method
Method Show(cap:String, xpos:Int, ypos:Int, vspc:Int = 12)
DrawText cap+".x=" + self.x,xpos,ypos
DrawText cap+".y=" + self.y,xpos,ypos + vspc * 1
DrawText cap+".z=" + self.z,xpos,ypos + vspc * 2
End Method
Method Times:TVec3(v:TVec3)
Local res:TVec3 = New TVec3
res.x = self.x * v.x
res.y = self.y * v.y
res.z = self.z * v.z
Return res
End Method
Method TimesScalar:TVec3(scalar#)
Local res:TVec3 = New TVec3
res.x = self.x * scalar
res.y = self.y * scalar
res.z = self.z * scalar
Return res
End Method
Method Zero()
self.x = 0
self.y = 0
self.z = 0
End Method
End Type
Rem
bbdoc:
EndRem
Function Vec3:TVec3(x#=0, y#=0, z#=0)
Local v:TVec3 = New TVec3
v.x = x
v.y = y
v.z = z
Return v
End Function
Hmmm, well I've double-checked your code Tony and it seems to behave identically to mine... probably because mine was taken from yours in the first place!
So I still have the problem of jerkiness when the graphics take more than one logic frame to redraw. I need to solve this to continue, but I'm at a loss.
Perhaps it's a problem with my introducing a Delay (17) artificially, but I don't have a slow machine to test on, and I can't think of any other way to force the redraw to run at half the logic rate.
[edit] Oh and Chroma - I can't really follow your code, even though it's well written! The main stuff seems to be the same, but could you explain the integration stuff to me?
There are posts which discuss the jerkiness and GA created a test to even it out.
Seems like the code tony posted only runs smooth on my machine if it renders at refresh (vsync) speed. If you add delays so it skips vsyncs, you get the normal jerkiness.
If your game misses a vsync or 2, once in a while, you will get jerkiness, that's 100% unavoidable. The trick is to "do something" to stablize the frame rate or rendering so it's not so obvious when it happens. Averaging the delta over a period of frames can help, but it doesn't 100% get rid of it. What I do is give an option to the end user to run the game at a slower render speed. Sometimes it's better to run at a stable slower frame rate compared to the render loop speed being chaotic.
Yep, that was the point of the post as I didn't understand why it wasn't smooth. Since then hrdnutz posted
this with a
code archive and GA posted
this .
@JGOware, did you expand your code from
this ?
option to the end user to run the game at a slower render speed.
How did you do that?
but I don't have a slow machine to test on, and I can't think of any other way to force the redraw to run at half the logic rate.
write a program that will take a lot of cpu time, allow control of that with a press of a button. now run that in the background when testing your app.
@Tonyg - Yes. I actually use two different timing options in my game engine. Dellta timing with averging or the more simplistic option you linked to. I choose which timing method I need based on the game type. For games that you pretty much need to run at full vsync all the time, I prefer the more simplistic timing method and if the end user can't run it full vsync, then they can turn some graphics options on/off render in lower resolutions, etc, which should allow it to run at 1 vsync.
@GA - Nothing techy, just using millisecs() to loop a bit at the end of the render loop until the requested frame rate has passed. The user can attempt to run the game at 20,30,50,60 if desired. Logic still happens at regular speed so you don't get slow down. But ofcourse objects will move greater distances at the slower frame rates.
Hmm, I'm stumped, running a heavy program in the background produces the same effect as introducing a delay (ie stuttering). Unless I can figure out how to smooth out logic at 60hz and a frame rate below at, I can't realistically continue.
I think you need to post the smallest demo which shows the problem.
As long as your internal logic is accurate, 99.99% of your end users won't care if the game jitters once in a while. Unless ofcourse your delay introduces some weird error in your logic handling? Otherwise, welcome to the world of PC coding, where the fix for the jitters is an endless quest. At some point you just have to think "that's good enough..." and move on.
JGOware: OK thx.
Yeah post a demo, my PC never stutters on anything so we'll be able to find out if it's your PC or your code.
However it could just look bad simply BECAUSE it's at 30 fps. If the stuttering is regular and looks crap it's just due to the update being less that the refresh rate (like Amiga games running at 25Hz on a 50Hz TV (in UK)). If it's irregular then it's another problem...
I just wanted to explain what the alpha(tween) variable is.
alpha = accumulator / dt
Alpha is the amount of interpolation between the previous frame and the current frame. It is a value between 0 and 1. 0 means that the value is fully set to the previous state. 1 means that the value is fully set to the current state. So a .5 would set the value halfway between the value of the previous state and the current state.
previous = 10
current = 20
alpha of 0 means that the value of 10 will be used.
alpha of 1 means that the value of 20 will be used.
alpha of 0.5 means that the value of 15 would be used.
A lot may already know this but for those that don't, it helps to be able to envision exactly what it's doing.