Game Programming Book PDF
Game Programming Book PDF
Game Programming Book PDF
Brian Greenstone
Pangea Software, Inc.
12405 John Simpson Ct.
Austin, TX 78732
www.pangeasoft.net
ISBN: 0-9761505-0-6
2004 Pangea Software, Inc. All rights reserved. No part of this book or the included CD may be
reproduced or transmitted in any form or by any means, electronic or mechanical, including photo-
copying, recording, or by any information storage and retrieval system, without prior written
permission from Pangea Software, Inc.
Grant of License
Purchasers of this book may freely include the source code contained in this book in their own
software projects.
Trademarks
Pangea Software, the Pangea Software logo, Bugdom, Nanosaur, Cro-Mag Rally, Enigmo, Weekend
Warrior, and Otto Matic are registered trademarks of Pangea Software, Inc. Many of the designations
used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where
those designations appear in this book, Pangea Software, Inc. was aware of a trademark claim. Apple,
the Apple logo, Mac, Macintosh, QuickDraw, QuickTime, Sherlock, Carbon, Rendezvous are trade-
marks of Apple Computer, Inc. registered in the United States and other countries.
Special thanks to the following people who have made my life easier over the years in this business:
Chris Bentley and the entire ATI group, George Warner, Eric Klein, Tuncer Deniz, Peter Cohen, Phil
Sulak, and many others who Im sure Ill hear from since I forgot to put their names in this list.
iii
Table of Contents
Introduction .................................................................................................................................................. 1
Chapter 1: Development Tools for the Mac............................................................................................ 3
Xcode.......................................................................................................................................................... 3
Interface Builder ........................................................................................................................................ 5
CHUD......................................................................................................................................................... 6
OpenGL Profiler & Shader Builder.......................................................................................................... 8
Documentation & Sample Code ............................................................................................................... 9
Chapter 2: Choosing a Video Mode ....................................................................................................... 11
Getting a List of Valid Display Modes .................................................................................................. 13
Letting the User Choose a Video Mode ................................................................................................. 17
Chapter 3: OpenGL for the Mac ............................................................................................................ 25
Initializing OpenGL................................................................................................................................. 26
Drawing an OpenGL Scene .................................................................................................................... 29
Working with Text in OpenGL............................................................................................................... 30
Displaying Windows in a Full-Screen Context ..................................................................................... 33
Chapter 4: OpenGL Optimizations........................................................................................................ 37
Macro Optimizations ............................................................................................................................... 37
Caching the OpenGL State ..................................................................................................................... 38
The Transform Hint................................................................................................................................. 43
Normals .................................................................................................................................................... 43
Colors ....................................................................................................................................................... 44
Reading Pixels ......................................................................................................................................... 44
Know when to use glBegin/End or Vertex Arrays ................................................................................ 44
Optimizing VRAM .................................................................................................................................. 46
Chapter 5: PowerPC Math Optimizations............................................................................................ 49
AltiVec for Faster Matrix Multiplies...................................................................................................... 49
Fast Vector Normalizing ......................................................................................................................... 52
Chapter 6: Vertex Array Range ............................................................................................................. 55
Initializing Vertex Array Range ............................................................................................................. 55
Drawing with VAR.................................................................................................................................. 60
Issues with VAR ...................................................................................................................................... 62
Chapter 7: Calculating the Frame Rate ................................................................................................ 65
Chapter 8: Gamma Fades........................................................................................................................ 69
iv
CGSetDisplayTransferByFormula ......................................................................................................... 70
CGSetDisplayTransferByTable .............................................................................................................. 73
Chapter 9: Carbon Events ....................................................................................................................... 75
Event Loop Timers .................................................................................................................................. 75
Menu Bars ................................................................................................................................................ 78
Preventing Your Game From Going to Sleep........................................................................................ 83
Chapter 10: Audio..................................................................................................................................... 85
Quicktime for Music Playback ............................................................................................................... 85
The Sound Manager for Playing Effects ................................................................................................ 98
OpenAL.................................................................................................................................................. 111
Chapter 11: Simple Input ...................................................................................................................... 125
Reading the Keyboard with GetKeys() ................................................................................................ 125
Reading the Keyboard with Carbon Events......................................................................................... 129
Reading the Mouse ................................................................................................................................ 133
Chapter 12: Input with the HID Manager .......................................................................................... 139
The Good, the Bad, and the HID Manager .......................................................................................... 139
Some HID Manager Terminology ........................................................................................................ 141
Getting the List of HID Devices........................................................................................................... 142
Getting a Devices Elements................................................................................................................. 152
Reading Input Data................................................................................................................................ 169
Input.c..................................................................................................................................................... 171
Chapter 13: Writing a Maya Plug-in ................................................................................................... 175
Initializing A Maya Plug-In .................................................................................................................. 175
The Plug-In Entry Point ........................................................................................................................ 177
Getting the Scenes Layer Info ............................................................................................................. 179
Extracting Geometry Data .................................................................................................................... 184
Extracting Shader Data.......................................................................................................................... 189
Installing the Maya Plug-In................................................................................................................... 195
The BG3D File Format ......................................................................................................................... 198
BG3D Linker ......................................................................................................................................... 199
Loading and Using BG3D Files ........................................................................................................... 201
Chapter 14: Stereo 3D ............................................................................................................................ 203
Types of Stereo Glasses ........................................................................................................................ 203
Stereo Camera Calculations .................................................................................................................. 204
Rendering for Anaglyph Glasses .......................................................................................................... 209
Anaglyph Color Balancing.................................................................................................................... 212
Rendering for LCD Shutter Glasses ..................................................................................................... 219
Shutter Glasses Hardware ..................................................................................................................... 223
Fun with Anaglyphs .............................................................................................................................. 224
v
Introduction
Walk into any bookstore or do a search on Amazon.com, and youll find dozens and dozens
of game programming books. Unfortunately, all but a few of these books are written assum-
ing youre on a PC, using Direct 3D, Direct Sound, and all the other Windows-specific
technologies. Some of these books have good information about general game programming
techniques, but all that information wont do you any good on a Macintosh unless you know
how to build a Mac-specific game engine. There are many things you need to know about
building a game engine for Mac OS X that are different from the PC, and the goal of this
book is to show them to you.
Were going to start from the ground up as we build a small 3D game engine shell to get you
started in your Mac game programming endeavors. This book isnt about how to write or
design a game, but rather how to build a game engine for Mac OS X. Those other books on
game programming can teach you general game programming technique dealing with AI,
collision detection, world building, etc., but this books focus is specifically on the Macintosh
technologies that you need to know to get a game engine up and running. If you want to make
a career out of developing games for the Mac, this book also covers topics dealing with the
business end of things including copy protection, distribution, and marketing.
I have been programming games on the Mac since 1991, and running Pangea Software for
even longer. Over those years Ive accumulated quite a stockpile of useful techniques to get
the best performance out of a game engine on this platform. Ive also encountered several
bugs with the OS that can often present issues for game programmers, so Ill be sure to point
all of those out to make you aware of them.
The CD that comes with this book contains sample projects all written in straight C (none of
that C++ gibberish) using Apples Xcode compiler. These projects include all of the code
examples that are discussed in each of the following chapters, and as we progress through
these chapters, well continue to build upon the game engine. By the time we get to the end
of the book youll have a robust set of functions that you can use to start coding your own
games for the Mac.
3
Xcode
All of the sample projects in this book are built with Apples new Xcode compiler (version
1.5). Prior to Xcode the compiler of choice for any serious Mac programming was Metro-
werks CodeWarrior, but as the price of CodeWarrior has continued to increase over the years
it hasnt evolved enough to justify the expense. Luckily for Mac developers Apple has leap-
frogged over CodeWarrior with the totally free Xcode compiler. There are still some things
that CodeWarrior is better at than Xcode, but the technology behind Xcode is much more
modern and has a great future ahead of it.
Programmers are often afraid of moving from one development environment to another
because when things change theyre left a bit disoriented, and incompatibilities with existing
code can pop up. This was definitely the case when Apple released Project Builder at the
onset of OS X. Project Builder was a mess, and no serious game programmer would use it.
Apple knew this and wanted to make the transition from CodeWarrior to Xcode a smooth
one, so in addition to being able to import CodeWarrior projects, you can also configure
Xcode to use the CodeWarrior key shortcuts for just about everything.
The only significant difference between the CodeWarrior and Xcode interfaces is how the
project and target settings are handled. In CodeWarrior there was a nice, clean Project
Settings dialog that anyone could understand. Xcode has the Inspector window that lets
you set all sorts of project preferences. Unfortunately, the Inspector window is a bit
overwhelming and is poorly documented. It requires more low-level, command line compiler
knowledge than most Mac programmers are used to:
4 Chapter 1: Development Tools for the Mac
These are some of the key features that Xcode has over CodeWarrior:
DISTRIBUTED BUILDS
This feature lets Xcode send different source files to multiple processors and/or machines on
the network for simultaneous parallel compilation. So, if youre on a dual processor machine,
Xcode will compile two .c files simultaneously which obviously speeds up your total project
build time. If you have, say, a total of three dual-processor G5s on your local network then
Xcode can distribute .c files to all of them, and theoretically be compiling six files simultane-
ously!
PREDICTIVE COMPILATION
Most of the time when youre working on a project youre just sitting there typing, and the
CPU is spinning its wheels doing nothing. But Xcode is smart. Instead of just sitting there
doing nothing, it will start compiling files in the background. That way when youre ready to
build your project, most of the .c files have already been compiled, and all Xcode needs to do
is link.
GCC
While CodeWarrior uses its own proprietary compiler, Xcode uses GCC which is a much
more robust compiler that gives you access to just about every compilation parameter that
5
you could ever want. It also issues far superior compile warnings. For example, take a look
at the following code:
int TestFunc(void)
{
int a, b, c;
c = 0;
a = 5;
return(a);
}
CodeWarrior would not do a very good job with this function because it would only report a
warning that the variable b is unused. However, logically both b and c are not used, and
GCC lets us know it. Even though the variable c is assigned a value of 0 it is never used to
actually do anything. This one feature alone is worth switching from CodeWarrior to Xcode
since it really helps eliminate garbage from your code. GCC is smart enough to even detect
this on global variables. If you convert an old CodeWarrior project to Xcode and recompile,
youll be amazed at how many unused variable warnings come up that you never saw with
CodeWarrior.
Xcode is not perfect, however. I wont bother listing all the problems with it since Apple has
been rapidly fixing bugs and releasing updates, so any problems I were to list here would
likely become invalid in the near future. But suffice to say that Xcodes main flaws deal with
stability and ease of use. Xcodes Inspector interface is a nebulous monster compared with
CodeWarriors simple project Settings dialog, so just setting up the parameters of a project
with Xcode is a project all in itself and can be quite overwhelming to new users. The best
thing to do is to start with one of the sample projects included with this book, and then go
from there.
There is a wonderful document on Apples web site that details the differences between
Xcode and CodeWarrior, and it provides a very good guide for transitioning from CodeWar-
rior to Xcode. The document can be found here:
http://developer.apple.com/tools/switchtoxcode.html
Interface Builder
Interface Builder is a very powerful interface creation tool, but since games usually have their
own graphical interfaces for most things, we only need to use this tool to create some basic
dialogs and menus. In my games, I usually do the Video Configuration, Input Configuration,
6 Chapter 1: Development Tools for the Mac
Registration, and Settings dialogs as standard Mac OS X dialogs using Interface Builder. The
rest of the interface is done as custom in-game screens such as menu screens, high score
screens, etc.
For those programmers who are used to using ResEdit or Resourcerer on Mac OS 9, Interface
Builder is very similar, yet much more powerful.
CHUD
CHUD is the acronym that Apple uses to refer to its suite of Computer Hardware Understand-
ing Development Tools. A pretty lousy acronym I know, but the tools are incredible! There
are many tools that make up CHUD, and the most useful one is called Shark. Shark is a low-
level performance optimization tool. It shows you exactly how much time is being spent in
each function, on each line of each function, and even each opcode of each line.
7
Shark even goes so far as to offer suggestions on how to optimize specific parts of your code.
In Figure 1-4 below you can see that under the Comment column Shark indicates problems
with the execution performance, and if there is a ! icon you can click on it to reveal some
suggestions for improving performance:
The CHUD tools and documentation can be found on Apples web site at:
http://developer.apple.com/tools/performance
Once installed, the tools are found in your Developer/Applications/Performance Tools folder
of your boot drive. You must download and install these tools yourself because they are not
installed by default as part of the OS X Developer Tools.
OpenGL Profiler is similar to the Shark tool in CHUD except that it details the performance
of the OpenGL sub-system in your game. As you run your game the OpenGL Profiler tool
will track all of the OpenGL calls that you make, and build a statistical database of everything
thats going on. This way you can easily determine which OpenGL calls your application is
spending its time in.
Shader Builder on the other hand is not a performance tool at all. This is a great little
application for quickly writing and testing vertex and fragment shader programs supported by
all new 3D video hardware. You use this to quickly develop special effects for your game to
do things like bump mapping, cartoon shading, and fur.
Once youve written a shader program with Shader Builder you can save it out to a text file
and then your code can read it in with the usual OpenGL Shader Program function
glProgramStringARB(). For information on how to program and use Shader Programs with
OpenGL you should consult the book called The OpenGL Extensions Guide.
In the days of Mac OS 8 and 9 there was a technology called Game Sprockets. This was a set
of libraries that made writing games on the Mac a much easier process than they are today.
Unfortunately, Apple didnt bring most of the Game Sprockets to OS X, so weve now got to
do a lot of things by hand. One Game Sprocket that they did bring over to OS X is Draw
Sprocket, but I dont recommend using it. Instead, we will use Core Graphics API calls to
get information about our displays, and to set our screen mode. This gives us much better
control over our display than would Draw Sprocket.
gCGDisplayID = CGMainDisplayID();
CGDisplayCapture(gCGDisplayID);
refDisplayMode = CGDisplayBestModeForParameters(
gCGDisplayID,
12 Chapter 2: Choosing a Video Mode
if (refDisplayMode == nil)
DoFatalAlert("\pCGDisplayBestModeForParameters failed!");
/* SWITCH TO IT */
CGDisplaySwitchToMode(gCGDisplayID, refDisplayMode);
gGameWindowWidth = CGDisplayPixelsWide(gCGDisplayID);
gGameWindowHeight = CGDisplayPixelsHigh(gCGDisplayID);
}
The first thing that SwitchDisplayMode() does is to capture the main display (the main
display is whichever display has the menu bar):
CGDisplayCapture(gCGDisplayID);
What capturing does is tell the system that your application wants to reserve this display for
its own exclusive use. No other application can change this displays configuration. Then
we ask Core Graphics for the display mode that best matches our parameters:
refDisplayMode = CGDisplayBestModeForParameters(
gCGDisplayID,
depth, // 16 or 32-bit depth
width, // horiz rez
height, // vert rez
nil);
Since theres no guarantee that the resolution we got is the one we asked for, we need to
extract the actual resolution back out of our display:
gGameWindowWidth = CGDisplayPixelsWide(gCGDisplayID);
gGameWindowHeight = CGDisplayPixelsHigh(gCGDisplayID);
short gNumVideoModes = 0;
VideoModeType gVideoModeList[MAX_VIDEO_MODES];
void CreateDisplayModeList(void)
{
int i, numDeviceModes;
CFArrayRef modeList;
gNumVideoModes = 0;
gCGDisplayID = CGMainDisplayID();
modeList = CGDisplayAvailableModes(gCGDisplayID);
if (modeList == nil)
DoFatalAlert("\pCGDisplayAvailableModes failed!");
numDeviceModes = CFArrayGetCount(modeList);
14 Chapter 2: Choosing a Video Mode
/*********************/
/* EXTRACT MODE INFO */
/*********************/
/***************************/
/* SEE IF ADD TO MODE LIST */
/***************************/
if (CFDictionaryGetValue(mode,
kCGDisplayModeUsableForDesktopGUI) != kCFBooleanTrue)
continue;
skip = false;
for (j = 0; j < gNumVideoModes; j++)
15
{
if ((gVideoModeList[j].rezH == width) &&
(gVideoModeList[j].rezV == height))
{
skip = true;
break;
}
}
if (!skip)
{
/* THIS REZ NOT IN LIST YET, SO ADD */
modeList = CGDisplayAvailableModes(gCGDisplayID);
Then we iterate thru modeList finding modes that we want to keep by calling a series of Core
Foundation functions that give us information about each mode:
The variable mode is what Apple refers to as a dictionary. It contains a lot of data, and to
find the values of specific pieces of data we look them up in the dictionary with a call to
CFDictionaryGetValue():
The constant kCGDisplayWidth is a string that gets looked up in the dictionary and then that
entrys value is returned. Unfortunately, CFDictionaryGetValue() does not return an actual
integer number that we can use. Rather, it returns a CFNumberRef which is an opaque
structure that contains our value, so we need to make another function call to get the integer
value from it:
kCGDisplayMode
kCGDisplayBitsPerSample
kCGDisplaySamplesPerPixel
kCGDisplayRefreshRate
kCGDisplayModeUsableForDesktopGUI
kCGDisplayIOFlags
kCGDisplayBytesPerRow
So, if you wanted to determine the refresh rate of the current display mode then do this:
However, there is one very important thing to know about refresh rates, and that is that LCD
displays will return a value of 0 even though their true refresh rate is typically 60hz. So, you
should always special-case your code like this:
if (numRef == nil)
refreshRate = 60;
else
{
CFNumberGetValue(numRef, kCFNumberLongType, &refreshRate)
if (refreshRate == 0)
refeshRate = 60;
}
Going back to Listing 2-2, youll notice that were skipping any modes that are not 32-bit.
The reason for this is that any resolutions that support 32-bits will also support 16-bits,
however, there are times when the reverse is not true. So, we only need to track 32-bit modes
since we can always make then 16-bit if we want.
It is also important to note that not all modes returned in this list are actually valid. This is a
list of all video modes supported by the video card, but not necessarily by the display itself.
Therefore, we need call this:
CFDictionaryGetValue(mode, kCGDisplayModeUsableForDesktopGUI)
The result from this indicates if the mode is usable by the display. Some video cards can
support huge resolutions like 2048x1365, but good luck finding a 17 monitor that can
actually go that high. Testing for kCGDisplayModeUsableForDesktopGUI lets us know if the
resolution is physically possible.
17
If the video mode checks out, then we add it to our list. Since different modes can have the
same resolution, it is important to check for duplicate resolutions to avoid having duplicate
information in our list. For example, there may be 5 different video modes at the 1024x768
resolution. The resolutions of these modes are all 1024x768, but there may be different
refresh rates or other characteristics that we dont care about.
In the sample project youll find a file called Game.nib. This file contains the resources for
the dialog in Figure 2-1. In order to use this file in your game you need to get a reference to
it with these two lines of code:
gBundle = CFBundleGetMainBundle();
CreateNibReferenceWithCFBundle(gBundle, CFSTR("Game"), &gNibs);
18 Chapter 2: Choosing a Video Mode
The first line gets a reference to the applications main bundle, and then the next line gets a
reference to the nib file named Game.nib thats in that bundle. Note that you do not
include the .nib suffix in the filename. Once youve done this you can easily access any
nib resources in your application.
The following code shows how to load and process our dialog resource:
/*************************/
/* INITIALIZE THE DIALOG */
/*************************/
CreateDisplayModeList();
BuildResolutionMenu();
winEvtHandler = NewEventHandlerUPP(DoScreenModeDialog_EventHandler);
InstallWindowEventHandler(gDialogWindow,
winEvtHandler,
GetEventTypeCount(list),
list,
0,
19
&ref);
/**********************/
/* PROCESS THE DIALOG */
/**********************/
ShowWindow(gDialogWindow);
RunAppModalLoopForWindow(gDialogWindow);
/*********************/
/* GET RESULT VALUES */
/*********************/
idControl.signature = 'wind';
idControl.id = 0;
GetControlByID(gDialogWindow, &idControl, &control);
gPlayInWindow = GetControlValue(control);
idControl.signature = 'bitd';
idControl.id = 0;
GetControlByID(gDialogWindow, &idControl, &control);
if (GetControlValue(control) == 1)
gDesiredColorDepth = 16;
else
gDesiredColorDepth = 32;
idControl.signature = 'rezm';
idControl.id = 0;
GetControlByID(gDialogWindow, &idControl, &control);
i = GetControlValue(control);
gDesiredRezH = gVideoModeList[i-1].rezH;
gDesiredRezV = gVideoModeList[i-1].rezV;
/***********/
/* CLEANUP */
/***********/
DisposeEventHandlerUPP (winEvtHandler);
DisposeWindow (gDialogWindow);
20 Chapter 2: Choosing a Video Mode
The function CreateWindowFromNib() is all that we need to call to load the dialogs resource
data. We simply supply this function with the name of the resource to load, VideoMode,
and a reference to our nib file.
Dialogs created from nibs only need a small amount of maintenance code to function because
the OS handles most of the functionality of the dialogs controls. This maintenance is done
by a window event handler that is set up with this code:
winEvtHandler = NewEventHandlerUPP(DoScreenModeDialog_EventHandler);
InstallWindowEventHandler(gDialogWindow, winEvtHandler,
GetEventTypeCount(list), list, 0, &ref);
We wont spend too much time here discussing the details of processing Macintosh dialogs
since there is plenty of other literature that describes this process, but suffice to say that our
window event handler doesnt need to do much except check if the user clicks the OK or Quit
buttons:
switch(GetEventKind(event))
{
/*******************/
/* PROCESS COMMAND */
/*******************/
//
// We only care if the user pressed the OK or Quit buttons.
// Let the OS handle all other events.
//
case kEventProcessCommand:
GetEventParameter (event,
kEventParamDirectObject,
kEventParamHICommand,
NULL,
sizeof(command),
NULL,
21
&command);
switch(command.commandID)
{
/* "OK" BUTTON */
/* "QUIT" BUTTON */
case 'quit':
ExitToShell();
break;
}
break;
}
return (eventNotHandledErr);
}
The checkboxes and radio buttons are handled automatically by OS X, so you dont need to
manually deal with them like you would have had to do with the old fashioned resources on
Mac OS 9. When the dialogs event loop terminates weve got some code that reads back the
control values to determine what the user selected. Reading the value of a control is simple:
idControl.signature = 'wind';
idControl.id = 0;
GetControlByID(gDialogWindow, &idControl, &control);
gPlayInWindow = GetControlValue(control);
Just remember that the controls signature value is whatever youve assigned to that control
in Interface Builder. I like to choose signatures that have some meaning relevant to the
function of the control, so in the example above wind is a logical signature for the Play
Game in Window checkbox as shown in Figure 2-2 below:
22 Chapter 2: Choosing a Video Mode
Figure 2-2: The Play Game in Window checkbox has wind signature
We already learned how to get a list of valid video modes earlier, so now we use that list of to
build our dialogs pop-up menu:
idControl.signature = 'rezm';
idControl.id = 0;
GetControlByID(gDialogWindow, &idControl, &control);
GetControlData(control,kControlMenuPart,
kControlPopupButtonMenuHandleTag,
sizeof(MenuHandle), &hMenu,
&tempSize);
SetControlMaximum(control, gNumVideoModes);
SetControlValue(control, 1);
}
To create a menu on the Mac you simply pass each menu items Pascal string to the function
AppendMenu(), and building these is just a matter of converting the width and height resolu-
tion values to strings. There are many ways to work with Pascal strings on the Mac, but Ive
always been a fan of the API calls NumToString() and BlockMove(). NumToString()
creates a Pascal string from an integer value, and BlockMove() is an easy way to copy one
Pascal string into another.
The last line of BuildResolutionMenu() calls SetControlValue() to tell the menu which
menu item is selected by default. In an actual game you would want to set this to the menu
item of the matching resolution stored in the games preferences so that the user doesnt have
to keep resetting the resolution each time this dialog comes up.
25
This book is not an OpenGL programming tutorial, so I would recommend buying the official
OpenGL Programming Guide and OpenGL Reference Manual along with some other
general OpenGL programming books like The OpenGL Extensions Guide. What I will
primarily be covering are the parts of OpenGL that are specific to the Mac, and techniques for
optimizing your OpenGL code for this hardware.
The sample project named OpenGL Basics.xcode demonstrates everything presented in this
chapter. This sample application builds on what we learned in Chapter 2 by adding a new
source file named OpenGL.c. which contains all of the new code. The new functions that
well be writing all start with the prefix OGL_ to indicate that it is part of our new OpenGL
support library.
Figure 3-1: The OpenGL Basics sample application that draws a simple square
26 Chapter 3: OpenGL for the Mac
Initializing OpenGL
The Mac-specific subset of OpenGL is called AGL (Apple GL). The function prototypes and
constants for AGL are found in the header file agl.h. We use several of these AGL functions
to initialize an OpenGL draw context:
GLint attribWindow[] =
{
AGL_RGBA, AGL_DOUBLEBUFFER, AGL_DEPTH_SIZE, 32,
AGL_ALL_RENDERERS, AGL_ACCELERATED, AGL_NO_RECOVERY, AGL_NONE
};
GLint attribFullScreen[] =
{
AGL_RGBA, AGL_FULLSCREEN, AGL_DOUBLEBUFFER, AGL_DEPTH_SIZE, 32,
AGL_ALL_RENDERERS, AGL_ACCELERATED, AGL_NO_RECOVERY, AGL_NONE
};
/***********************/
/* CHOOSE PIXEL FORMAT */
/***********************/
/* PLAY IN WINDOW */
if (gPlayInWindow)
fmt = aglChoosePixelFormat(&gdevice, 1, attribWindow);
/* FULL-SCREEN */
else
fmt = aglChoosePixelFormat(&gdevice, 1, attribFullScreen);
/**********************/
/* CREATE AGL CONTEXT */
/**********************/
/* PLAYING IN A WINDOW */
if (gPlayInWindow)
{
gAGLWin = (AGLDrawable)GetWindowPort(gGameWindow);
aglSetDrawable(gAGLContext, gAGLWin);
}
/* PLAYING FULL-SCREEN */
else
{
gAGLWin = nil;
aglEnable(gAGLContext, AGL_FS_CAPTURE_SINGLE);
aglSetFullScreen(gAGLContext, 0, 0, 0, 0);
}
aglSetCurrentContext(gAGLContext);
aglDestroyPixelFormat(fmt);
}
AGL_RGBA
This tells OpenGL that we want our draw context to be in RGBA format, however, it should
be noted that OpenGL ignores the alpha component even though all of the buffers created for
the draw context are 32-bit, and thus, do have an alpha byte.
AGL_FULLSCREEN
This lets OpenGL know that we want to go full-screen so that it can optimize its pipeline to
make the best use of that. Full-screen contexts can benefit from hardware page flipping,
28 Chapter 3: OpenGL for the Mac
thereby avoiding a slower pixel blitting that would otherwise be required to copy the back
buffer to the screen.
AGL_DOUBLEBUFFER
This lets OpenGL know that we want to render double buffered. That means there are two
drawing buffers: one thats currently being draw into and one that is currently being dis-
played. OpenGL page flips between the two buffers to get flicker-free animation.
AGL_DEPTH_SIZE
This constant is followed by a number that is either 16 or 32 depending on the desired bit-
depth of the z-buffer. These days it is recommended that you always use a 32-bit z-buffer
since VRAM is plentiful, and 16-bit z-buffers often result in drawing artifacts. Only use a
16-bit z-buffer if youre running low on VRAM and need to conserve it.
AGL_ALL_RENDERERS
This doesnt do anything particularly useful from the game programmers point of view, but
internally to OpenGL it lets the system know that all rendering engines are acceptable to the
application.
AGL_ACCELERATED
This eliminates any software-only renderers from being chosen by OpenGL. It insures that
only the fast, hardware renderers will be allowed.
AGL_NO_RECOVERY
This lets OpenGL follow an optimized pipeline by disabling all failure recovery systems.
Typically OpenGL will resort to a software renderer if something were to go wrong with a
hardware renderer (such as running out of VRAM), but with this option enabled OpenGL will
simply fail instead.
AGL_NONE
You must put this at the end of the attributes list to indicate the end of the list.
The pixel format object is now used to create the draw context:
After creating the context we need to activate it, but there are two different ways to create the
draw context depending on if its going to be associated with a full-screen display or with a
window. To activate a context for a window we do this:
gAGLWin = (AGLDrawable)GetWindowPort(gGameWindow);
aglSetDrawable(gAGLContext, gAGLWin);
aglSetCurrentContext(gAGLContext);
29
aglEnable(gAGLContext, AGL_FS_CAPTURE_SINGLE);
aglSetFullScreen(gAGLContext, 0, 0, 0, 0);
aglSetCurrentContext(gAGLContext);
The input parameters for aglSetFullScreen() are actually the display mode parameters
width, height, and frequency; however, we pass 0s for all of these since weve already
manually set our display (see Chapter 2). I dont recommend setting those display parameters
with aglSetFullScreen() because each time you create and delete a draw context the screen
will switch. By setting the display mode manually the way we did earlier, you can create and
destroy OpenGL draw contexts as often as you want in your game, and there wont be any
visual glitching. Additionally, manually switching the display gives you more control over
it later should you ever need to do things with it.
aglSetCurrentContext(gAGLContext);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
DrawSceneGeometry();
aglSwapBuffers(setupInfo->drawContext);
}
30 Chapter 3: OpenGL for the Mac
Most games only have one draw context active at any given time, but its still a good idea to
call aglSetCurrentContext() each pass through your render loop just to be safe. Before we
start drawing anything we need to make sure that our drawing buffer and z-buffer are both
cleared:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
After this you are safe to start doing all your own OpenGL drawing to render your scene.
When youre done submitting your geometry call aglSwapBuffers() to cause a page flip to
occur.
aglSwapBuffers(setupInfo->drawContext);
If youre in full-screen mode this page flip will be a fast hardware toggle, but if youre
playing the game in a window then the drawing buffer gets blitted to the window instead.
An important thing to mention about aglSwapBuffers() is that the buffers are not necessar-
ily swapped the instant you call this function. Modern video cards will queue up all of your
drawing commands including the swap buffer command. Chances are that the video card is
still drawing your geometry when you make this call, so it simply puts the swap command
into its queue, and as soon as the video card is done drawing itll execute the swap command.
The beauty of this is that your program can proceed to calculating the next frames data while
the previous frame is being drawn by the video card its getting two things done simultane-
ously.
void OGL_InitFont(void)
{
GLboolean success;
31
gFontList = glGenLists(256);
if (!success)
DoFatalAlert("\paglUseFont failed");
}
To create a font for AGL we first create an empty OpenGL list to contain bitmaps of all of
the font characters:
gFontList = glGenLists(256);
Next, our OGL_InitFont() function calls aglUseFont() which automatically generates the
character bitmaps for each character in the requested font. You can specify which font you
want to use, along with all the usual font parameters such as style and point size. Be careful
about which font you choose to use because not all users may have your desired font in-
stalled. I always use Monaco since thats a standard Mac font, however, Ive seen many
situations where users have uninstalled Monaco from their systems and that causes some of
our games to break. My standard reply to them is Monaco is a system font which is required
by many applications, so reinstall it.
To draw a string with this font, there are only three simple calls to make:
glRasterPos2i(x, y);
glListBase(gFontList);
glCallLists(stringLength, GL_UNSIGNED_BYTE, cString);
Remember, however, that whats actually going on under the hood is that OpenGL is render-
ing a series of bitmaps using the current rendering state. Therefore, it is important to set the
OpenGL state to something consistent as is done in this function:
glMatrixMode (GL_MODELVIEW);
glLoadIdentity();
glMatrixMode (GL_PROJECTION);
glLoadIdentity();
glOrtho(0, 640, 0, 480, -10.0, 10.0);
glDisable(GL_LIGHTING);
glDisable(GL_TEXTURE_2D);
glColor4f (1,1,1,1);
glRasterPos2i(x, 480-y);
glListBase(gFontList);
glCallLists(s[0], GL_UNSIGNED_BYTE, &s[1]);
}
The most important thing that needs to be done before drawing the string is setting the
projection matrix to a nice, flat, orthographic matrix so that we can draw the string to a
specified screen coordinate. I like to set my ortho matrix to 640x480. Keep in mind, how-
ever, that this is not 640x480 in actual screen pixel coordinates, but rather it is 640x480 in
world-space that gets scaled by OpenGL to whatever your true screen size is. An x-
coordinate of 320 will always be in the middle of the screen no matter what resolution the
screen is.
After this matrix is set, lighting and texturing are turned off, and the current color is set to
white. To set the coordinates of the first character in the string do this:
glRasterPos2i(x, 480-y);
Drawing the string is done by setting the font list as the current list, and then performing the
draw with the glCallLists() function:
glListBase(gFontList);
glCallLists(s[0], GL_UNSIGNED_BYTE, &s[1]);
33
So, before bringing up any windows or dialogs we need to enter a window-safe mode, and
when were done with that window we can exit the window-safe mode. To do this well need
two new functions, Enter2D() and Exit2D():
CGDisplayRelease(gCGDisplayID);
/* DISABLE GL */
if (gAGLContext)
{
glFlush();
glFinish();
aglSetDrawable(gAGLContext, nil);
glFlush();
glFinish();
}
}
The Enter2D() function does two basic things: it releases the display and it disables
OpenGLs full-screen draw context. The function CGDisplayRelease() will not change the
displays mode at all it will remain at the current resolution and color depth. The
aglSetDrawable() call will then make the OpenGL drawing pane disappear so that we can
34 Chapter 3: OpenGL for the Mac
see the Finders desktop and interact with anything there. Were now free to bring up
dialogs, work with menus, run other applications, etc.
Since the video mode stays at its current setting, be sure that your games dialogs are never
larger than the minimum screen resolution that your game supports. For example, if youre
playing in 640x480 mode and you try to bring up a dialog thats 800x600 it obviously isnt
going to fit.
Once were ready to go back to the game, we need to exit the 2D mode and return to our full-
screen 3D mode by doing the reverse of what we did in Listing 3-7:
CGDisplayCapture(gCGDisplayID);
/* RE-ENABLE GL */
if (gAGLContext)
aglSetFullScreen(gAGLContext, 0, 0, 0, 0);
}
The first important place well put these calls is in our game engines DoAlert() and
DoFatalAlert() functions since those can bring up 2D dialogs at any time:
ExitToShell();
}
void DoAlert(Str255 s)
{
SInt16 alertItemHit;
Macro Optimizations
The smart folks at Apple have given us a way to make our OpenGL function calls just a little
bit faster by removing one level of indirection in each call that we make. Normally, when
you call an OpenGL function such as glEnable(), this glEnable() function doesnt actually
do any enabling. What its really doing is looking up the renderers internal enable()
function and then it jumps to that. Different renderers will have different internal enable
functions, so if youre running on ATI hardware then glEnable() is really just looking up
and calling the enable() function in the ATI driver.
The AGLContex structure that defines our draw context contains the entire jump table for
every OpenGL function supported by the renderer, so theres no reason that your code cant
do that function lookup and jump by itself, thus saving that extra branch to the bogus
glEnable() function. If you look in the aglmacro.h header file youll see every OpenGL
function call defined as a macro. Each macro takes the draw context and finds the jump
vector to the actual OpenGL function in question. For example, the macro for glEnable()
looks like this:
To use the AGL macros you first need to include the aglmacro.h header file at the top of
every .c file that you want optimized. Once you include this header in a file, all of the
OpenGL calls you make in that file will be done with these macros instead of calling the
wrapper functions.
#include <AGL/aglmacro.h>
Next, at the top of every function that makes an OpenGL call you must include this line of
code that assigns your draw context to the agl_ctx variable that the macros rely on:
void DoStuff(void)
{
AGLContext agl_ctx = gAGLContext;
glDisable(GL_RESCALE_NORMAL);
glDisable(GL_DITHER);
glCullFace(GL_BACK);
glEnable(GL_ALPHA_TEST);
}
Thats all there is to it! Your OpenGL function calls remain the same as before, theyll just
be a little bit faster. All those glDisable(), glEnable(), etc. calls look like regular function
calls, but with aglmacro.h theyre really just macros that branch directly to your renderers
internal functions.
We wont be using the macros for the remaining examples in this book since I want to
simplify things as much as possible in the code, but when you start building your own games
Id recommend you use this optimization.
So, instead of calling glColor() all the time, youll want to cache the RGBA color values in
your own variables, and then test any new color commands with those values to see if theyve
changed. If and only if theyve changed should you proceed and call glColor(). The same
goes for all glEnable() and glDisable() calls. Texture unit calls like glActiveTexture()
and glClientActiveTexture() also cause an OpenGL state change, so those should also be
cached. As a general rule, cache anything that causes an OpenGL state change.
39
The sample project OpenGL State Caching.xcode has an updated version of the OpenGL.c
source file that includes many example state-changing functions. For example, this function
is used to enable lighting:
void OGL_EnableLighting(void)
{
if (!gMyState_Lighting)
{
glEnable(GL_LIGHTING);
gMyState_Lighting = true;
}
}
The variable gMyState_Lighting is the cached lighting state. If it is already true then
theres obviously no need to re-enable the lighting since we know that its already enabled.
We only call glEnable() if gMyState_Lighting is false.
The function to disable lighting is almost identical except that it only calls glDisable() if
gMyState_Lighting is true:
Other caching functions are a little more complex, such as the function to set the current
color:
if ((r != gMyState_Color.r) ||
(g != gMyState_Color.g) ||
(b != gMyState_Color.b) ||
(a != gMyState_Color.a))
{
glColor4f(r, g, b, a);
gMyState_Color.r = r;
gMyState_Color.g = g;
gMyState_Color.b = b;
gMyState_Color.a = a;
}
}
Here we cache the RGBA values, and check each of them before we allow glColor4f() to
be called. This insures that we only call the OpenGL function if one of the color components
actually changed.
The important thing to remember when caching the state is that you must be consistent. You
cannot choose to call OGL_SetColor4f() some of the time, and then other times call
glColor4f() directly. If your cached state variables lose track of the current values then a
cascade of problems will appear as your game runs.
The preferred way to save and restore the current state is to manually do it yourself with the
use of your state caching functions. Which state parameters you save is up to you, but the
sample function below shows what I like to use in my games:
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
if (i >= STATE_STACK_SIZE)
DoFatalAlert("\pstack overflow");
gStateStack_Lighting[i] = gMyState_Lighting;
gStateStack_CullFace[i] = gMyState_CullFace;
gStateStack_DepthTest[i] = gMyState_DepthTest;
gStateStack_Normalize[i] = gMyState_Normalize;
gStateStack_Texture2D[i] = gMyState_Texture2D;
gStateStack_Fog[i] = gMyState_Fog;
gStateStack_Blend[i] = gMyState_Blend;
gStateStack_Color[i] = gMyState_Color;
gStateStack_DepthMask[i] = gMyState_DepthMask;
gStateStack_BlendSrc[i] = gMyState_BlendFuncS;
gStateStack_BlendDst[i] = gMyState_BlendFuncD;
}
As I said above, using the glPushAttrib() function is bad, however using the OpenGL
matrix push/pop calls glPushMatrix() and glPopMatrix() for preserving and restoring the
states of the matrices is perfectly fine. Just be aware that there is a limit to the matrix stacks
size, and if you exceed that size you will generate a GL_STACK_OVERFLOW error. The matrix
stacks are large enough in the Mac implementation of OpenGL that Ive never had a stack
overflow ever occur, but if you want to be safe you can determine the stack sizes for the
various matrices by doing this:
GLint modelViewStackDepth;
GLint projectionStackDepth;
glGetIntergerv(GL_MODELVIEW_STACK_DEPTH, modelViewStackDepth);
glGetIntergerv(GL_PROJECTION_STACK_DEPTH, modelViewStackDepth);
After pushing the matrices onto the matrix stack we save our current state variables into our
own private stacks. Then, to restore the state we pop all the data off of these stacks like so:
42 Chapter 4: OpenGL Optimizations
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
if (i < 0)
DoFatalAlert("\pstack underflow!");
glHint(GL_TRANSFORM_HINT_APPLE, GL_FASTEST);
Since games dont need 100% precise mathematical calculations (were not trying to put a
man on Mars here), you should always have this hint set to GL_FASTEST. Youll almost never
be able to see any visual difference with this, but on extremely rare occasions a vertex might
be out of place by 1 pixel a fair price to pay for a performance boost.
Normals
When you have a scaling component in your Model-View matrix (such as from a glScale()
call), any vertex normals will get scaled during the transform calculations. This would cause
your lighting to become distorted, so we always want vertex normals to be normalized to a
length of 1.0. Luckily, OpenGL can re-normalize any vertex normals after a transformation,
but doing so hurts performance. To enable this feature youd do this:
glEnable(GL_NORMALIZE);
All of your 3D model data should have vertex normals that have been pre-normalized.
Therefore, if you know that you are not doing any scaling on a model when you render it then
you can tell OpenGL to disable the automatic re-normalization code:
glDisable(GL_NORMALIZE);
glDisable(GL_RESCALE_NORMAL);
44 Chapter 4: OpenGL Optimizations
If it is necessary to scale a model, you can still get some optimization by just enabling
GL_RESCALE_NORMAL:
glEnable(GL_RESCALE_NORMAL);
What GL_NORMALIZE does is to entirely re-normalize the vector a costly calculation, but
GL_RESCALE_NORMAL simply resizes the normal back to a length of 1.0. This is faster than
doing a full-fledged normalize on that vector, but it only works when the scaling is uniform.
That means that the x. y, and z components of the scale must all be the same value. If youre
doing a non-uniform scale then the only option is to let OpenGL do a full vector normalize by
enabling GL_NORMALIZE. So, you have to be smart in your code and know when to enable and
when to disable these various forms of normalizing states to get the best performance.
Colors
Always be sure to have this code in your draw context initialization:
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
This allows you to use glColor() to set the states color instead of using the slower
glMaterial() function.
Reading Pixels
You should never ever read data out of the draw buffer or the z-buffer! Doing this
causes the entire rendering pipeline to stall big-time, thus killing your performance. You can
literally chop your games frame rate in half by reading a single pixel from these buffers.
Remember that the 3D hardware is still rendering your geometry long after youre done
submitting it; therefore, if you try to read a pixel from the frame buffer, OpenGL has to sit
there and wait for the scene to finish drawing before it can return the pixel value to you.
OGLTextureCoord uvs[4] =
{
0,0, 0,1, 1,1, 1,0
};
OGL_SetColor4f(1,1,1,1);
glBegin(GL_QUADS);
glTexCoord2f(0,1); glVertex2f(-1, -1);
glTexCoord2f(1,1); glVertex2f(-1, 1);
glTexCoord2f(1,0); glVertex2f(1, 1);
glTexCoord2f(0,0); glVertex2f(1, -1);
glEnd();
}
46 Chapter 4: OpenGL Optimizations
The code for drawing Vertex Arrays in listing 4-7 appears much more complex than the code
using glBegin() in Listing 4-8, however, if you count the number of OpenGL function calls
youll see that its actually less in the Vertex Array code. Despite this fact, using glBegin()
is still more efficient because of the way that OpenGL deals with vertex array data. Inter-
nally, there is more overhead with Vertex Arrays.
There is another optimization for Vertex Arrays that is the mother lode of all OpenGL
optimizations called Vertex Array Range. However, this optimization is a huge topic so it
has its own chapter in this book. For more information see Chapter 6.
Optimizing VRAM
A well-optimized piece of code may still run unexpectedly slow if youre trying to cram too
much texture data into a video card with too little VRAM. If youre rendering a complex
scene into a large frame buffer then you might not be leaving enough VRAM for all of the
scenes textures to fit. This causes textures to dynamically get paged in and out of VRAM,
and paging anything into VRAM is slow.
So, its important for our game to know how much VRAM it has to work with:
port = CGDisplayIOServicePort(CGMainDisplayID());
classCode = IORegistryEntryCreateCFProperty(port,
CFSTR(kIOFBMemorySizeKey),
kCFAllocatorDefault,
kNilOptions);
/* EXTRACT VALUE */
if (CFGetTypeID(classCode) == CFNumberGetTypeID())
{
CFNumberGetValue(classCode, kCFNumberSInt32Type, &gDisplayVRAM);
}
else
{
47
With the value gDisplayVRAM you can make tweaks to your game based on various VRAM
quantities. For example, in Nanosaur 2 we limit the maximum screen resolution that the user
can choose based on this value. If the display only has 32MB of VRAM then we limit the
maximum resolution to 1024x768 to insure that there will be enough VRAM remaining to
hold textures. Additionally, we avoid loading certain large textures into VRAM at all if the
display is below a certain threshold, so things like sky clouds dont get drawn in low-VRAM
situations.
49
Not all PowerPC chips come with AltiVec, however. The G4 and G5s do, but the G3s and
earlier do not, so we must check for the presence of AltiVec when we launch our game and
then handle the matrix multiplies in either the traditional way or with AltiVec if it is present.
To determine if AltiVec is available we need a function like this:
if (!Gestalt(gestaltNativeCPUtype, &response))
{
// skip if on G3 or go into this if > G3
if (response > gestaltCPU750)
{
// see if AltiVec available
Gestalt(gestaltPowerPCProcessorFeatures,(long *)&flags);
gAltiVec = ((flags & (1 <<
gestaltPowerPCHasVectorInstructions)) != 0);
}
}
}
50 Chapter 5: PowerPC Math Optimizations
You might think that the first Gestalt() test isnt needed, but it is. Testing for vector
instructions on old Macintoshes tends to generate errors or incorrect results, so we first check
for PowerPC 750s which are the G3s. Only if the CPU is newer can we test for the pres-
ence of vector instructions.
Next we need to write our Matrix Multiply function, but first be sure that you have AltiVec
enabled in Xcode:
We must do the AltiVec and float versions of the matrix multiply in separate functions. This
is very important! Mixing AltiVec and regular floating-point code in the same function causes
massive stack thrashing which kills any performance we hope to gain here. The AltiVec
version is shown below:
/* LOAD MATRIX A */
A1 = vec_ld( 0, (float*) mA );
A2 = vec_ld( 16, (float*) mA );
A3 = vec_ld( 32, (float*) mA );
A4 = vec_ld( 48, (float*) mA );
B1 = vec_ld( 0, (float*) mB );
B2 = vec_ld( 16, (float*) mB );
B3 = vec_ld( 32, (float*) mB );
B4 = vec_ld( 48, (float*) mB );
Knowing how the above code actually works is not really important since its unlikely that
youll ever use AltiVec for anything else in your game, but if you do want to learn more
about AltiVec programming then Apple has lots of information here:
http://developer.apple.com/hardware/ve/
What is important, however, is knowing that the OGLMatrix4x4 structure must be aligned to
16-bytes since all AltiVec memory accesses are 16-byte aligned. To do this we build the
matrix structure as a union with an AltiVec vector type:
typedef union
{
GLfloat value[16];
vector float v[4];
}OGLMatrix4x4;
With this structure we can still access the individual matrix elements which are 32-bit float
values, yet the entire structure is guaranteed to always be 16-byte aligned so that we can work
with it using AltiVec.
The drawback, however, is that frsqrte only calculates an estimate value which is not
particularly precise. Luckily, there is a method for improving the accuracy of the results
called Newton-Rhapson refinement, and the following example shows how this all works:
float len;
float isqrt, temp1, temp2;
len = (x * x) + (y * y) + (z * z);
outV->x = x * len;
outV->y = y * len;
outV->z = z * len;
}
Even with the extra overhead of the Newton-Rhapson refinement, this method of normalizing
vectors is up to 16x faster than the more accurate method with the sqrt() function. I cannot
stress enough, however, that you should not use this for calculations that require high
accuracy. For example, when I was developing the game Enigmo I couldnt understand
why the collision physics were behaving so strangely when water droplets would bounce off
of objects. After a few hours of investigating I discovered that the anomaly was a result of
using frsqrte to normalize the bounce vectors during the collision detection. This caused a
domino effect of errors in the physics math. This optimization is best used for normalizing
things like vertex normals and other things that dont need high accuracy.
To use the frsqrte opcode (or any other PowerPC intrinsic) like we did in Listing 5-4,
youve got to include a special header file in your Xcode project:
#include <ppc_intrinsics.h>
This wasnt necessary with CodeWarrior since the intrinsics were built into the CodeWarrior
compiler, but with Xcode you need to include that header since its actually a collection of
macros that build the intrinsics as assembly code. This file is included in our sample codes
precompiled header file, MyPCH.pch.
54 Chapter 5: PowerPC Math Optimizations
It should also be noted that the frsqrte opcode does not exist on the PowerPC 601, but if
youre still writing games to run on a PowerPC 601 then you should seek professional
therapy.
55
If youre experienced with OpenGL then you should already be familiar with Vertex Arrays
since those are the preferred way to submit geometry meshes for rendering. A small example
using Vertex Arrays was shown in Listing 4-7. The new optimization is called Vertex Array
Range, otherwise known as VAR, and the idea behind this optimization is very simple. By
marking a Vertex Arrays data as accelerated, any rendering done with that data gets a
massive speed boost. Its that simple.
Marking a range of memory as VAR data, speeds up the time it takes OpenGL to transfer that
data to the video card for processing. You may not realize it, but a huge amount of time is
spent during the rendering process to dump the thousands of points, normals, texture UVs,
and vertex colors to the video card. You probably thought that it was the pixel drawing that
took so long, but in reality its the uploading of the raw data thats the speed killer.
The concept behind this optimization is easy, but doing a good implementation of it does take
some effort, so were going to cover the whole enchilada step by step. The sample project
named Vertex Array Range.xcode contains all of the code in this chapter, and demonstrates
how to use it to draw an object on the screen. All of the new code dealing with VAR is in the
source file VertexArrayRange.c.
glGenVertexArraysAPPLE(NUM_VERTEX_ARRAY_RANGES,
&gVertexArrayRangeObjects[0]);
Not all hardware actually supports the VAR feature, so the first thing the initialization
function does is check if VAR is supported. To determine this we simply get the current
OpenGL Extensions list, and then look for the string GL_APPLE_vertex_array_range. 3D
hardware at least as new as the Nvidia GeForce 2MX or ATI Radeon support VAR, but older
cards like the ATI Rage 128 do not.
Were going to need a function that we can use to assign blocks of memory to our VAR
engine:
gVertexArrayRangeData[whichVAR].rangeSize = size;
gVertexArrayRangeData[whichVAR].dataBlockPtr = data;
gVertexArrayRangeData[whichVAR].forceUpdate = true;
}
size = gVertexArrayRangeData[i].rangeSize;
if (size == 0)
continue;
if (!gVertexArrayRangeData[i].forceUpdate[i])
continue;
glBindVertexArrayAPPLE(gVertexArrayRangeObjects[i]);
if (!gVertexArrayRangeData[i].activated)
{
glVertexArrayRangeAPPLE(size,
gVertexArrayRangeData[i].dataBlockPtr);
glVertexArrayParameteriAPPLE(
GL_VERTEX_ARRAY_STORAGE_HINT_APPLE,
GL_STORAGE_SHARED_APPLE);
58 Chapter 6: Vertex Array Range
glFlushVertexArrayRangeAPPLE(size,
gVertexArrayRangeData[i].dataBlockPtr);
glEnableClientState(GL_VERTEX_ARRAY_RANGE_APPLE);
gVertexArrayRangeData[i].activated = true;
}
else
{
glFlushVertexArrayRangeAPPLE(size,
gVertexArrayRangeData[i].dataBlockPtr);
}
gVertexArrayRangeData[i].forceUpdate = false;
}
}
There are lots of important things to discuss about this update function. First it determines if
a VAR needs to be updated by checking a flag that we set, and if it does then it calls
glBindVertexArrayAPPLE() to make that VAR object active:
glBindVertexArrayAPPLE(gVertexArrayRangeObjects[i]);
If this is the first time that this VAR is being updated then we have some special initialization
to do to it. Its here that we finally get around to telling OpenGL about the block of memory
that we wanted accelerated:
glVertexArrayRangeAPPLE(size, gVertexArrayRangeData[i].dataBlockPtr);
This tells OpenGL that the currently active VAR object uses this block of memory. The
block of memory is defined by the pointer to the start of the memory block and the size to
indicate the number of bytes in the block.
The next step is to tell OpenGL what kind of VAR this is: shared or cached. In our case, we
set it to shared:
glVertexArrayParameteriAPPLE(GL_VERTEX_ARRAY_STORAGE_HINT_APPLE,
GL_STORAGE_SHARED_APPLE);
59
Ill discuss the differences between shared and cached VAR data in a moment, but first lets
see how we complete our update function. After marking our VAR as shared we call
glFlushVertexArrayRangeAPPLE() to get OpenGL to do its magic:
glFlushVertexArrayRangeAPPLE(size,
gVertexArrayRangeData[i].dataBlockPtr);
This flush call is very important! It basically primes the data thats in our VARs memory
block, and if you dont do this flush youll likely get a bunch of garbage rendered instead of
your model.
After doing this we activate this VAR object with a call to glEnableClientState(). Thats
it! Were ready to start submitting geometry whose data resides in our VAR memory, and
itll get drawn super-fast.
Once weve done all that setup the first time though, we only need to make one call to update
the VAR the next time around:
glFlushVertexArrayRangeAPPLE(size,
gVertexArrayRangeData[i].dataBlockPtr);
Here, the flush function simply lets OpenGL know that the data in our VAR memory has
changed since the last time we rendered with it, so OpenGL can do whatever internal updat-
ing it needs to do. Once again, failure to do this flush will result in garbage being drawn,
therefore, it is important to always do this update whenever data in your VAR memory block
has changed, even if its only one byte thats different.
then OpenGL has to re-upload it each time you make a modification, hence, more hiccups in
your game.
So, shared mode is the way to go, but if you do want to experiment with the cached mode you
would need to pass GL_STORAGE_CACHED_APPLE to glVertexArrayParameteriAPPLE()
instead of GL_STORAGE_SHARED_APPLE. Then, to do an update (which is very expensive in
cached mode), you would do this:
glVertexArrayRangeAPPLE(0, nil);
glVertexArrayRangeAPPLE(size,
gVertexArrayRangeData[i].dataBlockPtr);
glFlushVertexArrayRangeAPPLE(size,
gVertexArrayRangeData[i].dataBlockPtr);
If you read Apples documentation about VARs you would be lead to believe that updating a
cached VAR is exactly the same as updating a shared VAR just use the
glFlushVertexArrayRangAPPLE() call. However, this is not true. There was a bug in Mac
OS 10.2.x which caused that flush call to basically have no effect on cached VAR memory,
so, to be 100% certain that your update works we have to do a hard reset of the VARs
memory allocation. Passing 0 into glVertexArrayRangeAPPLE() effectively kills that VAR,
and then we reset it with another call to glVertexArrayRangeAPPLE(). This is yet another
reason to avoid using cached VARs.
typedef struct
{
OGLPoint3D points[8];
OGLColorRGBA colors[8];
GLint quads[6];
}CubeDataType;
To mark the cubes Vertex Array data for VAR use well need to assign that block of RAM
to our VAR engine:
OGL_AssignVertexArrayRangeMemory(sizeof(CubeDataType),
&gCubeMesh,
0);
The final step in this whole process is to bind the VAR object, and submit the geometry. So,
before making any of the usual Vertex Array calls, we need to do this:
glBindVertexArrayAPPLE(gVertexArrayRangeObjects[0]);
glBindVertexArrayAPPLE(gVertexArrayRangeObjects[0]);
glDisableClientState(GL_TEXTURE_COORD_ARRAY); // no uv's
glDisableClientState (GL_NORMAL_ARRAY); // no normals
You wont be able to notice any performance change with this simple VAR sample applica-
tion because were not drawing enough geometry to be able to tell, but the benefits become
amazingly obvious once your game starts drawing lots of geometry data in the thousands or
tens of thousands of vertices per frame range. Most high-end games on the Mac wouldnt be
able to function without using VAR to get massive performance boosts.
Luckily, not all is lost. We can still modify our mesh data via three different methods:
glFinish(), Fencing, and double-buffering.
glFinish
This is not a viable solution to the problem, but it is useful for debugging. If you need to
modify some geometry data, you can always call glFinish() since that will wait until the
video card is done drawing the scene. At that point you know its safe to modify the data in
the VAR memory. Never ever use this option for anything but debugging since it totally kills
any performance gained by using VAR in the first place!
Fencing
OpenGL has a feature called fencing which is a way of inserting a marker in the render
queue that lets you know when something is done drawing. This is like a localized version
of glFinish() except that it lets you wait around only until a specific piece of geometry is
done instead of the entire scene. In a typical application, fences wont totally kill the per-
formance gained from using VARs, but they do come close since they still result in a stall.
However, there are times when fences are the only solution to the problem.
63
An OpenGL fences is another type of object just like texture objects or even VAR objects, so
they have a glGen() call to create them:
glGenFencesAPPLE(1, &gMyFenceObject);
To use this fence object we need to insert it into the rendering queue immediately after weve
submitted the geometry that were interested in:
glSetFenceAPPLE(gMyFenceObject);
If we want to modify the geometry data that we just submitted then we make the following
call to wait for it to finish rendering:
glFinishFenceAPPLE(gMyFenceObject);
As you can see, using fences is very easy and its better than doing a glFinish(), but we still
have one better option
Double-Buffering
To get the maximum performance from your game engine using VAR youre going to have to
double-buffer any geometry data that you make modifications to on a regular basis. Double-
buffering your geometry means that youve got to have two completely separate copies of it,
and each copy needs to go into a different VAR object. That way, you can submit copy A
for rendering while you edit copy B for the next frame. Then, on the next frame you
submit copy B while you edit copy A.
Obviously, this uses a lot of RAM. The reason that Nanosaur 2 required so much memory to
run compared with our older games was simply because of the huge amount of double-
buffered geometry data that that game needed. Double-buffering probably increased that
games memory footprint by over 100MB, but the speed increase we got from it was well
worth it.
Most of the geometry in your scene is going to be static, meaning that you wont ever be
physically modifying it. But there are lots of cases where you may be modifying data such as
character animation and particle effects. As you design your game engine you will need to
come up with a system for organizing your vertex arrays. My games use a very complex
system that allows me to put static geometry into simple VARs while putting dynamic
geometry into double-buffered VARs. The VAR system presented in the sample project
64 Chapter 6: Vertex Array Range
here is a good starting point. You can build on top of that to make a more powerful system to
meet your specific needs.
65
Calculating the frame rate is as simple as calling the following function once before every
frame of animation in your game loop:
loop:
/* GET CURRENT TIMER */
currTime = UpTime();
goto loop;
gOneOverFramesPerSecond = 1.0f/gFramesPerSecond;
}
The function UpTime() returns the amount of time since your Mac has been booted. To
determine how much time has passed since the last time CalcFramesPerSecond() was called
we simply subtract the previous timer value from the current timer value. A quick call to
AbsoluteToNanoseconds() will convert the delta value into nanoseconds so that we can
easily work with it. Dividing the nanoseconds by kSecondScale gives us our frames-per-
second value.
This gOneOverFramesPerSecond is what youll be using almost all the time in any game
physics. The reason we calculate this value is because multiplying by a pre-calculated value
1.0/n is always faster than dividing by n. For example, the value of t is the same in each
line below, but the second line is much more efficient:
This limits the minimum frame rate to some default value. The reason for doing this is that in
real-world games things can break if the frame rate drops too low. For example, in Enigmo
we had to limit the minimum frame rate to about 13fps because at frame rates lower than that
the water droplets would move so far from frame to frame that the collision detection and
physics response system would start to break down. In other games such as Nanosaur 2, if
the frame rate drops too low then objects can move through other solid objects for the same
reasons. This is a common problem in almost all games, so the easy solution is to determine
what the minimum frame rate is before things start to break and then make sure you never
drop below that. In reality the game will still be running at a lower frame rate, but as far as
your physics are concerned youre running at that minimum DEFAULT_FPS value. Things on
the screen will begin to look like they are moving in slow motion, but at least nothing breaks.
67
Similarly, we also should check for frame rates that are too high. Now you might be wonder-
ing whats wrong with high frame rates? Isnt that what we want? Well, yes and no. The
fact of the matter is that as the frame rate increases the deltas of the game physics values will
start to become very small. They can get so small that they cannot accurately be represented
in a 32-bit float value. Values approaching 0.00001 will have worse and worse accuracy, and
will eventually completely break down and be considered to be 0.0 by the FPU. So, you
either have to use floating point doubles instead of single precision floats, or you just
have to be careful to be sure that no critical motion values get too small.
My favorite example of floating point breakdown is in the game Carmageddon. This game
was written in the days of fairly slow hardware - typically in the 180mhz range. The pro-
grammers never had anything super-fast to test the game on back when they wrote it, so they
had no way of knowing what would happen when faster machines did come out. Well, I
found out. When I bought my G4/400mhz I noticed that Carmageddon would exhibit some
very strange behavior in the physics: the motion became very erratic and other related visual
errors became noticeable. Then, when I bought a G4/1000mhz the super-high frame rate in
the game was causing the physics to completely malfunction. Apparently the game had lost
all floating-point precision, and it rendered the game completely unplayable.
Since there really isnt any point in having a game run faster than the refresh rate of the
display its running on, I like to limit my games in the range of 85 to 120 fps. To do this
limiting we essentially have to slow down our game by just sitting in a loop until its time to
go:
The sample project Frames Per Second.xcode uses our new frames per second calculation
to display the frame rate on the screen, and also to rotate the colored cube at an even speed.
In previous projects, this cube would spin at different speeds depending on the speed of your
computer because we were simply rotating the cube by 0.1 degrees each frame. By using the
gOneOverFramesPerSecond value we can adjust that rotation speed to be the same no matter
what the frame rate is.
68 Chapter 7: Calculating the Frame Rate
As you see, we can easily tell the cube to rotate on the x-axis at a rate of 300 degrees per
second, and on the y-axis at 100 degrees per second. Multiply any constant value by
gOneOverFramesPerSecond to get its per-frame value.
69
To fade the screen 50%, we would change the displays gamma curve like so:
Implementing gamma fades in your game code is very easy, and there are two ways to do it:
You can either use the Core Graphics function CGSetDisplayTransferByFormula() or
CGSetDisplayTransferByTable().
70 Chapter 8: Gamma Fades
CGSetDisplayTransferByFormula
This is the easiest way to change the gamma of your display, and the first step is to grab the
displays current gamma values so we know from what values to start fading:
gGammaBrightness = 1.0;
}
All this initialization function does is read the current gamma settings with a single call to
CGGetDisplayTransferByFormula() which returns the min, max, and gamma value of the
red, green, and blue channels. The global variable gGammaBrightness holds the current fade
value for our game, so we initialize it to 1.0. This value will range from 0.0 to 1.0 depending
on how much we want our gamma faded, as youll see below.
To fade the screen to black, all we have to do is lower the Max values of the RGB channels,
and this is done by calling CGSetDisplayTransferByFormula(). For example, to set the
gamma to 50% brightness as shown in Figure 8-2, we simply do a call like this:
CGSetDisplayTransferByFormula(gCGDisplayID,
&gGammaRedMin,
&gGammaRedMax * 0.5f,
&gGammaRedGamma,
&gGammaGreenMin,
&gGammaGreenMax * 0.5f,
&gGammaGreenGamma,
&gGammaBlueMin,
&gGammaBlueMax * 0.5,
&gGammaBlueGamma);
Heres a function that does a fade-out over time using our FPS calculation that we learned in
the previous chapter:
71
SetGammaFade(gGammaBrightness);
/* DECAY BRIGHTNESS */
CalcFramesPerSecond();
gGammaBrightness -= gOneOverFramesPerSecond / fadeDuration;
SetGammaFade (0);
gGammaBrightness = 0;
}
The GammaFadeOut() function is a loop that decays our global gGammaBrightness value over
a specified duration, making use of the gOneOverFramesPerSecond. At the end of the
function we make sure that the gamma is totally black by forcing a brightness of 0.0. The
function that physically sets the gamma brightness is SetGammaFade():
CGSetDisplayTransferByFormula(gCGDisplayID,
gGammaRedMin, redMax, gGammaRedGamma,
gGammaGreenMin, greenMax, gGammaGreenGamma,
gGammaBlueMin, blueMax, gGammaBlueGamma);
}
72 Chapter 8: Gamma Fades
This single function is what all of our other gamma utility functions will ultimately call to
make any gamma changes. If we want to do a fade-in while our game is playing, then well
need a new fade-in function that we can call on each pass through our main loop:
SetGammaFade(gGammaBrightness);
}
}
By calling GammaFadeInOneFrame() from our games animation loop we will get a nice
scene fade-in transition when our game starts up. However, since our application doesnt
start dimmed out, weve got to turn the gamma brightness all the way down before entering
the main animation loop. To instantly darken the gamma we just do this:
gGammaBrightness = 0;
SetGammaFade(gGammaBrightness);
The sample project Gamma Fades.xcode contains a full set of these gamma-fading func-
tions in the source file Screen.c. When you run this sample application youll see the
spinning cube fade in as its animating, and when you click the mouse button, youll see a
fade out done with our GammaFadeOut() function.
As you may have realized by now, you can write specialized fade functions to do color fades
and not just these black fades. The CGSetDisplayTransferByFormula() function takes
separate red, green, and blue values, so if you wanted to do a fade to red you would simply
decay the green and blue channels leaving only red.
73
CGSetDisplayTransferByTable
The other way to modify the displays gamma is with CGSetDisplayTransferByTable().
This function lets you specify the entire 256-entry gamma table for the RGB channels. What
this means is that you can specify an exact gamma curve if you wanted to do some really
funky gamma effects. The CGSetDisplayTransferByFormula() function that we used
earlier essentially does linear gamma curves, so you dont really have much control over it,
but the Table method gives you total control. The fact of the matter is, however, that youre
unlikely to ever need or have any desire to use your own custom gamma tables unless youve
got some bizarre visual effect in mind.
One last thing to note about gamma fades: if your display is faded out then you wont be able
to see anything in your debugger. When I know Im going into a debug session, I always
disable my gamma fading functions. For that matter, when Im debugging I usually run the
game in windowed mode, where I dont do any gamma fades at all because a gamma fade
affects the whole screen, not just the window that were rendering into.
75
We can install our own Carbon Event Handlers to manage many things in our game, and in
this chapter were going to learn how to use them to process our games main loop, and also
how to process its main menu. In Chapter 11 well see how to use events generated by the
keyboard and mouse to do simple forms of input.
if (gMainLoopTimer != nil)
DoFatalAlert("\p timer already set!");
mainLoop = GetMainEventLoop();
eventLoopTimerUPP = NewEventLoopTimerUPP(myMainLoopFunc);
InstallEventLoopTimer(mainLoop,
kEventDurationNoWait, // delay before first shot
kEventDurationMillisecond, // delay until next shot
eventLoopTimerUPP, // which event loop timer to install
nil, // no user data
&gMainLoopTimer); // returnedtimer reference
/* CLEANUP */
DisposeEventLoopTimerUPP(eventLoopTimerUPP);
}
Every application has a Carbon Event main loop by default. Dont get confused between
the two different loops that were both calling main loop. Theres the applications Carbon
Event Loop which is automatically processed by the OS, and then theres our games own
main loop which is our code that moves objects and draws them. To get a reference to the
applications Carbon Event main loop, we do this:
mainLoop = GetMainEventLoop();
Next, we create a new Timer event that will trigger a callback to our games main loop code:
77
eventLoopTimerUPP = NewEventLoopTimerUPP(myMainLoopFunc);
To install this Timer into the Carbon Event main loop we call InstallEventLoopTimer()
which takes several input parameters that define how the Timer will function:
kEventDurationNoWait
This tells the Event Manager that we want our Timer event to call our callback function
immediately.
kEventDurationMillisecond
This tells the Event Manager to trigger the Timer event once every millisecond if possible. If
the CPU is still in our main loop callback after one millisecond has expired, then theres no
way that another event will get triggered, but as soon as we exit our main loop code and
return control to the Event Manager, it will issue another callback to us.
if (Button())
QuitApplicationEventLoop();
Once weve set all of this up we need to give control of our application to the OS. The
Carbon Event Manager will have complete control, and it will issue callbacks to our main
loop as our Timer Event fires away. To get the ball rolling we do this:
First we create the main loop timer callback by calling our SetMyMainLoopEventTimer()
function. Then we enter the applications event loop via RunApplicationEventLoop(). As
soon as RunApplicationEventLoop() is called our Timer event will start calling
MyMainLoop(). When MyMainLoop() returns after processing one frame of animation,
control returns to the OS until the Timer fires again.
Once RunApplicationEventLoop() is called it does not return until our callback function
calls QuitApplicationEventLoop(), but our Timer event is still installed, so we need to
remove it:
The sample project Carbon Events.xcode shows the spinning cube that were familiar with,
but this time the animation is running entirely off of one of these Carbon event timers.
Menu Bars
If your game were running full-screen then there obviously wouldnt be any use in having a
menu bar since it wouldnt be visible. However, if your game supports a windowed mode
then its a good idea to have a menu bar, and luckily Carbon Events and Interface Builder
make supporting menus very easy.
The first step in supporting menus is to build the menu bar in Interface Builder. The
Game.nib file in the Carbon Events.xcode project has a new menu bar resource:
79
By default, Mac OS X treats the first menu in the list as the application menu which means
that certain things will happen to it when you see it in the program. In Figure 9-1 you can see
that we named the first menu My App Menu, and it contains just two menu items: About
My App and Preferences, but that is not exactly what will appear in the game:
Figure 9-2: The Application Menu is automatically given our applications name
Mac OS X automatically renames the application menu to our applications actual file name
regardless of what youve named it in Interface Builder. So, we see Carbon Events instead
of My App Menu. Additionally, the OS attaches several default menu items to this
application menu. The first two menu items are the ones from our resource, but the addi-
tional ones are all default menu items added by Mac OS X, including the Quit menu item.
80 Chapter 9: Carbon Events
As you know from Chapter 2, resources built with Interface Builder all operate off of four-
character command values, so we assign commands to each menu item that we want to
handle. In Figure 9-1 you can see that we have assigned the command pref to the menu
item for Preferences. The command quit is assigned by the OS to the Quit menu item that it
created. These commands are handled by a new event handler that we install into our
application, but first we need to load our menu bar resource and make it active. This is all
done with one call:
SetMenuBarFromNib(gNibs, CFSTR("MainMenu"));
The name of the menu resource that we want to load is passed in, and OS X takes care of the
rest. The menu bar will appear at the top of the screen, and the user will be able to navigate
though it and make selections. This magical, automatic handling of menu bars is one of the
things that the Event Manager does for us when weve called RunApplicationEventLoop()
to process our games main loop. In addition to handling those Timer events that we in-
stalled, it also handles menu clicks and things of that nature.
Even though we can navigate through our menu at this point, selecting a menu item has no
effect because we havent written any code to handle menu selection events. To do that we
need to install an event handler that will receive and process the menu item commands:
/*******************************************/
/* LOAD AND SET MENU BAR FROM OUR NIB FILE */
/*******************************************/
/************************/
/* CREATE EVENT HANDLER */
81
/************************/
gMyEventHandlerUPP = NewEventHandlerUPP(MyEventHandler);
InstallEventHandler(GetApplicationEventTarget(),
gMyEventHandlerUPP,
1,
events,
nil,
&gMyEventHandlerRef);
}
The OS will handle most of the events that occur in the system, so all we want to do is handle
the command events those events that have four-character command signatures. There-
fore, we define only one type of event in our EventTypeSpec array:
switch (command.commandID)
{
case 'quit': // Quit menu item
gQuitApplication = true;
result = noErr;
break;
default:
result = eventNotHandledErr;
}
return(result);
}
One of the cool things about Carbon Events is evident when you select the Say Hello menu
item in the Special menu. This brings up a dialog that says Hello, and even as this dialog is
displayed and you manipulate it, the cube continues to spin in the background because our
Timer events keep firing.
Figure 9-3: Spinning cube in the background while an Alert dialog comes up
As cool as that is, youd normally want your game to pause when any dialogs come up like
that, so in your games you might make some modifications. In MyEventHandler(), do this:
if (gPauseGame)
return;
The main loop callback will still occur, but it will simply bail out without actually doing
anything, thus, giving the illusion that the game has paused.
On Mac OS 9 there was a system function named AutoSleepControl() that you could call
that would prevent the machine from going to sleep. Unfortunately, this call does absolutely
nothing on OS X, so if you were thinking about using it, think again. Instead, we have a
different way to prevent the Mac from going to sleep on OS X:
UpdateSystemActivity(UsrActivity);
Calling UpdateSystemActivity() about once every 30 seconds should prevent the computer
from going to sleep by tricking it into thinking there was some user activity. I like to insert
this code into the OGL_DrawScene() function, but you can stick it anywhere inside the game
loop that you like. The chunk of code looks like this:
timer -= gOneOverFramesPerSecond;
if (timer < 0.0f) // see if time to update system activity
{
UpdateSystemActivity(UsrActivity);
timer = 30.0f; // reset timer to 30 seconds
}
85
Luckily, the future is starting to look a little brighter now that Apple has formally adopted
OpenAL as the high level sound API for OS X. This API is very efficient and very powerful,
so game programmers should now be able to start supporting cool audio effects in their
games. Ill be discussing OpenAL in detail in the final section of this chapter, but first Im
going to discuss some other ways of producing sound effects and music.
To get started with Quicktime, we need to make one initialization call in our applications
startup code:
EnterMovies();
Before we discuss how to actually load a sound file into Quicktime for playback, we first
need to have a short discussion on Files with Mac OS X.
86 Chapter 10: Audio
Figure 10-1: The Resources folder in the app bundle is where the Data resides
Nobody in his or her right mind would actually use stdio in a real Mac application, so we
wont even discuss that here. That leaves us with the option of using FSSpecs or FSRefs.
Well, the fact of the matter is that well want to use both. FSRefs are the most modern way
of accessing files since they support Unicode and long filenames, but FSSpecs are still used
by much of the OS including calls to Quicktime.
void GetMyApplicationResourcesFolder(void)
{
CFBundleRef appBundle;
CFURLRef appResourcesURL;
appBundle = CFBundleGetMainBundle();
appResourcesURL = CFBundleCopyResourcesDirectoryURL(appBundle);
if (appResourcesURL == nil)
DoFatalAlert(\pYou dont seem to have a Resources folder);
CFURLGetFSRef(appResourcesURL, &gMyResourcesFolderFSRef);
FSGetCatalogInfo(&gMyResourcesFolderFSRef,
kFSCatInfoNone, // no catinfo needed
nil,
nil,
&gMyResourcesFolderFSSpec, // fsspec to save into
nil);
/* A TRICK TO GET THE VOLUME & DIRECTORY IDS INTO THE FSSPEC */
iErr = FSMakeFSSpec(gMyResourcesFolderFSSpec.vRefNum,
gMyResourcesFolderFSSpec.parID,
"\p:Resources:Game.nib",
&gMyResourcesFolderFSSpec);
if (iErr != noErr)
DoFatalAlert("\perror converting FSRef to FSSpec");
The first part of Listing 10-1 is fairly straightforward. We get the applications main bundle
with a call to CFBundleGetMainBundle(), and then we pass that bundle reference to
88 Chapter 10: Audio
This next part is a bit of a trick. When we call FSGetCatalogInfo() we get an FSSpec back,
but its not quite the FSSpec that we want. The vRefNum and parID (volume and directory
ID) fields are not set to the Resources folder. Instead, they are set to the Resources folders
parent folder, and the name field of the FSSpec is Resources. What we want is for the
vRefNum and parID fields to point directly to the Resources folder. Heres how we can do
that:
FSMakeFSSpec(gMyResourcesFolderFSSpec.vRefNum,
gMyResourcesFolderFSSpec.parID,
"\p:Resources:Game.nib",
&gMyResourcesFolderFSSpec);
When we call FSMakeFSSpec() to create a new FSSpec, we pass the volume and directory
IDs of that parent folder that we found, along with a pathname to a file that we know exists
in our Resources folder (in this case Game.nib). Upon return, gMyResourcesfolderFSSpec
contains the volume and folder IDs of the Resources folder. Mission accomplished!
iErr = FSMakeFSSpec(gMyResourcesFolderFSSpec.vRefNum,
gMyResourcesFolderFSSpec.parID,
songFileName,
&spec);
if (iErr)
DoFatalAlert("\pSong file not found");
CloseMovieFile(myRefNum);
SetMoviePlayHints(gSongMovie,
0, // turn these hints off
hintsUseSoundInterp|hintsHighQuality);
if (loopFlag)
{
// get timebase of movie
TimeBase timebase = GetMovieTimeBase(gSongMovie);
StartMovie(gSongMovie);
gSongPlayingFlag = true;
gLoopSongFlag = loopFlag;
SetPort(oldPort);
}
The first thing that PlaySong() does is call FSMakeFSSpec() to create a FSSpec for the audio
file we want to play:
91
iErr = FSMakeFSSpec(gMyResourcesFolderFSSpec.vRefNum,
gMyResourcesFolderFSSpec.parID,
songFileName,
&spec);
Here we pass in the volume and directory IDs of our Resources folder that we found earlier
along with the filename of the audio file. The result is a new FSSpec that we will soon pass
to Quicktime.
Even though we are playing an audio file, Quicktime treats everything as a Quicktime
Movie, and since Quicktime movies play into GrafPorts, we need to create an empty
dummy GrafPtr to use. This code will create a new GrafPtr, and then it sets it as the
current port after backing up the existing port.
Then we create a new Movie from the file, and then close the file since its not needed
anymore:
CloseMovieFile(myRefNum);
This doesnt seem like the logical thing to do, does it? Why would we want to close the
movie file before weve even played it? Well, the NewMovieFromFile() call actually creates
a new internal reference to that open file, so when we close it theres still an open reference to
the file in the movie object. In other words, the file isnt really completely closed.
Technically, were ready to play the movie now, but there are several things we can do to try
and improve playback performance. Quicktime will normally stream data from the file as it
plays it back, but if we pre-load the entire sound file into memory, then the playback will be
faster. There are also some hints we can give Quicktime about how to play the movie which
may also improve performance, and well do those first:
92 Chapter 10: Audio
SetMoviePlayHints(gSongMovie, 0,
hintsUseSoundInterp | hintsHighQuality);
if (loopFlag)
SetMoviePlayHints(gSongMovie, hintsLoop, hintsLoop);
The function SetMoviePlayHints() can be used to turn various flags on and off. In our code
were turning off sound interpolation and turning off high quality mode. In theory this will
improve performance of the sound playback at the cost of some audio quality, but in practice
Ive never noticed it to make any perceptible difference at all either in sound quality or in
performance. Nonetheless, just to be safe I always clear those flags.
Similarly, we use SetMoviePlayHints() to then set the hintsLoop flag which is supposed to
be a hint to Quicktime that this movie is going to loop. Unfortunately, Ive never found this
to make any difference since with or without this flag there is always a short delay between
loops of the movie as Quicktime rewinds it. Regardless, I always set this flag in the off
chance that Apple some day makes it work correctly.
We want to pre-load the entire movie into RAM, and to do this we first gather some informa-
tion about the Quicktime movie that were playing:
This information is used in the following calls to do the pre-load and the pre-roll. Pre-loading
the movie places all of the data into RAM while pre-rolling the movie then causes Quicktime
to pre-initialize everything it needs to play the movie.
There are two parts of a Quicktime movie: the Movie and the Media. We pre-load both into
RAM with the LoadMovieIntoRam() and LoadMediaIntoRam() calls. You can think of the
Movie as the header data, and the Media as the actual sound wave information.
One last thing that we need to do is to set up a callback function to handle looping. Even
though we set the hintsLoop flag earlier, this does not actually cause a movie to loop when it
reaches the end. Unfortunately, all looping in Quicktime needs to be handled manually, but
at least we can ask Quicktime to issue a callback when the movie reaches the end. Four
function calls are needed to create this callback and install it into the movie that were about
to start playing:
timebase = GetMovieTimeBase(gSongMovie);
gMovieCallback = NewCallBack(timebase, callBackAtExtremes);
gMovieCallbackUPP = NewQTCallBackUPP(EndOfSongCallback);
CallMeWhen(gMovieCallback,
gMovieCallbackUPP,
0,
triggerAtStop,
0,0);
GetMovieTimeBase() extracts some timing and identity information out of the movie, and
then we pass that to NewCallBack() along with the constant callBackAtExtremes to define
what kind of callback were creating. Our callback function is defined with
NewQTCallBackUPP() and then CallMeWhen() is used to tell Quicktime to issue the callback
when the movie stops. Well go into the details of this callback in a few pages.
Finally, were ready to play the music, so we make one last call to Quicktime to get it going:
StartMovie(gSongMovie);
Calling MoviesTask() at such a high rate can be very difficult sometimes, especially if you
want your music to keep playing during level loading when youre not in any kind of loop in
which you could continuously call it. I used to just insert MoviesTask() calls all over the
place in my code, so that it would constantly get called during file loading and initialization
functions. However, this began to break down when we were working on Nanosaur 2.
Nanosaur 2 used some huge textures, and lots of them. As the textures were read in from the
data files, they would get pre-loaded into VRAM by a glBindTexture() call, but as VRAM
94 Chapter 10: Audio
started to fill up it would take OpenGL longer and longer to complete this upload. On
machines with very low VRAM it would sometimes take as long as 3 seconds to complete a
glBindTexture() call. Unfortunately, during this time there was no way to call
MoviesTask(), so the music would stutter horribly.
After working with Apple for a solid 24 hours on this problem, we came to the only solution
which seemed to work: wed have to create a separate code thread whos sole purpose in life
was to call MoviesTask() at the constant rate of 10 times per second. Threads are part of the
Macs multi-tasking capabilities that allow different pieces of code run in different threads.
To update MoviesTask() we are going to create a pthread since theyre easy to set up and
easy to use.
Somewhere in your games InitAudio() function youll need to put this line of code:
err = pthread_create(&myThread, nil, MySongThread, nil);
That simple call to pthread_create() is all that is needed to start a new processor thread to
our function MySongThread(), but things do get a little trickier inside that threads function:
if (!gInQuicktimeFunction)
{
if (gSongMovie)
{
gInMoviesTask = true;
95
MoviesTask(gSongMovie, 0);
gInMoviesTask = false;
}
}
MPDelayUntil(&expTime);
}
return 0;
}
Unlike Timer events that we discussed in Chapter 9, threads are not continuously called by
some thread manager somewhere. Instead, this Thread function is called just once, so we
must stay inside of it for as long as we want the thread running. The OS will allocate CPU
time to each thread running, so just think of the thread as its own little application running
all by itself.
We dont want our thread to use up any more CPU time than is necessary, therefore, rather
than just sitting in a tight loop constantly calling MoviesTask() we will call it once and then
tell the OS to go work on other treads until we need to call it again. MoviesTask() only
needs to be called about 10 times per second, so to regulate that we essentially set an alarm
clock that will wake us up when its time to call MoviesTask() again. To set the alarms
wakeup time, we get the current system clock time with UpTime() and then add the equiva-
lent of 1/10th of a second to it:
The value expTime now contains the system clock time when well want our alarm to wake
us up. MoviesTask() is called, and then we turn on our alarm clock by calling
MPDelayUntil():
MPDelayUntil(&expTime);
This tells the CPU to go off and do other tasks until the system clock reaches our calculated
alarm time. When that time is reached, MPDelayUntil() will exit, and our thread will once
again have control and the whole process repeats. If we did not include this delay in our
thread loop, the OS would assume that this thread needs a huge amount of CPU time, so it
would try to give it all it could. As a result, everything else on the computer would slow
down, so calling MPDelayUntil() is critically important.
Threads are pieces of code that are running in parallel with other parts of your program, so if
we were to call MoviesTask() from our thread while the main application thread was making
96 Chapter 10: Audio
some other Quicktime call, it could result in a crash. We get around this problem by setting
flags to indicate that Quicktime is busy, therefore, we shouldnt try to make any other
Quicktime calls until its safe.
First we set the gInQuicktimeFunction flag to true so that our thread wont call
MoviesTask() while were doing this. Then we check if MoviesTask() was already being
called. If so, then we just sit in a while loop until its done.
Note that these flags must be declared as volatile variables. This is critical! These vari-
ables are being modified off of an interrupt, so they can change at any moment. By making
them volatile, the compiler will generate code that insures that the values are re-loaded
from memory every time theyre referenced.
Looping a Song
Earlier in this chapter we discussed setting up a callback function to handle the looping of our
Quicktime movie to cause the song to repeat, so now its time to talk details. MoviesTask()
will cause our callback function EndOfSongCallback() to be called when it detects that the
song has ended. It is important to remember that were only calling MoviesTask() from
MySongThread(), and not all Mac OS calls are thread-safe. For this reason, we want to limit
what we do inside of our callback:
97
GoToBeginningOfMovie() will rewind the song back to the beginning, and then
StartMovie() will cause it to continue playing. In between those calls we call
CallMeWhen() to reinstate our callback. Once the callback has occurred, it wont get trig-
gered again unless we reinstate it each time.
Note that we do not need to do any of our safety checks with the gInQuicktimeFunction or
gInMoviesTask flags because we already did this safety check before we called EnterMov-
ies(), so were still safe in our callback which was called from inside EnterMovies().
The nice thing about this method of looping our game music is that it happens automatically
since its running on that dedicated thread. However, our thread is only updating
th
MoviesTask() about 10 times per second which means that there could be as much as a 10
of a second from the time a song ends to the time we become aware of that and try to rewind
it. This is not the sole cause of the delay that Quicktime experiences when looping audio, but
it certainly contributes to it.
Your game will be running at a rate higher than 10 frames per second (hopefully), so you can
choose to manually check the movie status in the games main loop with a test like this:
gInQuicktimeFunction = true;
while(gInMoviesTask);
if (IsMovieDone(gSongMovie))
{
98 Chapter 10: Audio
GoToBeginningOfMovie(gSongMovie);
StartMovie(gSongMovie);
}
gInQuicktimeFunction = false;
This may trim a fraction of a second off of the delay between loops in your song, but theres
still going to be a delay. For this reason, I wouldnt worry about doing this extra test.
The Quicktime Music.xcode sample project demonstrates all of this code, and it includes
several more utility functions for working with Quicktime. See the new source file Audio.c.
Sound Channels
The Sound Manager works on the concept of Sound Channels. A channel plays a single
digital audio sample, and you can set the pitch, volume, and other parameters of the channels
playback. When we initialize our game, we need to initialize all of the sound channels that
we think well need. If you think your game will need to play up to 30 simultaneous sound
effects, then initialize 30 sound channels. Allocating and initializing a sound channel would
normally be a very simple thing to do, but since were writing a game and we need maximum
performance, things get a little more complicated.
short i;
ExtSoundHeader sndHdr;
const double rate = rate44khz;
SndCommand mySndCmd;
SndChannelPtr channel;
/***************************/
/* MAKE DUMMY SOUND HEADER */
/***************************/
//
// This mimics the header of a sound file with no data.
// it is needed below to issue sound commands to optimize
// the channels.
//
sndHdr.samplePtr = nil;
sndHdr.sampleRate = rate44khz;
sndHdr.loopStart = 0;
sndHdr.loopEnd = 0;
sndHdr.encode = extSH;
sndHdr.baseFrequency = 0;
sndHdr.numFrames = 0;
sndHdr.numChannels = 2;
dtox80(&rate, &sndHdr.AIFFSampleRate);
sndHdr.markerChunk = 0;
sndHdr.instrumentChunks = 0;
sndHdr.AESRecording = 0;
sndHdr.sampleSize = 16;
sndHdr.futureUse1 = 0;
sndHdr.futureUse2 = 0;
sndHdr.futureUse3 = 0;
sndHdr.futureUse4 = 0;
sndHdr.sampleArea[0] = 0;
/*********************/
/* ALLOCATE CHANNELS */
/*********************/
//
// The initNoInterp command used above is ignored by
// the Sound Manager. To actually cause initNoInterp
// to work, we must do it this way:
//
gNumChannels = i;
}
The first thing this function does is to define a dummy sound header. Sound effects will have
headers on them that contain all the important information about the sound such as size,
frequency, loop points, etc. We will use this dummy sound header for the sole purpose of
issuing some sound commands to initialize certain Sound Channel parameters, but first we
create a new sound channel:
The constant sampledSynth tells the Sound Manager that this sound channel plays sampled
digital data (as opposed to algorithmically generated sounds). Notice that we pass in the
initStereo+initNoInterp init flags, but due to a bug in the Sound Manager these flags are
usually ignored.
SndNewChannel() also takes a pointer to a callback function that is used to loop sound
effects. Looping sound effects with the Sound Manager is very similar to looping music with
Quicktime as we discussed earlier in this chapter. Luckily, however, with the Sound Man-
ager the audio will loop seamlessly instead of having a short pause between loops. In older
101
versions of the Sound Manager, manual looping was not necessary. The Sound Manager
used to recognize the loop data embedded in the sound header, therefore, it would automati-
cally loop any effects that needed it. But something broke in OS 9 that resulted in the need
for these callbacks to manually handle looping effects.
After weve allocated our new sound channel, we need to re-initialize it since the init flags we
passed to SndNewChannel() were ignored. To do this we attach our dummy sound header to
the sound channel, and then issue a reInitCmd command to set the flags we want. The
dummy header is attached to the channel like this:
mySndCmd.cmd = soundCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = (long)&sndHdr;
SndDoImmediate(channel, &mySndCmd);
Then the re-initialize command is issued and we pass in those initialization flags:
mySndCmd.cmd = reInitCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = initNoInterp | initStereo;
SndDoImmediate(channel, &mySndCmd;
Sound channels work by issuing commands to them. There are commands to set the play-
back frequency, change the volume, stop the channel, start the channel, etc. But for now we
just need to issue two commands:
soundCmd
This command attaches a sound to the channel. In our case above, were attaching a dummy
sound defined by our dummy header.
reInitCmd
This command reinitializes the sound channel with the flags in param2. There must be a
sound attached to the channel before this command will work. Thats why we issue the
soundCmd first.
theCmd.cmd = bufferCmd;
theCmd.param1 = 0;
theCmd.param2 = cmd->param2;
SndDoCommand (chan, &theCmd, true);
// Just reuse the callBackCmd that got us here in the first place
Listing 10-8 shows how our callback function works. It sends a bufferCmd to the sound
channel to cause the sound to play from the beginning, and then it puts the current command
(the command which caused the callback) back into the channel so that well get called again
the next time the sound needs to loop.
Sound Resources
There are a number of ways to load sound effects for the Sound Manager to use, but the
easiest and cleanest method is to use old-fashioned Sound Resources. Apple frowns on
modern OS X applications having any files with resource forks, but since they havent
provided us with any better way to store and access the hundreds of sound effects that our
games will need, feel free to use them. The only downside to using Sound Resources is that
no modern audio application supports them, so to work with them youve got to either write
your own utility for copying sound files into resources, or youve got to use something like
SoundEdit 16 to do it for you.
SoundEdit 16 is an old but wonderful sound effect editing application. It does pretty much
everything that Ive ever needed for any sound effects in my games, so I still use it to this
day, and its the only Classic application I ever have to run.
103
Figure 10-2: SoundEdit 16 with the sound resources from Enigmo open.
Using sound resources in our game engine is really easy. When the game starts up we need
to pre-load all of the sound resources into memory with a function like this:
UseResFile(refnum);
/****************************/
/* LOAD EACH SOUND RESOURCE */
/****************************/
if (gSndHandles[i] == nil)
DoFatalAlert("\pGetResource failed!");
DetachResource((Handle)gSndHandles[i]);
HNoPurge((Handle)gSndHandles[i]);
HLockHi((Handle)gSndHandles[i]);
GetSoundHeaderOffset(gSndHandles[i], &gSndOffsets[i]);
}
CloseResFile(refnum);
}
The code in Listing 10-8 is pretty standard Resource Manager stuff that youve probably seen
a million times before if youre used to programming on older versions of Mac OS. We open
the resource fork of the desired file, count the number of snd resources in the file, and then
start loading them in with GetResource(). It is important that this data doesnt move around,
so we detach the handle from the resource and then lock it down and make sure that its not
purgeable:
HNoPurge((Handle)gSndHandles[i]);
HLockHi((Handle)gSndHandles[i]);
The sound resource has some extra data in it that we dont need, so we call
GetSoundHeaderOffset() to find the offset in the resource to the start of the actual sound
header that the Sound Manager needs:
GetSoundHeaderOffset(gSndHandles[i], &gSndOffsets[i]);
Well see how this offset is used later when we actually play one of these sound effects.
105
Once weve got our sound resources loaded, its quite easy to play one of them. The first step
will be to locate an available sound channel to play it on. Weve already initialized our list of
sound channels, so the new function FindSilentChannel() will scan our list and return the
first available channel a channel that is not currently playing any sound:
if (gChannelInfo[c].isLooping)
continue;
iErr = SndChannelStatus(gChannelInfo[c].channel,
sizeof(SCStatus),
&theStatus);
if (iErr != noErr)
continue;
if (theStatus.scChannelBusy)
continue;
return(c);
}
/* NO FREE CHANNELS */
return(-1);
}
There are two reasons why we check the isLooping flag of each channel: For starters, this is
simply faster than calling SndChannelStatus(), so if a sound effect is looping then well
know right away and can avoid one extra call to that function. Additionally,
SndChannelStatus() can sometimes give incorrect information on channels that have been
looped. When we call SndChannelStatus() the scChannelBusy flag is supposed to be set to
106 Chapter 10: Audio
true if the channel is playing audio, however, this flag will sometimes appear as false if an
effect has been looped. The flag is always valid on single-shot effects, however.
sndPtr = (SoundHeaderPtr)(((long)*gSndHandles[sndNum]) +
gSndOffsets[sndNum]);
chanNum = FindSilentChannel();
if (chanNum == -1) // no free channels
return(-1);
/*********************************/
/* ISSUE COMMANDS TO THE CHANNEL */
/*********************************/
chanPtr = gChannelInfo[chanNum].channel;
mySndCmd.cmd = flushCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = 0;
SndDoImmediate(chanPtr, &mySndCmd);
mySndCmd.cmd = quietCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = 0;
SndDoImmediate(chanPtr, &mySndCmd);
//
// param2 = 32-bit word containing the right
// volume in the upper 16 bits, and the
// left volume in the lower 16 bits.
//
mySndCmd.cmd = volumeCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = (rightVolume << 16) | leftVolume;
SndDoCommand(chanPtr, &mySndCmd, true);
mySndCmd.cmd = bufferCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = sndPtr;
SndDoCommand(chanPtr, &mySndCmd, true);
mySndCmd.cmd = rateMultiplierCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = rateMultiplier;
SndDoImmediate(chanPtr, &mySndCmd);
/***********************************/
/* SEE IF THIS IS A LOOPING EFFECT */
/***********************************/
//
// We look in the sounds header to see if it loops.
//
loopStart = sndPtr->loopStart;
loopEnd = sndPtr->loopEnd;
if ((loopStart + 1) < loopEnd)
{
/* SET THE CALLBACK COMMAND */
mySndCmd.cmd = callBackCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = sndPtr;
SndDoCommand(chanPtr, &mySndCmd, true);
gChannelInfo[chanNum].isLooping = true;
108 Chapter 10: Audio
}
else
gChannelInfo[chanNum].isLooping = false;
/* SET MY INFO */
Our PlayEffect() function takes a left and right volume value and something called a rate
multiplier value which determines the frequency at which the sound is played. In the Sound.h
framework file the constant kFullVolume is defined. Passing this value to PlayEffect()
will cause the left and/or right channels to be played at their maximum volume without over-
amplifying it. If you wanted to play the sound at half volume then you would pass in
kFullVolume / 2. Or, if you did want to over-amplify the effect you could pas in
kFullVolume * 2 which would double the volume of the channel, but it would most likely
cause some waveform clipping and distortion since the sound wave is being amplified beyond
its limits.
Unfortunately, there are no built-in constants in Sound.h for the rate multiplier, so weve got
to make our own:
This rate value is a fixed-point multiplier where the lower 16-bits represent the fractional
value, so 0x10000 is essentially 1.0. If we wanted to lower the pitch by 50% then wed pass
in kNormalRate / 2. Or, to double the pitch, wed pass kNormalRate * 2.
So, to play sound effect #0 with the stereo volume shifted right, but at the default pitch, we
call our PlayEffect() function like so:
Once a sound channel is playing we can continue to send commands to it to alter the playback
parameters. Heres a function to change the volume of the channel as its playing:
109
mySndCmd.cmd = volumeCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = (rightVol << 16) | leftVol;
SndDoImmediate(chanPtr, &mySndCmd);
}
The results of a volume command are pretty much instantaneous thanks to the
SndDoImmediate() call. You can very smoothly ramp the volume of each channel up and
down as you need in your game, and it will sound very even. The same goes for changing the
pitch of the sound channel:
mySndCmd.cmd = rateMultiplierCmd;
mySndCmd.param1 = 0;
mySndCmd.param2 = rateMult;
SndDoImmediate(chanPtr, &mySndCmd);
}
mally when you drag an .rsrc file into an Xcode project it adds that file to the Targets
Resource Manager Resources build phase (see Figure 10-3):
Figure 10-3: The Sound.rsrc file in the Resource Manager Resources build phase
This wont actually work, because Xcode will not properly copy that Sound.rsrc file. The
results of this would be a corrupt resource file, an incorrectly named resource file, or both.
To force Xcode to copy any old-style resource file, you must add a new Copy Files build
phase, move the .rsrc file to that phase, and then delete the Resource Manager Resources
build phase. It should end up looking like this:
Figure 10-4: A Copy Files build phase is the only way to include .rsrc files
You create a new Build Phase by right-clicking on the Target and then selecting a new Build
Phase type from the pop-up menus list. When you create the Copy Files build phase, be sure
to specify that its Destination is set to Resources:
111
Figure 10-5: Set the Copy Files build phase destination to Resources
Compile and run the sample project. Then look in the games Special menu. Youll see
some menu items for playing different sound effects so that you can test sounds from there.
Every game that I have ever written has used the Sound Manager to play effects. Even
though Apple has essentially discontinued the Sound Manager, it is still a part of OS X and it
works great! But, if youre feeling adventurous you might want to consider using the next
technology that Im going to discuss: OpenAL.
OpenAL
OpenAL is the audio equivalent of OpenGL. It is a standardized audio library that exists on
many platforms, and has recently been adopted by Apple as the way of the future for playing
sound in applications such as games. One of the nice things about OpenAL is that unlike the
Sound Manager which just plays basic waveforms with some easy pitch and volume options,
OpenAL supports full 3D spatial placement effects like Doppler shift, reverb, etc. In theory,
if any hardware accelerated audio cards ever becomes available for the Mac then this tech-
nology should make use of it.
Thats the good news. Unfortunately, the first version of OpenAL that will be feature
complete is the upcoming version for Mac OS 10.4. The current version of OpenAL at the
time of this writing is 1.0.12 and its not very good. It only does simple attenuated and
panned 3D sounds. No Doppler shifting or anything else.
Support for full 3D sound including panning, attenuation, Doppler shifts, and even support
for multi-speaker sound systems, not just 2-speaker stereo systems.
If sound hardware ever becomes readily available for the Mac, your old games using
OpenAL may just suddenly support it.
OpenAL is standardized across many platforms, so porting your game will be much easier
since you wont have to re-write the entire sound engine.
No way to handle audio in games that have 2-player split-screen modes. OpenAL only has
one Listener so if you have more than one camera as in a split-screen game like Cro-Mag
Rally or Nanosaur 2, youre out of luck. In my games that support a split-screen mode I
manually calculate the sound volume such that each sound plays at the loudest volume of
either Player 1 or Player 2. This gives the best sound for such a situation, but OpenAL has no
means of doing this. It will only play audio based on either Player 1 or Player 2s camera, but
not both. I consider this to be a major, major oversight by the OpenAL design committee.
As of Mac OS 10.3, OpenAL is not actually a part of the OS. If your game supports
OpenAL, youve either got to include the OpenAL libraries with your games installer, or tell
the user to go download the latest OpenAL installer from www.openal.org. From a market-
ing standpoint, this is extremely bad. However, Apple will include OpenAL as part of future
versions of Mac OS X.
No support for non-zero loop-back points. OpenAL supports looping sounds, but it only
loops back to the very beginning of the sound; you cannot have a loop point thats elsewhere
in the sound like you can with the Sound Manager. Suppose you wanted to have a guitar
string effect in your game. The first part of the sound is the attack and the second part is
the sustain - the part that loops. If you looped the whole sound, it would have a Pling!
Pling! Pling! Pling! sound to it, but if you set a loop-point at the start of the sustain section,
it would have a Plinggggggggggg sound to it which is the correct behavior.
Faking panning is a hassle. Sometimes you simply want to play an effect, and have it sound
like its coming from the left speaker (such as is often done on menus screens). With the
113
Sound Manager this is as easy as just passing a volume command to the sound channel. But
OpenAL has no manual way to do this. You would actually have to play the sound in 3D
space in such a way that it does the desired panning.
Hopefully, over time many of the cons will go away as more features are added to OpenAL,
but until then youll have to consider whether to go with OpenAL or Sound Manager for your
immediate game programming needs. If your primary concern is making sure that everyone
can play your game, then just stick with the Sound Manager, but if your primary concern is
having cool 3D audio then use OpenAL. For the remainder of the sample code in this book
were going to be using OpenAL for playing sound effects, and Quicktime for streaming our
music.
Initializing OpenAL
There are four fundamental parts of OpenAL that need to be set up in order to play sounds:
the AL Context, Buffers, Sources, and the Listener. Before we proceed, we should define
what these terms mean:
AL Context
In the same way that an OpenGL context refers to the window or display that you are drawing
graphics into, an OpenAL context refers to the sound system on your computer.
Buffers
A Buffer is your sound wave data plus some state information that determines how that sound
should be played.
Sources
This is a point in your 3D world that makes sound a sound source. If there is an explosion
in your game, then youll create a Source at the coordinate of the explosion.
Listener
There is only one Listener in an OpenAL context. It represents your ears in space, which, in
a game is typically the same as the camera location.
al_device = alcOpenDevice(nil);
al_context = alcCreateContext(al_device,0);
alcMakeContextCurrent(al_context);
if (alGetError() != AL_NO_ERROR)
DoFatalAlert("\pcreating OpenAL context failed");
alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
alEnable(ALC_CONVERT_DATA_UPON_LOADING);
alSetInteger(ALC_SPATIAL_RENDERING_QUALITY,
ALC_SPATIAL_RENDERING_QUALITY_LOW);
alSetInteger(ALC_RENDER_CHANNEL_COUNT,
ALC_RENDER_CHANNEL_COUNT_STEREO);
iErr= alGetError();
if (iErr != AL_NO_ERROR)
DoFatalAlert("\psetting OpenAL state failed");
/* INIT ALUT */
alutInit(nil, nil);
}
We pass nil to alcOpenDevice() which tells OpenAL to open a connection to the default
audio device. Next we call alcCreateContext() to create our audio context reference, and
then alcMakeContextCurrent() to set it as the current context. Thats all there is to it!
Much easier than creating an OpenGL context, eh?
Now that we have an active OpenAL context we need to set up some state information just
like we would do in OpenGL. The first thing we set is the distance model. The distance
model determines how the volume of a sound will decay over distance. The best setting for
this is AL_INVERSE_DISTANCE_CLAMPED. This causes the correct decay in volume over
115
distance, but also keeps the audio from getting over amplified if the camera gets too close to
the sound Source. Its the virtual equivalent of not blowing out your eardrums.
alEnable(ALC_CONVERT_DATA_UPON_LOADING);
OpenAL on the Mac is built on top of Core Audio, and Core Audio uses floating-point values
to represent data in a digital waveform. This is very unusual since most sound data is in the
form of 8, 16, or 24-bit integer values. Converting this traditional waveform data on the fly
while a sound is playing is expensive, so by enabling ALC_CONVERT_DATA_UPON_LOADING
were telling OpenGL to do this integer to float conversion when sounds are loaded.
Games require a lot of CPU power, so sometimes sacrifices must be made in the name of
frame rate. Audio processing can be a very CPU intensive thing, especially if there are
dozens of sound effects playing simultaneously, therefore, it is wise to set the OpenAL spatial
rendering quality to the low setting:
alSetInteger(ALC_SPATIAL_RENDERING_QUALITY,
ALC_SPATIAL_RENDERING_QUALITY_LOW);
Odds are that nobody will be able to tell much difference between the low and high settings,
but the low setting will save some CPU time.
The next parameter that we set is an interesting one. Most Mac users dont have fancy
quadraphonic sound systems, but rather they have simple, stereo, 2-speaker systems. We
should let OpenAL know this:
alSetInteger(ALC_RENDER_CHANNEL_COUNT,
ALC_RENDER_CHANNEL_COUNT_STEREO);
In rare cases, however, a user might actually have a multi-speaker setup for doing true
surround sound on their computer. In this case you could pass the constant
ALC_RENDER_CHANNEL_COUNT_MULTICHANNEL to alSetInteger(). With this parameter set,
OpenAL will correctly handle these systems.
Just like in OpenGL where there is a glut (GL Utility) library, in OpenAL there is an alut (AL
Utility) library. Before making any alut calls, we need to initialize it with a call to
alutInit(). The most important alut call that well be using is alutLoadWAVFile().
116 Chapter 10: Audio
/*************************************/
/* LOAD EACH WAV INTO A SOUND BUFFER */
/*************************************/
//
// Buffers store the information about how a sound should
// be played and the sound data itself. So, here we
// essentially create a buffer and load a WAV file into it.
//
iErr = FSMakeFSSpec(gMyResourcesFolderFSSpec.vRefNum,
gMyResourcesFolderFSSpec.parID,
filenames[i],
&spec);
if (iErr != noErr)
DoFatalAlert("\pCannot find our WAV file");
/* ALLOCATE A BUFFER */
alGenBuffers(1, &gSoundBuffers[i]);
The OAL_LoadWAVFiles() function takes a list of filenames as input, and then it goes through
this list loading each WAV file into an OpenAL Buffer. The process is straightforward. First
we need to generate a full pathname to the WAV file were trying to open. Our WAV files
are in the applications Resources folder, so we create an FSSpec for the file with a simple
call to FSMakeFSSpec(). To convert this FSSpec to a text pathname we need to write a new
utility function:
The process of getting a path from an FSSpec is just a matter of first converting our FSSpec to
an FSRef with a call to FSpMakeFSRef(), and then we can get the path by calling
FSRefMakePath().
118 Chapter 10: Audio
Weve got the full pathname of the WAV file, so we pass that text string to
alutLoadWAVFile():
We dont need to know anything about the format of a WAV file since OpenAL automati-
cally takes care of everything. The format, data, size, and freq values that are returned
contain some information about the WAV file that will be needed to install it into an OpenAL
Buffer, but first we need to create the Buffer to hold all this information:
alGenBuffers(1, &gSoundBuffers[i]);
This generates one Buffer object for us which can now be loaded with data from the WAV
file:
This copies all of the WAV files data into the Buffer, so the original WAV file is no longer
needed, therefore, we unload it:
Playing a Sound
Weve now got a Sound Buffer loaded with audio data, and were almost ready to try playing
it, but first we need to tell OpenAL about our Listener. Remember that the Listener is you, or
more accurately, its the camera location. Every time we update the OpenGL camera
information in our game, we also need to update the OpenAL Listener information:
orientation.aim = *aim;
orientation.up = *up;
All OpenAL calls that affect the Listener are made through alListener() by passing in a
parameter constant followed by the value of that parameter. The most basic parameters are
the position, velocity, and orientation of the Listener. The position of the Listener is the
camera coordinate in our 3D world, the velocity is a vector representing the speed of the
camera in our 3D world, and the orientation is the cameras look-at vector and up-vector.
When we call alListener() and pass in AL_ORIENTATION, OpenAL expects the input value
to be 6 consecutive floats. Those floats make up two vectors: the first vector is the aim
or look-at vector of the Listener, and the second is the Listeners up-vector. To make things
nice and clean weve defined an OALOrientation structure:
typedef struct
{
OALVector3D aim;
OALVector3D up;
}OALOrientation;
OpenAL does not automatically calculate the Listener velocity from frame to frame. Instead,
weve got to manually calculate that in our camera update code with something like this:
Motion values are used to calculate Doppler shift, so if youre not planning on supporting
Doppler effects in your game then you really dont need to worry about the Listener velocity.
Everything is now in place for us to play some sound, so lets get to it:
alGenSources(1, &theSource);
if (alGetError() != AL_NO_ERROR)
DoFatalAlert("\palGenSources failed");
alSourcePlay(theSource);
return(theSource);
}
The first step in playing a sound is to create the OpenAL sound Source:
alGenSources(1, &theSource);
This allocates one new sound Source object for us (its kind of like allocating a new Sound
Channel with the Sound Manager). Then we need to assign a Buffer to this Source so that
OpenAL will know what sound we want this Source to play. Remember that the Buffer is
what contains our WAV files data, so by assigning it to the Source were telling the Source
to play this sound data.
121
As with the Listener, sound Sources also have a position and velocity value. The position is
used by OpenAL to calculate the volume of the sound, and the velocity is used in Doppler
shift calculations. The velocity is not needed if youre not doing Doppler shift in your game.
All calls that modify Source parameters are done with the alSource() function, so the
position and velocity are set like this:
Next, we need to set some information about how we want our sounds volume to decay with
distance from the Listener. In our OpenAL Context initialization function we told OpenAL
that we wanted to use the AL_INVERSE_DISTANCE_CLAMPED method, but we have the ability to
set specific parameters of this calculation on a per-Source basis:
The Reference Distance is the distance at which the sound is at 100% volume. If you move
the Listener farther from the Source it will start to decay in volume. The Roll-Off Factor
regulates this decay. The higher the Roll-Off Factor, the faster the volume will decay as the
Listener moves away from the Source. The lower the Roll-Off Factor, the less the volume
decays as the Listener moves away. Even though there is a precise mathematical formula
describing how these values work with the inverse distance calculation, the best way to make
things work in your game is to play it by ear. In other words, play with these values until
you get the desired volume in your game.
If you want to change the pitch or volume of a sound Source, just do this:
The input values to these calls are multipliers. So, passing a pitch of 1.0 would not modify
the sound samples playback pitch at all, but passing 2.0 would double the frequency of the
pitch. The same goes for the Gain value. Passing 0.5 would cut the volume in half, while
passing 2.0 would double it.
OpenAL can automatically handle sounds that loop. Another alSource() call is all thats
needed:
Finally, weve got our Source configured how we want it, so, to make it start playing
alSourcePlay(theSource);
Doppler Shift
In the previous pages Ive mentioned that OpenAL supports the Doppler shift effect, so now
Ill talk a little more about it. For starters, what is Doppler shift? Simply put, its the effect
that causes a freight train to sound higher pitched as it approaches, and then lower pitched as
it moves away. Sound travels at a constant speed through the air, but if a sound emitting
object is traveling toward you, the sound waves get slightly compressed which causes the
frequency to become higher. As the object moves away, the sound waves get slightly
stretched, thus, the frequency becomes lower.
alDopplerFactor(1.0);
alDopplerVelocity(1000);
The Doppler Factor value gives you a way to tweak the Doppler effect, making it more
exaggerated or less exaggerated. The value of 1.0 is the normal value, but higher numbers
will increase the effect by making the pitch of the sound raise higher and faster as objects
approach, and values below 1.0 will decrease the effect. The default value is 0.0 which tells
OpenAL not to do any Doppler shifting effects. Once you change this to a value over 0.0
Doppler calculations will kick in and youll get the effect with a small CPU cost.
The Doppler Velocity value is the speed of sound in your universe, so the value of 1000 tells
OpenAL that sound travels at 1000 units per second. You will probably need to tweak this
value quite a bit until you find the sweet spot where the Doppler shift sounds the best in your
game.
These Doppler parameters are global state settings, so once you set them it will affect all of
the sound Sources played on the current OpenAL context.
There is one major caveat to using Doppler shifts in your games sound engine: it isnt
implemented in OpenAL 1.0. All of the Doppler function calls are there in OpenAL 1.0, but
the functions dont actually work. Whats worse, they will generate an AL_INVALID_ENUM
error message even if you try to call them with legitimate values. This should be fixed in the
next major release of OpenAL.
123
The fact is that the problems with Doppler shifts in OpenAL are indicative of the 1.0 library
in general. If you just want to play simple, attenuated stereo effects it will work fine, but
honestly, you could do the same thing with a little extra coding using the Sound Manager.
Like Ive said before, if you want to be on the cutting edge of audio on the Mac then dive
right into OpenAL, but if youre more concerned about releasing a stable game that every
Mac owner can play then use the Sound Manager. When the next major release of OpenAL
is available it will likely be more robust and reliable, but the jury is still out.
125
Lets start with the keyboard. The Mac keyboard generally can only read three keys simulta-
neously, plus modifiers. That means if you hold down the A, B, C, and D keys, only three of
those four keys will register a key press. The modifier keys such as Shift, Option, Control,
and Command dont count in that three-key limit. So, for example, you can press the Option,
E Y, P, and Shift keys all at the same time and theyll all be read correctly. A common
mistake made by many Mac gamers is that they will try to assign non-modifier keys to all of
their games controls, and then they dont understand why they cant sometimes fire when
theyre moving around. You should assign modifier keys to game controls whenever
possible since they dont count in that 3-key limit.
Heres a better example of this going awry: Say you have player movement assigned to the
arrow keys and the player is moving diagonally by pressing the up and left arrow keys.
Thats two keys down. Now suppose youve got the duck key set to the D key and the fire
key set to F. Well, thats four keys total. Something has got to give, so either the player
wont be able to duck, or to shoot, or to move in the direction they wanted. The solution is to
assign the duck and shoot keys to modifier keys.
void UpdateKeyMap(void)
{
GetKeys(gKeyMap);
gOldKeys[0] = gKeyMap[0];
gOldKeys[1] = gKeyMap[1];
gOldKeys[2] = gKeyMap[2];
gOldKeys[3] = gKeyMap[3];
}
The type KeyMap is defined by the system headers as an array of four 32-bit longs which
gives us 128 bits total. The current KeyMap is acquired by calling GetKeys():
GetKeys(gKeyMap);
Next we determine which key bits are new this time by XORing the old bit-field with the
new bit-field, and then ANDing it with the new bit-field. Did I just confuse you? Lets take
a closer look at that logic with this example:
newBits = 000110010
oldBits = 100010000
new XOR old = 100100010 // these are the bits that changed
AND with newBits = 000100010 // these are the new bits
This leaves 1s in the bits of keys that are pressed now, but were not pressed previously.
With gNewKeys or gKeyMap we can easily test the value of any key. The only problem is that
we have to know which keys are assigned to which bits in this 128-bit KeyMap. Theres no
rhyme or reason to how this works since its not in any ASCII or alphabetical order. The
easiest way to deal with this is to just use a large list of constants that define the values of all
of the commonly used keys:
127
This list of constants tells us which bit to test for each key. The A key is bit #0, the Tab key
is bit #0x30, and so on. So, to test our bits well write a function like this:
We get a char pointer to our gKeyMap value and then do some fancy shifting and masking on
it. The result is either a 0 or a 1 to indicate the bit was set.
Thats all there is to it, but there are a few issues to be aware of: Unfortunately, there is a bug
in OS X (up to version 10.3 as far as I know) in which GetKeys() will simply begin to fail,
and the only way to fix it is to reboot the computer. What will happen is that certain keys
will fail to set their corresponding bits in the KeyMap bit-field. This happens about 1 in 300
times a person plays any game that uses GetKeys(). Once the failure starts, it will affect any
and all applications that use GetKeys() until the machine is rebooted.
The other issue to be aware of is that international keyboards may alter some key mappings.
The letters, numbers, and other general keys always stay the same, but some keyboards
simply dont have certain keys or do have others. For example, the tilde key doesnt exist on
the German keyboard, while the French keyboard has additional keys for their fancy accented
letters. These issues are minor and manageable, so Ive continued to use GetKeys() to some
degree in all of my games.
/*******************************************/
/* LOAD AND SET MENU BAR FROM OUR NIB FILE */
/*******************************************/
/************************/
/* CREATE EVENT HANDLER */
/************************/
gMyEventHandlerUPP = NewEventHandlerUPP(MyEventHandler);
InstallEventHandler(GetApplicationEventTarget(), gMyEventHandlerUPP,
4, events, nil, &gMyEventHandlerRef);
}
This new version of InitMyEventHandler() looks basically the same as that from Listing 9-
4. The only difference is that weve added more event types to look for:
kEventRawKeyDown
These events occur whenever a key is pressed.
kEventRawKeyUp
These events occur whenever a key is released.
kEventRawKeyModifiersChanged
These events occur when a modifier key is pressed or released.
Now we need to update our event handler function to process these keyboard events as they
come in:
eventClass = GetEventClass(event);
eventKind = GetEventKind(event);
switch (eventClass)
{
/*************************/
/* HANDLE COMMAND EVENTS */
/*************************/
131
case kEventClassCommand:
GetEventParameter(event, kEventParamDirectObject,
typeHICommand, nil,
sizeof(HICommand), nil, &command);
switch (command.commandID)
{
case 'quit': // Quit menu item
gQuitApplication = true;
result = noErr;
break;
}
break;
/**************************/
/* HANDLE KEYBOARD EVENTS */
/**************************/
case kEventClassKeyboard:
switch(eventKind)
{
case kEventRawKeyDown:
GetEventParameter(event,
kEventParamKeyMacCharCodes,
typeChar, nil,
sizeof(charCode), nil,
&charCode);
gKeyState[charCode] = true;
break;
case kEventRawKeyUp:
GetEventParameter(event,
kEventParamKeyMacCharCodes,
typeChar, nil,
sizeof(charCode), nil,
&charCode);
gKeyState[charCode] = false;
break;
case kEventRawKeyModifiersChanged:
GetEventParameter(event,
kEventParamKeyModifiers,
typeUInt32, nil,
sizeof(gModifiers), nil,
&gModifiers);
132 Chapter 11: Simple Input
break;
}
break;
}
return(result);
}
Half of this function looks the same as it did in Chapter 9, but weve added a bunch of new
code to handle our new keyboard events. First, we need to determine what kind of event our
handler has received:
eventClass = GetEventClass(event);
eventKind = GetEventKind(event);
The event class and event kind tell us exactly what event occurred. When a key-down or
key-up event occurs, we need to find out which key was pressed:
The charCode returned is the ASCII character value of the key. So if the G key was pressed,
the charCode will be G. To track which keys are pressed and not pressed, we have an
array of 256 Boolean flags. The ASCII character is an index into this array.
The modifier keys are handled separately by the kEventRawKeyModifiersChanged event, and
the value of all of the modifier keys is obtained with another GetEventParameter() call:
The gModifiers variable will contain a bit-mask where each bit represents a modifier key;
similar to how the KeyMap bits worked earlier. To test the value of a modifier key, there is a
list of constants in the system header Events.h that we can mask against:
cmdKeyBit
shiftKeyBit
alphaLockBit
optionKeyBit
controlKeyBit
rightShiftKeyBit
rightOptionKeyBit
rightControlKeyBit
So, for example, if we wanted to see if the Option key was pressed or not, we do this:
133
This method of reading the keyboard is quite common in Mac games, but as you can see it is
a lot more work than just using GetKeys(). The benefit, however, is that you get the actual
ASCII value of a key which is nice for things like High Scores screens where the user is
actually typing something.
if (!gPlayInWindow)
{
GetMouse(&pt);
*x = pt.h;
*y = pt.v;
}
/* WINDOWED MODE */
else
{
GetPort(&oldPort);
SetPort(gGameWindowGrafPtr);
GetMouse(&pt);
SetPort(oldPort);
*x = pt.h;
*y = pt.v;
134 Chapter 11: Simple Input
}
}
The call GetMouse() will return the mouse coordinates within the current GrafPort. If were
in full-screen mode then we just call this and were done, but if were playing in a window
then we need a few more lines to get the window-relative coordinates. This is done by setting
the port to our window before calling GetMouse().
mouseButtonDown = Button();
There is other mouse data that you may want to receive such as mouse delta values, or
additional buttons and scroll wheels on complex mice devices. All desktop Macs ship with
that lousy one-button mouse, but there are a lot of people out there who toss that mouse as
soon as they unpack their new computer. There are much better mice out there that have
scroll wheels and additional buttons. If youre not already using one of those mice, I highly
recommend switching to one. Reading the delta values, scroll wheel values, and additional
buttons is simply a matter of adding more Carbon Events to our event handler:
kEventClassMouse, kEventMouseMoved,
kEventClassMouse, kEventMouseDragged,
kEventClassMouse, kEventMouseUp,
kEventClassMouse, kEventMouseDown,
kEventClassMouse, kEventMouseWheelMoved
};
These five new event types let us find out all sorts of great information about the mouse:
kEventMouseMoved
This event occurs whenever the mouse moves.
kEventMouseDragged
This event occurs whenever the mouse moves while the button is held down.
135
kEventMouseDown
This event occurs when any button on the mouse is pressed.
kEventMouseUp
This event occurs when any button on the mouse is released.
kEventMouseWheelMoved
This event occurs when the scroll wheel on the mouse is spun.
We need to add some code to MyEventHandler() to take care of processing these mouse
events:
case kEventMouseMoved:
case kEventMouseDragged:
gMouseDeltaX = mouseDelta.h;
gMouseDeltaY = mouseDelta.v;
gMouseCoordX = mouseCoord.h;
gMouseCoordY = mouseCoord.v;
break;
case kEventMouseWheelMoved:
GetEventParameter(event,
kEventParamMouseWheelDelta,
136 Chapter 11: Simple Input
typeSInt32, nil,
sizeof(gMouseWheelDelta), nil,
&gMouseWheelDelta);
break;
case kEventMouseDown:
// which button was pressed?
GetEventParameter(event,
kEventParamMouseButton,
typeMouseButton,
nil ,sizeof(whichButton), nil,
&whichButton);
switch(whichButton)
{
// left button?
case kEventMouseButtonPrimary:
gMouseLeftButtonDown = true;
break;
// right button?
case kEventMouseButtonSecondary:
gMouseRightButtonDown = true;
break;
// middle button?
case kEventMouseButtonTertiary:
gMouseMiddleButtonDown = true;
break;
}
break;
/* MOUSE BUTTON UP */
case kEventMouseUp:
// which button was released?
GetEventParameter(event,
kEventParamMouseButton,
typeMouseButton,
nil ,sizeof(whichButton), nil,
&whichButton);
switch(whichButton)
{
// left button?
case kEventMouseButtonPrimary:
gMouseLeftButtonDown = false;
137
break;
// right button?
case kEventMouseButtonSecondary:
gMouseRightButtonDown = false;
break;
// middle button?
case kEventMouseButtonTertiary:
gMouseMiddleButtonDown = false;
break;
}
break;
}
break;
The function GetEventParameter() is used extensively here. When there is a mouse moved
or dragged event we call GetEventParameter() to get the mouse deltas and coordinates.
The same goes for getting the scroll wheel delta.
Technically, OS X can handle mice with up to 32 buttons, but most standard mice have
between one and three buttons, so the CarbonEvents.h header file defines constants for the
first three buttons:
kEventMouseButtonPrimary = 1,
kEventMouseButtonSecondary = 2,
kEventMouseButtonTertiary = 3
We use these constants to determine which button generated the mouse up or mouse down
event, however, you can also just pass in the numbers 1 through 32 to specify a button. How
the buttons from 4 to 32 map to any particular mouse is not defined, but for the first three
buttons the primary button is always the left button, the secondary button is always the right
one, and the tertiary is always the middle.
Some really funky mice may have more than one scroll wheel, so if you want to support this
you can find out which scroll wheel triggered the event with another call to
GetEventParameter():
EventMouseWheelAxis axis;
GetEventParameter(event, kEventParamMouseWheelAxis,
typeMouseWheelAxis, nil, sizeof(axis),
nil, &axis);
kEventMouseWheelAxisX = 0,
kEventMouseWheelAxisY = 1
It is important to mention that the delta values returned for the mouse and scroll wheel
movement are only relative to the last time that a delta event occurred. Youre responsible
for figuring out what any given delta value really means to your application. Faster machines
may be able to detect mouse movement faster, thus, there will be more kEventMouseMoved
events, and so the delta reported at each event will be smaller. Also, it is very important to
understand that your global delta variables will never go to zero because theyll always hold
the delta value from the most recent event. If the user stops moving the mouse, no event will
be generated, so, the mouse delta variables will still have whatever value was in it previously.
The best way to handle this is to process your delta values from inside the event handler
function. In other words, if you get a kEventMouseWheelMoved event, then read the delta
event and immediately process it. If the scroll wheel zooms your camera in your game, then
do that zooming now -dont even bother saving that delta in a global variable.
The sample project Simple Input.xcode contains all of this sample code and displays mouse
deltas and scroll wheel deltas on the screen.
139
Figure 12-1: The ugly yet functional Input Sprocket Configuration Dialog
So, rather than just fixing the ugly issue with Input Sprocket and moving it over to OS X,
Apple decided to kill the technology altogether. For a few years this left us with basically no
way of doing input on OS X. The answer to Input Sprocket was supposed to be this new
thing called the HID Manager (HID is an acronym for Human Interface Device). Unfortu-
140 Chapter 12: Input with the HID Manager
nately, it wasnt until Mac OS 10.2 that the HID Manager became usable, but even then, it
was so loaded with bugs that using it was difficult.
Saying that using the HID Manager is difficult is something of an understatement because
even to this day its still a nightmare for both game developers and users. The HID Manager
constantly loses devices, and gives incorrect information about devices. In some cases this
misinformation is the fault of the device itself, but Input Sprocket somehow always handled
these devices 100% correctly. The HID Managers success rate is probably only around
80%. For example, Ive yet to find a gamepad whos D-Pad gets correctly recognized by the
HID Manager as a D-Pad, and Ive only found one type of joystick whos hat switch gets
correctly identified as a hat switch. The bottom line is that the data youll get about devices
from the HID Manager is extremely unreliable, but its all weve got to work with on OS X.
This is the main reason why so many games on OS X simply dont support input devices. Its
just not worth the trouble, and it generates a huge number of tech support calls from users
who cannot get their gamepads to work as they should. If you choose to support input
devices on OS X, you can expect to be flooded with emails like this one that I coincidentally
just received as I was writing this chapter:
Sincerely,
Seth
Luckily, Ive been using the HID Manager for quite some time now, and Ive discovered
ways to work around many of these bugs with code hacks and physical coercion. For
example, I know that sometimes just plugging a device into a different USB port can fix
problems like the one Seth was seeing with his gamepad. Other times just rebooting makes
devices work again. The file Input.c in the HID Manager Input.xcode project contains an
entire input system using the HID Manager, and its whopping 3000 lines of code with lots of
special cases to handle the various bugs in the HID Manager. The HID Manager is a low-
141
level API, so, were essentially writing our own version of Input Sprocket from the ground
up.
Device
A Device is any HID compliant input device such as a gamepad, keyboard, mouse, joystick,
steering wheel, etc.
Element
An Element is generally any control on the Device which causes input. This can be a key on
a keyboard, the x-axis of a joystick, the button on a gamepad, etc. These types of Elements
are represented by the following constants:
kIOHIDElementTypeInput_Misc
kIOHIDElementTypeInput_Button
kIOHIDElementTypeInput_Axis
Elements can also be groups or collections of other elements. These types of Elements,
kIOHIDElementTypeCollection, dont actually have any physical control associated with
them, but rather theyre just a logical grouping of other controls. This makes things rather
confusing when trying to get the list of actual control Elements on a Device as youll see
later.
Usage Page
The Usage Page of a Device is just a weird way to indicate the genre or class of a Device.
This is an area where the HID Manager is poorly designed because every gamepad and
joystick that Ive ever plugged in has come up with the Usage Page of type
kHIDPage_GenericDesktop. This happens despite the fact that there are other Usage Page
types defined in IOHIDUsageTables.h that would seem to be much more game-device-
related, including one specifically called kHIDPage_Game:
kHIDPage_Simulation
kHIDPage_VR
kHIDPage_Sport
kHIDPage_Game
kHIDPage_Arcade
142 Chapter 12: Input with the HID Manager
Usage
The Usage specifies a sub-type of the Usage Page. There is a huge list of Generic Desktop
Usage constants in IOHIDUsageTables.h, but its the following constants that well primarily
be using to identify our gamepads, joysticks and such:
kHIDUsage_GD_Mouse
kHIDUsage_GD_Joystick
kHIDUsage_GD_GamePad
kHIDUsage_GD_Keyboard
kHIDUsage_GD_MultiAxisController
void InitMyHIDManagerStuff(void)
{
short i;
io_iterator_t hidObjectIterator = nil;
IOReturn ioReturnValue = kIOReturnSuccess;
/*******************************/
/* BUILD A LIST OF HID DEVICES */
/*******************************/
gNumHIDDevices = 0;
FindHIDDevices(gHID_MasterPort, &hidObjectIterator);
if (hidObjectIterator == nil)
143
ParseAllHIDDevices(hidObjectIterator);
IOObjectRelease(hidObjectIterator);
ResetAllDefaultControls();
}
This function starts off by creating a connection to the I/O Kit in the Mac OS X:
IOMasterPort(bootstrap_port, &gHID_MasterPort);
hidMatchDictionary = IOServiceMatching(kIOHIDDeviceKey);
ioReturnValue = IOServiceGetMatchingServices(masterPort,
hidMatchDictionary, hidObjectIterator);
if ((ioReturnValue != kIOReturnSuccess) ||
(*hidObjectIterator == nil))
{
DoFatalAlert("\pFindHIDDevices: No matching HID devices
found!");
}
}
hidMatchDictionary = IOServiceMatching(kIOHIDDeviceKey);
ioReturnValue = IOServiceGetMatchingServices(masterPort,
hidMatchDictionary, hidObjectIterator);
Youll notice that we check both the ioReturnValue and hidObjectIterator to make sure
we actually got something. Youd be amazed how often this fails because the HID Manager
will suddenly stop functioning, and wont find your mouse or keyboard or anything else
plugged in. If this happens, the only solution is to reboot your computer. I get customers
emailing me with support questions about this all the time, so when it happens I just tell them
that everything should work after a reboot. However, rebooting does not always fix the
problem. Sometimes you have to physically unplug the keyboard, and plug it into a different
USB port before the device will be seen again. A customer of mine had this happen just the
other day, and he was lucky because the USB swapping worked for him. But there are times
when rebooting and swapping USB ports still doesnt work. I have yet to find a solution to
this rare case, but users who experience this have reported that later in the day or the next
day, the devices will start to be seen by the HID again.
Ok, weve now got the master list of all HID devices available on this computer, so now we
need to scan this list, and keep only the Devices that we want our game to use:
/***************************************************/
/* ITERATE THRU ALL OF THE HID OBJECTS IN THE LIST */
/***************************************************/
result = IORegistryEntryCreateCFProperties(hidDevice,
&properties,
kCFAllocatorDefault,
kNilOptions);
object = CFDictionaryGetValue(properties,
CFSTR(kIOHIDPrimaryUsagePageKey));
if (!object)
{
// on 10.3 this is probably a legit error, but on 10.2 it's
// probably the keyboard
if (gPantherOrBetter)
goto error;
}
else
{
if (!CFNumberGetValue(object, kCFNumberLongType, &usagePage))
{
if (gPantherOrBetter)
goto error;
else
usagePage = kHIDPage_GenericDesktop;
}
}
object = CFDictionaryGetValue(properties,
CFSTR(kIOHIDPrimaryUsageKey));
if (!object)
{
if (gPantherOrBetter)
goto error;
}
146 Chapter 12: Input with the HID Manager
else
{
if (!CFNumberGetValue(object, kCFNumberLongType, &usage))
{
if (gPantherOrBetter)
goto error;
else
usage = kHIDUsage_GD_Keyboard;
}
}
object = CFDictionaryGetValue(properties,
CFSTR(kIOHIDLocationIDKey));
if (!object)
locationID = 0xdeadbeef;
else
{
if (!CFNumberGetValue(object, kCFNumberLongType, &locationID))
{
locationID = 0xdeadbeef;
}
}
/***********************************************/
/* SEE IF THIS IS A DEVICE WE'RE INTERESTED IN */
/***********************************************/
switch(usagePage)
{
case kHIDPage_GenericDesktop:
switch(usage)
{
case kHIDUsage_GD_Joystick:
case kHIDUsage_GD_GamePad:
case kHIDUsage_GD_Keyboard:
allowThisDevice = true;
break;
}
break;
}
/***********************************/
/* ADD THIS HID DEVICE TO OUR LIST */
/***********************************/
if (allowThisDevice)
{
147
This function is riddled with error checking and hacks to handle the bugs in older versions of
the HID Manager on OS 10.2.8 and earlier. Youll notice that we check the variable
gPantherOrBetter quite often. This variable is set in our applications initialization func-
tion, and it is set to true if were running on Mac OS 10.3 or later. This is because many
HID Manager bugs were fixed in 10.3, but we still have to work around these bugs in 10.2.
To scan through our list of Devices we use the IOIteratorNext() function which returns a
device reference in the form of an io_object_t. With this device reference we can easily
generate a Core Foundation Dictionary that contains a list of all of the parameters describing
the device:
IORegistryEntryCreateCFProperties(hidDevice,
&properties, kCFAllocatorDefault, kNilOptions);
Its really easy to extract all sorts of information about each device by making calls to
CFDictionaryGetValue(). The first thing we want to do is get the Usage Page of this
device:
object = CFDictionaryGetValue(properties,
CFSTR(kIOHIDPrimaryUsagePageKey));
kIOHIDTransportKey
kIOHIDVendorIDKey
kIOHIDVendorIDSourceKey
kIOHIDProductIDKey
kIOHIDVersionNumberKey
kIOHIDManufacturerKey
kIOHIDProductKey
kIOHIDSerialNumberKey
kIOHIDLocationIDKey
kIOHIDDeviceUsageKey
148 Chapter 12: Input with the HID Manager
kIOHIDDeviceUsagePageKey
kIOHIDDeviceUsagePairsKey
kIOHIDPrimaryUsageKey
kIOHIDPrimaryUsagePageKey
kIOHIDMaxInputReportSizeKey
kIOHIDMaxOutputReportSizeKey
kIOHIDMaxFeatureReportSizeKey
If the value returned is nil then that means that CFDictionaryGetValue() was unable to
find that key anywhere in the devices Dictionary. Unfortunately, there was a major bug in
the version of the HID Manager prior to Mac OS 10.3 where the keyboard device might be
missing all or some of its standard key values, including the critical information such as the
Usage Page and Usage you know, that little thing that helps us determine what the device
actually is.
If were on Mac OS 10.2 and we get an error from one of these CFDictionaryGetValue()
calls, its safe to guess that this device is actually a keyboard. Theres no way to know for
sure, but it probably is. So, when an error occurs we just fake it with a hack to force the
Usage Page and Usage to kHIDPage_GenericDesktop and kHIDUsage_GD_Keyboard respec-
tively.
Another piece of information that we extract from the Device is its Location ID via
kIOHIDLocationIDKey. The Location ID uniquely identifies this devices location on the
USB chain. The reason this value is important is because the user might have two or more
totally identical devices plugged in, and we need a way to differentiate between them. For
example, I have two identical Saitek gamepads plugged in. They both have 100% identical
information such as Page Usage, Page, Device Name, Manufacturer Name, etc. The only
way to tell them apart is by their location ID. This is useful when saving and restoring user
settings for the different devices.
Once weve gotten the Usage Page and Usage values, we check them to see if theyre what
were looking for. In our engine were only looking for Generic Desktop devices which are
keyboards, gamepads, or joysticks. If the device meets our criteria then we add it to our list
of devices that were interested in. Otherwise, we skip it.
d = gNumHIDDevices;
/*********************************/
/* GATHER SOME INFO THAT WE NEED */
/*********************************/
/***********************/
/* SAVE INFO INTO LIST */
/***********************/
/****************************/
/* RECURSIVELY ADD ELEMENTS */
/****************************/
RecurseDictionaryElement(properties, CFSTR(kIOHIDElementKey));
MyOpenHIDDeviceInterface(hidDevice,
&gHIDDeviceList[d].hidDeviceInterface);
gNumHIDDevices++;
}
Once again, this function is loaded with hacks to work around the various bugs in the HID
Manager. The function starts out by getting the name of the device:
Unfortunately, there will be random times when the name string simply isnt in the devices
property list. Its rare, but it does happen. Also, some older USB devices dont have product
names in their ROMs, so this will also fail in those cases. Such a device is the Ariston Ares
joystick. So, if we do get an error trying to extract the name string from the device, then we
just set it to some default value like Unnamed Device.
Next, we get some additional information about the device that is useful for identifying it
when we save and restore the users configuration settings. However, these values will
randomly fail, especially in older versions of the HID Manager. The Vendor ID and Product
ID values seem to be especially prone to failure if the device happens to a PowerBook
keyboard. So, we check for these failures, and if one occurs we just set the IDs to some
arbitrary value.
151
After saving all of the devices information into our gHIDDeviceList array, the next step is
to recursively scan the device for all of its control Elements by calling our
RecurseDictionaryElements() function. Well discuss this in a moment, but for now lets
look at the next line of code from Listing 12-4:
CreateHIDDeviceInterface(hidDevice,
&gHIDDeviceList[d].hidDeviceInterface);
Creating an interface to the device is a fancy way of saying that were simply connecting to
that device, thus, making it available to our application. In more complex terms, the device
interface provides jump pointers to functions that your application can use to access it.
ioReturnValue = IOCreatePlugInInterfaceForService(hidDevice,
kIOHIDDeviceUserClientTypeID,
kIOCFPlugInInterfaceID,
&plugInInterface, &score);
if (ioReturnValue != kIOReturnSuccess)
DoFatalAlert("\pIOCreatePlugInInterfaceForService failed!");
plugInResult = (*plugInInterface)->QueryInterface(plugInInterface,
CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID),
(LPVOID)hidDeviceInterface);
if (plugInResult != S_OK)
DoFatalAlert("\pCouldnt create HID class device interface");
(*plugInInterface)->Release(plugInInterface);
This function is perhaps the most cryptic function in our entire input engine, and frankly its
not important that you understand what its doing. Just know that its opening up a connec-
tion to the Device so that we can read data from it later.
To extract a list of elements from the device weve got to parse through it recursively:
153
One of two things happens in this function: If the Element passed in is an Array then we
need to parse that array for all of the Elements inside it. Otherwise, if the Element is a
Dictionary, then we try to add that Element to our list of Elements for the Device. The
physical control Elements that were looking for are always Dictionary Elements.
range.location = 0;
range.length = CFArrayGetCount(object);
Core Foundation does the parsing of the array somewhat automatically for us. We simply
pass it the array object, the number of elements in the array to parse, and a pointer to a
callback function. Then Core Foundation will parse the array for us, and call the
MyCFArrayCallback() function for each element it finds in the array. Since we know that
only Dictionary elements are actual input device controls, we toss out anything else, but if we
come across a Dictionary we try to add it to our list.
/*******************************************************/
/* FIRST DETERMINE IF THIS IS AN ELEMENT WE CARE ABOUT */
/*******************************************************/
elementType = GetCFNumberFromObject(object);
switch(elementType)
{
case kIOHIDElementTypeInput_Misc:
case kIOHIDElementTypeInput_Button:
case kIOHIDElementTypeInput_Axis:
break;
default:
goto skip_element;
}
/*************************************************************/
/* THIS IS AN ELEMENT WE LIKE, SO EXTRACT THE IMPORTANT DATA */
/*************************************************************/
155
object = CFDictionaryGetValue(dictionary,
CFSTR(kIOHIDElementCookieKey));
cookie = (IOHIDElementCookie)GetCFNumberFromObject(object);
object = CFDictionaryGetValue(dictionary,
CFSTR(kIOHIDElementUsagePageKey));
usagePage = GetCFNumberFromObject(object);
if (usagePage == kHIDPage_PID)
goto skip_element;
if (usagePage == kHIDPage_LEDs)
goto skip_element;
object = CFDictionaryGetValue(dictionary,
CFSTR(kIOHIDElementScaledMinKey));
scaledMin = GetCFNumberFromObject(object);
object = CFDictionaryGetValue(dictionary,
CFSTR(kIOHIDElementScaledMaxKey));
scaledMax = GetCFNumberFromObject(object);
/*********************/
/* SAVE ELEMENT INFO */
/*********************/
d = gNumHIDDevices;
e = gHIDDeviceList[d].numElements;
if (e >= MAX_HID_ELEMENTS)
goto skip_element;
gHIDDeviceList[d].elements[e].elementType = elementType;
gHIDDeviceList[d].elements[e].cookie = cookie;
gHIDDeviceList[d].elements[e].usagePage = usagePage;
gHIDDeviceList[d].elements[e].usage = usage;
gHIDDeviceList[d].elements[e].min = min;
gHIDDeviceList[d].elements[e].max = max;
gHIDDeviceList[d].elements[e].scaledMin = scaledMin;
gHIDDeviceList[d].elements[e].scaledMax = scaledMax;
strncpy(gHIDDeviceList[d].elements[e].name, elementName,
ELEMENT_NAME_MAX_LENGTH); // copy device name string
gHIDDeviceList[d].numElements++;
/**********************************/
/* TRY TO SUB-RECURSE THE ELEMENT */
/**********************************/
skip_element:
RecurseDictionaryElement(dictionary, CFSTR(kIOHIDElementKey));
}
Thats a pretty huge chunk of code, so here goes the explanation. Before we can add the
Element to our list, weve got to make sure its an element that we care about, so we get the
type out of the Dictionary:
switch(elementType)
{
case kIOHIDElementTypeInput_Misc:
case kIOHIDElementTypeInput_Button:
case kIOHIDElementTypeInput_Axis:
break;
default:
goto skip_element;
}
Obviously we want to keep all Button and Axis control elements, but we also want to keep
the Misc types too because those tend to be joystick hat switches.
Next we gather all sorts of information about the Element in a similar manner to how we
gathered information about Devices earlier. And just like Devices, Elements also have their
own Usage Page and Usage values. The Usage Page for an Element identifies the specific
type of control that it is. The ones we care about are:
kHIDPage_GenericDesktop
kHIDPage_KeyboardOrKeypad
kHIDPage_Button
For unknown reasons, the HID gods didnt add a Usage Page type to identify axes, sliders,
hat switches, start buttons, D-Pads, etc. Instead they just grouped all of those into the
kHIDPage_GenericDesktop which doesnt really make any sense (welcome to the HID
Manager).
Anyway, there are a few Usage Page types that we need to specifically look for and elimi-
nate:
if (usagePage == kHIDPage_PID)
goto skip_element;
if (usagePage == kHIDPage_LEDs)
goto skip_element;
These tend to come up for keyboard devices and cause all sorts of problems if you dont skip
them. Yes, even the LEDs on your keyboard are considered HID elements, and if you dont
toss them here theyll appear to be keys.
158 Chapter 12: Input with the HID Manager
Now we gather some additional information that tells us about the minimum and maximum
values of the control. Different joysticks, for example, will have different ranges of their axis
values. Some may go from 0 to 255 as you move the joystick left to right, while higher-end
ones may go from 1024 to +1024. So, its important to know the range so that we can
calibrate the controls, and scale them to numbers our game can use later.
The next step is important, and it is one of the big headaches of the HID Manager. That step
is the act of trying to figure out the name of the Element.
Only in very rare cases does the HID Manager successfully return the names of the controls
on any given device, so, weve got to manually assign them in a new function,
CreateElementNameString(). We pass the Usage Page and Usage values to this function,
and based on those values it will return a string containing the name to give the control.
switch(usagePage)
{
/**************************/
/* GENERIC DESKTOP DEVICE */
/**************************/
case kHIDPage_GenericDesktop:
switch(usage)
{
case kHIDUsage_GD_X:
c = "X-Axis";
break;
case kHIDUsage_GD_Y:
159
c = "Y-Axis";
break;
case kHIDUsage_GD_Z:
c = "Z-Axis";
break;
case kHIDUsage_GD_Rx:
c = "Rotate X-Axis";
break;
case kHIDUsage_GD_Ry:
c = "Rotate Y-Axis";
break;
case kHIDUsage_GD_Rz:
c = "Rotate Z-Axis";
break;
case kHIDUsage_GD_Slider:
c = "Slider";
break;
case kHIDUsage_GD_Dial:
c = "Dial";
break;
case kHIDUsage_GD_Wheel:
c = "Wheel";
break;
case kHIDUsage_GD_Hatswitch:
c = "Hat Switch";
break;
case kHIDUsage_GD_Start:
c = "Start";
break;
case kHIDUsage_GD_Select:
c = "Select";
break;
case kHIDUsage_GD_DPadUp:
c = "D-Pad Up";
break;
case kHIDUsage_GD_DPadDown:
c = "D-Pad Down";
break;
case kHIDUsage_GD_DPadRight:
160 Chapter 12: Input with the HID Manager
c = "D-Pad Right";
break;
case kHIDUsage_GD_DPadLeft:
c = "D-Pad Left";
break;
}
break;
/***********/
/* BUTTONS */
/***********/
case kHIDPage_Button:
if (usage < 30)
c = buttonNames[usage-1];
else
c = "Button";
break;
/*******************/
/* KEYBOARD DEVICE */
/*******************/
case kHIDPage_KeyboardOrKeypad:
switch(usage)
{
case kHIDUsage_KeyboardA:
c = "A";
break;
case kHIDUsage_KeyboardB:
c = "B";
break;
case kHIDUsage_KeyboardC:
c = "C";
break;
case kHIDUsage_KeyboardD:
c = "D";
break;
case kHIDUsage_KeyboardE:
c = "E";
break;
case kHIDUsage_KeyboardF:
c = "F";
break;
case kHIDUsage_KeyboardG:
c = "G";
break;
case kHIDUsage_KeyboardH:
161
c = "H";
break;
case kHIDUsage_KeyboardI:
c = "I";
break;
case kHIDUsage_KeyboardJ:
c = "J";
break;
case kHIDUsage_KeyboardK:
c = "K";
break;
case kHIDUsage_KeyboardL:
c = "L";
break;
case kHIDUsage_KeyboardM:
c = "M";
break;
case kHIDUsage_KeyboardN:
c = "N";
break;
case kHIDUsage_KeyboardO:
c = "O";
break;
case kHIDUsage_KeyboardP:
c = "P";
break;
case kHIDUsage_KeyboardQ:
c = "Q";
break;
case kHIDUsage_KeyboardR:
c = "R";
break;
case kHIDUsage_KeyboardS:
c = "S";
break;
case kHIDUsage_KeyboardT:
c = "T";
break;
case kHIDUsage_KeyboardU:
c = "U";
break;
case kHIDUsage_KeyboardV:
c = "V";
break;
case kHIDUsage_KeyboardW:
c = "W";
break;
case kHIDUsage_KeyboardX:
c = "X";
break;
case kHIDUsage_KeyboardY:
c = "Y";
162 Chapter 12: Input with the HID Manager
break;
case kHIDUsage_KeyboardZ:
c = "Z";
break;
case kHIDUsage_Keyboard1:
c = "1";
break;
case kHIDUsage_Keyboard2:
c = "2";
break;
case kHIDUsage_Keyboard3:
c = "3";
break;
case kHIDUsage_Keyboard4:
c = "4";
break;
case kHIDUsage_Keyboard5:
c = "5";
break;
case kHIDUsage_Keyboard6:
c = "6";
break;
case kHIDUsage_Keyboard7:
c = "7";
break;
case kHIDUsage_Keyboard8:
c = "8";
break;
case kHIDUsage_Keyboard9:
c = "9";
break;
case kHIDUsage_Keyboard0:
c = "0";
break;
case kHIDUsage_KeyboardReturnOrEnter:
c = "Return";
break;
case kHIDUsage_KeyboardEscape:
c = "ESC";
break;
case kHIDUsage_KeyboardDeleteOrBackspace:
c = "Delete";
break;
case kHIDUsage_KeyboardTab:
c = "Tab";
break;
case kHIDUsage_KeyboardSpacebar:
c = "Spacebar";
break;
case kHIDUsage_KeyboardHyphen:
163
c = " - ";
break;
case kHIDUsage_KeyboardEqualSign:
c = "=";
break;
case kHIDUsage_KeyboardOpenBracket:
c = "[";
break;
case kHIDUsage_KeyboardCloseBracket:
c = "]";
break;
case kHIDUsage_KeyboardBackslash:
c = "Backslash";
break;
case kHIDUsage_KeyboardNonUSPound:
c = "Pound";
break;
case kHIDUsage_KeyboardSemicolon:
c = ";";
break;
case kHIDUsage_KeyboardQuote:
c = "Quote";
break;
case kHIDUsage_KeyboardGraveAccentAndTilde:
c = "~ (tilde)";
break;
case kHIDUsage_KeyboardComma:
c = ",";
break;
case kHIDUsage_KeyboardPeriod:
c = ".";
break;
case kHIDUsage_KeyboardSlash:
c = "/";
break;
case kHIDUsage_KeyboardCapsLock:
c = "CAPSLOCK";
break;
case kHIDUsage_KeyboardF1:
c = "F1";
break;
case kHIDUsage_KeyboardF2:
c = "F2";
break;
case kHIDUsage_KeyboardF3:
c = "F3";
break;
case kHIDUsage_KeyboardF4:
c = "F4";
164 Chapter 12: Input with the HID Manager
break;
case kHIDUsage_KeyboardF5:
c = "F5";
break;
case kHIDUsage_KeyboardF6:
c = "F6";
break;
case kHIDUsage_KeyboardF7:
c = "F7";
break;
case kHIDUsage_KeyboardF8:
c = "F8";
break;
case kHIDUsage_KeyboardF9:
c = "F9";
break;
case kHIDUsage_KeyboardF10:
c = "F10";
break;
case kHIDUsage_KeyboardF11:
c = "F11";
break;
case kHIDUsage_KeyboardF12:
c = "F12";
break;
case kHIDUsage_KeyboardF13:
case kHIDUsage_KeyboardPrintScreen:
c = "F13";
break;
case kHIDUsage_KeyboardF14:
case kHIDUsage_KeyboardScrollLock:
c = "F14";
break;
case kHIDUsage_KeyboardF15:
case kHIDUsage_KeyboardPause:
c = "F15";
break;
case kHIDUsage_KeyboardInsert:
c = "Insert";
break;
case kHIDUsage_KeyboardHome:
c = "Home";
break;
case kHIDUsage_KeyboardPageUp:
c = "Page Up";
break;
case kHIDUsage_KeyboardDeleteForward:
c = "Del";
break;
case kHIDUsage_KeyboardEnd:
c = "End";
165
break;
case kHIDUsage_KeyboardPageDown:
c = "Page Down";
break;
case kHIDUsage_KeyboardRightArrow:
c = "Right Arrow";
break;
case kHIDUsage_KeyboardLeftArrow:
c = "Left Arrow";
break;
case kHIDUsage_KeyboardDownArrow:
c = "Down Arrow";
break;
case kHIDUsage_KeyboardRightControl:
if (gPantherOrBetter)
c = nil;
else
c = "Down Arrow";
break;
case kHIDUsage_KeyboardUpArrow:
c = "Up Arrow";
break;
case kHIDUsage_KeypadNumLock:
c = "Num Lock / Clear";
break;
case kHIDUsage_KeypadSlash:
c = "Keypad Slash /";
break;
case kHIDUsage_KeypadAsterisk:
c = "Keypad Asterisk *";
break;
case kHIDUsage_KeypadHyphen:
c = "Keypad Hyphen -";
break;
case kHIDUsage_KeypadPlus:
c = "Keypad Plus +";
break;
case kHIDUsage_KeypadEnter:
c = "Keypad Enter";
break;
case kHIDUsage_Keypad1:
c = "Keypad 1";
break;
case kHIDUsage_Keypad2:
166 Chapter 12: Input with the HID Manager
c = "Keypad 2";
break;
case kHIDUsage_Keypad3:
c = "Keypad 3";
break;
case kHIDUsage_Keypad4:
c = "Keypad 4";
break;
case kHIDUsage_Keypad5:
c = "Keypad 5";
break;
case kHIDUsage_Keypad6:
c = "Keypad 6";
break;
case kHIDUsage_Keypad7:
c = "Keypad 7";
break;
case kHIDUsage_Keypad8:
c = "Keypad 8";
break;
case kHIDUsage_Keypad9:
c = "Keypad 9";
break;
case kHIDUsage_Keypad0:
c = "Keypad 0";
break;
case kHIDUsage_KeypadPeriod:
c = "Keypad Period .";
break;
case kHIDUsage_KeyboardNonUSBackslash:
c = "Keypad Backslash";
break;
case kHIDUsage_KeypadEqualSign:
c = "Keypad Equal =";
break;
case kHIDUsage_KeyboardHelp:
c = "Help";
break;
case kHIDUsage_KeypadComma:
c = "Keypad Comma ,";
break;
case kHIDUsage_KeyboardReturn:
c = "Return";
break;
case kHIDUsage_KeyboardLeftControl:
c = "CTRL";
break;
case kHIDUsage_KeyboardLeftShift:
167
c = "Shift";
break;
case kHIDUsage_KeyboardLeftAlt:
c = "Option";
break;
case kHIDUsage_KeyboardLeftGUI:
c = "Apple/Command";
break;
default:
c = nil; // key not supported so pass
// back nil so that we skip the
// element
}
break;
return(c);
}
Wow, that function was even more gigantic than the previous one, eh? It may be big, but it
doesnt really do anything magical. Its just a giant switch statement that sets the Elements
name string based on its usagePage and usage variables.
At the top of the function we have a large table of button names used to name the buttons on
a device:
We use this to assign names to any buttons on a device that dont report their own name
string to the HID Manager. This is unfortunate, because the user is not going to have any
idea which button on his gamepad is the one named Button 1. If the HID Manager were
smart like Input Sprocket, then it would return the correct name strings like Red Button, or
C Button to correctly identify it. This is one of the many fundamental problems in trying to
support these devices on OS X. Even Apples own devices like the keyboard dont even
return key names! Thats what most of CreateElementNameString() is doing its manu-
ally naming every kind of key that we have a case for. Good luck supporting non-English
keyboards! This is a disaster.
168 Chapter 12: Input with the HID Manager
The only way around this naming problem is to go out and buy every kind of input device on
the market, plug them in, and run your HID code to find out which button or key does what.
Then write a massive table into your code that assigns the correct name string to the various
devices. Since youre not likely to go to that much trouble, well stick with just using the
generic button names.
case kHIDUsage_KeyboardRightControl:
if (gPantherOrBetter)
c = nil;
else
c = "Down Arrow";
break;
Prior to Mac OS 10.3 (Panther) there was a bug that would cause the Down Arrow key to
show up as the Right Control key on some systems. Even though the usage value is
kHIDUsage_KeyboardRightControl, that element is actually the down arrow key! There
would be another element with the usage value of kHIDUsage_KeyboardDownArrow, but
thats not really the down arrow. That element appears to be totally bogus since it doesnt
seem to map to any key at all. This would happen 100% of the time on every Mac that I own,
however, there were certain models of Macs that seemed to work fine (according to Apple).
So, weve put in a hack here that checks if were on 10.3 or later, and if so we just toss the
Right Control key element. But if were on 10.2 then we set our name string to Down
Arrow.
You should also be aware that the keyboard does not work with the HID Manager at all on
Mac OS 10.1 or earlier. You must have 10.2.6 or later for the HID to be at all useful.
Preferably, your game should require 10.3 or later since that version of the HID is signifi-
cantly more reliable than the older versions.
The IOHIDUsageTables.h header file defines constants for the modifier keys as this:
kHIDUsage_KeyboardLeftControl
kHIDUsage_KeyboardLeftShift
kHIDUsage_KeyboardLeftAlt
kHIDUsage_KeyboardLeftGUI
kHIDUsage_KeyboardRightControl
kHIDUsage_KeyboardRightShift
kHIDUsage_KeyboardRightAlt
kHIDUsage_KeyboardRightGUI
169
Youll notice that the modifiers all have a right and left version. Well, even though most
keyboards do have different left and right modifier keys, these values are bogus in the world
of HID. None of the right-side modifier keys will respond at all! This is probably another
bug in the HID Manager, or maybe its just a feature, but either way if you see an Element
with a Usage value of kHIDUsage_KeyboardRightGUI it wont work. The user can hammer
on that right Command key to their hearts content, but the HID Manager will never register
key press on that key. Only the left modifier values work.
Polling
Polling data is the process of manually reading the value of each control element that our
game needs. The nice thing about polling is that the code is very simple and clean, but the
downside is that were constantly reading the values of lots of elements, and this is consid-
ered by some to be inefficient. However, most games only have a small number of control
needs (move, jump, fire, duck, etc), so the overhead of polling the controls for each of those
really isnt bad. It costs you maybe 1:10,000th of a second at worst.
Queuing
Queuing is more efficient than polling since youre not constantly reading data from your
USB devices, but the complexity of setting up a queue for every Element youre using adds a
lot more mess to an already messy API. Queuing does have one other small advantage: you
wont miss any button presses. When polling, its possible that youll miss a button press if
the user presses the button and releases it in the same frame before youve had a chance to
read the Elements state. With queuing, every press of every button gets added to the queue.
However, in the real world this just doesnt matter since games run at 30+ frames per second,
and I challenge anyone to try pressing and releasing the Fire button on a gamepad in 1/30th of
a second or faster.
In my humble opinion, polling is the way to go since it saves a lot of messy code, and the
downsides really arent a concern. So, in the game engine that were building here well be
sticking with polling, but in case you do want to set up queues for your Elements, heres how
you would do it:
170 Chapter 12: Input with the HID Manager
IOHIDQueueInterface **CreateQueueForHIDElements(
IOHIDDeviceInterface **hidDeviceInterface,
long numElementsToQueue,
IOHIDElementCookie *elementCookies)
{
IOHIDQueueInterface **queue;
long i;
queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface);
if (queue)
{
(*queue)->create(queue, 0, QUEUE_SIZE);
(*queue)->start(queue);
}
return(queue);
}
do
{
/* GET THE NEXT EVENT (IF ANY) OUT OF THE QUEUE */
171
MyHandleQueueEvent(event.elementCookie, event.value);
}
}while(!result);
}
Now to put things into perspective, lets see how we read an Elements value with polling
instead:
Much simpler than queuing, dont you think? There is only one thing to be aware of, and its
another feature of the HID Manager. Notice that we check the result value returned from
getElementValue(). This is critical because the HID will sometimes malfunction and spit
out an error code when you call this. Its very rare, but it happens now and then. So, if an
error occurs just assume that the value of the element is 0.
Input.c
The HID Manager Input.xcode project has a full implementation of everything weve
talked about in this Chapter. This is heavily based on the input system that I used in Nano-
saur 2, and it includes a configuration dialog for letting the user configure their devices based
on a needs list. It also has code for saving and restoring the configuration of all the devices
along with some calibration functions for joystick axes.
172 Chapter 12: Input with the HID Manager
If youre familiar with how Input Sprocket used to work, then youll understand how my little
HID system in Input.c works since it is also based on a needs list. A Need is a structure
that defines an action that the game performs based on input from a device. For example, a
standard game will have a Need for the fire weapon action, a Need for the turn left action, a
Need for the jump action, etc. The InputNeedType structure that were using is defined like
this:
typedef struct
{
char name[64];
short defaultKeyboardKey;
NeedElementInfoType elementInfo[MAX_HID_DEVICES];
long value;
long oldValue;
Boolean newButtonPress;
}InputNeedType;
name
This is a 64-character text string containing the name of the action to be performed such as
Jump or Turn Left. This is what the user will see in the configuration dialog.
defaultKeyboardKey
This is the default keyboard element usage value to assign to this action. For example, to
assign the right arrow key to the Turn Right action, you would set it to
kHIDUsage_KeyboardRightArrow.
173
elementInfo
You can assign multiple device elements to the same Need. For example, the user might
have assigned the spacebar to the Fire action, and also assigned the A button on his gamepad
to the Fire action. So, for each device there may be an element assigned to this Need, and the
elementInfo will tell us the status of each of those elements.
typedef struct
{
short elementNum;
long elementCurrentValue;
}NeedElementInfoType;
The elementNum value is just an index into the Element list for each device, and
elementCurrentValue is the most recently polled value of that Element.
value
This is the final value of the Need. We get the value by scanning each Element assigned to
this Need. The largest value is the one we keep. So, if the Fire button on the keyboard is not
pressed (giving a value of 0), but the Fire button on the gamepad is pressed (giving a value of
1), our engine will take the 1 and put that in the value field.
oldValue
Every time we update all of our Needs by polling all the elements assigned to them, its a
good idea to keep a copy of the previous value in case anything in the game needs to know
old info (say, for determining state changes).
newButtonPress
This gives our code an easy way to tell if a button press is new on this frame. Its calculated
simply by comparing value with oldValue. If oldValue was 0 and value is 1 then obvi-
ously this is a new button press.
Toward the top of the Input.c file all of the Needs for our sample application are defined:
{ // kNeed_TurnRight_Button
"Turn Right Button",
kHIDUsage_KeyboardRightArrow, // keyboard default = right arrow
174 Chapter 12: Input with the HID Manager
},
{ // kNeed_Forward_Button
"Forward Button",
kHIDUsage_KeyboardUpArrow, // keyboard default = up arrow
},
{ // kNeed_Backward_Button
"Backward Button",
kHIDUsage_KeyboardDownArrow, // keyboard default = down arrow
},
{ // kNeed_XAxis
"X-Axis",
0,
},
{ // kNeed_YAxis
"Y-Axis",
0,
},
{ // kNeed_Fire
"Fire Button",
kHIDUsage_KeyboardSpacebar, // keyboard default = spacebar
},
};
This is pretty straightforward stuff. Were just filling the array with some of the basic fields
for each Need. We set the name followed by the default keyboard key. Youll notice that we
dont set a default key for the X-Axis and Y-Axis Needs since theres no way to simulate an
axis on a keyboard. If you wanted, you could assign a key here, but the axis will only range
from 0 to 1 as you press and release the key.
There you have it. Thats the basic introduction to the HID Manager, and you have my
condolences. Unless your game will really benefit from gamepad or joystick input, my
recommendation is to steer clear of the HID Manager, and just stick with the other input
methods for reading the keyboard and mouse. Itll save you weeks of coding and debugging,
and years of your life since the stress involved with doing tech support for the HID Manager
will turn your hair gray in no time.
175
The de-facto standard in 3D modeling applications is Maya. There are plenty of other 3D
modeling programs on the Mac, but odds are that nobody will take you seriously unless
youre using Maya. So, if you havent done so already, you should shell out the $2000 and
go buy a copy along with some of the many Maya books that are available.
On the CD for this book youll see a project called bg3dExporter.xcode. This project
contains a full, working exporter plug-in for Maya 6. In this chapter Im going to discuss the
fundamental things you need to know about writing an exporter plug-in for Maya, and Ill
talk a bit about the BG3D file format that our game engine is going to use in all of the sample
code to follow.
There are several things that must be set up just right in order for Maya to recognize a plug-
in:
private:
void printType(const MObject& node,
const MString& prefix);
bool quiet;
};
This cryptic C++ code is essentially telling Maya that our command bg3d should call the
function doIt() whenever we execute the command from the Maya command line.
return status;
}
This initialization changes a little every time theres a new version of Maya. For Maya 6 we
need to set this up exactly as shown. When a new version of Maya comes out youll need to
check the sample code that comes with it to see what the new version requires on this line:
The plugin() call creates a plug-in object that we can use to call Mayas plug-in API calls.
The first API call that we make is used to register our plug-ins command name with Maya:
By passing bg3d to registerCommand(), Maya will know that when the user types bg3d
on the command line, it should invoke this plug-in.
177
/* PROCESS PLUGIN */
MayaDisplayMessage("Calling PluginEntry()");
PluginEntry();
return MS::kSuccess;
}
It is possible to issue commands to Maya from inside a plug-in. The first thing we do in the
doIt() function above is to issue a polyCleanupArtList command which will triangulate
the entire scene for us. The gibberish after the command is actually a list of parameters to
send to the command. I have no idea what that gibberish actually means, and I dont even
need to know because I got all of that text from Mayas Script Editor. Heres how its done:
Run Maya and go to the Polygon menu where youll see a menu item called Cleanup. Select
the Cleanup option box to get to the Polygon Cleanup Options dialog:
178 Chapter 13: Writing a Maya Plug-in
Make sure all of the tessellation options are checked. Then click the Apply button to cause
the scene to get tessellated into triangles. Now look in the Maya Script Editor window where
youll see the full command that was issued:
This commands text is exactly what weve copied into the doIt() function, so by issuing
that command in our plug-in were doing the exact same thing that the Polygon Cleanup
179
Options dialog did for us. This can be done for any command in Maya. Just see what the
command text is in the Script Editor, and then copy-paste it into your code.
The reason for this is that this Maya command is a little buggy. It doesnt always tessellate
everything down to triangles on the first pass, but doing it twice will ensure that everything
has been triangulated.
Once our plug-in is done exporting all of that triangle data, we need to return Maya to its
original state by issuing two Undo commands to undo both of our polyCleanupArgLlst
commands.
For example, below is a screenshot showing the Maya file that has all of the rock models
used in Nanosaur 2. The BG3D exporter has the option to save each rock model as a differ-
ent file whose name is the same as the Layer name. So, the layer tall_rock_1 will be
exported as tall_rock_1.bg3d.
Our plug-in simply has to scan Mayas Layer List, find the mesh data in each layer, and then
output that mesh data to a BG3D file.
/***********************************************************/
/* SCAN THRU ALL DISPLAY LAYER DG NODES TO GET ATTRIB INFO */
/***********************************************************/
gLayerInfoList[gNumLayers].isVisible = visible;
gNumLayers++;
}
/******************************************************************/
/* FIND THE NAMES OF ALL OF THE OBJECTS THAT BELONG TO EACH LAYER */
/******************************************************************/
numMembers = layerMemberString.length();
gLayerInfoList[i].numMembers = numMembers;
return(noErr);
}
This function has two sections: the first scans the layers and determines which ones are
visible (we dont want to export any layers that are hidden), and then the second section
determines which objects are assigned to each layer.
A scene in Maya is built out of DG Nodes which are all linked together to form a hierarchy.
Each node contains data of different types. There are layer nodes, texture nodes, geometry
nodes, etc. In this case, were looking for Layer nodes, so we start iterating through our
scenes nodes with this call:
This creates an iteration object that will iterate through only Display Layers. To increase the
iteration to the next layer, you invoke the iterations next() function like this:
dgIter.next();
Then, to determine if weve reached the end of the list we test this function:
dgIter.isDone();
This is the method for iterating through any kind of data in Maya, so youll see it elsewhere
in our exporters code. The iteration object has all sorts of functions that can be called to get
information about the node. The next thing we need to do is to get a reference to the data
Object in this node:
As we scan through these node objects, we get the MFnDependencyNode for each one:
MFnDependencyNode fnNode(dgItem);
This dependency node contains all of the function pointers relevant to this specific type of
object. This is a Layer object, so all of the functions in fnNode will be Layer-related. There
are two things we need to know about the Node: its name and its visibility flag. Getting the
name is easy:
The MString variable layerName is a Maya string object, so to make it useful we need to call
a sub-function to get the pointer to the actual C string text:
layerName.asChar();
Next we want to determine if this Layer is visible or not. Layers in Maya can be hidden or
shown by toggling the Visibility checkbox in the Layer Pane:
Getting the Visibility attribute of a Layer is different from getting the name since there isnt
an explicit getVisibility() function in the Layer Object. Instead, we have to search for an
attribute named visibility in the node by doing this:
The findPlug() function will look for an attribute named visibility in the Layer object.
Then, to get the value of the visibility attribute, we call getValue() which returns a Boolean
value in the variable visible.
At this point weve got a list of all of the Layers in our scene, so now our
Maya_GetLayersInfo() function needs to determine which geometry objects are assigned to
each layer. This is a bit tricky because theres no easy way to find this out. Youd think that
there would be a function call to get the referenced objects for each layer, but there isnt.
Instead weve got to do this the hard way by issuing a command to Maya which will spit out
a list of the names of all of the objects associated with a layer:
This code builds a command string that looks something like editDisplayLayerMembers
q layer1, and then we execute the command in the usual way:
The variable layerMemberString will contain an array of MString objects upon return from
executing this command. The number of members (i.e. geometry meshes) found is deter-
mined by counting the number of strings returned:
numMembers = layerMemberString.length();
The C string pointers are extracted from the MString objects the same way as we did for the
Layer names earlier:
When the Maya_GetLayersInfo() function completes well have a list of Layers, and for
each Layer well have a list of the names of the objects associated with that Layer. This
member list contains more than just the names of the geometry meshes in that layer. It may
also contain the names of transform objects and shapes as well. Well be tossing that data out
later once we identify those objects, but for now its all in each Layers member name list.
of the meshes in our scene, and then save them out to a BG3D file. We can iterate through all
of the meshes in our scene much in the same way that we iterated through all of the layers
earlier, but things get much more complicated here. The basic flow of things goes like this:
Iterating through meshes is exactly the same as iterating through layers, so we use the next()
and isDone() functions to move from the current mesh to the next. To get each meshs data
we first must create a path to its node:
dagIter.getPath( dagPath );
Then we can extract the function pointers for the node like this:
The dagNode variable is now what we can use to access the Meshs functions. This seems
like a long way to go to do this, and it is. If youve ever used the plug-in API for other 3D
modeling applications such as Lightwave then this probably seems overly complicated.
Even though we requested only Meshes when we initialized our iteration object dagIter, it is
still wise to verify the node since sometimes Maya will give us data that we dont want:
if ( dagNode.isIntermediateObject())
continue;
if ( !dagPath.hasFn( MFn::kMesh ))
continue;
186 Chapter 13: Writing a Maya Plug-in
if ( dagPath.hasFn( MFn::kTransform ))
continue;
There are three tests that are performed here. First, we test to see if the Node is an interme-
diate object. We dont want those, so skip them. Second, we double-check that the node
has function pointers to access Mesh data. If not then its an invalid mesh, so skip it. Finally,
if the node has transformation function pointers then something isnt right, so skip it. After
this, we can be assured that the node really is a Mesh object.
Our iteration is going through every mesh in the scene, but we only want meshes for the
current layer that were exporting, so, we need to get the name of this mesh and compare it to
the list of object names in our Layer. For each mesh, we get its name like so:
MString meshPathMString = dagNode.partialPathName();
const char *meshPathNamePtr = meshPathMString.asChar();
If that name string isnt in the current layers list then we skip it and iterate to the next mesh,
but if there is a match then its time to extract some data out of it.
MObjectArray sets;
MObjectArray comps;
MFnMesh fnMesh(dagPath);
fnMesh.getConnectedSetsAndMembers(instanceNumber, sets,
comps, true);
A mesh may have multiple materials assigned to it, but for simplicity were only going to
look for the first material, so we get the first element out of the material array:
MObject set = sets[0]; // first material in list
MObject comp = comps[0];
Maya has many types of materials, but the only kinds were interested in are Surface Shaders
since those are what we can easily export to the BG3D file and OpenGL can use. Determin-
187
ing if a material is a Surface Shader is similar to finding the Visibility attribute in a layer, but
this time we look for an attribute named surfaceShader:
MFnSet fnSet(set);
MFnDependencyNode dnSet(set);
MPlugArray srcPlugArray;
ssPlug.connectedTo(srcPlugArray, true, false);
if (srcPlugArray.length() == 0)
continue;
Once again, its a little cryptic whats going on here thanks to C++, but all that is happening
is that were making sure that this material object has a surfaceShader attribute. If it does,
then we can extract the Surface Shaders node:
Now things get easier because extracting polygon data is simple and logical. First well get
the number of vertices in this polygon:
numPoints = piter.polygonVertexCount();
Since we tessellated the entire scene earlier, numPoints should always come up as 3, but its
always a good idea to verify it just in case. To get the coordinates of each of the three
vertices we do this:
mpoint.get(pointtemp);
x[i] = pointtemp[0];
y[i] = pointtemp[1];
z[i] = pointtemp[2];
}
Note that we are asking for the coordinates in world-space by passing kWorld into the
point() function. This causes the coordinates to be returned as they appear in Maya, but the
scale may be different since the API always returns coordinates in the millimeter scale. In
other words, if youre in meter mode when youre modeling in Maya and then you export
your model, the coordinates will be 100x what youd expect. So, suppose youre in meter
mode and you have a vertex at the coordinate 45, 100, -17. Well, the API is going to give
you the value 4500, 10000, -1700 because thats what it is in millimeters. This means you
have a choice of either always working in millimeter mode in Maya to keep things even, or if
you prefer working in meters then youll need to divide each coordinate x,y,z by 100 to scale
it down. I prefer to work in meters in Maya, so the BG3D Exporter project does divide all the
coordinates to keep them at the proper scale. You may need to modify this depending on
what unit scale mode you prefer to work with in Maya.
Extracting the polygons vertex indices is very easy, and these will be in counter-clockwise
order so that backfaces will be correct:
vertexIndices[0] = piter.vertexIndex(0);
vertexIndices[1] = piter.vertexIndex(1);
vertexIndices[2] = piter.vertexIndex(2);
Getting the polygon vertex normals, UVs, and colors is just as easy as getting the coordi-
nates. When reading in the vertex normals its always a smart idea to make sure that theyre
normalized:
for (i = 0; i < 3; i++)
{
double normaltemp[3];
Mvector mvec;
triNormals[i].x = normaltemp[0];
triNormals[i].y = normaltemp[1];
triNormals[i].z = normaltemp[2];
OGLVector3D_Normalize(&triNormals[i], &triNormals[i]);
}
189
piter.getUV(i, tempUV);
triUVs[i].u = tempUV[0];
triUVs[i].v = tempUV[1];
}
Reading vertex colors is equally as easy, but we do need to be careful about one thing:
Calling the iterations getColor() function will return black (0,0,0) if no vertex color has
explicitly been set in Maya, so it is important to verify that the vertex actually has a color
before trying to get it:
triColors[i].r = mcolor.r;
triColors[i].g = mcolor.g;
triColors[i].b = mcolor.b;
triColors[i].a = mcolor.a;
}
else
{
triColors[i].r =
triColors[i].g =
triColors[i].b =
triColors[i].a = 1.0;
}
}
if (status != MS::kFailure)
{
dgIt.disablePruningOnFilter();
filenamePlug.getValue(textureName);
filePath = textureName.asChar();
strcpy (newPath, filePath); // copy to our buffer
ConvertFileRepresentation(newPath, kCFURLPOSIXPathStyle,
kCFURLHFSPathStyle);
c2pstrcpy(hfsPath, newPath);
hasTexture = true;
}
}
}
Once again, we start by looking for an attribute in an object. This time were looking for the
color attribute in our shader object:
Then we make another iteration object, this time of type kFileTexture, but we have no
intention of actually iterating through this because the first instance contains the data were
looking for. We test the iterations isDone() function, and if it returns true then this shader
object doesnt have any texture map. Otherwise, we know there is a texture to be found. To
get the pathname of the texture maps source file, we look for yet another attribute:
This returns a UNIX file path that is pretty much useless to us, so we call a new function that
uses some Core Foundation calls to convert the UNIX path to a nice HFS path. Then were
free to load the texture map, and do whatever we want with it.
ConvertFileRepresentation(newPath,
kCFURLPOSIXPathStyle,
kCFURLHFSPathStyle); // convert UNIX to HFS path
c2pstrcpy(hfsPath, newPath); // make Pascal string
if (fileName == nil)
return (false);
if (inStyle == outStyle)
return (true);
Personally, I like to use Quicktimes Image Importer features to load these texture maps since
Quicktime supports virtually every file format under the sun. Well talk more about import-
ing images with Quicktime in Chapter 17.
In addition to texture maps there are also color parameters associated with a surface shader.
Extracting the transparency and diffuse color information out of a shader object is really easy,
but to do it we first need to know what type of shader it is. Just as there are several types of
materials there are also several types of Surface Shaders. The two types we support in our
plug-in are the Phong shader and the Lambert shader. To find out which type of shader this
is, we call the shaders apiType() function:
193
Then we have some code that will extract the transparency and diffuse color information out
of it:
switch(apiType)
{
/************************/
/* EXTRACT PHONG SHADER */
/************************/
case MFn::kPhong:
// get functions for phong shaders
MFnPhongShader phong(shaderObj);
if (!hasTexture)
{
diffuseColor = phong.color() * phong.diffuseCoeff();
diffuseColor.get(tempColor);
materialColor.r = tempColor[0];
materialColor.g = tempColor[1];
materialColor.b = tempColor[2];
}
else
{
materialColor.r =
materialColor.g =
materialColor.b = 1.0f;
}
/* GET TRANSPARENCY */
transColor = phong.transparency();
materialColor.a = 1.0f - transColor.r;
194 Chapter 13: Writing a Maya Plug-in
break;
/**************************/
/* EXTRACT LAMBERT SHADER */
/**************************/
case MFn::kLambert:
// get functions for phong shaders
MFnLambertShader lambert(shaderObj);
if (!hasTexture)
{
diffuseColor = lambert.color() *
lambert.diffuseCoeff();
diffuseColor.get(tempColor);
materialColor.r = tempColor[0];
materialColor.g = tempColor[1];
materialColor.b = tempColor[2];
}
else
{
materialColor.r =
materialColor.g =
materialColor.b = 1.0f;
}
/* GET TRANSPARENCY */
transColor = lambert.transparency();
matData.diffuseColor.a = 1.0f - transColor.r;
break;
/***************************/
/* UNSUPPORTED SHADER TYPE */
/***************************/
default:
MGlobal::displayError("unsupported shader type");
return(-1);
}
In Maya the diffuse color of a material is ignored if the material has a texture map. OpenGL,
however, does not. OpenGL will apply the diffuse color to the material, essentially filtering
it. For example, if we have a sphere mapped with a baseball texture, but we also have a red
diffuse color, then OpenGL will render the baseball tinted red. In Maya, however, that
195
diffuse color would be ignored, and a regular white baseball would be drawn. Therefore, if
we know that we have a texture map associated with this shader, then we need to set the
diffuse color to white so that it will render the same in OpenGL as it does in Maya. But if
theres no texture then we can extract the diffuse color from the shader object.
To correctly calculate the materials color we need to multiply the diffuse color value by the
diffuse coefficient. These are separate values in Maya that affect how an object is displayed.
In Figure 13-6 you can see the material attributes for a shader that has an RGB color value of
(1,0,0), and the slider next to the color indicates the diffuse coefficient. Here it is set all the
way up to 1.0. Below the color setting is the transparency slider. Here it is set to 0.0, so the
material is totally opaque. Note that a transparency value of 0.0 is opaque, and a value of 1.0
is totally transparent. This is the opposite of how alpha values work, so, when we read the
transparency value from the shader, we have to invert it to make it into an alpha value:
transColor = lambert.transparency();
matData.diffuseColor.a = 1.0f - transColor.r;
Note, however, that this file path changes almost every time that a new version of Maya is
released. This is bound to change for the next version of Maya since Maya is no longer
owned by Alias, hence, the Alias folder in the pathname will likely be renamed.
In addition to the inconvenience of manually renaming the file, youve also got to manually
copy the plugin.rsrc file as well. Maya wont recognize a bundled shared library with .nib
files, so to make life much easier for us its best to use old-style .rsrc files for our dialogs.
This .rsrc file also needs to be in the same plug-ins folder as the shared library itself. This
way well be able to locate it with the following code:
If youve correctly renamed the shared library with the .lib extension and youve put it into
the correct plug-ins folder along with the .rsrc file then Maya will see the plug-in, but you
still have to tell Maya that you want to load it. Under the Window menu in Maya, select the
Settings/Preferences item and then the Plug-In Manager item. This will bring up the
Plug-in Manager dialog:
Toward the top youll see the bg3dExporter.lib plug-in. Be sure to check both the loaded
and auto load checkboxes. This way, the plug-in will be installed automatically every time
you run Maya. Whenever you want to export models in your scene, just type the command
bg3d on Mayas command line, press Return, and the plug-in will get called.
This pretty much covers all of the information you need to write a Maya file exporter plug-in.
The bg3dExporter.xcode project on the CD is fully functional, and its what well be using to
generate the models used by the sample code in the rest of this book. There is a lot of
additional code in the project that deals with exporting the 3D model files in the format we
want, but you may choose to export the data however you deem necessary for your game.
Should you decide to use this tool as-is then youll need to know a little about the files it spits
out: the BG3D files which are discussed next.
198 Chapter 13: Writing a Maya Plug-in
No existing 3D file format did what I needed for my games, so I decided to design my own
file format that all of my games and tools now use. This new 3D file format is called BG3D
(Brian Greenstone 3D), and it is a metafile consisting of tags followed by data. The tags
youll find in a BG3D file are as follows:
BG3D_TAGTYPE_GEOMETRY
This indicates that the data to follow is the header for a new geometry object. This header
contains the type of geometry, the number of materials assigned to the texture along with the
texture number. It also has basic geometry information such as the number of points and
triangles in the geometry.
BG3D_TAGTYPE_VERTEXARRAY
This indicates that the data to follow consists of x, y, z vertex coordinates for the current
geometry object.
BG3D_TAGTYPE_NORMALARRAY
This indicates that the data to follow consists of x, y, z vertex normals for the current geome-
try object.
BG3D_TAGTYPE_UVARRAY
This indicates that that data to follow consists of uv vertex texture coordinates for the current
geometry object.
BG3D_TAGTYPE_COLORARRAY
This indicates that the data to follow consists of RGBA vertex color values for the current
geometry object.
BG3D_TAGTYPE_TRIANGLEARRAY
This indicates that the data to follow consists of triangle vertex indices for the current
geometry object.
199
BG3D_TAGTYPE_MATERIALFLAGS
This indicates that the data to follow is a simple 32-bit value that contains information about
the next material were going to load.
BG3D_TAGTYPE_MATERIALDIFFUSECOLOR
This indicates that the data to follow is an RGBA color value representing the current
materials color.
BG3D_TAGTYPE_TEXTUREMAP
This indicates that the data to follow is a header plus texture map data. The header has the
width and height of the texture along with the texture format of the texture. The data after the
header will be of variable length depending on the size of the texture defined in that header.
BG3D_TAGTYPE_GROUPSTART
This indicates that any geometry to follow is part of a group. This is helpful for organizing
multiple pieces of geometry that belong to a single object; like wheels being part of a car
model.
BG3D_TAGTYPE_GROUPEND
This indicates the end of a group.
BG3D_TAGTYPE_ENDFILE
This indicates the end of the BG3D file.
In all of the sample projects to come youll see a new source file called BG3D.c. This file
contains all of the code needed to parse one of these BG3D files. Nothing particularly
interesting happens in this code. It just opens the BG3D file and starts reading the tags and
data out of it to build geometry objects in the form of OpenGL Vertex Arrays. You may wish
to change this implementation or design your own file format for your games. In my games,
the BG3D files have a few additional tags for other pieces of data such as bounding boxes
and compressed textures, and thats the beauty of using a metafile for your model data since
you can encapsulate anything you want in there depending on your needs.
BG3D Linker
In your game youll have hundreds of different models, so rather than having hundreds of
individual BG3D files to load, it makes much more sense to group lots of BG3D files into a
single, large BG3D file. That way, different models can share the same textures too. So,
youll find another useful tool on the books CD called BG3D Linker. You can use this
tool to merge multiple BG3D files together using a makefile that looks something like this:
200 Chapter 13: Writing a Maya Plug-in
LINK :Brach:Brachiosaurus.bg3d
LINK :Drums:BoosterDrum.bg3d
LINK :Drums:HardDrum.bg3d
LINK :Drums:Splitter.bg3d
%::Level1Models.bg3d
end
The BG3D Linker tool will scan this makefile for all of the LINK keywords, and merge each
of the referenced .bg3d files into the output file indicated by a % sign. So, in the above
example four different .bg3d files will be merged into a single BG3D file named
Level1Models.bg3d. If different models share the same texture map, BG3D Linker removes
the duplicates so that the final file is small and efficient.
When you run BG3D Linker, select Execute Makefile from the File menu, and then select
your makefile. As the makefile gets processed youll see the output in the Console window:
At this point youre probably asking yourself isnt this going to fuse all of my geometry
together into a massive blob of triangles and vertices? No, because the BG3D file format
has Groups, so this tool puts each separate model into its own group so that when we read this
BG3D file back into our game we can separate each model out. Every 3D game Ive ever
written for the Mac has used one version or another of this linker utility. I like to have one
201
large .bg3d file for each levels unique models, and then I also have a global.bg3d file that
contains models that are used throughout the game.
In the BG3D.xcode project there are two .bg3d model files that it loads in: Dinosaurs.bg3d
and Eggs.bg3d. The Dinosaurs.bg3d file contains two different dinosaur models, and the
Eggs.bg3d file contains three different egg models. When you run the project, youll see
several objects spinning on the screen:
ImportBG3D("\pDinosaurs.bg3d", MODEL_GROUP_DINOSAURS);
ImportBG3D("\pEggs.bg3d", MODEL_GROUP_EGGS);
202 Chapter 13: Writing a Maya Plug-in
The ImportBG3D() function in bg3d.c will parse the input .bg3d file and process all of the
tags to build a database of textures and meshes. All of the vertex and triangle information for
each mesh is stored into a contiguous block of memory so that when were done we can mark
that block of memory for use by Vertex Array Range as discussed in Chapter 6.
To create a meta-object representing one of the models that we want to draw, we follow
these steps: First, we create a Group Object to contain all of our objects info:
After that, we want to create a transformation Matrix Object, and then put it in our Group:
When the .bg3d file was read in, all of the materials and meshes were stored into meta-
objects, and we kept references to them in gBG3DGroupList[], so to add one of those models
to our Group Object we simply call MO_AppendToGroup() again, and pass in a reference to
the model we want:
MO_AppendToGroup(baseGroupObj, gBG3DGroupList[bg3dGroup][bg3dModel]);
To draw any object, all we have to do is pass it to MO_DrawObject(). Our code will parse the
meta-data, and process the matrix and mesh that it finds in there.
MO_DrawObject(gRaptorObject);
The details of what all this code is doing really are not important since youll probably want
to write your own object handling functions, but for the sake of getting something on the
screen I wanted to give you this brief explanation of how I like to do things in my games.
The code in BG3D.c and MetaObjects.c is pretty straightforward and well commented, so
you should be able to read through it and figure out how everything works if youre inter-
ested.
203
Anaglyph Glasses
These are the old fashioned red-blue glasses from the sci-fi B-movies of the 1950s. There
have been some advances in this field since 1950, and we now like to use red-cyan glasses
instead of red-blue. Even though theres quite a bit of color distortion with these glasses,
your eyes do adjust to some degree, and theyre a very economical way to do stereo rendering
on a home computer. The old red-blue type are still commonly found, so be sure that you are
using red-cyans since the red-blue type will not work well for what were going to do.
Shutter Glasses
These were popular in the heyday of Virtual Reality, and can still be found today. Theyre
basically a lightweight headset that has two LCD shutter panels; one over each eye. They
flicker rapidly in synchronization with the monitor to reveal the left and right views. Unfor-
tunately, these only work on CRT displays with high refresh rates, and since most computer
users are switching to LCD flat-panel displays there isnt much use for these now. That being
said, the effect is pretty awesome since theres no color distortion like you get with anaglyph
glasses, and Apple has put some special support into OpenGL for such hardware.
L R
Figure 14-3: Two cameras looking at focal point
Even thought the toe-in method mimics reality (since thats what our eyes actually do in the
real world), this method has serious problems in the realm of projected 3D math. The
problem is evident by looking at the projection planes in the figure above. Notice that theyre
not parallel, but instead they form an X. This is bad because it will result in vertical parallax
distortions in the rendered image. For example, suppose that there is a sphere on the left side
of the scene as shown in Figure 14-4:
L R
Figure 14-4: The sphere is closer to the right eyes projection plane than the left eyes
206 Chapter 14: Stereo 3D
The sphere is closer to the right eyes projection plane than the left eyes projection plane, so,
when the scene is rendered, the objects will be drawn at slightly different sizes. The scaling
effect gets worse toward the edges of the screen. The rendered scene will look something like
this:
Figure 14-5: The spheres suffer vertical parallax and dont line up well
If you were to view this scene with stereo glasses you would see a 3D effect, but it would be
difficult on the eyes, especially near the edges of the screen. What we want to see are two
spheres which have no parallax scaling, just horizontal shifting like this:
In order to achieve this we need to modify the view frustums like so:
207
L R
Figure 14-7: Modified view frustums to achieve a flat projection plane
As funky as those skewed view frustums look, they actually will yield excellent results!
Scenes drawn like this will have very pain-free stereo images that look great. This is no
longer a true perspective camera, so we cannot use gluPerspective() to easily set up the
cameras projection matrix. Weve now got to do some calculations to manually set our
cameras frustum, so instead of this
we need to do this:
/* DO FORMULA CALCULATIONS */
aspect = (float)gGameWindowWidth/(float)gGameWindowHeight;
b = CAMERA_HITHER / STEREO_FOCAL_LENGTH;
STEREO_FOCAL_LENGTH
This value determines the distance from the camera where objects appear to intersect our
display. Anything closer than this will appear to be popping out of the screen, and anything
farther than this distance will appear to be behind the screen. As cool as it might seem to
have your 3D objects hovering in front of the screen, its usually best to have most of your
scene appear behind it. Objects projected in front tend to cause a little more eye strain, and
can cause that cross-eyed effect that youve probably experienced if youve ever been to a
3D movie.
STEREO_CAMERA_SEPARATION
This value determines how far the left and right cameras are separated. The farther apart they
are, the more exaggerated the stereo 3D effect will be, but since separating the cameras
farther and farther is akin to making your head bigger and bigger in the universe, the objects
in the scene will seem to look like toys, and take on a surreal appearance. Moving the
cameras closer together will lessen the stereo effect, but it will also make the objects seem
larger since, in effect, our head is shrinking in the virtual universe.
Theres a general rule in stereo imaging that says that the camera separation should be 1/30th
of the distance to the focal plane. So, if your focal plane is 300 units away then your camera
separation should be about 10 units. This rule is not written in stone by any means, but it is a
good ratio to start with as you tweak things. Personally, I like to exaggerate the stereo 3D
effect in my games, so I tend to use ratios that are more like 1:20 rather than 1:30.
the left eye and once for the right eye, and when we do this we need to actually offset our
camera for each location:
Theres nothing particularly amazing about this code. Were simply calculating an x-axis
vector for the camera, and then offsetting the camera along that vector. If its the left camera
were dealing with then we offset it left, and if its the right camera then we offset it to the
right.
anaglyph glasses using the code above plus a few more items of interest. The first item of
interest we need to discuss is how to actually generate an image for anaglyph viewing.
Anaglyph glasses have a red filter over the left eye, and a cyan (a mix of blue and green)
filter over the right eye. When we render our scene, we need to draw everything for the left
eye using only the color red, and draw everything for the right eye with only blue and green.
Luckily, this is very easy to do with OpenGL because OpenGL has a simple way to turn on
and off color channels during rendering. The glColorMask() function lets you set the state
of the Red, Green, and Blue channels, so when we go to render the left eye we turn off the
Green and Blue channels, leaving only the Red channel active:
The cool thing about glColorMask() is that when we render the right eye (which needs only
the Green/Blue channels), OpenGL will not erase the Red channel thats already in the frame
buffer. This means it is not necessary to draw the left eye into one buffer and the right eye
into another and then composite them manually.
aglSetCurrentContext(gAGLContext);
/*********************/
/* CLEAR BACK BUFFER */
/*********************/
/***********************************************/
/* DRAW SCENE IN TWO PASSES (LEFT & RIGHT EYE) */
/***********************************************/
211
if (gStereoPass == 0)
{
// red-only for left eye
glColorMask(GL_TRUE, GL_FALSE, GL_FALSE, GL_TRUE);
}
else
{
// green-blue for right eye
glColorMask(GL_FALSE, GL_TRUE, GL_TRUE, GL_TRUE);
}
OffsetStereoCameraCoord(gStereoPass == 0);
SetCameraMatrices();
/* DRAW SCENE */
aglSwapBuffers(gAGLContext);
}
When we start to draw the frame, we turn on all of the color channels so that when we clear
the back buffer, itll clear the whole thing:
Then we enter a loop to cause the entire scene to be drawn twice, once for each eye. Each
time we draw the scene we need to clear the z-buffer:
glClear(GL_DEPTH_BUFFER_BIT);
212 Chapter 14: Stereo 3D
Then we set the color mask to either red or cyan depending on which eye were rendering for.
On the first pass we do the left eye, so we only turn on the Red channel:
And then for the second pass we do the right eye, so we draw only into the Green and Blue
channels for cyan:
Next, we move the camera to either the left or right eye position:
OffsetStereoCameraCoord(gStereoPass == 0);
Then we set the Projection and Model View matrices as needed for either the left or right eye
camera:
SetCameraMatrices();
With everything set, we draw the scene and do the next pass if needed.
There is also a physiological problem in that the human eye is much more sensitive to green
than any other color, so the image seen through the cyan filter will look much brighter than
the image seen through the red filter. The eye is so sensitive to green that you will often see
ghosting in the left eye as some of the green bleeds through the red filter. If youve got a
good pair of anaglyph glasses and a good display then your cyan filter should block almost
100% of the red light, but there will always be a small amount of green bleeding through your
red filter. Keeping this to a minimum is important, but to do so requires a darker red filter
thus dimming the brightness even more.
213
When I made the custom anaglyph glasses for Nanosaur 2, I had our manufacturer send me
gel samples for the different shades of red and cyan that they had. I picked red and a cyan
gels that worked best on Apples LCD displays, and Ive been very happy with the results.
Dont be mistaken. There is a huge difference in the quality of various anaglyph glasses.
Ive seen some that had so much color bleeding in both eyes that it was impossible to see any
3D images at all. Youve got to be sure youve got glasses with properly tuned filters.
Additionally, the color accuracy of the phosphors on a CRT display is nowhere near as good
as you get with an LCD. LCD colors are fairly pure, meaning that the red channel wont
have much green or blue in it, but CRTs tend to emit stray color wavelengths in each color
channel, so the red phosphor may be spewing out a fair amount of green or blue that you
cannot see until you look at it through a cyan filter. Anaglyph images are best viewed on
LCD displays since the color accuracy is so good, but you can use a CRT as long as you are
aware that there will be more ghosting.
So, what does this all mean to us as programmers? Well, it means that we need to color
balance all of the textures in our game so that there are no pixels that are severely red or cyan
shifted like the red apple would be. We also need to reduce any green peaks since our eyes
are too sensitive to that color.
If you do some research on the web, youll find all sorts of complex formulas for doing
anaglyph color balancing, and there are even some methods which people have patented.
There is a research paper from the Computer Science Department at North Carolina State
University that goes into more gritty details of anaglyph color balancing algorithms than you
could ever want:
http://research.csc.ncsu.edu/stereographics/ei03.pdf
The fact is, however, that you can get away with some pretty simple code to do basic color
balancing, and how you choose to color balance your textures is very dependent on how
much color distortion you can accept. If you were to do 100% color balancing youd end up
with a grayscale texture, so youve really got to play with your code to get the look that you
want. In Bugdom 2 the texture colors were so saturated that converting the textures to
grayscale usually yielded better results than any attempt at color balancing, but in Nanosaur 2
the colors were less saturated, so, it was much easier to get good color balance without
making everything go gray.
The following function is very similar to what was used in Nanosaur 2. Its a simple hack,
but its effective:
214 Chapter 14: Stereo 3D
/* BALANCE RED */
/* BALANCE GREEN */
/* BALANCE BLUE */
This function takes as input the Red, Green, and Blue component values of a textures pixel,
and outputs the color-balanced values. The first thing we do is to calculate the luminosity of
the Red and Cyan channels. The standard formula for calculating luminosity is:
lumR = fr * .299f;
lumGB = fg * .587f + fb * .114f;
These luminosity values indicate how well balanced each eye is. If lumR is greater than
lumGB then we know that this pixel will look brighter in the left eye than the right eye. What
we want to do is to try and balance out the RGB components based on these luminosities, so
we start with Red by calculating the luminosity ratio of Cyan to Red:
d = fr * ratio * RED_RATIO_ADJ;
The constant RED_RATIO_ADJ is a value that weve set based on what looks good in our game.
Youll want to tweak these values to make things look best for your own game. If the scene
in your game has a lot of greenery like trees and bushes, then you may need to bump the
RED_RADIO_ADJ value up to make the red channel better balanced against all that green.
However, if your scene contains a lot of red lava and fire, then you might need to bump up
the GREEN_RATIO_ADJ value.
216 Chapter 14: Stereo 3D
Since Anaglyph images tend to be dim due to all the filtering, we really want to avoid
lowering a pixels brightness, so if your calibrated component value is less than the original
value we dont do anything. Otherwise, we update it:
if (d > fr)
{
r = d;
if (r > 0xff)
r = 0xff;
}
lum = gAnaglyphGreyTable[lum];
}
pix32 += width;
}
}
Once again we use the standard NTSC luminance calculation to get the luminosity values of
the RGB components:
Then we add all of these luminosities together to get the pixels full luminosity value:
lum = r + g + b;
At this point we could simply use this luminosity value as the RGB component values and be
done with it, but a trick that works well to brighten up the scene is to amplify the luminosity
on a curve table. This table works just like a gamma table. Its 256 entries, one for each
possible value for the RGB components. We use the current luminosity as an index into the
table which gives us the new luminosity:
lum = gAnaglyphGreyTable[lum];
f = 0;
for (i = 0; i < 255; i++)
{
gAnaglyphGreyTable[i] = sin(f) * 255.0f;
f += (M_PI / 2.0) / 255.0f;
}
}
0x00 0xFF
Figure 14-8: A simple grayscale amplification curve
As you can see from the curve, all of the values are bumped up in luminosity since the
straight diagonal line represents the non-amplified luminosity values. The only problem with
amplifying the luminosity like this is that it washes out the contrast a little, but its the lesser
of two evils.
There are other tweaks you can make to the grayscale code if you like. For example, instead
of making it a true grayscale with equal Red, Green, and Blue values, you might want to
lower the Green values to reduce ghosting and balance the left and right eye luminosities. If
you wanted to totally eliminate the ghosting you could just set the green channel to 0x00 and
then pump up the blue to compensate.
pair of gels that worked best on an Apple LCD display we had them custom print up several
thousand pairs for us. There are many companies out there who sell anaglyph glasses, but
prices can vary wildly. The company we like to use is Rainbow Symphony:
www.rainbowsymphony.com
Even if youre only looking for a few pairs to play around with you can get our custom made
glasses from them since they sell the Nanosaur 2 glasses on their site:
www.rainbowsymphony.com/nano3d.html
The best way to test a pair of anaglyph glasses is to do this: Load up Photoshop and draw a
pure green square on the left side of the image, and then draw a pure red square on the other
side. Look through the anaglyph glasses and close your right eye. Observe how much green
is showing through the left filter. It should be only a little the less the better. Then close
your left eye and see if any red is visible in the right eye. There should be no red visible at all
if your glasses are of good quality.
If youre on a CRT display youll see much more ghosting of both colors in each eye, but on
an Apple LCD display you should see no red at all in the right eye, and only a small amount
of green in the left eye.
with OpenGL on the Mac. The way that shutter glasses work is that the LCD panel over each
eye will rapidly flicker from clear to opaque allowing the left eye to see the left image for a
fraction of a second, and then the right eye to see the right image. The trick is in synchroniz-
ing the shutter glasses with the display. The method that Apple implemented for OpenGL is
called Blue-Line Sync. Its a pretty crude, yet effective method. A blue line is drawn at
the bottom of the frame buffers for the left and right eyes. A short blue line for the left
buffer, and a long blue line for the right buffer. The Shutter Glasses hardware detects these
lines, and uses them as triggers to open and close the LCD panels.
Were still going to draw each frame twice, once for the left eye and once for the right eye,
but this time we have to draw them into two separate frame buffers. OpenGL will automati-
cally page flip between the two frame buffers, and the shutter glasses will detect the blue-line
sync signal as this happens.
To tell OpenGL that we will be doing stereo 3D with shutter glasses, we modify our Draw
Context attribute list:
This looks the same as the attribute list weve used in all of the other sample projects except
that one new attribute has been added: AGL_STEREO. When OpenGL sees this attribute it will
know to create two frame buffers, one for the left eye and one for the right eye. It should also
be noted that you must play full-screen to use AGL_STEREO. You cannot play in a window, so
the sample project Shutter Glasses.xcode doesnt have a windowed option.
The extra frame buffer needed for AGL_STEREO uses up a significant amount of VRAM, so,
you should be very careful about trying to activate this mode on systems with very little
VRAM. Back in Chapter 4 we learned how to determine how much VRAM was available on
the main display, and this is a good place to use that function. If the amount of VRAM is less
than 16MB then you should probably not allow AGL_STEREO.
Luckily, we dont need to worry about ghosting or color balancing when using shutter
glasses, so we dont need to have any crazy texture whacking functions. We leave all of our
textures as they are, and when we render the images we dont need to mess with any color
channels. The only thing special we have to do is tell OpenGL which buffer were drawing
into:
221
if (gStereoPass == 0)
glDrawBuffer(GL_BACK_LEFT);
else
glDrawBuffer(GL_BACK_RIGHT);
aglSetCurrentContext(gAGLContext);
OGL_UpdateVertexArrayRange(); // update VAR memory
/***********************************************/
/* DRAW SCENE IN TWO PASSES (LEFT & RIGHT EYE) */
/***********************************************/
if (gStereoPass == 0)
glDrawBuffer(GL_BACK_LEFT);
else
glDrawBuffer(GL_BACK_RIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
OffsetStereoCameraCoord(gStereoPass == 0);
SetCameraMatrices();
/* DRAW SCENE */
DrawBlueLineSync();
aglSwapBuffers(gAGLContext);
}
Youll notice that once were done drawing our scene we call a new function
DrawBlueLineSync() which takes care of drawing correct blue-line sync signals into each
frame buffer:
OGL_PushState();
OGL_DisableTexture2D();
OGL_DisableBlend();
OGL_DisableLighting();
OGL_DisableFog();
OGL_DisableDepthTest();
/* DRAW A DIFFERENT BLUE LINE INTO THE LEFT & RIGHT BUFFERS */
glDrawBuffer(buffer);
glGetIntegerv(GL_VIEWPORT, vp);
glViewport(0, 0, w, h);
glGetIntegerv(GL_MATRIX_MODE, &matrixMode);
223
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glScalef(2.0f / w, -2.0f / h, 1.0f);
glTranslatef(-w / 2.0f, -h / 2.0f, 0.0f);
OGL_PopState();
}
One other issue to be aware of is that you should not do gamma fades when in AGL_STEREO
mode. If the gamma is dark, then the blue line will obviously not be very blue, thus, the
shutter glasses wont have anything to sync from. Youve got to keep the gamma at full
brightness so that the blue line is always visible to the hardware.
almost completely undetectable. Since no LCD does this, youve got to have a good CRT
display.
There are many brands of shutter glasses out there, and most of them are very inexpensive
(you can get a pair for under $100). However, none of those $100 pairs will work on the Mac
because they do not do blue-line syncing. Some pairs require special drivers and special
coding to trigger the shutters, while others do really lame things like interlacing to sync
everything. The only company that currently manufactures and sells shutter glasses with
controllers that on with blue-line sync is the Stereo Graphics Corporation:
www.stereographics.com
Their Crystal Eyes shutter glasses will cost you well over $1000 for a single pair! Ouch! Its
unlikely that youre going to go out and drop that kind of money for a gimmick like stereo 3D
gaming, but thats the only easy option right now. If youre a technically inclined individual,
however, there is another option, but its ugly. Someone has figured out how to build their
own blue-line sync controller that will work with cheap shutter glasses, and theyve posted an
article to the web:
http://aberco.free.fr/3d.html
Youve got to speak French, have a lab and an electrical engineering degree to figure out how
to do this, but if youre looking for a challenge there ya go. You can probably build the
whole thing for less than $50, and Ive been told that it works great with our game, Nanosaur
2, which supports AGL_STEREO.
http://anabuilder.free.fr/download/MacOsX/
225
Next, load the two images into Photoshop and shrink them down to a reasonable size like
1024x768. AnaBuilder is a Java application, so its a little sluggish. Trying to load a giant 5
mega-pixel image into it is just asking for trouble. After youve done this, launch AnaBuilder
and load the left and right images from AnaBuilders File menu. Note that all of this utilitys
menus are in the AnaBuilder window, not in the usual menu bar.
slider controls, and then start moving things around to get the two images lined up how you
want:
Figure 14-11: Drag the alignment sliders to get the left & right images lined up
Youll need to be wearing your red-cyan anaglyph glasses as you do this in order to see how
things are looking.
When you moved the camera from left to right to take these two pictures, odds are that you
wobbled a little and that the pictures are slightly out of rotation. There are slider bar controls
to adjust the rotation, but one of the nicest features of AnaBuilder is the AutoFit feature.
Once youve got the images roughly lined up, you can select AutoFit from the Actions menu.
It may take a little while (15 to 30 seconds) to perform its calculations, but this feature will
usually do a very good job of making the two images line up nicely. Once its done perform-
ing the AutoFit, you can tweak the X slider to change the depth of the stereo image.
Theres no real rhyme or reason to how you should tweak these values. Just start playing
with them until the left and right eyes seem well balanced, and the 3D image is clear. I
usually lower the Red weight value, and raise the -Red value to equalize things a bit, but
varies for each image Im working with. For example, if your scene has a lot of green trees in
it, then you may need to boost the red instead of decreasing it since the scene may be too
saturated with green. You can spend 20 minutes playing with these filter values before
youre 100% satisfied with the results.
are very easy to find on the web, and they come in all sizes. Just do a search for anaglyph
slide bar and youll get lots of results.
You can also just do a search on the web for anaglyph pictures, and youll be amazed at
what comes up. There are some truly spectacular homemade anaglyph images out there. I
highly recommend checking them out. Heres a short list of some worthy sites:
www.marsunearthed.com
www.fotocommunity.de/pc/pc/pcat/41027
www.alpix.com/nice/htmlen/pictstereo.htm
www.geocities.com/Paris/Parc/4239/lesGrands.htm
229
The Networking.xcode sample project has a simple networking implementation that lets
you either Host a network game, or Join a network game. When the game starts, each player
can move a colored sphere around the screen and itll all be networked together. Its simple,
but it works, and it uses all of the basic OS X networking technologies that I am going to
discuss in this chapter. The two technologies Im referring to are Rendezvous and BSD
Sockets. Rendezvous is used to allow players to find each other on the network. Its like
announcing to the world Hey Im here! Anyone want to play? BSD Sockets are what well
use to actually transfer data between players in a network game. Sockets are part of OS Xs
UNIX core, so the function calls are a bit archaic, but theres a plethora of information that
you can find on the web dealing with it.
Rendezvous
As previously stated, Rendezvous is what we use so that players can locate each other on a
network. It provides an easy way for a Host to advertise a game, and for Clients to locate that
Host. When you start a networked game you need to decide if youre going to be the Host or
if you want to be a Client. The Host is the player who announces a new game to the world,
and is responsible for managing all of the players. The Client is a player who locates a Hosts
game and joins it. As a general rule, the person with the fastest computer should always be
the Host since most Host implementations require a little extra processing.
Hosting a Game
To advertise a game as a Host well need a new function:
CFStreamError error;
Boolean registered;
gMyRVService = CFNetServiceCreate(kCFAllocatorDefault,
CFSTR(""), // use default domain
kServiceType, // service type
CFSTR(""), // use computer's name
MY_PORT_NUM); // port #
if (gMyRVService == nil)
DoFatalAlert("\pCFNetServiceCreate() failed!");
/* HANDLE ERRORS */
if (!registered)
{
CFNetServiceUnscheduleFromRunLoop(gMyRVService,
CFRunLoopGetCurrent(),
kCFRunLoopCommonModes);
CFNetServiceSetClient(gMyRVService, nil, nil);
CFRelease(gMyRVService);
DoFatalAlert("\pCFNetServiceRegister() failed!");
}
}
gMyRVService = CFNetServiceCreate(kCFAllocatorDefault,
CFSTR(""), // use default domain
kServiceType, // service type
CFSTR(""), // use computer's name
231
MY_PORT_NUM); // port #
The parameters we pass to CFNetServiceCreate() describe our game to the network. The
constant kServiceType is a string defined like this:
This is the most important parameter because this is the string that other Clients will search
for when looking for games to join. The first part of the string identifies our game, and it
must be preceded with an underscore character as in the example above. The second part of
the string is the connection type - in this case its a TCP/IP connection. We could this to a
UDP network connection by putting _udp. at the end, but for the examples in this book
were going to stick with just TCP/IP connections.
The fourth parameter to CFNetServiceCreate() is the name that we want other players to
see when they join the game. In practice, you would want the user to be able to name their
games, like Brians Battle Arena, but in our example we pass an empty string. The empty
string tells the OS to assign the computers default name to the service. The default name is
whatever youve named your computer in the Sharing Preferences pane. If the name youve
chosen is already in use, then youll get a name collision error, but by letting the OS assign
the default computer name youll avoid this because the OS detects any name collisions and
appends a number to the end of the name to make it unique. For example, if there are two
computers named My Computer that are trying to host a net game, then the second one to
start hosting will automatically be renamed to My Computer.1.
The final parameter sent to CFNetServiceCreate() is the port number that we want to use to
play over. This value is completely arbitrary, but to be safe you should use values over
20,000 since the OS reserves many lower port numbers.
After weve created this new Network Service object, we need to register it so that it will start
advertising itself:
The CFNetServiceSetClient() call is used to set a callback function that will handle any
errors that occur. Typically, the only error youre going to ever get would be a name colli-
232 Chapter 15: Networking
sion error, and those will only occur if you decided not to let the OS set the Service name as
explained above.
Next, we install the Service into our applications run loop. This way, as we sit in our Host
Game dialog, the OS can be updating this service as it needs. The final call to
CFNetServiceRegister() gets it all going, so now we just sit back and wait for other players
to find us.
Locating Hosts
If were looking to join a game as a Client, then we need to set up Rendezvous to locate any
games being advertised on the network. To do this, we create a Network Browser Service
with the following function:
gServiceBrowserRef = CFNetServiceBrowserCreate(kCFAllocatorDefault,
HostBrowserCallback,
&clientContext);
if (gServiceBrowserRef == nil)
DoFatalAlert("\pCFNetServiceBrowserCreate() failed!");
CFNetServiceBrowserScheduleWithRunLoop(gServiceBrowserRef,
CFRunLoopGetCurrent(),
kCFRunLoopCommonModes);
result = CFNetServiceBrowserSearchForServices(
gServiceBrowserRef,
233
if (!result)
{
CFNetServiceBrowserUnscheduleFromRunLoop(gServiceBrowserRef,
CFRunLoopGetCurrent(),
kCFRunLoopCommonModes);
CFRelease(gServiceBrowserRef);
gServiceBrowserRef = nil;
}
}
Once again, we start by creating a Network Service object, but this time were creating a
Browser service:
gServiceBrowserRef = CFNetServiceBrowserCreate(kCFAllocatorDefault,
HostBrowserCallback,
&clientContext);
result = CFNetServiceBrowserSearchForServices(
gServiceBrowserRef,
CFSTR(""), // in default domain
kServiceType, // look for our service type
&error);
Here we pass our kServiceType string, which, as you remember, contains the name of the
Service that the Host registered earlier. The Browser object will start searching the network
for Net Services that match that string. When something is found, our callback is called:
Here, we determine if the callback occurred because a new network Host was located, or if
one that was already located has gone away. To do this, we simply check the flags variable
and see if kCFNetServiceFlagRemove is set. If not set then we add this Host to our list of
available Hosts, or if it is set then we need to remove that Host from the list.
Here is the FoundAHost() function that will extract the Hosts name and IP address, and then
store it into our list:
CFStringGetCString(name,
gHostList[gNumHostServices].name,
MAX_NAME_SIZE,
kCFStringEncodingMacRoman);
gHostList[gNumHostServices].portNum =
GetIPAddressOfHost(service, gHostList[gNumHostServices].ipAddress);
gNumHostServices++;
BuildHostListMenu();
}
235
The function CFNetServiceGetName() easily extracts the name string for the Host, but
getting the IP address and port # are a little more complicated, so we need another new
function:
CFNetServiceResolve(service, nil);
addrList = CFNetServiceGetAddressing(service);
ConvertIPAddressToString(theIPAddr, outString);
return(portNum);
}
The first step in getting the IP address from the Host is to resolve the Services address:
CFNetServiceResolve(service, nil);
addrList = CFNetServiceGetAddressing(service);
This function actually returns a CFArray, but its an array with only one element, so we get
the address like so:
Different network connection types will return different address formats. We set up our
network as a TCP/IP network by putting _.tcp. at the end of our Service Type string. This
means that we know were getting an IP address here, and IP addresses are stored in a
sockaddr structure. The CFDataGetBytePtr() function will return a pointer to the sockaddr
value:
The sockaddr structure contains an array of bytes where the first two bytes are the socket
number, and the next four bytes are the four values that make up an IP address, such as
168.0.1.4. Wed like to convert those four bytes into an actual IP address string, so weve
written yet another function to do this:
j = 0;
for (i = 2; i <= 5; i++) // the IP addr is in bytes 2,3,4,5
{
Str32 s;
UInt8 num = ipAddr->sa_data[i]; // get this # of the IP address
237
Even if you only have two computers on your network, its possible that the
HostBrowserCallback() will get called multiple times, each time appearing to have found a
new Host. This happens if there are multiple connection pathways on your network, for
example, you might have both a wireless Airport connection and an Ethernet connection. Or,
there may even be a Firewire network connection as well. When youve got a setup like this
you may see the same Host multiple times, however, each one is going to have a different IP
address. Most network applications would look for these duplicates and eliminate them,
however, I prefer to show all of them to the user and let him or her choose which connection
to use. The Host pop-up menu in our sample projects Join Game dialog shows the name of
the Hosts game along with the Hosts IP address:
Showing the IP address may assist the player in choosing the fastest network connection type.
If youve got a gigabit Ethernet connection and an Airport connection, which would you want
to choose? The trick is that youve got to be able to recognize IP addresses. If it starts with
the number 10 then its probably an Airport network.
238 Chapter 15: Networking
BSD Sockets
Thats all there is to advertising your game to other players, and having those players locate
you. Now we need to actually set up a connection between the Host and the Clients, and start
sending data between them. All communication is done with Sockets, and a Socket is
basically like a telephone. Youve got a telephone in your house, and your friend has a
telephone in his house. If your friend knows your telephone number, he can dial you up.
Once you pick up on your end, a connection has been established, and you can start talking.
This is exactly how BSD Sockets work.
/* CREATE SOCKET */
socketAddr.sin_addr.s_addr = htonl(INADDR_ANY);
Here we pass AF_INET which tells the Socket to use Internet addressing, and then
SOCK_STEAM which indicates that its a streaming socket.
To assign this Socket to our network port, we first set up some addressing information and
then call bind() to bind the Socket to a port. You may notice that we use some strange
looking functions when we set certain values in the socketAddr structure. The htons() and
htonl() functions are used to make sure the endian order of the bytes is correct. Remem-
ber that the Mac uses big-endian ordering for values, yet the PC uses little-endian ordering.
Calling this function ensures that the bytes making up these address values are in the correct
order.
Once the Socket has been bound to the port, we call listen() which will cause the Socket to
start checking for incoming connection requests. This is like plugging in the telephone, and
now youre waiting for someone to call you. In a little while well discuss how a Client
would dial up a Host, but for now lets just assume that theres someone out there dialing
our IP address, asking for a connection. How do we pick up the phone? In our Host Game
dialog, we continuously call the following function to see if there is anyone calling us:
if (!IsDataWaitingToBeRead(gHostListenerSocket))
240 Chapter 15: Networking
return(-1);
return(connectionSocket);
}
timeout.tv_sec = 0;
timeout.tv_usec = 0; // don't block at all
result = select(socket+1,
&connectionSet,
nil,
nil,
&timeout ); // pass timeout info
/* CHECK RESULTS */
if (result < 0)
return(false);
if (FD_ISSET(socket, &connectionSet))
return(true);
else
return(false);
}
return(false);
}
This function is a bit cryptic, but what it does is quite simple. We call the select() function
to determine if there is data on a Socket, but it takes a few interesting input parameters. First,
we have to create a connection set that contains a list of Sockets that we want to test. In
our case, we only want to test one Socket, so we set up connectionSet like this:
These are two macros used to first zero out the set, and then to put a value in it. The one
value being set is our socket value.
Note that the first parameter sent to select() is socket + 1. Instead of setting this value to
the number of entries in the connection set, this value is always set to your Socket number
plus one. Strange, I know, but thats how some genius decided this should work.
The other parameter sent to select() is a timeout value. This indicates how long we want
the select() function to wait for any data to arrive. In our case, we dont want it to wait at
all, so we set the tv_sec and tv_usec values to 0.
With those parameters set, select() can now check if theres data waiting for us on our
Socket. If the result returned is negative, then an error occurred. If the result is 0 then no
data is waiting us, but if its greater than 0 thats still no guarantee that there is data. Well,
thats not totally true. In our case a return value of 1 would probably indicate that theres
data waiting on the Socket, but to be totally sure, we call another macro to test it:
if (FD_ISSET(socket, &connectionSet))
return(true);
else
return(false);
receive data between the Host and the Client. The Listener Socket is no longer needed once
all of the Clients have joined the Hosts game and youre ready to play. That is, of course,
unless you want to allow new Clients to join the game even while its playing. You can close
down any Socket by calling close().
Requesting a Connection
So, we know how to pick up the phone, but how do we place the call? The first step was
locating the Host and extracting its IP address, which is what we did earlier. That was like
looking in the Yellow Pages for a plumber that you like. The IP Address that we got for the
Host was like getting that plumbers phone number. Now the Client needs to create a Socket,
and issue a connection request to the Host.
Next, we set the address that we want this socket to connect to:
The addresss family is set to AF_INET since its an Internet connection that were making,
and then we set the port number to MY_PORT_NUM. We could also set the port number to the
value that was returned by GetIPAddressOfHost() earlier, but since were hard-coding our
port numbers in our sample project, we might as well just use the constant.
The IP address is set with the cryptic function inet_pton(). This converts the IP address
string into the format that hostAddr needs.
To issue the connection request we simply call connect(). Even though the Host hasnt had
time to accept() that request yet, we can go ahead and start sending data which will get
queued up on the Hosts end. The first thing that we want to send the Host is our player
name. This is just a C string containing the name that the player wants to be seen as.
Sending Data
To send data from a Client to the Host or from the Host to a Client, well want to call this
new function:
244 Chapter 15: Networking
n = count = 0;
while (count < numBytes) // loop until we've sent all the bytes
{
n = send(socket, bytes, numBytes - count, 0); // write data
return(count);
}
There are several Socket functions that we can use to send data, but the one weve chosen to
use here is send(). There is also a write() function that does the exact same thing as
send() except that it does not have the flags parameter. Use whichever function you prefer,
but be aware that calling send() or write() does not guarantee that all of the bytes you
requested got sent. These functions return a byte count indicating how many bytes were
actually sent, so we sit in a while loop to make sure that everything goes out.
Remember how we needed a function to determine if a Socket had data waiting to be read on
it before we called accept()? Well, the same thing goes for writing data. We need a
function that checks the Socket to let us know if its safe to write data to it. This function is
nearly identical to IsDataWaitingToBeRead() since it works the same way:
timeout.tv_sec = 0;
timeout.tv_usec = 0; // don't block at all
result = select(socket+1,
nil,
&connectionSet,
nil,
&timeout );
/* CHECK RESULTS */
if (result < 0)
return(false);
return(false);
}
The only difference between this function and IsDataWaitingToBeRead() is that the
rd nd
connectionSet is passed in as the 3 parameter, not the 2 parameter. This configures the
call so that were checking the Sockets output queue instead of its input queue. If the output
queue is clear then the result will be 1, so were safe to send data over the Socket.
It is not absolutely necessary that you call IsSocketReadyForWrite() before sending any
data. If you do call send() and the Socket wasnt ready for it, then send() will just block
until the Socket is ready. So, if we want to prevent our application from stalling its a good
idea to check the write status of the Socket before trying to send() data through it. As youll
see in the sample project, I only call IsSocketReadyForWrite() when passing game data
during gameplay. I dont bother with it when Im sending setup information before the game
starts.
246 Chapter 15: Networking
Unfortunately, when sending data over a TCP/IP connection there is one big problem that
pops up all the time. The data were sending between players is generally pretty small, and
the OS doesnt immediately send network data unless theres a lot of it. If we try sending say
100 bytes, that 100 bytes goes into an output queue where it sits until one of two things
happen: either more data comes in forcing the queue to flush, or some time has passed
(around 1/5th of a second) in which case the OS goes ahead and sends the data.
One way to get around this problem is to use UDP connections instead of TCP/IP connec-
tions. UDP data is always sent immediately, however, it is not guaranteed to ever get to its
destination. TCP/IP data is guaranteed to get where its going. You can fiddle with whatever
connection types you want to try for your game, but if you want to make sure that TCP/IP is
flushing its queue every time you send data then theres a little trick that you can do: send a
lot of data! In the sample project, youll see this call in a few places:
I call this after Ive sent the important information that I want the other players to get. What
this does is send a block of 3500 bytes to the Socket, thereby causing the output queue to
flush and send everything immediately. This is a total hack and will cause problems if youre
on a slow network, but for the purposes of our sample project it actually works quite well.
You just need to remember to read all that garbage padding on the receiving end.
Receiving Data
To receive data from a remote Socket we have a function that should look rather familiar:
n = count = 0;
while (count < numBytes) // loop until we've got all the bytes
{
n = recv(socket, bytes, numBytes - count, 0); // read data
else
if (n < 0) // error
return(-1);
}
return(count);
}
The ReadNetData() function looks virtually identical to the WriteNetData() function. The
only difference is that we call recv() instead of send(). We could have also used the
function read() instead of recv() since its completely identical minus the flags parameter.
Just like when writing data, reading data is not guaranteed to get all the bytes you requested,
so ReadNetData() sits in a loop until all of the needed data has been received.
Now you know the basics of networking on OS X, but believe me, there is a lot more to
networking if you want there to be. I recommend reading up on BSD Sockets as there are
lots of resources on the Internet dealing with this technology.
249
Serial Numbers
Using unique serial numbers is the first thing that all of your games should do whether
theyre downloadable or come on a CD. If your games are downloadable and the user buys
the serial numbers on your web site, then you can write an algorithm that embeds their credit
card number into the serial numbers that they purchase. This is highly recommended, and if
you do it you should publicize it. If a person knows that their credit card number is encoded
in their serial number, then theyll be much less likely to share that serial number with anyone
else.
Youll need to spend a little bit of time each week on the internet looking for serial numbers
that people have posted to pirate boards and other places. When you find a serial number,
you can add it to your list of known voided serial numbers so that they wont work anymore.
This technique is becoming very popular in all sorts of applications, not just games. The trick
is in figuring out a good way to void those serial numbers on users machines after theyve
already started using them.
Your game should have an internal copy of this pirate serial number list, and when a user
enters their serial number it should get verified against it. Every time you post an update to
your game it should include an updated list. One trick that I do is to update this list con-
stantly, and re-upload the files, but I keep the games version number the same. This totally
confuses pirates. Someone might post a serial number to a pirate board and say that it works
with version 1.0.5, but once I see that and update the list, any more people downloading 1.0.5
will be totally confused why that serial doesnt seem to work with their copy.
250 Chapter 16: Copy Protection
Phone Home
Not everyone is going to download updates, however, so we need to find another scheme for
dynamically updating the pirate serial numbers list on a users computer. This is done by
phoning home every few days. In other words, every so often you can have your game
read the current pirate serial number list off of your web site, and then compare the users
existing serial number to those in the pirate list. If there is a match, then the user is using a
pirated serial number. This is actually quite easy to implement, and all of the games that
Pangea Software has released since around 1999 do this.
You should never dial out a modem since this would violate the customer aggravation
credo, so before trying to read a file off of a web site see if there is currently a live connection
to the Internet.
if (iErr)
return(false);
return(true);
}
All this function does is make a single call to OTInetGetInterfaceInfo() which returns
true if the Open Transport IP protocol stack is loaded. If theres no IP stack loaded then
theres no Internet connection. Downloading a data file is equally as simple:
if (buffer == nil)
DoFatalAlert("\pDownloadURL: buffer == nil");
/* DOWNLOAD DATA */
251
err = URLSimpleDownload(
urlString, // pass the URL string
nil, // download to buffer, not FSSpec
buffer, // handle to buffer
0, // no flags
nil, // no callback eventProc
nil); // no userContex
return(err);
}
To call DownloadURL() we would first need to allocate a handle to hold the data were
reading, and then pass a string containing the full URL of the file were looking for. You can
read any data from a web page with this function. For example, heres how to read the
Pangea Software homepage:
buffer = AllocHandle(100000);
DownloadURL(http://www.pangeasoft.net/index.html, buffer);
As long as were going through all of this trouble to download a data file, why not make it do
more than just obtain a list of known pirated serial numbers? I like to embed all sorts of
useful information in these data files including version information and messages. The
version information can let the user know if there is an update of your game available, and
putting messages in there is a great way to let customers know about new products and things
of that nature. The data files that my games download look something like this:
#$#$
#BVER
2.0.0
#BSER
DJCDIADPOOOT
JDCQWWOBVLKA
ARBIICOSLNCP
*
#NOTE0003
Our new game, Nanosaur 2: Hatchling is now available! Get the free demo at
www.pangeasoft.net/nano2
#TEND
#$#$
This tag indicates the start of the file. Its important to look for this because your buffer
might have been filled with a 404 File Not Found message from the server if the data file
252 Chapter 16: Copy Protection
wasnt found. DownloadURL() wont return an error if the server kicks out a message like
that. As far as the Mac is concerned it read valid data, so weve got to check that the data is
actually our file by looking for this tag.
#BVER
This is short for Best Version, and the data to follow is the version number of the most
recent update to the game. The game compares this version number with its own version
number, and if its newer then it will display a message to the user informing them that an
update is available.
#BSER
This is short for Bad Serials. The data following this tag is a list of known pirate serial
numbers. The end of the list is indicated by an asterisk.
#NOTE
This tag also has a number after it indicating the Note ID #. We track these ID numbers so
that we know what messages have already been displayed to the user. Obviously, we dont
want to display the same message every time the user runs the game, so we track which
messages have already been viewed. In the example above, the message is #3 so the follow-
ing text will only be displayed once, but if we later change the message to #4 then whatever
text follows it will be displayed once.
#TEND
This marks The End of the data file.
In the sample project Copy Protection.xcode youll see a new source file called Inter-
net.c. This file contains all of the code needed to load this type of metafile from a web site
and then parse the tags. This sample code is fairly stripped down since it doesnt really do
anything with the serial number data that it extracts out of the metafile. It does notify you
about newer versions, and it will display the embedded message, but all it does with the pirate
serial number list is print it to the Console. Additionally, the embedded message will be
displayed every time you run the sample application because were not tracking the Note ID
numbers here.
Hackerproofing
Hackers will do everything in their power to figure out your copy protection schemes and
find ways around them. It is possible, however, to make your game such a pain to hack that
most people wont bother. Thats what Ive done with most of my games, and as a result they
are very difficult to pirate on a mass scale.
253
One of the first things that hackers do is try to work around your phone-home code. There is
a free utility that they use called Little Snitch. What Little Snitch does is notify the user
whenever an application attempts to access the internet, and then it lets the user choose to let
it happen or to deny it. If the user denies the access, then URLSimpleDownload() will return
an error as though the web site was not found. Unless you take action on this problem,
hackers can quite easily get around your phone-home calls, so what Ive chosen to do in my
games is to simply not let the game run if Little Snitch is found. In other words, Ive inten-
tionally made my games incompatible with that utility.
To detect if Little Snitch (or any other process of interest is running), we do this:
/* SCAN ALL OF THE PROCESSES RUNNING & LOOK FOR LITTLE SNITCH */
while(GetNextProcess(&psn) == noErr)
{
iErr = GetProcessInformation(&psn, &info); // get process's info
if (iErr) // if err then no more processes
break;
return(true);
next_process:;
}
return(false);
}
Its up to you to decide how to handle things if Little Snitch is found, but my advice is to not
bring up a dialog saying Warning, this game is not compatible with Little Snitch. Anything
that obvious is only going to tell hackers what to look for when hacking your game. In my
games, I just quietly set a flag, and then I check that flag later when I load a level. If the flag
is set then I just quit the game no explanation given.
In your games youll need to try hard to find ways to make your code hackerproof. Another
tactic that hackers always use is to scan through your applications binary looking for the
hard-coded list of pirate serial numbers. If they can find the list, then theyll usually clear it
out to a bunch of 0s. What Ive done in my games to avoid this problem is to checksum the
list of serial numbers. Every time the game launches itll re-calculate the checksum, and if it
doesnt match my pre-calculated checksum then I bring up a message saying This applica-
tion appears to be corrupt, and then bail. Additionally, never store the raw serial number
data in a list. You should always encode it somehow so that its more difficult for hackers to
see. Even just XORing each character with some value like 0xC5 will make it much harder
to find.
Another thing that Ive learned to do is to rename my anti-piracy functions. That will make it
harder for hackers to locate the functions in question. For example, it would be unwise to
actually leave the CheckForLittleSnitch() function named like that because hackers will
see that, and immediately go to work on it. Instead, rename that function to something totally
misleading like AudioInitChannels().
Even if a hacker does locate some of your critical copy protection functions, its still easy to
make their lives difficult. Sometimes hackers will just put a return opcode at the front of
your critical functions which will cause them to be skipped. You can get around this by
making sure that flags are set inside each critical function, and then later in the game you
randomly check these flags to see if they were set. If not, then your copy protection code was
never called.
Never just set simple True/False flags either because a good hacker might figure this out.
Always set the flags to some sort of cookie value that will be much more difficult for a
hacker to understand. For example, in your serial number verification function you might
255
have a flag called gSerialWasVerified that gets set to indicate that verification did occur.
Set that flag to some crazy value like 0x1FA394 to indicate a False response, and maybe
0x42EA901 to indicate a True response. Things like that will drive hackers crazy!
One other important thing that I do with my serial number code is to put the functions in a
header file, and make them inline. Then I call the verification functions from several differ-
ent places in my game. This will cause your verification code to be duplicated in many
places, so even if a hacker thinks theyve worked around your functions they might not
realize that there are actually multiple copies of each function scattered all over the place.
No matter what you do, youll never be able to stop 100% of the piracy of your game. Thats
ok because all you need to do is stop the mass piracy, and thats pretty easy to do if you
take the precautions described above. Dont worry about little Jimmy giving his friend
Stephen a copy of your game thats an issue, but at least one of those two people actually
did buy the game. When your game is on a pirate board, however, the gloves come off
because nobody there is paying anything for anything.
257
The volume and directory IDs are set to 0 to indicate that were giving a path from the
default directory, and the default directory back on OS 9 was always the running applica-
tions folder. On OS X, however, the default directory is set to garbage when your
application launches. You need to manually set it with this code:
serial.highLongOfPSN = 0;
serial.lowLongOfPSN = kCurrentProcess;
info.processInfoLength = sizeof(ProcessInfoRec);
info.processName = NULL;
info.processAppSpec = &app_spec;
wpb.ioVRefNum = app_spec.vRefNum;
wpb.ioWDDirID = app_spec.parID;
wpb.ioNamePtr = NULL;
PBHSetVolSync(&wpb);
}
iErr = FindFolder(kOnSystemDisk,
kPreferencesFolderType,
kDontCreateFolder, // only locate the folder
&gPrefsFolderVRefNum,
&gPrefsFolderDirID);
The constant kPreferencesFolderType indicates that were looking for the Preferences
folder, but you can use this same call with other constants to locate all sorts of other system
folders. A complete list is found in the CarbonCore framework file named Folders.r, but
some of the more commonly used ones are:
Once youve located the Preferences folder youll usually want to create a folder for all of
your games preference files:
DirCreate(gPrefsFolderVRefNum,gPrefsFolderDirID,
"\pMyGame",
&createdDirID);
Language Determination
The correct way to handle localization on OS X is to have localized (translated) resources
for each language that you support, and put them into the appropriate folders in the applica-
tion bundle. For example, heres a directory listing for iTunes showing all of the languages
that it supports:
260 Chapter 17: Miscellaneous Mac Tidbits
The OS will automatically load the correct resources based on the current language settings
for the computer. This is all fine and dandy except that games often dont work quite that
way. Sometimes we really need to know what language is set so that we can manually handle
it in our game. To find this out we do this:
keyboardScript = GetScriptManagerVariable(smKeyScript);
languageCode = GetScriptVariable(keyboardScript, smScriptLang);
switch(languageCode)
{
case langFrench:
261
case langGerman:
printf(We support German!);
break;
case langSpanish:
printf(We support Spanish!);
break;
case langItalian:
printf(We support Italian!);
break;
case langSwedish:
printf(We support Swedish!);
break;
case langDutch:
printf(We support Dutch!);
break;
default:
printf(Unsupported language, so using English by default);
}
return(languageCode);
}
Determining what language the computer is using is the easy part. Doing the actual localiza-
tion of your game is not. If youve got no budget to pay a professional translator to localize
the text in your game then you can always revert to using Sherlock. Sherlock translations
tend to be quite horrible, but Ive used it on many occasions.
262 Chapter 17: Miscellaneous Mac Tidbits
Even though Sherlock translations are bad, youll end up getting emails from users who offer
better translations that you can put into the next update. Even when I hire professional
translators to localize my games, there are always some errors, and my customers always
catch them. So, when version 1.0 of a game comes out, the localization is awful, but within a
week or so youll have enough free feedback from your international customers that you can
update all the text and release version 1.0.1.
Should you choose to make a concerted effort to do good localization, then you can do a
search on the web for software localization service and youll have many companies to
choose from. Send your English resources and instruction manual to several companies for a
bid, and then go with the one you like the most. Youd be surprised how different the bids
for this will be. For example, when we bid out Nanosaur 2 we got bids ranging from $1700
to $5000. Obviously, we didnt go with the $5000 option.
Filenames
The standard file system on OS X is HFS, and HFS is not a case sensitive system. As far as
the OS is concerned, the names myfile.c and MyFile.c are exactly the same. Because of
this, many game programmers have been lazy about capitalization in their filenames and the
matching strings in the code. In Figure 17-1 youll notice that all of the data files for Bug-
dom were capitalized. However, in the code I wasnt very careful, and would often just use
all lower-case in the filename strings. Ive probably done this in every game Ive ever
written, but Ive really got to stop myself from doing this because it causes problems on OS
X.
263
Some people have formatted their drives with UFS (Unix File System) which actually is case-
sensitive. When they install any of my games on their UFS drives and then try to run them,
they eventually get a File Not Found error. There are other issues with running a true Mac
application on a UFS volume, so its not recommended either way, but to avoid problems
with future versions of HFS you should really try to be consistent with your filenames and
name strings in the code.
/* MAKE GWORLD */
iErr = NewGWorld(theGWorld, 32, &r, nil, nil, 0); // try app mem
if (iErr)
264 Chapter 17: Miscellaneous Mac Tidbits
DoLockPixels(*theGWorld);
(**hPixMap).cmpCount = 4;
When you allocate a 32-bit GWorld the component count (cmpCount) is actually set to 3. This
would tell Quicktime to only recognize the images RGB channels, but not the alpha channel.
We want to preserve any alpha channel information in the image, so weve got to manually
set the cmpCount to 4.
Before drawing the image into the GWorld, it is a good idea to call
GraphicsImportSetQuality() to make sure we get the best image we can. For most
standard image formats there is no difference in import quality, but to be safe well always set
this to codecLosslessQuality.
To draw the image, we simply call GraphicsImportDraw(), and Quicktime takes care of
everything.
265
When I tell people that I make games for the Mac, I usually get a funny look, but the fact is
that the Mac is an excellent platform for developing games. While we may only have 5% of
the worldwide computer market, its better to be a big fish in a small pond than a small fish in
a big pond, and it doesnt take much effort to become a big fish in Apples tiny pond. The
PC game market is so overcrowded that youd have a better chance of making ice cream in
Hell than starting a successful game company on that platform. The Mac, however, is an
easy market to break into and get attention. As long as youve got a good game, you can
make good money.
Here is a list of the best Mac Marketing Resources you should pursue:
This is the premier web site for gaming on the Mac, and it has been around since 1993. You
should always send any press releases to IMG since they are always happy to announce new
game information. If they like you enough theyll usually do previews and reviews of your
games.
266 Chapter 18: Marketing & Selling
You can also take out banner ads for a very reasonable price, and theyll be seen by your core
gaming audience, so its well worth it. To get information about current advertising rates
contact [email protected]
This site is actually run by Inside Mac Games, so there is some cross-linking between the two
sites. This is the main repository for Mac game downloads. Just about every demo, update,
and downloadable game can be found here along with user reviews and comments. To
submit your game to MGF click on the submit a file link at the top of the homepage.
While MGF is a good place to host your files, it should not be your only host. The file
transfer speeds on MGF are generally quite slow. I usually get between 2k/sec and 12k/sec.
Not exactly high-speed bandwidth, but its free so dont complain.
Mac Gamer
www.macgamer.com
This is another one of the big Mac gaming web sites. Always send your Press Releases to
these guys as well.
MacCentral
www.maccentral.com
This is not a game-specific web site, but it is the premier Mac news site and one of their
primary writers, Peter Cohen, is a big fan of games, so be sure to keep these guys in the loop
as your game nears release. MacCentral is actually operated by Macworld Magazine, so you
can get cross-promotional advertising rates. Ads here are a bit pricey for a small game
publisher, but they will reach a wide audience so if you can work out a good deal then go for
it.
VersionTracker
www.versiontracker.com
267
This is a hugely popular site that lists software updates and new releases every day. Its a
good marketing resource because just by having your game listed in their daily updates youll
get a ton of traffic to your site. Be sure to always let them know when youve released an
update to your game since thats just more free publicity.
Macworld Magazine
www.macworld.com
Macworld is the oldest major Mac magazine still around, and they have a very large reader-
ship base. Advertising in this magazine is very expensive, and you usually have to commit to
a series of ads, but Ive always found this to be an effective way to get the word out.
MacAddict Magazine
www.macaddict.com
This magazine has really increased in popularity over the last few years, and it probably
caters more to the gamer crowd than Macworld magazine does. The other nice thing about
MacAddict is that they ship a CD with every issue, and if youre lucky, theyll ask you if they
can put your game demo on there. This is a very huge deal! Whenever MacAddict has put
one of our games on their CD, our sales of that game will typically triple during that month!
You can also pay to have your game put on their CD.
Apple claims to get a massive number of hits on their games page every month, but honestly,
Ive never found any marketing here to make much difference in sales. Theyll often do
articles about new games, and theyll host movie trailers, but advertising here doesnt seem to
pay off at least it didnt for us when we tried it. You might have better luck.
Game Demos
The single most important thing you can do to market your game is to release a demo. People
dont like to shell out hard earned money for something theyve never seen, so a free demo is
the best selling gimmick you can do, and theyre easy to make. It usually only takes me
about 2 hours to build demo versions of any of my games. If youre just porting a PC game
268 Chapter 18: Marketing & Selling
thats already well known, then a demo probably isnt too important, but if youre developing
an original title then it is critical.
In the old days of video games (and by old I mean like 1986) most game demos would expire
after a certain amount of use. For some reason, people stopped doing this, but I highly
recommend that all game demos have an expiration time bomb in them. While its true that
an expiring demo will annoy some people, the fact is that for every one person that gets angry
with you for doing that, there will be ten other people buying the game who might not have
done so otherwise. Besides, if someone doesnt know if they want to buy your game after
playing it for an hour then its unlikely they will ever buy it.
You would not believe how many times Ive picked up the phone and heard a woman telling
me that she needed to order a game right away, and needed it shipped Next Day because her
son was having a tantrum since he couldnt play the demo anymore. I could always hear a
crying kid in the background. Im not making this up! This happens all the time, so trust me,
expiring demos are a great motivation to get people to buy your game. Small children,
especially, will just play a demo forever since they dont know better, but if that demo
expires you bet youll be hearing from the parents with a credit card number in hand.
The other important thing about game demos is that you should never give too much away.
Youll always be tempted to show the player all the cool stuff in your game, but resist! You
need to keep the demo downloads small, and you should always leave the customer wanting
more. Smaller demos are faster to download, thus, more people will try it out. On a similar
note, never ever release a game demo before a game has shipped! You need to get every
impulse buy that you can, and if the game isnt available when the user tries out your demo,
theyre going to blow it off, and youve just lost a sale.
My favorite example of a demo gone wrong is the one for the old EA game called Future-
Cop. This was a really cool game, but they released the demo about 2 months before the
game actually shipped, and the demo had way too much stuff in it. When I played the demo I
really wanted to go buy the game, but by the time that FutureCop finally shipped several
months later, I was tired of it. I had played the demo to death, and had no desire for more.
One of the troublesome issues with game demos is getting them hosted for people to down-
load them. Our game demos are usually 20-30 Megs in size, and if youve got thousands of
people downloading it every month, the bandwidth gets pretty heavy. As I mentioned above,
you can submit your game files to MacGameFiles.com which is free, but their bandwidth is
very poor, so you need to provide a faster way for people to get the files. You can occasion-
ally find sites that offer unlimited bandwidth for $29.95 month, but those are always garbage.
Your downloads will be even slower than MacGameFiles, and theres usually fine print that
269
says that unlimited bandwidth really means 30gigs per month. Anything over that costs
you $5 per gig or something ridiculous like that.
There is no magic bullet solution to the bandwidth problem, however, there is one service that
I can recommend which has been amazingly reliable:
www.fileburst.com
FileBurst charges about $1.00 per gigabyte of bandwidth, with discounts over 100 gigs. The
download speeds are wicked fast too. Ive never gotten less than 250k/sec off of their site
even during high traffic time periods. We use FileBurst to host all of our files including
game demos, updates, and full versions.
Statistically speaking, Ive found that about 1 in 20 people who download a game demo will
end up buying it (5% sell-through rate). If the demo is around 100 Megs (like Billy Frontier
was), then that means it costs about 10 cents in bandwidth per download. If only 1 in 20
people end up buying it then that means youve paid about $2.00 in bandwidth fees to make
the sale. You always need to work this into your business plan. I always assume $1.00 to
$2.00 in costs for bandwidth on each sale.
Selling Retail
Many people are going to think Im crazy when I say this, but here it goes Theres no
money to be made in retail on the Mac. There I said it. To start with, there are only a few
stores selling Mac software, and only two really big ones with significant sales volume:
CompUSA and the Apple Store. The only way to get into either of these stores is to use one
of the big distributors like Ingram Micro or Navarre. Without going into a long tirade, the
bottom line is that the middle men take so much money from you that theres no way you can
actually make a decent profit. Getting paid by a distributor is worse than pulling teeth, and
you should consider yourself lucky if you get paid at all. If I were to add up all of the money
owed to us from various distributors who simply stiffed us over the years, it would come
close to $20,000!
You dont want to mess with the mail order catalogs either. In order to get picked up by a
mail order catalog you have to agree to buy a huge number of expensive ads in their mailers,
and the sales are always very low. For example, we spent about $12,000 on an advertising
campaign for Cro-Mag Rally in one of the big Mac mail order catalogs. Guess how many
copies they sold just guess It was around 30. Yes, we spent $12,000 of our marketing
budget to sell 30 copies of a game that had a profit margin of around $10. That was a fair
trade dont you think?
270 Chapter 18: Marketing & Selling
Now I should point out that its not all bad. As long as you deal with the small guys, your life
can be much better. We still do some retail distribution, but now we use a nice little distribu-
tor called Visco Entertainment. These guys are honorable and always pay their bills on time,
so I recommend inquiring with them about distribution:
www.viscoent.com
Selling direct to the smaller retail stores is usually a pleasant experience as well. The small
companies are run by caring human beings who arent out to screw you, so you can feel safe
doing business with them.
Selling Online
Online sales are the future of software distribution, and the future is now. About two years
ago Pangea Software switched its business model from retail sales to online sales, and it was
the best business decision Ive ever made. Our profits skyrocketed since wed cut out all of
the middlemen. Plus, we got immediate payment from the credit card companies since all of
these sales were direct-to-end-user sales. On a $40 game we would be lucky to have a $10
profit from that when we went through the big distribution channels, because by the time you
deduct the manufacturing and shipping costs, discounts, rebates, returns, and lack of pay-
ments, that was about all that remained. But with direct online sales a $40 game sells for $40
and our only costs are the $1 to $2 in bandwidth fees plus the 3% that the credit card com-
pany takes. The rest is all profit.
While it is true that youll sell more units in retail than you can online, the profit margin is so
much higher with online sales that it more than makes up the difference. We make over 3x as
much profit on an online sale as we did with retail sales. So, even if we only sell half as
many copies with online distribution, were still making a whole lot more profit. Profit aside,
think about all the stress that you dont have to deal with when doing online distribution.
You dont have to worry about when or if youll ever get paid, and you dont have to deal
with buyers or any of those people who are out to squeeze you for every penny.
Figure 18-1: The Pangea Software store front for downloaded games
Once the user purchases a serial number for a downloaded game they will be emailed the
serial number.
It will cost you a few thousand dollars to get a store like this up and running, but if you
cannot afford that then there is another option open to you. There is a well-known company
named Kagi that, for many years, has offered a shareware payment collection service, and
they too can spit out serial numbers for these kinds of purchases. The two biggest downsides
to using Kagi are that they charge a pretty big fee for each sale (usually around $2.50), and it
takes a while to get paid. If you make a sale on January 1st, you will likely not get paid for
that sale until around February 24th. In contrast, a sale on a custom order page might only
cost you $1.00 in credit card fees, and the money will be deposited into your account the next
business day.
272 Chapter 18: Marketing & Selling
Even though we have our own custom store to handle all of our orders, I still keep my Kagi
account up and running in case there are malfunctions with my store and I need to point
customers elsewhere. It doesnt cost anything to have a Kagi account, so its good to always
have it as a standby.
Weve actually used a few different custom store technologies over the years, but by far the
best one is the one that we use today that XACT Commerce built for us:
www.xactcommerce.com
Theyve designed a fantastic order page and processing system, and they are very quick to fix
any problems that come up. If youre going to sell your games online then I highly recom-
mend contacting these folks to have them build your store for you.
273
Youve got a lot of options open to you when it comes to manufacturing, but remember that a
penny saved is a penny earned, so dont go overkill on the packaging for your game. In the
old days when game boxes were really large, it would cost us about $2.50 per unit. These
days we ship everything in standard DVD style cases, and that has reduced the cost to about
$1.50 per unit. In addition to that, our shipping costs have gone way down because a DVD
case fits quite nicely in a standard UPS letter shipper. The old boxes required larger packing
envelopes, and UPS charged us accordingly.
Remember that this is the Mac were talking about, so sales volumes are not going to be in
the hundreds of thousands. If youre also offering a download version of the game then
assume that most of your sales are going to be from that, not the CD version. What Im
trying to say is that you dont want to have 10,000 units of your game manufactured since
youll probably never sell all of those. Do small builds of 2,500 units at a time. These
smaller builds will cost a little more per unit, but at least you wont have excess inventory to
use as firewood for the next eight years.
There are many companies out there who will manufacture your packages for you, but you
want to use a manufacturer who is also an Order Fulfillment company. An Order Fulfillment
company receives the orders from your order page, and processes them in their warehouse.
You dont have to worry about stuffing games into envelopes and all that mess. They do it
for you for a fee, of course. The typical fulfillment fee on a package will cost you around
$3.50 more for international orders.
The company that we have used for the last 5 years is Software Packaging Associates in
Cincinnati, OH.
www.softpack.com
In addition to the fact that we like working with these people, there is one other thing that
makes them a good fulfillment option: location. Ohio is centrally located in the US which
means that Ground shipping to anywhere in the country is only a few days. If you choose a
fulfillment company in California, then any of your customers on the East Coast are going to
have to wait a very long time for their packages to arrive. It is always a good idea to find a
fulfillment company that is geographically central.
274 Chapter 18: Marketing & Selling
The other great thing about Software Packaging Associates is that theyre in DHLs hub city:
Cincinnati. We do all of our international shipping with DHL because they have the best
international rates, and since were shipping from their hub city weve managed to get an
incredibly good deal from them. If you were shipping from, say Phoenix, DHL would
charge you more because every package still has to make its way to Cincinnati. So, its a
good idea to use a fulfillment company thats located in a DHL, UPS, or Fed-Ex hub city.
One final note about shipping: make these companies fight for your business. Dont just call
up UPS and ask for an account. Ask them what kind of discount you can get since youre
going to be using UPS a lot to ship a lot of packages. Tell them youre also talking to the
other guys, and you want the best deal they can give you. I kid you not when I say that you
can get discounts of 30-50% right off the bat if you try hard enough.
276
Index
AbsoluteToNanoseconds, 65, 66
AGL, 26-31, 37, 38, 220, 223, 224 backfaces, 188
aglChoosePixelFormat, 26, 27 bandwidth, 266, 268-270
AGLContex, 37 BG3D, 175, 179, 180, 185, 186, 188, 197-202
aglCreateContext, 26, 28 bit-depth, 12, 17, 28
aglEnable, 27, 29 black & white mode, 216
aglSetCurrentContext, 27-30, 210, 221 BlockMove, 22, 23
aglSetDrawable, 27, 28, 33 blue-line sync, 220-224
aglSetFullScreen, 27, 29, 34 Browser, 232, 233
aglSwapBuffers, 29, 30, 211, 222 BSD Sockets, 229, 238, 247
aglUseFont, 31 Buffers, 113, 116
AGP, 60 Bugdom, 2, 140, 198, 213, 257, 262
AIFF, 85 BuildGreyCurve, 217
alBufferData, 117, 118 BuildResolutionMenu, 18, 22
alcCreateContext, 114 bump mapping, 9
alcMakeContextCurrent, 114 button names, 167, 168
alcOpenDevice, 114
alDistanceModel, 114 cache, 38
alEnable, 114, 115 Cached Mode, 59
alGenBuffers, 117, 118 CalcDisplayVRAM, 46
alGenSources, 120 CalcFramesPerSecond, 65, 66, 71, 77
alListenerfv, 118, 119 calibrate, 158
alpha, 27, 195, 264 callback, 75-78, 81, 83, 90, 93, 96, 97, 100,
alSetInteger, 114, 115 102, 154, 231, 233, 234, 251
alSourcef, 120, 121 CallMeWhen, 90, 93, 97
alSourcefv, 120, 121 capturing, 12
alSourcei, 120, 121 Carbon Events, 75-82, 129, 134
alSourcePlay, 120, 122 Carmageddon, 67
AltiVec, 49-52 cartoon shading, 9
alutInit, 114, 115 CFArrayGetCount, 13, 153
alutLoadWAVEFile, 116 CFArrayGetValueAtIndex, 14, 15, 235, 236
alutLoadWAVFile, 115, 117, 118 CFBundleCopyResourcesDirectoryURL, 87,
alutUnloadWAV, 117, 118 88
AnaBuilder, 224-227 CFBundleGetMainBundle, 17, 87
Anaglyph Glasses, 203, 209, 218 CFDictionaryGetValue, 14-16, 145-150, 153-
AppendMenu, 23 158
ATI, 37, 56 CFNetServiceBrowserCreate, 232, 233
audio, 85-123 CFNetServiceBrowserScheduleWithRunLoop,
AutoSleepControl, 83 232
Axis, 141, 154, 157-159, 174
277