Proper game update loop in Cocoa?

Nibbie
Posts: 3
Joined: 2008.05
Post: #1
I've searched through the archives on this forum (and the internet at large) for a bit, and haven't found a satisfactory answer to this question:

How do you implement a half-decent loop in a Cocoa application, with a framerate limiter, that doesn't use an NSTimer with all its obvious problems?

My first thought was to create a thread that would repeatedly call my game's "update" method and use system time and thread api's to limit the framerate and not consume cpu resources when idle, and also, unlike a timer, reduce the idle time if the update took a while to try and keep the framerate at 60hz.

My first thought was to just start a new thread and do the old "while (!done)" thing, but I can't even get the NSThread to work right, let alone figure out how to do a passive wait. I have the following "ticker" class that handles starting and stopping the thread. The implementation (using a synchronized instance variable BOOL for "done") is simplistic, but it does actually work just fine:

Code:
@implementation SFLTicker

@synthesize done;

- (void) startUpdateLoopWithObject:(id)anObject {
    if (![anObject respondsToSelector:@selector(updateWithTimeInterval:)]) {
        NSLog(@"SFLTicker error: game object doesn't seem to have an update method");
        return;
    }
    
    [NSThread detachNewThreadSelector:@selector(doUpdateLoopWithObject:) toTarget:self withObject:anObject];
}

- (void) doUpdateLoopWithObject:(id)anObject {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    while ([self done] == NO) {
        NSLog(@"lol");
        
        // sleep thread to limit frame rate to 60fps somehow ???
    }
    
    [pool release];
    NSLog(@"properly exiting thread??? reached the end...");
}

- (void) stop {
    [self setDone:YES];
}

My Controller (app delegate) object calls stop when its applicationShouldTerminate method is called and the final message ("properly exiting thread???") is logged. However, the thread never exits. After hitting Apple+Q I still need to click the "stop" button in Xcode to kill the app.

What am I doing wrong with this thread? Or is there a better way to do this entirely?

By the way, just using SDL or something simple is right out since this project needs a Cocoa GUI and I don't want to block the main thread with a neverending while loop. (Or would that not happen?)
Quote this message in a reply
Member
Posts: 320
Joined: 2003.06
Post: #2
What is wrong with an NSTimer calling update on the main thread? That is what I usually use.

Chopper, iSight Screensavers, DuckDuckDuck: http://majicjungle.com
Quote this message in a reply
Nibbie
Posts: 3
Joined: 2008.05
Post: #3
NSTimer-driven animation always seems a bit impeded to me. I could be wrong, but before I had a MacBook Pro every OpenGLView I set up to animate via a timer would never run at anywhere near the framerate I had specified. Now with my new laptop I get okay FPS, but I'm planning on programming much more complicated OpenGL scenes than a few test geometric figures.

The NSTimer documentation is not clear about its behavior. Does it wait for the method to return before waiting the specified interval, or does it actually do what I want which is reduce the interval from update method return to next calling to maintain the framerate?
Quote this message in a reply
Moderator
Posts: 3,574
Joined: 2003.06
Post: #4
Don't try to dictate framerate, just set your NSTimer for your rendering "loop" to get called at 1kHz (0.001 seconds) and turn on vbl synch, which automatically blocks your main thread and doesn't waste cycles. It works flawlessly for rendering and automatically adjusts drawing to happen at a multiple of the display refresh so you get zero visual tearing. Apple has some documentation which says otherwise and suggests you should calculate your timer to fit the display refresh, which yields awful results in my experience. Just set your timer to 1k and turn on vbl and be done with it -- it's rock-solid.

For your update you can do a couple different things. You can calculate motion based on a variable delta, based on the frame rate calculated in your rendering thread from frame to frame (definitely not the best way to do it). Or you can do a more fancy version of that and get a fixed delta: see ThemsAllTook's tutorial on that (sorry I don't have that link handy, search the forums for it and/or he'll be along shortly I'm sure...). Or you can spin off another thread and another NSTimer at whatever delta you want and you'll get it rock solid. ThemsAllTook's method is the way I would (highly) recommend unless you have a pressing need for full multi-thread performance (and can handle the extra complications that come with it).
Quote this message in a reply
Member
Posts: 320
Joined: 2003.06
Post: #5
If you're taking longer than 1/60th of a second to render you'll drop frames whatever method you choose. Also there are probably some things you can do to screw up the use of a timer. To be clear I use the following timer setup code:

Code:
NSTimeInterval timeInterval = 1.0 / 60.0;

NSTimer* timer = [[NSTimer scheduledTimerWithTimeInterval:timeInterval
                            target:self
                            selector:@selector(update)
                            userInfo:nil
                            repeats:YES] retain];
[[NSRunLoop currentRunLoop] addTimer:timer
                            forMode:NSEventTrackingRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer
                            forMode:NSModalPanelRunLoopMode];

And then call [view setNeedsDisplay:YES]; in the update method, and override drawRect in the view to do the OpenGL stuff. It's not perfect for all cases, but it's simple.

To answer your question, NSTimer does do what you want. Set it up to fire every 60th of a second and as long as you do not exceed that time in your update method you will get called again 1/60th of a second after the first call was made.

Have a look at this thread too: http://www.idevgames.com/forum/showthread.php?t=14023

Chopper, iSight Screensavers, DuckDuckDuck: http://majicjungle.com
Quote this message in a reply
Member
Posts: 320
Joined: 2003.06
Post: #6
huh posting that made me realise I'm leaking that timer. I'd never noticed that before.

Chopper, iSight Screensavers, DuckDuckDuck: http://majicjungle.com
Quote this message in a reply
Moderator
Posts: 3,574
Joined: 2003.06
Post: #7
That technique works terrible for me Reubert! I will always get intermittent stuttering if I assume I'm doing 60 fps. Displays have different refresh rates and cannot reliably be accounted for. Some people have displays that can do up to 120 Hz, some do 75, some do 80, etc, etc... The only way to do it (in my experience) is over-rev the timer so it is always ready to draw when the rendering thread is unblocked by VBL synch. Assuming 60 all the time will even drop frames on an LCD since they are rarely exact.
Quote this message in a reply
Member
Posts: 320
Joined: 2003.06
Post: #8
I always use VBL syncing too, but have not seen any problems lately using a 60hz timer. You're right that higher frequencies are better though, Chopper runs on a 1000hz timer, and everything else I'm working on (with 60hz timers) has probably had unique circumstances that make greater than 60hz not useful anyway.

So yes, sorry to be misleading there, use VBL sync and a fast timer Smile

Chopper, iSight Screensavers, DuckDuckDuck: http://majicjungle.com
Quote this message in a reply
Moderator
Posts: 3,574
Joined: 2003.06
Post: #9
reubert Wrote:I always use VBL syncing too, but have not seen any problems lately using a 60hz timer.

Ah yes, "lately" being the operative term there... That reminds me that I noticed that timing and "stuttering" issues, which I've complained about incessantly, dropped dramatically with one of the recent OS releases within the last year to 18 months or so. I don't remember which one or exactly when I noticed it (somewhere near the end of 10.4?), but yeah, things definitely seem more reliable in the timing department lately. Smile
Quote this message in a reply
Moderator
Posts: 613
Joined: 2004.09
Post: #10
AnotherJake Wrote:Ah yes, "lately" being the operative term there... That reminds me that I noticed that timing and "stuttering" issues, which I've complained about incessantly, dropped dramatically with one of the recent OS releases within the last year to 18 months or so. I don't remember which one or exactly when I noticed it (somewhere near the end of 10.4?), but yeah, things definitely seem more reliable in the timing department lately. Smile

There was an OpenGL update to tiger around 10.4.6 or so. Now if you don't mind I'm off to fix my timer issues that were brought to light with this thread, see you never know who is getting help in the shadows.
Quote this message in a reply
Nibbie
Posts: 3
Joined: 2008.05
Post: #11
Okay, cool. vsync and 1000Hz timer it is. Makes my life a lot easier.

However, how come this: "[view setNeedsDisplay:YES]" never works for me. I call it, but no redraw ever happens, and I end up making all my projects work by calling drawRect manually.

Edit: Nevermind, now it does. Wtf.

Edit 2: Turns out my program not exiting was something the Xcode debugger was doing, not me. Ugh.
Quote this message in a reply
Member
Posts: 320
Joined: 2003.06
Post: #12
Quote:However, how come this: "[view setNeedsDisplay:YES]" never works for me. I call it, but no redraw ever happens, and I end up making all my projects work by calling drawRect manually.
My guess would be that that would be caused by having a tight loop on the main thread? You should find that it works fine in combination with NSTimer.

Chopper, iSight Screensavers, DuckDuckDuck: http://majicjungle.com
Quote this message in a reply
Luminary
Posts: 5,143
Joined: 2002.04
Post: #13
kodex Wrote:There was an OpenGL update to tiger around 10.4.6 or so.

OpenGL was updated in all of 10.4.1-10.4.3 (ppc only) and all of 10.4.4-10.4.11 (intel only) IIRC.
Quote this message in a reply
Moderator
Posts: 1,560
Joined: 2003.10
Post: #14
AnotherJake Wrote:Or you can do a more fancy version of that and get a fixed delta: see ThemsAllTook's tutorial on that (sorry I don't have that link handy, search the forums for it and/or he'll be along shortly I'm sure...).

Here you go: http://www.sacredsoftware.net/tutorials/...tion.xhtml
Quote this message in a reply
Post Reply 

Possibly Related Threads...
Thread: Author Replies: Views: Last Post
  iPhone game loop agreendev 2 4,264 Jul 27, 2010 11:32 AM
Last Post: AnotherJake
  Trouble with the canonical game loop TomorrowPlusX 27 10,873 Aug 12, 2009 04:22 AM
Last Post: TomorrowPlusX
  Yield in a Cocoa while loop Fenris 7 4,839 Dec 28, 2007 02:00 AM
Last Post: Fenris
  Controlling Game Loop Nick 2 3,241 Jul 21, 2007 07:12 PM
Last Post: Nick
  Proper Way to Exit an NSThread AnotherJake 3 6,333 Jun 6, 2006 11:14 PM
Last Post: AnotherJake