2007-12-23

Ruby.framework broken in MacOSX10.5.sdk

If you're trying to build against Ruby.framework on Mac OS X 10.5 you may have come across a problem with the SDK. This manifests itself as this error:

gcc test.c -framework Ruby -mmacosx-version-min=10.5 -isysroot /Developer/SDKs/MacOSX10.5.sdk
ld: framework not found Ruby
collect2: ld returned 1 exit status

This appears to be fixable like this:

cd /Developer/SDKs/MacOSX10.5.sdk/System/Library/Frameworks/Ruby.framework/Versions/Current
sudo ln -s usr/lib/libruby.dylib Ruby

I don't know whether that's the right fix, but it seems to do the trick. Radar 5661551.

2007-12-13

GCC Warning Flags

Xcode's default warning settings are useless, not informing you of many real problems in your source code. I compile all my code with this set of warning flags:

-Wall -Wextra -Wno-unused-parameter -Wnewline-eof -Werror

-Wall

These are warnings that always indicate a problem with your code. I have no idea why they''re not enabled by default.

-Wextra

Also known as -W. These are warnings that indicate potential problems with your code. They will often appear in code that's correct, but they will also often expose code that's incorrect.

-Wno-unused-parameter

-Wextra warns about unused parameters to functions. Since this is a common occurrence in object-oriented code, I prefer not to see these warnings.

-Wnewline-eof

Warns if a source file doesn''t end with a newline character. If you're just coding for the Mac, you can ignore this one, but GCC on Linux will error in this situation, so if you're writing portable code, it's nice to know about it.

-Werror

Treats all warnings as errors. To make sure your code is top quality! It's easy to lose warnings in files that aren't changed often. It's impossible to lose errors.

2007-12-12

Xcode/GLUT Tutorial

This was the most popular thing on my old site. I've updated it for Xcode 3 on Leopard, but that means that the instructions may be a little misleading for Xcode 2 or earlier...

Xcode? OpenGL? GLUT?

Xcode is Apple's integrated software development environment.

OpenGL is a graphics library. It does both 2D and 3D graphics, and takes advantage of hardware acceleration. It's cross-platform; that is, the same code will run on the Mac, Windows and Linux.

OpenGL itself is cross-platform, but each OS has a different way of making an OpenGL window and getting events. GLUT is a simple cross-platform wrapper for this functionality.

Making a new project

Open the Xcode application. You'll find it in /Developer/Applications/:

From Xcode's File menu, choose "New Project...". Select "Empty Project" from the list, then click "Next":

Xcode will ask you what to call your project and where to save it. I'm going to call mine "XcodeGLUT" and put it on my Desktop.

Now click "Finish". Your project's window will appear:

Making a GLUT target

Xcode projects can have any number of targets. A target describes how to make a product such as an application, library, framework or plug-in.

Our empty project didn't come with a target, so we need to make one. From the "Project" menu, choose "New Target...". Choose "Cocoa Application" and click "Next":

You can name your target whatever you like. I'm going to call mine "XcodeGLUT". Then click "Finish":

Xcode presents you with your new target's settings. Xcode's default Cocoa Application template is designed for writing a Cocoa application in Objective C. We're just writing a GLUT application in C, so we need to make a small adjustment to the target settings. Change the value of the GCC_PREFIX_HEADER setting to be empty:

In order to compile a GLUT application we need a couple of extra frameworks. Right- or control-click on your target's icon, and choose "Add ▸ Existing Frameworks...". You'll be presented with an Open dialog showing /System/Library/Frameworks. If you have trouble with the contextual menu, you can also use "Add to Project..." in the "Project" menu, but you'll need to navigate to /System/Library/Frameworks yourself. Choose both GLUT.framework and OpenGL.framework. You can use command (⌘)-click to select both at once. Then click "Add":

Xcode will ask you for some settings for the added frameworks; the defaults are fine. Just click "Add":

Finally, we need to make a file for the source code. From the "File" menu, choose "New File...". Select "Empty File in Project", then click "Next":

Xcode will ask you what you want to call the file, and where you want to save it. Call it "main.c"; the default settings for everything else are fine. click "Finish":

Building your first Application

Copy and paste the following code into "main.c". Don't worry at this stage what it all means, we'll go through it line-by-line later:

#include <stdlib.h>

#include <GLUT/glut.h>

void display(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    glutSwapBuffers();
}

void reshape(int width, int height)
{
    glViewport(0, 0, width, height);
}

void idle(void)
{
    glutPostRedisplay();
}

int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
    glutInitWindowSize(640, 480);
    
    glutCreateWindow("GLUT Program");
    
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutIdleFunc(idle);
    
    glutMainLoop();
    return EXIT_SUCCESS;
}

Save the file, then click "Build and Go" on the toolbar, or choose "Build and Run" from the "Build" menu. After some time, your new application should run. At this stage, it's just a black window. When you're done admiring your handiwork, quit it and return to Xcode.

What does all that code mean?

This section assumes you're not totally new to programming in C. If you are, you should probably go away now and find a quick introduction to the language.

#include <stdlib.h>

#include <GLUT/glut.h>

stdlib.h includes some fundamental constants and functions for programming in C. GLUT/glut.h includes the constants and functions for GLUT and OpenGL. On Linux and Windows, GLUT/glut.h is called GL/glut.h.

int main(int argc, char** argv)
{
    glutInit(&argc, argv);

main is where the program begins executing. argc and argv are used to pass command-line arguments to the program; you needn't worry about them other than that they get passed to glutInit. glutInit is always the first function you call in a GLUT program; Bad Things™ will happen if it's not.

    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
    glutInitWindowSize(640, 480);

The call to glutInitDisplayMode here is telling GLUT that all our windows should be in red-green-blue-alpha mode, be double-buffered (to avoid flickering when drawing), and have a depth buffer (to ensure that 3D objects overlap correctly regardless of the order they're drawn in). The call to glutInitWindowSize tells GLUT that our windows should initially be 640 pixels wide by 480 pixels high.

    glutCreateWindow("GLUT Program");

The call to glutCreateWindow actually makes a window. The string passed is the title of the window. Until we've created a window, we can't make any OpenGL calls. Now, it's safe.

    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutIdleFunc(idle);

glutDisplayFunc, glutReshapeFunc and glutIdleFunc tell GLUT to call particular functions when certain things happen. The function we pass to glutDisplayFunc is called whenever it's time to draw a frame; the function we pass to glutReshapeFunc is called whenever the window is resized; and the function we pass to glutIdleFunc is called whenever GLUT isn't doing anything else.

    glutMainLoop();
    return EXIT_SUCCESS;
}

Finally, glutMainLoop tells GLUT to go off and do its thing, calling the functions we specified before whenever the appropriate things happen. glutMainLoop never returns, so returning EXIT_SUCCESS at the bottom is redundant, but it pacifies the compiler (which doesn't know that glutMainLoop never returns).

void idle(void)
{
    glutPostRedisplay();
}

Our idle function simply tells GLUT that we'd like to draw another frame now. This, combined with the fact that idle will be called whenever GLUT is not doing anything else, ensures that our program will run at the maximum possible frame rate.

void reshape(int width, int height)
{
    glViewport(0, 0, width, height);
}

Our reshape function simply informs OpenGL that the window size has changed. width and height are the new width and height of the window in pixels.

void display(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    glutSwapBuffers();
}

Our display function asks OpenGL to clear the color and depth buffers, then to swap the back buffer onto the screen.

Because we asked for a double-buffered window, we have two mages, called the front and back buffer. The front buffer is what's visible on-screen in the window. All OpenGL commands modify the back buffer. The glutSwapBuffers call swaps the back buffer to become the front buffer, instantly making all our drawing visible.

The color buffer is cleared to black by default; this can be changed with the glClearColor function.

Actually drawing something

First, let's draw a white rectangle. Edit the display function so it looks like this:

void display(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    glBegin(GL_QUADS);
    glVertex2f(-0.75f, -0.75f);
    glVertex2f( 0.75f, -0.75f);
    glVertex2f( 0.75f,  0.75f);
    glVertex2f(-0.75f,  0.75f);
    glEnd();
    
    glutSwapBuffers();
}

Again, save the file and "Build and Go". You should see a white rectangle filling most of the window.

A "Quad" is OpenGL's term for a four-sided polygon. The four glVertex2f calls say where the vertices (corners) of this polygon ie. By default, OpenGL's coordinate system has (-1, -1) in the lower left corner of the window, and (1, 1) in the upper right. Note that since our window isn't square, we get a rectangle rather than a square, as you might expect. We'll see how to fix this in the next section.

A pixel-perfect projection

In OpenGL, you can use whatever coordinate system is useful to you. One of the most useful for 2D graphics is the projection where OpenGL units are precisely one pixel.

Edit the reshape function to look like this:

void reshape(int width, int height)
{
    glViewport(0, 0, width, height);
    
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0, width, 0, height);
    glMatrixMode(GL_MODELVIEW);
}

If you've done any linear algebra, that will make some kind of sense to you. If not, don't worry too much — just accept it as some necessary mumbo-jumbo.

glMatrixMode tells OpenGL which of the matrices it uses for projecting OpenGL coordinates onto pixels we want to modify. Most changes are done to the model-view matrix, but in this case we want to change the projection. glLoadIdentity resets the matrix to the identity matrix, and gluOrtho2D sets up the 2D projection. The arguments to gluOrtho2D are left, right, bottom, top. Note that in OpenGL it's usual to put (0, 0) in the lower left (as is usual in mathematics) rather than the upper left (as is usual in computer graphics).

With this change to the coordinate system, the quad we're drawing is only three quarters of a pixel in size, right in the bottom left of the window. We should probably change that:

    glBegin(GL_QUADS);
    glVertex2f(  0.0f,   0.0f);
    glVertex2f(128.0f,   0.0f);
    glVertex2f(128.0f, 128.0f);
    glVertex2f(  0.0f, 128.0f);
    glEnd();

Again, build and run. You should see a 128- by 128-pixel white square in the lower left of the window.

Animation and timing

Let's get this square moving. How are we going to do that? One obvious way of doing it would be to move it a certain number of pixels every frame. That has a serious flaw, however — the speed of the square would be relative to the speed of the computer, since a faster computer will draw more frames per second than a slower one. That means we have to build the actual amount of time elapsed into our movement calculations.

GLUT has a handy way to get the number of milliseconds that have passed since glutInit() was called — glutGet(GLUT_ELAPSED_TIME). We'll want to work with the time in seconds, so we'll need to divide the number we get back by 1000.

Let's get this square moving. We'll move it right at 512 pixels per second, and when it's past the edge of the window, we'll move it back to the left.

We can also use glutGet to find the current width of the window in pixels, rather than storing it ourselves in the reshape function. We'll use this to make sure the square always goes to the edge of the window.

We'll need two global variables. One (lastFrameTime) to store the number of milliseconds elapsed at the last frame, so we know how much time we need to account for this frame, and one (boxX) to store the horizontal position of the box. These go just under the #includes:

int lastFrameTime = 0;
    
float boxX = 0.0f;

Then, we need to rewrite the display function:

First, we check if lastFrameTime is still zero. If it is, this is the first frame, and we need to set it to something sensible.

void display(void)
{
    if (lastFrameTime == 0)
    {
        lastFrameTime = glutGet(GLUT_ELAPSED_TIME);
    }

Next, we calculate how many seconds have elapsed since the previous frame. We get the current time, calculate how many milliseconds have elapsed since the last frame, convert that to seconds (being very careful to divide by a floating point number rather than an integer, which would give zero), then set lastFrameTime to the current time, in preparation for the next frame.

    int now = glutGet(GLUT_ELAPSED_TIME);
    int elapsedMilliseconds = now - lastFrameTime;
    float elapsedTime = elapsedMilliseconds / 1000.0f;
    lastFrameTime = now;

Next, we retrieve the window width in pixels from GLUT, then move our box. It goes right by 512 pixels every second, so we multiply 512 by the number of seconds elapsed, and add that to its current position. If it's now beyond the edge of the window, we move it back to the start again.

    int windowWidth = glutGet(GLUT_WINDOW_WIDTH);
    
    boxX += 512.0f * elapsedTime;
    if (boxX > windowWidth)
    {
        boxX -= windowWidth;
    }

We clear the color and depth bufers as usual.

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glTranslatef(x, y, z) moves everything drawn after it the specified number of units. We'll use this command to draw the square in a different place, rather than calculating the new vertex positions by hand. Because we don't want this change to apply over multiple frames, we use glPushMatrix and glPopMatrix around the commands we want to be affected. We draw the square and swap the buffers just as before.

    glPushMatrix();
    glTranslatef(boxX, 0.0f, 0.0f);
    
    glBegin(GL_QUADS);
    glVertex2f(  0.0f,   0.0f);
    glVertex2f(128.0f,   0.0f);
    glVertex2f(128.0f, 128.0f);
    glVertex2f(  0.0f, 128.0f);
    glEnd();
    glPopMatrix();
    
    glutSwapBuffers();
}

Build and run the program again. Notice that no matter how jerky the movement is, the square always moves at the same speed. Try resizing the window, or running another OpenGL game at the same time to make it jerky.

Also notice that no matter how big the window is, the square always moves at the same speed, and always goes back when it eaches the window's edge.

2007-12-06

Finding Your App's Files

The current working directory for your application depends on how it was run, and what API you're using:

  • If your application is using GLUT, the current working directory is your bundle's Resources directory.
  • If your application is using SDL, the current working directory is the directory containing your application bundle, by default. This is set in SDLMain.m. You may wish to change it (see below).
  • If you run your application from Xcode, the current working directory is the directory containing your application bundle.
  • If you run your application from Finder, the current working directory is /
  • If you run your application from the command-line, the current working directory is the current working directory of the controlling shell.

Generally, you'll want the current working directory to be your bundle's Resources directory. You'll set it correctly once at startup.

Cocoa

NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
[[NSFileManager defaultManager] changeCurrentDirectoryPath:resourcePath];

CoreFoundation

CFBundleRef mainBundle = CFBundleGetMainBundle();
CFURLRef resourcesURL = CFBundleCopyResourcesDirectoryURL(mainBundle);
char path[PATH_MAX];
if (!CFURLGetFileSystemRepresentation(resourcesURL, TRUE, (UInt8 *)path, PATH_MAX))
{
    // error!
}
CFRelease(resourcesURL);
chdir(path);

2007-12-04

iDevGames is Back

Carlos blames "IP address issues". I'm skeptical.

2007-12-03

CreateMacGames Lives!

Well... kinda. Boards at http://cmg.onesadcookie.net.

iDevGames is Down

iDevGames is down (as usual) and there's no sign of Carlos (as usual). Until stuff gets fixed, head over to #idevgames on FreeNode. Colloquy is a good IRC client if you haven't used one before.

Further bulletins as events warrant.