Monday, May 2, 2011

Scribble Worm iPad Post-Mortem


Scribble Worm is our new puzzle game for iPad hitting stores any minute now. It's also our second game after making Big Mountain Snowboarding for iPhone and porting it to Android, iPad, and Mac. It started as an experiment to try to make a small game and get it out quickly, but even small games seem to be much bigger than first expected. It ended up taking us a little over 5 months, and we still have to do a small screen version for iPhone and Android, and a version for Mac.


The Game:


You draw a worm with your finger and it follows your pattern to try to reach the apple. We throw various obstacles in the way such as a wall that worms bounce off of and a wall that kills the worm when touched. We then animate the walls around the screen for an extra challenge.


I've never seen a game like this before, so it's definitely not a "yet another" game. It's not a jumper, an angry animals, a flight control, or a match 3. If we can get people to try it out I think a lot of people are going to like it.




Art Style:


The art is a combination of procedural effects and hand drawn "scribble" images done by myself inside ArtStudio for iPad. It looks a lot like a doodle game, but there's actually a lot of stuff going on behind the scenes to get the crayon and paper look. Neither of us is particularly good at making 3D models without outsourcing, so it seemed like a good approach that we could pull off well.


I'm pretty happy with the end result, but it's going to be terribly hard to stand out among the millions of doodle games out there. I think it's several steps above Doodle Jump when in motion, but it's hard to sell the differences in screenshots. Our next title might have to be more visually unique in order to stand out.


Music and Sound Effects:


All of the sound in this game is created on the iPad. I wrote two songs in Garage Band for the main menu and in-game music. Actually I wrote about a dozen songs while trying to make some cutesy music, but everything kept turning out as inappropriate death metal. The sound effects are also recorded in Garage Band using the sampler feature. The eat apple sound is actually me chomping on a carrot with an iPhone microphone next to my mouth.


Puzzle Design:


Making the actual puzzles turned out to be the single biggest chunk of work in the game. It seems obvious now that making puzzles is going to be a huge part of making a puzzle game, but I underestimated it by several orders of magnitude. Fortunately Katie picked up this ball and ran with it.


We have an in-game editor that lets you drag and drop walls, and then try it out in real time and make adjustments. This WYSIWYG interface was infinitely better than trying to edit the XML files and reload.


Play Testing:


The first line of defense in play testing is yourself and then your friends, but your friends aren't going to be likely to tell you that something sucks. We're fortunate to have the Boston Indies group that meets up nearby to try out each others games. We actually went through at least 3 revisions of show it to lots of people, then tune, then show it again. It really helped us out to both reinforce that we have something cool, and to see where people struggle or do things we don't like. Always try to find some people who don't like you to try out your game before releasing.


Our first version of the game was entirely black and white, trying to look like pencil on paper. One of the big things to come out of play testing was we had to add more color to the game. People were identifying the worms as various bad things. The crayon scribble shader was born and I think the game looks much more appealing now.


Another thing to come out of play testing was we had to fix worm spamming. After a few levels people would stop trying to solve the puzzles and instead just try to make as many worms as possible until one randomly reached the apple. While this may be entertaining for a short period there's not as much gratification as actually figuring out how to solve the puzzles. Our first attempt to fix this had the number of worms limited. If there were already several worms in the world you couldn't spawn a new one. This could get frustrating if worms got stuck anywhere, so we changed it to spawning a new worm removes the oldest worm from the game.


Marketing:


It feels like whispering into a hurricane sometimes to try to get a game noticed. The most effective ways of marketing an iOS game are picking a good name, having a good logo, and having good screenshots. All of these are free. You then hope and pray and try to find ways to stack the odds of making it into the top charts or getting featured, and even then your game could disappear quickly. Ads on big sites run at around $600/month with a waiting list, and seem to have greatly limited effectiveness.


We did make a video as you can see in the section about the game itself. Some of the art and UI is slightly outdated now, but overall I'm pretty happy with it. However, at the time of this writing we only have 28 views of the video! Some of this might be due to embedded views, but youtube by itself is not a great marketing tool. I do plan on linking to this video or a newer one from the app description though.


Twitter is something new that I've been trying out (@DJWGoldenHammer). It's fun, but so far has not been very useful. I have 134 followers, but I think a lot of them are just following me so that I'll follow them, and don't actually care about anything I'm tweeting. It hasn't been a great place for marketing, but it has turned into a really good way for me to find other dev blogs to read.


We have a press release ready to go on prmac.com. Hopefully that will help some, but it's a new thing that we have not tried before. I must admit that getting on any of the big sites is a mystery to me. Even after close to a million copies and steady sales for two years Big Mountain Snowboarding has just been a blip in the blogosphere.


Our ace in the hole is the free version of Big Mountain Snowboarding, which gets 1000-2000 iOS downloads per day. We can use this as an advertising platform for Scribble Worm. The problem right now though is adMob does not support house ads for iPad and iAds does not support house ads at all. We'll run house ads through adMob, but we might need to make a new version of BMS to really push the Scribble Worm ads to the right people. The danger is that BMS Lite currently has a really solid 3.5 star average with over 200 ratings for the current version that I'm a little afraid of jeopardizing with a new version. If all else fails we can put some ads in BMS full and do a free for a day promotion.


For the screenshots, we're trying out the Chillingo approach of having images with extra taglines on them to try to sell it better.



Why just iPad?


I love to talk about how we have a cross platform engine and want to be on every device in the universe, and yet the first release of Scribble Worm is only on iPad. This was mainly in order to limit the amount of work so we can have a high quality focused version. The iPad is a dream to develop for. It has exactly one screen resolution and aspect ratio, all of the hardware supports shaders, and the dev tools are pretty good. To be on iPhone we need to make new versions of most of our UIs for the different aspect ratio, and make a fallback version of the graphics for older devices. The Android port is even more work on top of that. We're planning for OSX as our second release because we don't have to redo most of the UI.


A slightly sneakier reason is that the iPad market is much smaller than iPhone. There are about a dozen new iPad releases per day, compared with more than a hundred on iPhone. If we can manage to get any publicity off the iPad or Mac versions, then maybe it will make the iPhone release a little easier. We're planning on converting the iPad version to universal once we make the iPhone version.


What went right:

It's not a proper post-mortem without a what went right and what went wrong section!

  1. The initial idea was a solid one. I'm very happy with the end result in terms of gameplay.
  2. We made good use of our existing tech. Every project has us expanding the engine, but much of what we developed for Big Mountain Snowboarding and OverPowered applied to this game as well!
  3. Boston Indies was great for play testing, and really helped us refine the game and the look.
  4. Being on iPad exclusively really let us work faster by only having to worry about one set of variables.
  5. Between ArtStudio, Garage Band, Pixelmator, and XCode all of our tools were cheap and awesome.
  6. Somehow we managed to nail the cute factor of the worms. Adding bright colors to the worms and the obstacles seems to have helped a lot.
  7. Having a level editor was essential for making this game.

What went wrong:

  1. Underestimating the size of the game. I've had to revise my definition of a "small game" to not include anything that has proper level design.
  2. Marketing black hole. You probably have not heard of this game unless you know one of us.
  3. Dropping a video on youtube does not seem like a good way to get people to actually watch it!
  4. While the art style may look good and even better in motion, it may end up making it harder for us to get noticed than something more unique.

What we still don't know:

  1. Are our name, screenshots, and icon good enough?
  2. Will linking to a video in the app description help at all?
  3. Is our staggered release plan actually a good one?
  4. Can we get Apple to notice us and show us some love?
  5. What effect will a prmac.com press release have?



Tuesday, April 5, 2011

Sales per platform - Big Mountain Snowboarding


Big Mountain Snowboarding has been out for a while now on iPhone, iPad, Android, and OSX. I thought it might be interesting to see a breakdown of where the sales are coming from, so without further ado:


I wish I had numbers for iPad vs iPhone, but that's one of the many downsides of releasing a universal binary. We're looking at adding Flurry to future releases to get that kind of info. If you look at our free version, it looks like about 40% of our users are on iPads.

The big surprise to me is iAds. We first ask for an iAd, then if none are available we go to Admob. As you can see, it fails to get an iAd fairly often, and the income wasn't even worth paying attention to. Sometime recently that changed and iAds picked up a huge amount of steam. Here's a picture of the iAds trend.

Additional Platforms:
  • Windows Phone 7 - No C++, so we decided to pass. Reading this link makes me not regret this decision.
  • WebOS - We'll probably eventually port to here.
  • Blackberry - Still waiting to hear details on the NDK and how they support Android apps.
  • Amazon Market - This is looking like it has potential. Once we finish up our current project we'll make the adjustments to get on it.

Tuesday, January 11, 2011

Getting on the Mac App Store

Big Mountain Snowboarding was just released on the Mac App Store, 1 week late for the grand opening. We started on the port as soon as the "within 90 days" announcement was made.


Technical Difficulties


I talked about a lot of the details of the port in a previous blog post.


Playtesting and Revision


Big Mountain Snowboarding was created as a handheld tilt game. Obviously that's not the ideal control scheme for someone on a 27" iMac. I wanted to do something cool with the touchpad so we turned it into a joystick, and made a couple fallback controls for people who don't have a touchpad. We had some back and forth and some revision on some pretty bad early controls.


We then took a macbook over to the bostonindies demo night and got some people we don't know to play it. The people struggled with it. The controls were not good enough to release at that point, so we spent another couple days trying to improve it. They are much improved, but I'm not convinced they are awesome. We'll probably have to make a couple patches before we really get there.


Apple Approval


Our first attempt at submission happened on 11/19/2010. We never left the waiting for approval stage. I had made several improvements and decided to pull the app to upload another attempt on 12/1/2010. After a couple weeks of waiting we got a rejection because the .app name was not the same as the application name, "Big Mountain Snowboarding OSX". I fixed this and resubmitted. About another week later we were rejected for having "OSX" in the name. I think whoever said the names didn't match up could have also mentioned that the name was unacceptable, but ok.


On 12/30/10 we were approved and all set to be a launch title. Then a few days later I noticed a game-breaking bug that slipped in to the game when making the build to remove "OSX" from the name. We share our engine between all of the games we make and we don't branch, so development of one game can cause us some bugs in other games.


I submitted another build at about 3 in the morning of 1/4/2011, with the app store set to launch sometime on 1/6/11.


Actual Launch


We had to make the decision between missing the app store launch or shipping a broken build. We were worried about getting nothing but 1 star reviews early on killing any chance of future sales, so we removed the app from sale before launch. We watched 1 million downloads happen on the store the first day while not being a part of it. It was a tough call, and I'm still not sure which was right. I woke up early on Tuesday to pull the app and we had already sold 4 copies at $4.99 each.


I was getting a little anxious earlier today about whether or not being "developer removed from sale" was preventing our approval so I sent Apple a mail asking about it. I guess someone from Apple liked the game because we were approved shortly after and placed in the "Staff Favorites" section of all games. I also dropped the price to $0.99 for the launch to try to get us some initial chart placement.


I'm hopeful. BMS isn't the game I'd make from scratch for the Mac platform, but it wasn't a viable market back when we started. The more platforms with an audience available for the little guy the better.


Sunday, December 12, 2010

Android NDK and Storage Size

I decided to update our Android build of Big Mountain Snowboarding today because we have made a lot of progress in the game and engine while porting to OSX for the upcoming mac app store.

Eventually the build was ready, and uploaded! Somehow updating a patch put us back on the "Just In" tab on the app store for some nice visibility. Reception has been pretty ugly. One guy sent us a support email that just says "U suck". Thanks, not helpful.

Then we got a 1 star review with actual good info in it:
by Joe (December 12, 2010)
Really? More than 20MB used in phone storage even
after moving it to SD? BUH-BYE!

This confused me for a bit. Our APK file is only 13 megs, and it supports moving to the SD card! After some digging I found out that the .so files generated by the NDK are copied out of the APK file on the SD card into main storage. It's also 20 megs!

After some more digging I found out that we were exporting all of our symbols, all 33,000 of them. Time for fvisibility=hidden.

In the makefile:
LOCAL_CFLAGS += -fvisibility=hidden

In our JNI files for any functions we need to call from java:
extern "C"
__attribute__((visibility("default")))
void
Java_goldenhammer_BMSnowBase_YrgEngineInterface_ourFunc(JNIEnv*
env, jobject thiz)
I was happy, surely this was going to solve all problems! After a full rebuild taking 30 minutes our .so file was down to a disappointing 19 megs. More digging in the google groups led to a little command line tool shipped with the NDK.
arm-eabi-strip --strip-debug --strip-unneeded libxxx.so

This brought us down to 1 meg! Woot! arm-eabi-strip is located in android-ndk-r5/toolchains/arm-eabi-4.4.0/prebuilt/windows/bin.

Saturday, October 30, 2010

Porting from iPhone to OSX

If you've followed us at all you know by now that I like porting to new platforms, at least new platforms that support C++. Every port seems to make the base engine a bit better, and it brings in a few extra bucks to support continued development. With the news of the Mac app store coming soon I had to jump on porting the Golden Hammer engine and Big Mountain Snowboarding to OSX.


Our engine started on Windows, moved to OSX (carbon), then to iPhone/iPad, then to Android. The OSX port was never really finished because the iPhone took off. Carbon is outdated technology (see below), so for the port back to OSX I started over using Cocoa.


The port isn't fully ready for release, but within a week I was able to get the game pretty much working. This was definitely the easiest port so far.


Carbon vs Cocoa (use Cocoa)


OSX supports two different platform layers, Carbon and Cocoa. Carbon is in C, only runs on 32 bit, and does not seem to be fully supported anymore. The Carbon implementation in my engine gets a ton of deprecated code warnings, and there's a Mac App Store requirement that apps not use deprecated technology. I have not seen an official and specific announcement from Apple on Carbon, but to be safe it's better to go with Cocoa.


Also, Cocoa is almost a direct equivalent of Cocoa Touch, the API used for iPhone development. There are a few little differences that I'll note in the next section, but for the most part you can make copies of your iPhone platform layer with different includes, fix the compiler errors, and be ready to go.


Cocoa vs Cocoa Touch


As I said, Cocoa is almost exactly like Cocoa Touch. For most of my classes I was able to just make a copy of the iPhone version, add some different frameworks, rename a couple classes and be good.


The frameworks I'm currently using are:

  • Cocoa.framework
  • OpenGL.framework
  • ApplicationServices.framework
  • OpenAL.framework
  • AudioToolbox.framework
  • AppKit.framework
  • CoreData.framework
  • Foundation.framework

Most UIKit classes have an AppKit equivalent. Instead of UIView, there's NSView. Instead of CGPoint, there's NSPoint. The first line of defense on a compile error is to stick an NS in front of the class name and see if that works.


32 bit vs 64 bit


This probably won't matter to most developers, but Cocoa will compile for 64 bit systems. We're doing some behind the scenes magic with pointers and such, so I had to go through the codebase and replace a bunch of long data types with int32_t and u_int32_t, and remove some of the more questionable pointer code.


OpenGL vs OpenGL ES


For a first pass implementation, you can think of OpenGL as a near direct equivalent to OpenGL ES1. This is a horrible simplification, and I fear a lot of OpenGL users coming at me with pitchforks for saying it. A better way to think of it is ES1 is almost a direct subset of the full OpenGL, and you can get ES1 code running on OpenGL pretty easily. I have not yet ported our ES2 shader code, so I can't comment much on that aspect.


The includes you want are:

  • #include "OpenGL/gl.h"
  • #include "OpenGL/glu.h"

Make a copy of your ES1 implementation, change the includes, and hit compile. You will get a ton of compile errors. Most of them are easy to fix. For any function or variable that has "OES" at the end of it, simply delete the OES part. For any function named something like "glFrustumf", delete the "f". This will take care of 99% of the compile errors.


I'm not quite 100% sure, but I don't think PVR4 support is available. If I'm wrong on this let me know! It would save me some work. Right now I just have uncompressed textures, but DXT support seems to be available for use.


OSX Input


Modern macs support multitouch through the touchpad. The gotcha is that the mouse needs to be over the window in order for the app to receive any touch events. I'm planning on supporting keyboard inputs for those without a touchpad, and making the game fullscreen so we can always get the touch events.


You'll want the following functions in your NSView:

- (void)mouseMoved:(NSEvent *)theEvent

- (void)mouseDragged:(NSEvent *)theEvent

- (void)mouseEntered:(NSEvent *)theEvent

- (void)mouseExited:(NSEvent *)theEvent

- (void)mouseDown:(NSEvent *)theEvent

- (void)mouseUp:(NSEvent *)theEvent

- (void)keyDown:(NSEvent *)theEvent

- (void)keyUp:(NSEvent *)theEvent

- (void)touchesBeganWithEvent:(NSEvent *)event

- (void)touchesMovedWithEvent:(NSEvent *)event

- (void)touchesEndedWithEvent:(NSEvent *)event

- (BOOL)acceptsFirstResponder { return YES; }


You'll also want to call these somewhere:

[window setAcceptsMouseMovedEvents:YES];

[view setAcceptsTouchEvents:YES];


Additional platform considerations


The straight port of snowboarding runs at 2ms/frame on my 13" macbook, or 500fps. This is without making any use of multithreading or doing any real hardcore optimizations anywhere. I think any halfway decent straight port of an iPhone app should run at crazy speeds on a low end Mac.


So is a straight port good enough? I have no idea and neither will anyone else really until the Mac App Store has been out a while. Is it competing with Steam and games like Half Life 2, or will the audience want smaller simple games? Can't tell yet! I'm exporting higher res maps and doing something in the middle.


Tuesday, October 12, 2010

Converting from OpenGL ES1 to ES2 on the iPhone

I recently got through upgrading our engine to support ES2 and GLSL shaders. It took about a week to get the game just looking the same as it did before, but rendering with shaders instead. I'm sharing some info that might be worthwhile to anyone else trying to update their iPhone renderers. This is not a how-to on using GLSL to achieve different effects, you can find plenty of that elsewhere.


ES2 is not an incremental improvement to ES1, it is a total paradigm shift in how pixels get rendered. You can't just take an ES1 renderer and add a couple shaders here and there like you can in DirectX. In ES2, you write vertex and pixel/fragment shaders in GLSL, and then pass values to the shaders at runtime.


The vertex shader reads in any values from VBOs or vertex arrays, and outputs any values that are useful for the pixel shader. Any values created in the vertex shader are interpolated along the triangle edges and raster lines before being passed to the pixel shader. The pixel/fragment shader has only one job, to output a color value.


I've found this site to be a nice reference for GLSL functions, starting at section 8.1: http://www.khronos.org/files/opengl-quick-reference-card.pdf

3GS/iPhone4/iPad or bust:

ES2 is not supported on the 3G, first gen iPhone, or first gen iPod. It's possible to support both ES1 and ES2 in the same codebase, but you will need two entirely different render paths. There's no mixing and matching allowed, so within a run you are either entirely ES1 or entirely ES2.

EAGLContext* eaglContext = 0;

eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

if (eaglContext && [EAGLContext setCurrentContext:eaglContext])

{

// initialize a renderer that uses ES2 imports

}

else

{

eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];

if (!eaglContext || ![EAGLContext setCurrentContext:eaglContext]) {

// total failure!

}

// initialize a renderer that uses ES1 imports

}

Loading and assigning shaders:

The shader compiler deals in character buffers. You will need to either create a GLSL stream in code, or more sanely load up a file containing a shader and pass the contents to the compiler.

int loadShader(GLenum type, const char* glslSourceBuf)

{

int ret = glCreateShader(type);

if (ret == 0) return ret;


glShaderSource(ret, 1, (const GLchar**)&glslSourceBuf, NULL);

glCompileShader(ret);

int success;

glGetShaderiv(ret, GL_COMPILE_STATUS, &success);

if (success == 0)

{

char errorMsg[2048];

glGetShaderInfoLog(ret, sizeof(errorMsg), NULL,

errorMsg);

outputDebugString("%s error: %s\n", fileName, errorMsg);

glDeleteShader(ret);

ret = 0;

}

return ret;

}

int loadShaderProgram(const char* vertSource, const char* pixelSource)

{

// load in the two individual shaders

int vertShader = loadShader(GL_VERTEX_SHADER, vertSource);

int pixelShader = loadShader(GL_FRAGMENT_SHADER, pixelSource);

// create a "program" which is a vertex/pixel shader pair.

int ret = glCreateProgram();

if (ret == 0) return ret;

glAttachShader(ret, vertShader);

glAttachShader(ret, pixelShader);


// assign vertex attributes to positions inside

// glVertexAttribPointer calls

glBindAttribLocation(ret, AP_POS, "position");

glBindAttribLocation(ret, AP_NORMAL, "normal");

glBindAttribLocation(ret, AP_DIFFUSE, "diffuse");

glBindAttribLocation(ret, AP_SPECULAR, "specular");

glBindAttribLocation(ret, AP_UV1, "uv1");

glLinkProgram(ret);

int linked;

glGetProgramiv(ret, GL_LINK_STATUS, &linked);

if (linked == 0)

{

glDeleteProgram(ret);

outputDebugString("Failed to link shader program.");

return 0;

}

return ret;

}

void drawSomething(void)

{

// tell opengl which shaders to use for rendering

glUseProgram(mShaderProgram);

// set any values on the shader that you want to use.

// set up the vertex buffer using glVertexAttribPointer

// calls and the same positions used during the linking.

// then draw like usual.

glDrawElements(GL_TRIANGLES, numTris, GL_UNSIGNED_SHORT, 0);

}

No matrix stack:

All transformations are done in the shader, so anything using glMatrixMode is automatically out. glFrustumf and glOrthof are also gone, so you will need to write replacements. You can find examples of these two functions in the Android codebase at http://www.google.com/codesearch/p?hl=en#uX1GffpyOZk/opengl/libagl/matrix.cpp&q=glfrustumf%20lang:c++&sa=N&cd=1&ct=rc&l=7.


For the transforms used by shaders, I have callbacks to grab values like ModelToView and ViewToProj from a structure that I calculate once per render pass.


In C++:

unsigned int transformShaderHandle = glGetUniformLocation(shaderId, "ModelToScreen");

glUniformMatrix4fv(transformShaderHandle, 1, GL_FALSE, (GLfloat*)mModelViewProj );

In the vertex shader:

uniform mat4 ModelToScreen;

attribute vec4 position;


void main()

{

gl_Position = ModelToScreen * position;

}

More textures!

You only get two texture channels to use under ES1. ES2 gives you 8. Setting up a texture in ES2 is similar to ES1, but you don't get the various glTexEnvi functions to define how multiple texture channels blend together. You do that part in GLSL instead.


In C++:

unsigned int textureShaderHandle = glGetUniformLocation(shaderId, "Texture0");


// tell the shader that Texture0 will be on texture channel 0

glUniform1i(textureShaderHandle, 0);

// then set up the texture like you would in ES1

glActiveTexture(GL_TEXTURE0);

glEnable(GL_TEXTURE_2D);

glBindTexture(GL_TEXTURE_2D, mTextureId);

In the pixel shader:

uniform lowp sampler2D Texture0;

void main()

{

gl_FragColor = texture2D(Texture0, v_uv1);

}

No such thing as glEnableClientState(GL_NORMAL_ARRAY)

GL_NORMAL_ARRAY, GL_COLOR_ARRAY, etc have all gone away. Instead you use the unified glVertexAttribPointer interface to push vertex buffer info to the shaders. This is a pretty simple change.

glEnableVertexAttribArray(AP_NORMAL);

glVertexAttribPointer(AP_NORMAL, 3, GL_FLOAT, false, vertDef.getVertSize(), (GLvoid*)(vertDef.getNormalOffset()*4));

Tuesday, September 28, 2010

100k animated triangles at 30fps on iPhone

I've been optimizing OverPowered all week, and have managed to more than double the amount of bad guys we can support, and removed a 10 meg memory spike on load that was sending us low memory warnings. Here is some info that I've picked up in the process.


The strategy: Find the bottlenecks, kill the bottlenecks.


The iPhone is actually two processors, the CPU and the GPU. If one of those is eating up all the time, then it's not worth optimizing the other. OverPowered is an action game, so it's important to keep the framerate at least 30fps. At the start of the week we were overwhelmingly GPU bottlenecked at around 20k animated triangles per frame. I managed to narrow down enough fixes that it's now more worthwhile to optimize the CPU than to try to keep shrinking the GPU cost.


I used 4 tools this week.

  • An onscreen FPS counter with number of triangles drawn. This is the only really definitive way to know how fast your game is running, but it's extremely low resolution. The iPhone is vsync locked, so if you are at 30fps it will take a huge change to make it display anything else.
  • An in-game timer. I wrap timer calls around various functions to measure their real cost. This is a very useful way to get a high level view of where your frame time is going with a high degree of confidence. It can be run in release without much profiling overhead to skew the results. I have it spitting out the frame breakdown every time I exit a level.
  • Instruments: CPU sampler. This is a fairly lightweight sampling profiler. As long as you sanity check the results with the in-game timer it can be used to get a higher resolution view of bottlenecks.
  • Instruments: Allocations. This tool is absolutely awesome for telling you where your memory is being spent. All platforms should have a tool like this.
  • I did not use Shark. This can give you a better view than the CPU sampler, but it's much heavier weight. It takes longer to get results and try out changes. It's good if you have a specific set of functions that you really want to optimize at a low level.

Pixel fill rate:


The amount of pixels drawn seems to be the biggest deal on this platform. I read somewhere that you can draw the full screen about 5 times at 30 fps on the 3GS if nothing else is going on, and my own tests are about the same. If you draw a background image, then the ground, then a gui and a bunch of little objects you can easily be drawing the screen three times already if you set it up wrong.


The iphone supports a fast hidden surface removal with the deferred tile renderer. Opaque objects drawn on top of each other largely avoid the overdraw issue by doing an early cull of objects that will be fully drawn behind other objects within a tile. So…make your gui out of opaque rectangular textures? This isn't really an option.


Just be aware of the limitations on fill rate and design appropriately is all the advice I can give on this one. If your game design requires 10 fullscreen blended textures to be drawn on top of each other every frame, it's not going to work no matter how much work you do. Try to avoid drawing large blended textures if possible, and a large alpha-tested object is one of the worst things you can do for rendering performance.


I was trying to do an effect that draws the entire world to an offscreen buffer, then overlays that on the screen for pixel shader effects. I ended up having to abandon this approach after getting it working due to the pixel fill rate getting in the way.


Vertex upload speed:


When you use vertex arrays, the entire vertex buffer is uploaded to the GPU every frame. This causes the whole pipeline to stall out while the GPU waits for the memory transfer. VBOs can be used to eliminate this lag for static data, things that you rarely change. All things being equal the difference between vertex arrays (20k verts) and VBOs (100k verts!) is huge. The max number of verts you can push is probably much higher, but I have a game running and lots of pixel overdraw.


Our scene is now entirely static buffers. We use a vertex shader to do all of the animation on the GPU by passing static buffers that represent the frames to interpolate between, and a float argument to represent position in between the frames. This change alone let me double the amount of onscreen badguys.


It's also the reason why we won't be able to support the original iphone and 3g for OverPowered. The difference in power is too much to be able to max out the newer phones while still trying to run on the older ones for a small company. When I pause the game I can fill the screen completely with quake 3 models without dropping below 30fps, so the bottleneck has been moved away from the rendering code and into the game/physics/render-setup code.


The vertex processor seems to be very powerful compared to the rest of the pipeline. I have not seen any slowdown from making the vertex shader more complicated so far, so I plan on abusing this as much as possible. Here is the relevant part of my shader code.

uniform mat4 ModelViewProj;

// the position of the low frame

attribute vec4 position;

// the position of the high frame from a different buffer

attribute vec4 diffuse;

// the pct of progress the animation has run between the two frames

uniform mediump float PctLow;


void main()

{

vec4 interpolatedPos = mix(position, diffuse, PctLow);

gl_Position = ModelViewProj * interpolatedPos;

}


Texture size is important:


I reduced a gui texture from 256x256 png to 64x64 and saw good results. This is a texture that's drawn a bunch of times every frame. All of our opaque textures are pvr4 compressed, and that was also a big win over uncompressed textures.


Memory spikes in ObjectiveC:


The 10 meg spike that I mentioned was in our platform layer due to bad use of the garbage collector. We generally garbage collect at the end of every frame. Usually we can get away with this because most of our allocations are in C++, and they go away as soon as we tell them to. Texture loading is an exception because it happens in platform code.


During the course of loading a level, we'd read in a texture buffer, create the OpenGL texture, and then release the texture buffer. This works ok if only one texture is loaded that frame, but not if we are loading a whole level at once. If you identify a place like this in ObjectiveC code, an easy fix is to put a NSAutoreleasePool around it.

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// do stuff

[pool release];

OpenGL State Changes:


These don't seem to be causing me any troubles right now. Our renderer has always been pretty good at batching materials, so it's not an issue I had to touch on this week.