When to create custom OpenGL view instead of subclass NSOpenGL view

Member
Posts: 35
Joined: 2009.10
Post: #1
When exactly would it be appropriate to create my own subclass of NSView instead of subclassing NSOpenGLView?

I intend to use a Core Video display link to drive my game loop and also have toggle-able fullscreen. I get the impression that I wouldn't be doing my drawing routines inside of drawRect if I use a display link, and fullscreen support seems to suggest having to handle OpenGL contexts myself (not that I'm any expert at that, mind you) so I kind of thought a custom OpenGL view would be appropriate.

I'm actually asking because the Apple documentation on writing custom OpenGL views mentions sample code available on ACD that I can't seem to find it. Blush
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #2
Feel free to subclass all the way from NSView at any time. There is no particular benefit to subclassing from NSOpenGLView except that maybe you might use a little less code. Some of the guys around here prefer to *always* subclass from NSView.

[edit] arekkusu once mentioned that there may be threading issues you may have to contend with when subclassing from NSView. I don't know if there are still issues, but using a display link will necessarily mean you're threaded, so you might want to keep that in mind.
Quote this message in a reply
Member
Posts: 35
Joined: 2009.10
Post: #3
O.K. Thanks.

I'm still wondering about that sample code though. I actually started working on an NSView subclass but it still isn't drawing anything or even displaying the clear-colour. And I'm not even using the display link yet (well, I was, but the first time I ran that code caused my computer to spectacularly crash).
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #4
Coyote Wrote:And I'm not even using the display link yet (well, I was, but the first time I ran that code caused my computer to spectacularly crash).

Uhh... Well, I was just putting together a small working demo using a display link in an NSOpenGLView (subclassed in code, not from IB) and I just kernel panicked too. I don't think I was doing anything wrong, but it crashed instantly on launch one time. Ran fine a whole bunch of other times. I'm on the mighty, bug-free Snow Leopard. I've never had a kernel panic using this code for the display link on Leopard that I recall. I'm not sure what to make of it, but clearly it isn't a useful demo like this, sorry Sad

I wonder if this is actually an OS bug or if I'm doing something wrong myself? Kernel panics of any kind usually are bugs. I don't really have time to investigate this right now, but it's now on my list of things to do when/if I get some free time.

[adding] This kinda sucks too, because I was actually seeing some slightly smoother performance than I recalled seeing on Leopard. Just when display link was looking great I get a kernel panic... Go figure.
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #5
Well, I still don't think it should've kernel panicked, but I did notice that I forgot to lock the GL context from one entry point in the code, so that darn-well may have precipitated the event. If I gain some confidence back in it, I might post the demo later for you to look at.
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #6
Wow, I'm getting some absolutely *fantastic* results from display link for the first time ever! I got black and white Pong running smooth as glass on the Mac. Yeah, just Pong. I haven't tried anything more complicated yet. In the past, even if I tried to synch my delta time to the actual display rate I'd get a dropped frame or at least some stuttering. After fifteen minutes of staring at it in amazement, I've only seen like one tiny little hiccup. Unbelievable. Definitely as close to video irq I've ever seen on OS X... of course, I'm just looking at Pong, but hey Rasp

I'll post a tiny demo with code tomorrow after I clean out the mess and verify that I haven't been hallucinating. Sneaky
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #7
Yeah, the kernel panic must've been rooted in the fact that I forgot to lock the gl context at one point in the code. I haven't had any troubles since.

Here's an absolute bare-bones example of getting a display link working with OpenGL. Here's how to put it together:

- create a new Cocoa Application
- ceate glView.h/glView.m
- copy and paste in this code and save files
- add frameworks: QuartzCore and OpenGL
- open up MainMenu.xib
- drag Custom View into main window and resize to fill window
- set the autosizing of view to have the two inner arrows on so it resizes automatically with the window
- set the class of the Custom View to be glView
- save nib
- go back to Xcode and run project

Should show a yellow window. Here's the code:

glView.h

Code:
#import <Cocoa/Cocoa.h>

// for display link
#import <QuartzCore/QuartzCore.h>

@interface glView : NSOpenGLView
{
    CVDisplayLinkRef displayLink;
    
    double    deltaTime;
    float    viewWidth;
    float    viewHeight;
}

@end

glView.m

Code:
#import "glView.h"

@interface glView (InternalMethods)

- (CVReturn)getFrameForTime:(const CVTimeStamp *)outputTime;
- (void)drawFrame;

@end

@implementation glView

#pragma mark -
#pragma mark Display Link

static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now,
                            const CVTimeStamp *outputTime, CVOptionFlags flagsIn,
                            CVOptionFlags *flagsOut, void *displayLinkContext)
{
    // go back to Obj-C for easy access to instance variables
    CVReturn result = [(glView *)displayLinkContext getFrameForTime:outputTime];
    return result;
}

- (CVReturn)getFrameForTime:(const CVTimeStamp *)outputTime
{
    // deltaTime is unused in this bare bones demo, but here's how to calculate it using display link info
    deltaTime = 1.0 / (outputTime->rateScalar * (double)outputTime->videoTimeScale / (double)outputTime->videoRefreshPeriod);

    [self drawFrame];

    return kCVReturnSuccess;
}

- (void)dealloc
{
    CVDisplayLinkRelease(displayLink);

    [super dealloc];
}

- (id)initWithFrame:(NSRect)frameRect
{
    // context setup
    NSOpenGLPixelFormat        *windowedPixelFormat;
    NSOpenGLPixelFormatAttribute    attribs[] = {
                        NSOpenGLPFAWindow,
                        NSOpenGLPFAColorSize, 32,
                        NSOpenGLPFAAccelerated,
                        NSOpenGLPFADoubleBuffer,
                        NSOpenGLPFASingleRenderer,
                        0 };

    windowedPixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs];
    if (windowedPixelFormat == nil)
    {
        NSLog(@"Unable to create windowed pixel format.");
        exit(0);
    }
    self = [super initWithFrame:frameRect pixelFormat:windowedPixelFormat];
    if (self == nil)
    {
        NSLog(@"Unable to create a windowed OpenGL context.");
        exit(0);
    }
    [windowedPixelFormat release];

    // set synch to VBL to eliminate tearing
    GLint    vblSynch = 1;
    [[self openGLContext] setValues:&vblSynch forParameter:NSOpenGLCPSwapInterval];

    // set up the display link
    CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
    CVDisplayLinkSetOutputCallback(displayLink, MyDisplayLinkCallback, self);
    CGLContextObj cglContext = [[self openGLContext] CGLContextObj];
    CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
    CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(displayLink, cglContext, cglPixelFormat);

    return self;
}

- (void)awakeFromNib
{
    NSSize    viewBounds = [self bounds].size;
    viewWidth = viewBounds.width;
    viewHeight = viewBounds.height;

    // activate the display link
    CVDisplayLinkStart(displayLink);
}

- (void)reshape
{
    NSSize    viewBounds = [self bounds].size;
    viewWidth = viewBounds.width;
    viewHeight = viewBounds.height;

    NSOpenGLContext    *currentContext = [self openGLContext];
    [currentContext makeCurrentContext];

    // remember to lock the context before we touch it since display link is threaded
    CGLLockContext([currentContext CGLContextObj]);

    // let the context know we've changed size
    [[self openGLContext] update];

    CGLUnlockContext([currentContext CGLContextObj]);
}

- (void)drawRect:(NSRect)rect
{
    [self drawFrame];
}

- (void)drawFrame
{
    NSOpenGLContext    *currentContext = [self openGLContext];
    [currentContext makeCurrentContext];

    // must lock GL context because display link is threaded
    CGLLockContext([currentContext CGLContextObj]);

    glViewport(0, 0, viewWidth, viewHeight);

    glClearColor(0.9f, 0.9f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // draw here

    [currentContext flushBuffer];

    CGLUnlockContext([currentContext CGLContextObj]);
}

@end
Quote this message in a reply
Member
Posts: 35
Joined: 2009.10
Post: #8
Nice work, but I noticed you subclassed NSOpenGLView instead of NSView.

Also, could I use deltaTime to regulate my framerate, or would I need something different?
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #9
Coyote Wrote:Nice work, but I noticed you subclassed NSOpenGLView instead of NSView.
Honestly, I've never attempted to subclass directly from NSView since NSOpenGLView has worked fine for everything for me over the years.

Coyote Wrote:Also, could I use deltaTime to regulate my framerate, or would I need something different?

Yeah you can do that, but I don't think it's possible to change the display link callback rate, if that's what you mean. deltaTime is so important for timing in general and I discovered how to calculate it with display link info, which is why I included it, but don't actually use it in the example. For some unknown reason, it seems a more stable calculation from display link than calculating between frames with mach time. What I do is *draw* the frame every time, regardless, and *update* at a separate rate so that the update is at a solid rate and compatible with physics engines. As you may know, the constant update rate is needed for stable physics integration. Frame rate itself may vary as the system gets overtaxed from heavy drawing, so it's important to keep them separated.
Quote this message in a reply
Member
Posts: 35
Joined: 2009.10
Post: #10
AnotherJake Wrote:Honestly, I've never attempted to subclass directly from NSView since NSOpenGLView has worked fine for everything for me over the years.

I guess it's just as well then. It seems all of the sample code on the Mac Dev Centre just subclass NSOpenGLView as well.

Quote:Yeah you can do that, but I don't think it's possible to change the display link callback rate, if that's what you mean. deltaTime is so important for timing in general and I discovered how to calculate it with display link info, which is why I included it, but don't actually use it in the example. For some unknown reason, it seems a more stable calculation from display link than calculating between frames with mach time. What I do is *draw* the frame every time, regardless, and *update* at a separate rate so that the update is at a solid rate and compatible with physics engines. As you may know, the constant update rate is needed for stable physics integration. Frame rate itself may vary as the system gets overtaxed from heavy drawing, so it's important to keep them separated.

Well, I actually meant something along the lines of this guide on fixed interval time-based animation. This is the approach I generally use, and was wondering if deltaTime is appropriate for that.
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #11
Coyote Wrote:Well, I actually meant something along the lines of this guide on fixed interval time-based animation. This is the approach I generally use, and was wondering if deltaTime is appropriate for that.
Yes, that is exactly what I use. In fact, that's where I learned the technique. Yes that deltaTime is perfect for that.

I should clarify. I would use it in that technique as the frame delta time, not the fixed-rate delta. Here's a snippet of how I'd use it:
Code:
    frameDeltaTime = 1.0 / (outputTime->rateScalar * (double)outputTime->videoTimeScale / (double)outputTime->videoRefreshPeriod);
    prevFrameTime = currTime;
    tickInterval = deltaTime;
    maxUpdatesPerFrame = UPDATE_RATE / MINIMUM_FRAME_RATE;
    maxTimeAllowed = maxUpdatesPerFrame * tickInterval;
    timeAllowed = frameDeltaTime + timeLeftOverForUpdate;
    if (timeAllowed > maxTimeAllowed)
        timeAllowed = maxTimeAllowed;
    while (timeAllowed > tickInterval)
    {
        timeAllowed -= tickInterval;
        [self update];
    }
    timeLeftOverForUpdate = timeAllowed;
Quote this message in a reply
Member
Posts: 35
Joined: 2009.10
Post: #12
AnotherJake Wrote:I should clarify. I would use it in that technique as the frame delta time, not the fixed-rate delta. Here's a snippet of how I'd use it:
Code:
    frameDeltaTime = 1.0 / (outputTime->rateScalar * (double)outputTime->videoTimeScale / (double)outputTime->videoRefreshPeriod);
    prevFrameTime = currTime;
    tickInterval = deltaTime;
    maxUpdatesPerFrame = UPDATE_RATE / MINIMUM_FRAME_RATE;
    maxTimeAllowed = maxUpdatesPerFrame * tickInterval;
    timeAllowed = frameDeltaTime + timeLeftOverForUpdate;
    if (timeAllowed > maxTimeAllowed)
        timeAllowed = maxTimeAllowed;
    while (timeAllowed > tickInterval)
    {
        timeAllowed -= tickInterval;
        [self update];
    }
    timeLeftOverForUpdate = timeAllowed;

Would you mind clarifying that code a bit? I'm not sure what each of the variables are. For one thing, you put the time delta into frameDeltaTime instead of deltaTime, but you still have deltaTime in your code.

Though I suppose I could just look over the time-based animation guide. The only difference between them and myself is that I calculated the time delta directly instead of getting it from the current time and the last recorded time.
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #13
Coyote Wrote:Would you mind clarifying that code a bit?

Sure. Gimme a little bit and I'll post a tiny Pong demo using it, along with a thread friendly way of grabbing input.
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #14
Alright, well it's not as tiny as I hoped (maybe should've linked a project instead...), but it should illustrate everything and then some in as small amount of code as I could come up with. It builds off the previous example. You'll find the timing section in the getFrameForTime: method.

Code:
#if USE_FIXED_RATE_TIMING
    
    double        timeAllowed, frameDeltaTime, currTime;

    currTime = [self currentTime];
    if (prevFrameTime == 0.0) // <-- should only happen on first frame
        prevFrameTime = currTime;
    frameDeltaTime = 1.0 / (outputTime->rateScalar * (double)outputTime->videoTimeScale / (double)outputTime->videoRefreshPeriod);
    //frameDeltaTime = currTime - prevFrameTime; // <-- other way to do it without using display link info
    prevFrameTime = currTime;
    timeAllowed = frameDeltaTime + timeLeftOverForUpdate;
    if (timeAllowed > MAX_TIME_ALLOWED)
        timeAllowed = MAX_TIME_ALLOWED;
    while (timeAllowed > TICK_INTERVAL)
    {
        timeAllowed -= TICK_INTERVAL;
    
        [self getInput];
        [self updatePong];
    }
    timeLeftOverForUpdate = timeAllowed;

dt is what I use for deltaTime in the update and is set in the initWithFrame method, and is "fixed", although it could actually be changed for effects like slo-mo. frameDeltaTime is the time between frames, not updates. It's pretty hard to fully describe exactly what's happening, but I can add detailed comments for this timing section if you'd like me to try.

As noted in the code, I'm using 110 Hz here, but you can change it to whatever. If you change it to near 60 and you're using an LCD it'll appear to smooth out, but beware that other displays like CRTs could be many other refresh rates and it won't look as smooth on them.

Speaking of "smoothness", you may notice that the fixed rate timing isn't quite as glass smooth as just using the delta time from display link alone. To get it back to ultra smooth, you'd have to do sub-update interpolations (that is, interpolating data between two updates to precisely match where it would be in actual time) -- I think Bullet physics has support for this BTW. That is way too much to describe right here, so I hope you're already aware of how to do that. Normally though, while the tiny un-smoothness sticks out in pong because the contrast is so high and the objects are simple, in a regular game with action you probably won't even notice it. I have had user reviews claiming the animations were silky smooth, even though I knew I could make them even better because I wasn't doing sub-update interpolation.

To really see the difference in what I'm talking about with smoothness, change the #define for USE_FIXED_RATE_TIMING to 0 to get back to the same, but non-fixed, delta time in the previous example I gave a few posts up.

You might also notice that I'm using a thread-friendly way of grabbing input, using queues.

Similar to before (just different class name), to try it out yourself:

- create a new Cocoa Application
- ceate gameView.h/gameView.m
- copy and paste in this code and save files
- add frameworks: QuartzCore and OpenGL
- open up MainMenu.xib
- drag Custom View into main window and resize to fill window
- set the autosizing of view to have the two inner arrows on so it resizes automatically with the window
- set the class of the Custom View to be gameView
- save nib
- go back to Xcode and run project

gameView.h
Code:
#import <Cocoa/Cocoa.h>

// for display link
#import <QuartzCore/QuartzCore.h>

typedef enum
{
    ENTER    = 3,
    TAB        = 9,
    RETURN    = 13,
    ESC        = 27,
    SPACE    = 32,
    DEL        = 127,
    UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW,
    NUM_KEY_CODES
} KeyCode;

@interface gameView : NSOpenGLView
{
    CVDisplayLinkRef displayLink;

    double    dt;
    double    prevFrameTime;
    double    timeLeftOverForUpdate;
    float    viewWidth;
    float    viewHeight;
    float    viewAspect;
    BOOL    key[NUM_KEY_CODES];
    BOOL    keyDown[NUM_KEY_CODES];
    BOOL    keyUp[NUM_KEY_CODES];
    BOOL    keyDownQueue[NUM_KEY_CODES];
    BOOL    keyUpQueue[NUM_KEY_CODES];
    
    NSLock    *inputLock;

    float    paddlePos;
    float    ballPosX;
    float    ballPosY;
    int        ballYDir;
    int        ballXDir;
    BOOL    ballMoving;
    BOOL    ballMissedPaddle;
}

@end

gameView.m
Code:
#import "gameView.h"
#import <mach/mach_time.h>

// cheat 1 always bounces ball off bottom as if it hit paddle, 0 is no cheating
#define CHEAT                    0
#define USE_FIXED_RATE_TIMING    1

// slo-mo only works with fixed rate, although it can be modified to work otherwise as well
#define USE_SLO_MO                0

// arbitrary -- you can pick whatever rate you want. I like 110 because it seems to be the
// smoothest compromise across different display refresh rates, but that is completely subjective
#define UPDATE_RATE            110.0

// we stop updating below this frame rate, which effectively pauses the game until drawing catches up,
// so not a good idea to go too high with this constant
#define MINIMUM_FRAME_RATE    4.0

#define MAX_UPDATES_PER_FRAME    UPDATE_RATE / MINIMUM_FRAME_RATE
#define TICK_INTERVAL            1.0 / UPDATE_RATE
#define MAX_TIME_ALLOWED        MAX_UPDATES_PER_FRAME * TICK_INTERVAL


#define BOUNDS_LEFT            -1.0f
#define BOUNDS_RIGHT        1.0f
#define BOUNDS_BOTTOM        -1.0f
#define BOUNDS_TOP            1.0f

#define PADDLE_HALF_WIDTH    0.2f
#define PADDLE_HALF_HEIGHT    0.02f
#define PADDLE_SPEED        2.5f
#define PADDLE_VERT_POS        0.85f
#define BALL_RADIUS            0.025f
#define BALL_SPEED            1.0f

@interface gameView (InternalMethods)

- (CVReturn)getFrameForTime:(const CVTimeStamp *)outputTime;
- (void)getInput;
- (void)processKeys:(NSEvent *)theEvent;
- (double)currentTime;

// pong methods
- (void)initPong;
- (void)updatePong;
- (void)moveBall;
- (void)drawPong;

@end

@implementation gameView

#pragma mark -
#pragma mark Display Link

static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now,
                            const CVTimeStamp *outputTime, CVOptionFlags flagsIn,
                            CVOptionFlags *flagsOut, void *displayLinkContext)
{
    // go back to Obj-C for easy access to instance variables
    CVReturn result = [(gameView *)displayLinkContext getFrameForTime:outputTime];
    return result;
}

- (CVReturn)getFrameForTime:(const CVTimeStamp *)outputTime
{

#if USE_FIXED_RATE_TIMING
    
    double        timeAllowed, frameDeltaTime, currTime;

    currTime = [self currentTime];
    if (prevFrameTime == 0.0)
        prevFrameTime = currTime;
    frameDeltaTime = 1.0 / (outputTime->rateScalar * (double)outputTime->videoTimeScale / (double)outputTime->videoRefreshPeriod);
    //frameDeltaTime = currTime - prevFrameTime; // <-- other way to do it without using display link info
    prevFrameTime = currTime;
    timeAllowed = frameDeltaTime + timeLeftOverForUpdate;
    if (timeAllowed > MAX_TIME_ALLOWED)
        timeAllowed = MAX_TIME_ALLOWED;
    while (timeAllowed > TICK_INTERVAL)
    {
        timeAllowed -= TICK_INTERVAL;
    
        [self getInput];
        [self updatePong];
    }
    timeLeftOverForUpdate = timeAllowed;
    
#else
    
    dt = 1.0 / (outputTime->rateScalar * (double)outputTime->videoTimeScale / (double)outputTime->videoRefreshPeriod);
    [self getInput];
    [self updatePong];
    
#endif
    
    [self drawPong];

    return kCVReturnSuccess;
}

// we specifically prefer to use mach time because it is monotonic (i.e. it will not
// accidentally shift with the user changing their system clock or from an auto time
// synch of some sort)

- (double)currentTime
{
    static uint64_t        timebase = 0;
    uint64_t            time, nanos;
    double                seconds;

    // calculate the time base for this platform only on the first time through
    if (timebase == 0)
    {
        mach_timebase_info_data_t    timebaseInfo;
        mach_timebase_info(&timebaseInfo);
        timebase = timebaseInfo.numer / timebaseInfo.denom;
    }

    time = mach_absolute_time();
    nanos = time * timebase;
    seconds = (double)nanos * 1.0e-9;

    return seconds;
}

- (void)getInput
{
    int            i;

    // clear current input values
    for (i = 0; i < NUM_KEY_CODES; i++)
    {
        keyDown[i] = NO;
        keyUp[i] = NO;
    }

    // transfer input values since last update
    [inputLock lock];
    for (i = 0; i < NUM_KEY_CODES; i++)
    {
        keyDown[i] = keyDownQueue[i];
        if (keyDown[i])
        {
            key[i] = YES;
        }
        keyDownQueue[i] = NO;
        keyUp[i] = keyUpQueue[i];
        if (keyUp[i])
        {
            key[i] = NO;
        }
        keyUpQueue[i] = NO;
    }
    [inputLock unlock];
}

#pragma mark -
#pragma mark NSView Support

- (void)dealloc
{
    CVDisplayLinkRelease(displayLink);
    [inputLock release];

    [super dealloc];
}

- (id)initWithFrame:(NSRect)frameRect
{
    // context setup
    NSOpenGLPixelFormat                *windowedPixelFormat;
    NSOpenGLPixelFormatAttribute    attribs[] = {
                        NSOpenGLPFAWindow,
                        NSOpenGLPFAColorSize, 32,
                        NSOpenGLPFAAccelerated,
                        NSOpenGLPFADoubleBuffer,
                        NSOpenGLPFASingleRenderer,
                        0 };

    windowedPixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs];
    if (windowedPixelFormat == nil)
    {
        NSLog(@"Unable to create windowed pixel format.");
        exit(0);
    }
    self = [super initWithFrame:frameRect pixelFormat:windowedPixelFormat];
    if (self == nil)
    {
        NSLog(@"Unable to create a windowed OpenGL context.");
        exit(0);
    }
    [windowedPixelFormat release];

    // set synch to VBL to eliminate tearing
    GLint    vblSynch = 1;
    [[self openGLContext] setValues:&vblSynch forParameter:NSOpenGLCPSwapInterval];

    // set up the display link
    CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
    CVDisplayLinkSetOutputCallback(displayLink, MyDisplayLinkCallback, self);
    CGLContextObj cglContext = [[self openGLContext] CGLContextObj];
    CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
    CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(displayLink, cglContext, cglPixelFormat);

    // set up our fixed delta time (which we maintain as an instance variable in case you want to change
    // it on the fly somewhere down the road, for whatever reason -- you could just as well use a constant
    // or define if you prefer instead)
    dt = 1.0 / UPDATE_RATE;

#if USE_SLO_MO

    // as mentioned, note that dt can be changed for interesting time effects, like "bullet time" with
    // something like:
    dt *= 0.25;

#endif
    
    inputLock = [[NSLock alloc] init];

    // init game
    [self initPong];

    return self;
}

- (void)awakeFromNib
{
    // for simplicity, we'll make the window a 1 to 1 aspect ratio in size
    NSSize    viewBounds = [self bounds].size;
    viewWidth = viewBounds.width;
    viewHeight = viewWidth;
    viewAspect = 1.0f;
    [[self window] setContentSize:NSMakeSize(viewWidth, viewWidth)];
    [[self window] setContentAspectRatio:NSMakeSize(1.0f, 1.0f)];
    [[self window] setTitle:@"Spacebar to shoot -- arrows to move left/right"];

    // activate the display link
    CVDisplayLinkStart(displayLink);
}

- (BOOL)acceptsFirstResponder
{
    return YES;
}

- (void)reshape
{
    NSSize    viewBounds = [self bounds].size;
    viewWidth = viewBounds.width;
    viewHeight = viewBounds.height;
    viewAspect = viewWidth / viewHeight;

    NSOpenGLContext    *currentContext = [self openGLContext];
    [currentContext makeCurrentContext];

    // remember to lock the context before we touch it since display link is threaded
    CGLLockContext([currentContext CGLContextObj]);

    // let the context know we've changed size
    [[self openGLContext] update];

    CGLUnlockContext([currentContext CGLContextObj]);
}

- (void)drawRect:(NSRect)rect
{
    [self drawPong];
}

- (void)keyDown:(NSEvent *)theEvent
{
    [self processKeys:theEvent];
}

- (void)keyUp:(NSEvent *)theEvent
{
    [self processKeys:theEvent];
}

- (void)processKeys:(NSEvent *)theEvent
{
    NSEventType        type;
    unsigned int    characterIndex, characterCount;
    NSString        *characters;
    unichar            c;
    int                convertedCode;

    type = [theEvent type];
    characters = [theEvent charactersIgnoringModifiers];
    characterCount = [characters length];
    [inputLock lock];
    for (characterIndex = 0; characterIndex < characterCount; characterIndex++)
    {
        c = [characters characterAtIndex: characterIndex];

        if (c < 128)
            convertedCode = c;
        else if (c == NSLeftArrowFunctionKey)
            convertedCode = LEFT_ARROW;
        else if (c == NSRightArrowFunctionKey)
            convertedCode = RIGHT_ARROW;
        else if (c == NSUpArrowFunctionKey)
            convertedCode = UP_ARROW;
        else if (c == NSDownArrowFunctionKey)
            convertedCode = DOWN_ARROW;
        else
            break;
        if (type == NSKeyDown)
            keyDownQueue[convertedCode] = YES;
        else
            keyUpQueue[convertedCode] = YES;
    }
    [inputLock unlock];
}

#pragma mark -
#pragma mark Pong

- (void)initPong
{
    paddlePos = 0.0f;
    ballPosX = 0.0f;
    ballPosY = -PADDLE_VERT_POS + PADDLE_HALF_HEIGHT + BALL_RADIUS;
    ballMoving = NO;
    ballYDir = 1.0f;
    ballXDir = 1.0f;
    ballMissedPaddle = NO;
}

- (void)updatePong
{
    if (key[LEFT_ARROW])
    {
        paddlePos -= PADDLE_SPEED * dt;

        // make sure the paddle doesn't go too far left
        if ((paddlePos - PADDLE_HALF_WIDTH) < BOUNDS_LEFT)
                paddlePos = BOUNDS_LEFT + PADDLE_HALF_WIDTH;
    }

    if (key[RIGHT_ARROW])
    {
        paddlePos += PADDLE_SPEED * dt;

        // make sure the paddle doesn't go too far right
        if (paddlePos > (BOUNDS_RIGHT - PADDLE_HALF_WIDTH))
                paddlePos = (BOUNDS_RIGHT - PADDLE_HALF_WIDTH);
    }

    if (!ballMoving && keyDown[SPACE])
    {
        ballMoving = YES;
    }

    if (ballMoving)
    {
        [self moveBall];
    }
    else
    {
        // ball is still on paddle, not launched yet
        ballPosX = paddlePos;
    }
}

- (void)moveBall
{
    float    penetration;

    ballPosX += BALL_SPEED * dt * ballXDir;
    ballPosY += BALL_SPEED * dt * ballYDir;

    // check for collisions with right and left bounds
    if ((ballPosX + BALL_RADIUS) > BOUNDS_RIGHT)
    {
        ballXDir = -1.0f;
        penetration = (ballPosX + BALL_RADIUS) - BOUNDS_RIGHT;
        ballPosX -= penetration * 2.0f;
    }
    if ((ballPosX - BALL_RADIUS) < BOUNDS_LEFT)
    {
        ballXDir = 1.0f;
        penetration = BOUNDS_LEFT - (ballPosX - BALL_RADIUS);
        ballPosX += penetration * 2.0f;
    }

    if (ballMissedPaddle)
    {
        // waiting for ball to fall through floor before reset
        if ((ballPosY + BALL_RADIUS) < BOUNDS_BOTTOM)
        {
            // new game
            ballMoving = NO;
            ballMissedPaddle = NO;
            ballXDir = 1.0f;
            ballYDir = 1.0f;
            ballPosX = paddlePos;
            ballPosY = -PADDLE_VERT_POS + PADDLE_HALF_HEIGHT + BALL_RADIUS;
        }
    }
    else if ((ballYDir == -1.0f) && ((ballPosY - BALL_RADIUS) < (-PADDLE_VERT_POS + PADDLE_HALF_HEIGHT)))
    {
#if CHEAT

        // cheat always bounces ball off bottom as if it hit paddle
        ballYDir = 1.0f;
        penetration = (-PADDLE_VERT_POS + PADDLE_HALF_HEIGHT) - (ballPosY - BALL_RADIUS);
        ballPosY += penetration * 2.0f;

#else

        // see if it hit the paddle
        if (!ballMissedPaddle &&
            (ballPosX - BALL_RADIUS) < (paddlePos + PADDLE_HALF_WIDTH) &&
            (ballPosX + BALL_RADIUS) > (paddlePos - PADDLE_HALF_WIDTH))
        {
            ballYDir = 1.0f;
            penetration = (-PADDLE_VERT_POS + PADDLE_HALF_HEIGHT) - (ballPosY - BALL_RADIUS);
            ballPosY += penetration * 2.0f;
        }
        else
            ballMissedPaddle = YES;

#endif
    }

    // just bounce the ball off the top
    if ((ballPosY + BALL_RADIUS) > BOUNDS_TOP)
    {
        ballYDir = -1.0f;
        penetration = (ballPosY + BALL_RADIUS) - BOUNDS_TOP;
        ballPosY -= penetration * 2.0f;
    }
}

- (void)drawPong
{
    NSOpenGLContext    *currentContext = [self openGLContext];
    [currentContext makeCurrentContext];

    // must lock GL context because display link is threaded
    CGLLockContext([currentContext CGLContextObj]);

    glViewport(0, 0, viewWidth, viewHeight);

    glClearColor(0.0f, 0.0f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // setup projection matrix
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();

    // matches (BOUNDS_LEFT, BOUNDS_RIGHT, BOUNDS_BOTTOM, BOUNDS_TOP, -1.0, 1.0)
    glOrtho(-viewAspect, viewAspect, -1.0, 1.0, -1.0, 1.0);

    // setup modelview matrix
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    // begin actual drawing
    glColor4f(1.0, 1.0, 1.0, 1.0f);

    // draw the  paddle
    glPushMatrix();
        glTranslatef(paddlePos, -PADDLE_VERT_POS, 0.0f);
        glBegin(GL_QUADS);
        glVertex2f(-PADDLE_HALF_WIDTH, -PADDLE_HALF_HEIGHT);
        glVertex2f(PADDLE_HALF_WIDTH, -PADDLE_HALF_HEIGHT);
        glVertex2f(PADDLE_HALF_WIDTH, PADDLE_HALF_HEIGHT);
        glVertex2f(-PADDLE_HALF_WIDTH, PADDLE_HALF_HEIGHT);
        glEnd();
    glPopMatrix();

    // draw the ball
    glPushMatrix();
        glTranslatef(ballPosX, ballPosY, 0.0f);
        glBegin(GL_QUADS);
        glVertex2f(-BALL_RADIUS, -BALL_RADIUS);
        glVertex2f(BALL_RADIUS, -BALL_RADIUS);
        glVertex2f(BALL_RADIUS, BALL_RADIUS);
        glVertex2f(-BALL_RADIUS, BALL_RADIUS);
        glEnd();
    glPopMatrix();

    // pop modelview
    glPopMatrix();

    // pop projection
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();

    [currentContext flushBuffer];

    CGLUnlockContext([currentContext CGLContextObj]);
}

@end
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #15
AnotherJake Wrote:Speaking of "smoothness", you may notice that the fixed rate timing isn't quite as glass smooth as just using the delta time from display link alone. To get it back to ultra smooth, you'd have to do sub-update interpolations (that is, interpolating data between two updates to precisely match where it would be in actual time) -- I think Bullet physics has support for this BTW.

Follow-up: Yes Bullet does indeed do this, although I haven't had time to make my own subjective assessment of the quality. I've just been feeding it my fixed dt and it seems to work fine, although I've noticed what appears to be some temporal aliasing now that I was looking for it. I need to try feeding it the deltaTime between frames instead of the fixed rate and see if it handles things better. I have heard others say that it's not quite what they would expect. The problem with feeding it the frame rate delta and letting it do its thing means it'll be out of step with my own game logic, which may be a serious issue to contend with...

Anyway, I remembered someone posting a link to a site which describes how this temporal aliasing can be remedied. Since I don't have time to describe it myself, here's that link, in case you aren't already aware of the technique. It's the same thing as ThemsAllTooks' tutorial you linked to earlier; it just adds an extra step which is simple in the timing section, but you have to interpolate all the data from (between) your updates during render which may or may not be a pain, depending on your game logic/physics implementation.
Quote this message in a reply
Post Reply 

Possibly Related Threads...
Thread: Author Replies: Views: Last Post
  OpenGL view first frame flickers garbage mk12 8 6,349 Sep 4, 2010 06:06 PM
Last Post: mk12
  View Controllers MacSteve85 2 2,329 Jan 27, 2010 07:22 PM
Last Post: MacSteve85
  multiple nsopengl subviews problem NelsonMandella 0 2,089 Nov 9, 2009 09:44 PM
Last Post: NelsonMandella
  GL view bounds question/quirk rezwits 3 3,201 Jul 12, 2009 08:45 AM
Last Post: rezwits
  How do I place all of my UI elements to be subviews of another view/window? xenocide 2 2,621 Feb 1, 2009 08:32 PM
Last Post: xenocide