Welcome!
Welcome to my site. I initially created it with the purpose of showcasing my development experience while creating my first iPhone game, StickWars. However, after the release and huge market response to the game, this site mainly serves to provide news and support for StickWars. You can find answers to many questions in my forums as well as post any other questions you might have.
While a lot of the posts are about updates about StickWars, I try to post code snippets and tips to get through roadblocks that I experienced while building my first iPhone game. If you have questions or would like to see some code to help you along, contact me and I’ll try to throw something up.
StickWars gets a Second Wind
Posted by Eric in Slime Ball, StickWars on December 29th, 2009
StickWars was featured on Apple’s list of best games of 2009 and the list of best selling apps of 2009. Each of the lists features 30 apps each and are very well promoted within iTunes. I have no idea why they would chose to feature it twice on somewhat similar lists, but I can imagine it has to do something with the ~3 month delay getting my last update approved. While this is nothing but a guess, if true then it’s fantastic of Apple to take that into account. Or maybe they just really like playing StickWars…
Between the new visibility from being featured as well as what I imagine to be a lot of word of mouth promotion during the holiday season as people open their new iPod Touches, sales have jumped up and StickWars has risen up the lists. It is now the top 40 paid app in the US, and it is actually back on the top grossing list at 99. For anybody who doesn’t know, the spike in sales of apps during Christmas holiday is like a big fat Christmas bonus to iPhone developers, and it’s a great feeling getting this final push before headed into the new year.
I meant to make a long post detailing my frustrating experience trying to get version 1.7 of StickWars approved for over 3 months, but I’ll sum it up by saying Apple had some unexpected technical problems with their approval process, they worked on it for a while, fixed the issues, and called me personally to help StickWars through the approval process once the issues were fixed. While it was very frustrating to develop a big release (including all those new challenge mini-games available as an in-app purchase) and then have to sit on it as StickWars slipped down the charts for 3 months, Apple has done what they can to make the process quicker and less painful, and being featured prominently twice by them is a unexpected gift that really helps me let go of my anger over the previous delay
.
Slime Ball has completed its first cycle of rising up and then falling back down the charts. Both the free and the paid version are now of the top 100 lists, although we have some fun updates planned that will hopefully breath some life back into them. It’s time to go hit up the keyboard and get back to work…
Slime Ball Release on the AppStore
Posted by Eric in News, Slime Ball on December 1st, 2009
I finally have something new I’ve worked on in the AppStore! While the game is created and owned by Pinger, I had a part in bringing it to life.
The game is called Slime Ball, and it’s a fun remake of Slime Volleyball. It’s very simple to pick up and play, and is addictive to play both against the computer and against another human.
I had a fun time working on it and can say that I believe most people who try it out will find it fun. We spent a ton of time tweaking the controls and other subtle elements that really make gameplay fun, and I think that effort shows in this release.
You can play against many levels of computer players which get progressively more difficult, or you can play against another player over Bluetooth. The game uses OpenFeint to track achievements and leaderboards.
StickWars Ultimate Challenge hits the AppStore
I’m pleased to announce that StickWars Ultimate Challenge is live on the AppStore. It is a collection of mini-games based on the addictive StickWars gameplay, and allows you to directly challenge your OpenFeint friends to compete for the top scores.
You can read more about it here, or just click here to visit the iTunes page.
I have been working on this update for months, and while it was originally slated to be an in-app purchase included with StickWars, technical limitations imposed by Apple dictated that I release it as a completely separate app. You can try out two of these minigames for free in StickWars or StickWars Lite version 1.7. Have fun, and as always I welcome all feedback, so visit my forums and let me know what you think.
Easy To Create Buttons with Cocos2D
Posted by Eric in cocos2d iPhone, iPhone Development on October 8th, 2009
Those of you who use cocos2d a lot might understand why I created this class as some point. Hopefully it may save some of you those nasty 5 line blobs that you normally need to create a simple button. Usage is simple, just do:
[self addChild:[Button buttonWithText:@"back" atPosition:ccp(80, 50) target:self selector:@selector(back:)]]; [self addChild:[Button buttonWithImage:@"openFeint.png" atPosition:ccp(400, 50) target:self selector:@selector(openOpenFeint:)]];
You’ll need to create your own button.png and button_p.png (the second one is the image shown when you are touching the button). Also you’ll need to choose your own font. Here is the code…
// // Button.h // StickWars - Siege // // Created by EricH on 8/3/09. // @interface Button : Menu { } + (id)buttonWithText:(NSString*)text atPosition:(CGPoint)position target:(id)target selector:(SEL)selector; + (id)buttonWithImage:(NSString*)file atPosition:(CGPoint)position target:(id)target selector:(SEL)selector; @end @interface ButtonItem : MenuItem { Sprite *back; Sprite *backPressed; } + (id)buttonWithText:(NSString*)text target:(id)target selector:(SEL)selector; + (id)buttonWithImage:(NSString*)file target:(id)target selector:(SEL)selector; - (id)initWithText:(NSString*)text target:(id)target selector:(SEL)selector; - (id)initWithImage:(NSString*)file target:(id)target selector:(SEL)selector; @end
// // Button.m // StickWars - Siege // // Created by EricH on 8/3/09. // #import "Button.h" @implementation Button + (id)buttonWithText:(NSString*)text atPosition:(CGPoint)position target:(id)target selector:(SEL)selector { Menu *menu = [Menu menuWithItems:[ButtonItem buttonWithText:text target:target selector:selector], nil]; menu.position = position; return menu; } + (id)buttonWithImage:(NSString*)file atPosition:(CGPoint)position target:(id)target selector:(SEL)selector { Menu *menu = [Menu menuWithItems:[ButtonItem buttonWithImage:file target:target selector:selector], nil]; menu.position = position; return menu; } @end @implementation ButtonItem + (id)buttonWithText:(NSString*)text target:(id)target selector:(SEL)selector { return [[[self alloc] initWithText:text target:target selector:selector] autorelease]; } + (id)buttonWithImage:(NSString*)file target:(id)target selector:(SEL)selector { return [[[self alloc] initWithImage:file target:target selector:selector] autorelease]; } - (id)initWithText:(NSString*)text target:(id)target selector:(SEL)selector { if(self = [super initWithTarget:target selector:selector]) { back = [[Sprite spriteWithFile:@"button.png"] retain]; back.anchorPoint = ccp(0,0); backPressed = [[Sprite spriteWithFile:@"button_p.png"] retain]; backPressed.anchorPoint = ccp(0,0); [self addChild:back]; self.contentSize = back.contentSize; Label* textLabel = [Label labelWithString:text fontName:@"take_out_the_garbage" fontSize:22]; textLabel.position = ccp(self.contentSize.width / 2, self.contentSize.height / 2); textLabel.anchorPoint = ccp(0.5, 0.3); [self addChild:textLabel z:1]; } return self; } - (id)initWithImage:(NSString*)file target:(id)target selector:(SEL)selector { if(self = [super initWithTarget:target selector:selector]) { back = [[Sprite spriteWithFile:@"button.png"] retain]; back.anchorPoint = ccp(0,0); backPressed = [[Sprite spriteWithFile:@"button_p.png"] retain]; backPressed.anchorPoint = ccp(0,0); [self addChild:back]; self.contentSize = back.contentSize; Sprite* image = [Sprite spriteWithFile:file]; [self addChild:image z:1]; image.position = ccp(self.contentSize.width / 2, self.contentSize.height / 2); } return self; } -(void) selected { [self removeChild:back cleanup:NO]; [self addChild:backPressed]; [super selected]; } -(void) unselected { [self removeChild:backPressed cleanup:NO]; [self addChild:back]; [super unselected]; } // this prevents double taps - (void)activate { [super activate]; [self setIsEnabled:NO]; [self schedule:@selector(resetButton:) interval:0.5]; } - (void)resetButton:(ccTime)dt { [self unschedule:@selector(resetButton:)]; [self setIsEnabled:YES]; } - (void)dealloc { [back release]; [backPressed release]; [super dealloc]; } @end
2D Scrolling Game with Cocos2D TileMap with Zoom!
Posted by Eric in cocos2d iPhone, iPhone Development on October 6th, 2009
A common question I see on the cocos2d forums is ‘when I want to make my game scroll, do I move the camera or the layer?’ or some variant of that. I also got some more detailed questions about how to make a functioning 2D scroller, so I’m going to described how I got it to work.
First of all, the answer to the above question is you move the layer. If you follow the examples in the cocos2d download, you have a “GameScene” and a “GameLayer”. Well, everything that needs to be moved when your game scrolls should be added as a child to that GameLayer. If you are using a TileMap as a background, this includes that tilemap. The only thing that you don’t add as a child to the GameLayer is stuff that does not move with the scrolling view, such as your HUDLayer that has text that shows your characters health. Other than that, your character, the background, other characters, should all be added to the GameLayer.
You have to remember which objects are absolute (attached to your GameScene or other layers) versus those that are relative (a child of your GameLayer) when you set up your touch handling code. For my HUD that has buttons you can press at any time, say to pause the game, you want to add the touch handling object to your GameScene or HUDLayer class, since it doesn’t move. But if you want to be able to touch objects that scroll along with your view in the game itself, your touch handling code needs to be in an object that is a child of your GameLayer.
This might be a little confusing, so let’s see some code:
gameLayer = [GameLayer node]; [gameScene addChild:gameLayer z:zOrder_GameLayer]; hudLayer = [HUDLayer node]; [gameScene addChild:hudLayer z:zOrder_HudLayer]; tileMap = [BGTileMap node]; [gameLayer addChild:tileMap z:-1]; [gameScene addChild:[PauseGameButton node] z:zOrder_GameButtons]; [gameLayer addChild:[FireGunAtTouchPoint node]];
The pause game button is always on your screen, while the point at which your character fires the gun depends on how how far your view has been scrolled (by moving GameLayer).
Let’s see some of the code that actually moves this game layer:
- (void)setViewpointCenter:(CGPoint)point { CGPoint centerPoint = ccp(240, 160); viewPoint = ccpSub(centerPoint, point); // dont scroll so far so we see anywhere outside the visible map which would show up as black bars if(point.x < centerPoint.x) viewPoint.x = 0; if(point.y < centerPoint.y) viewPoint.y = 0; // while zoomed out, don't adjust the viewpoint if(!isZoomedOut) gameLayer.position = viewPoint; }
When do you call that method? Well, it depends on what you want, but generally these scrolling games follow around the movement of your ‘main’ character, right? So whatever character the you want to follow, add this to override the standard CocosNode setPosition method so you update your viewpoint whenever the character moves
- (void)setPosition:(CGPoint)point { [[StandardGameController sharedSingleton] setViewpointCenter:point]; [super setPosition:point]; }
Note that the StandardGameController is a construct of mine that I use to separate the game logic out from the display code. It doesn’t matter exactly how you do it, you just need a way to have your main character object call back to something that contains a reference to GameLayer so it can adjust the position of your GameLayer.
Now remember, for your background to scroll properly, you need to add your background tileMap as a child of your GameLayer that is being moved around.
That being said, I found that an important method was missing from the cocos2d tilemap that I need to use in order to detect collisions based on the types of tiles encountered. I created a subclass of TMXTiledMap and added in these methods:
- (CGPoint)coordinatesAtPosition:(CGPoint)point { return ccp((int)(point.x / self.tileSize.width), (int)(self.mapSize.height - (point.y / self.tileSize.height))); } - (unsigned int)getGIDAtPosition:(CGPoint)point { return [layer tileGIDAt:[self coordinatesAtPosition:point]]; }
That way it’s easy to figure out what tile any individual object is colliding with. For example, in my main character object I can have code that runs in step: function with this:
BGTileMap* tileMap = [StandardGameController sharedSingleton].tileMap; CGPoint coordinate = [tileMap coordinatesAtPosition:self.position]; BBLog(@"Right now on tile %d",[tileMap.layer tileGIDAt:coordinate]);
Now I know what type of tile I am overlapping, and I can respond to the environment accordingly.
This is really all the code that you need to make a scrolling game view…I think some people overthink it and try adjusting the position of every object individually with some offset, but it’s not necessary since your objects can use relative positions with their parent.
I have one last bit of code to add, and this is something kind of fun. It’s not complete, but at least it’s a start. What this allows you to do is ‘zoom out’ so you can see your entire map with ALL the objects shrunk down, and then zoom back in to your character. The only tricky part is when you zoom back in, you have to slowly adjust your viewpoint in steps so the zoom in action is centered on your character, instead of jumping at the end.
#define ZOOM_BACK_IN_INTERVALS 10 #define ZOOM_OUT_RATE 0.3 // TODO need to refine this so for each step it uses the new viewpoint - (void)setZoom:(BOOL)zoomedIn { BBLog(@"Zooming in %d", zoomedIn); // this scales it out so the whole height of the tilemap is in the screen float zoomScaleFactor = 320 / (tileMap.mapSize.height * tileMap.tileSize.height); if(zoomedIn) { [gameLayer runAction:[ScaleTo actionWithDuration:ZOOM_OUT_RATE scale:1.0]]; [gameLayer runAction:[MoveTo actionWithDuration:ZOOM_OUT_RATE position:viewPoint]]; [self schedule:@selector(setZoomedBackIn:) interval:ZOOM_OUT_RATE]; // need this for the transistion } else { [gameLayer runAction:[ScaleTo actionWithDuration:ZOOM_OUT_RATE scale:zoomScaleFactor]]; [gameLayer runAction:[MoveTo actionWithDuration:ZOOM_OUT_RATE position:ccp(0,0)]]; isZoomedOut = YES; } } // need this small correction at the end to account of the player is moving and the viewpoint has changed to avoid jitter #define ZOOM_OUT_CORRECTION_RATE 0.3 - (void)setZoomedBackIn:(ccTime)dt { [self unschedule:@selector(setZoomedBackIn:)]; [gameLayer runAction:[MoveTo actionWithDuration:ZOOM_OUT_CORRECTION_RATE position:viewPoint]]; [self schedule:@selector(setZoomedBackInFinished:) interval:ZOOM_OUT_CORRECTION_RATE]; // need this for the transistion } - (void)setZoomedBackInFinished:(ccTime)dt { [self unschedule:@selector(setZoomedBackInFinished:)]; isZoomedOut = NO; }
Now you see why my viewPoint variable was a class member…you’ll need it for these methods to work.
The reason this code isn’t complete is when it starts zooming in, it creates the actions to zoom in on the current viewpoint, but if the player is moving by the time the zoom animation is done, that viewpoint is changed and needs to ’snap’ to the new viewpoint. That is the reason for the setZoomedBackIn: method, which really shouldn’t have to move the gameLayer anymore. However, I haven’t yet written the code to continuously create smaller move actions to take into account a moving viewpoint as the animation continues, but doing so shouldn’t be that hard. If you want to see that bit when I finish, post in the comments and I’ll add it in.
Using Box2D Physics Engine with Cocos2D iPhone
Posted by Eric in cocos2d iPhone, iPhone Development on July 27th, 2009
Starting work on my new project, I’ve found Box2D to be a far superior physics engine than chipmunk. It is more mature, the API more flexible, and it seems to even perform faster. However, the cocos2d code for it was rather space, so here is a sort of helper file I had to create to make it work with my game.
Keep in mind this game is in progress and I’ve only been using this for a few days, so it may have issues that might pop up later on. It seems to be working very well for now though, with 40-50 objects on screen moving around with ~40 FPS.
Here is the header
// // Box2DEngine.h // // Created by EricH on 7/22/09. // Copyright 2009 __MyCompanyName__. All rights reserved. // #import "Box2D.h" // made this an extern constant to avoid obj c lookup overhead extern b2World *bb_world; @interface Box2DEngine : CocosNode { } + (Box2DEngine *)sharedSingleton; - (void)createWorld:(CGSize)size; - (void)deleteWorld; - (void)runSimulation; @end
The .mm file
// // Box2DEngine.mm // // Created by EricH on 7/22/09. // Copyright 2009 __MyCompanyName__. All rights reserved. // #import "HookActor.h" #import "Box2DEngine.h" #import "SuperBox2DActor.h" b2World* bb_world; const float32 timeStep = 1.0f / 60.0f; const int32 velocityIterations = 10; const int32 positionIterations = 10; #define MAX_NUM_COLLISIONS 2048 // TODO make sure this buffer is the right size b2ContactResult contactResultCache[MAX_NUM_COLLISIONS]; int32 contactResultCount = 0; class MyContactListener : public b2ContactListener { public: void Add(const b2ContactPoint* point) { } void Persist(const b2ContactPoint* point) { } void Remove(const b2ContactPoint* point) { } void Result(const b2ContactResult* point) { // TODO we are making a deep copy of every contact point here // check the box2d contact masks to make sure we minimize unwanted contact results contactResultCache[contactResultCount++] = *point; } }; void handleCachedContactResults() { b2ContactResult contactResult; for(int i = 0; i < contactResultCount; i++) { #ifdef BBDEBUG if(contactResultCount >= MAX_NUM_COLLISIONS) { NSLog(@"RAN OUT OF BUFFER"); assert(NO); } #endif contactResult = contactResultCache[i]; SuperBox2DActor* actorOne = (SuperBox2DActor*)contactResult.shape1->GetBody()->GetUserData(); SuperBox2DActor* actorTwo = (SuperBox2DActor*)contactResult.shape2->GetBody()->GetUserData(); if(!actorOne.isDead && !actorTwo.isDead) { [actorOne collisionResultOne:&contactResult withActor:actorTwo]; [actorTwo collisionResultTwo:&contactResult withActor:actorOne]; } } // clear the collision cache contactResultCount = 0; } void removeDeadActors() { b2Body* node = bb_world->GetBodyList(); while (node) { b2Body* b = node; node = node->GetNext(); SuperBox2DActor* actor = (SuperBox2DActor*)b->GetUserData(); if (actor.isDead) { // remove from physics engine bb_world->DestroyBody(b); // remove from game engine (cocos2d) [[StandardGameController sharedSingleton] removeGameActor:actor]; } } } @implementation Box2DEngine + (Box2DEngine*)sharedSingleton { static Box2DEngine* sharedSingleton; if (!sharedSingleton) sharedSingleton = [[Box2DEngine alloc] init]; return sharedSingleton; } - (void)createWorld:(CGSize)size { b2AABB worldAABB; worldAABB.lowerBound.Set(0, 0); worldAABB.upperBound.Set(size.width * BOX2D_SCALE_FACTOR_INVERSE, size.height * BOX2D_SCALE_FACTOR_INVERSE); // TODO this shouldnt be inverse? b2Vec2 gravity(0.0f, -30.0f); bool doSleep = true; bb_world = new b2World(worldAABB, gravity, doSleep); bb_world->SetContactListener(new MyContactListener()); } - (void)deleteWorld { [self unschedule:@selector(step:)]; delete bb_world; bb_world = NULL; } - (void)runSimulation { [self schedule:@selector(step:)]; } - (void)step:(ccTime)dt { // step the world bb_world->Step(dt, velocityIterations, positionIterations); // TODO do i use timestep or dt here? //BBLog(@"Num of contact results is %d",contactResultCount); // do stuff with collisions handleCachedContactResults(); // remove all actors that are marked as dead removeDeadActors(); // update cocosnode positions for (b2Body* b = bb_world->GetBodyList(); b; b = b->GetNext()) { if (b->GetUserData() != NULL) { SuperBox2DActor *actor = (SuperBox2DActor*)b->GetUserData(); b2Vec2 position = b->GetPosition(); actor.position = b2toCGPoint(position); actor.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle()); //NSLog(@"obj %@ %4.2f %4.2f\n",actor, position.x, position.y); } } } @end
StickWars 1.5 Live after nearly a Month
After nearly a month of delay, StickWars 1.5 is live with important bug fixes and a few new features, including a few achievements. Luckily I’m all ready to push out v1.6 which includes a new boss enemy, the giant stick figure. I also added a lot of small fun features, so as the ability to throw and slam stick figures into one another to kill them. You can also now kill the more powerful enemies by slamming many small stick figures into them. I think this adds yet another way for talented players to save money and reach higher levels and scores
.
In addition, I’ve re-written parts of the code for the basic functions behind levels and sound, and somehow I managed to fix a bug that I didn’t know existed. The game no longer crashes sometimes when you attempt to load a new level–the bug before was due to my background sound engine, except none of my debugging tools had located it there. Basically the last crash bug has been fixed, and since I’ve been adding features to this version for the past week I have not seen it crash once. I’m really proud of the stability of the game now.
There is a new loading screen that I’m particularly proud of–it allows me to include short hints that answer the most common questions I get from users based on reviews, emails, and forum posts. It’s a lot nicer to be able to read a short hint while you are waiting 3-6 seconds for the level to load, and maybe help out your gameplay, rather then just looking at the word ‘loading’ over your current screen. I can also throw in a few words about upcoming versions, such as my brief request for feedback about charging in game $0.99 for the new multi-player challenges coming in the next version. I’d love to get some player feedback on this–I feel most people are happy paying only $0.99 for StickWars, and I don’t want to raise the price for the same basic game (campaign mode), but these multi-player challenges are a whole new ballpark that I feel deserves another $0.99. However, I’m willing to listen to my users who feel strongly enough about this to contact me. I will be providing a few of the challenges free so a player can test them out.
The bad news is StickWars has fallen off the top 10 for the first time since it first rose up there in April. But sitting at #11, I’m hoping these new features and fixes will bring new life back to the game and drive more players to it.
StickWars in the News – First Adoptor of OpenFeint 2.1 OS 3.0 Features
One of the many articles is up at marketwire.com. Here is my blurb:
Eric Hartzog, StickWars: “StickWars will include a new gameplay mode that will allow a player to battle for a limited period of time and challenge his friends for top scores. His friends will be notified of the challenge through push notifications, powered by OpenFeint, and they will be able to easily load StickWars and respond to the challenge. These challenges will flow back and forth in near real-time, and be easily visible to players even if StickWars is not currently loaded.
A single-player game inevitably has a limited amount of engagement — developers can only add so much content to a game. But by drawing from existing social networks and encouraging social competition between gamers, the players themselves create new tasks for one another and new obstacles to overcome. This user-generated content expands the value of any game far beyond what was initially put into it, and continues to expand as the popularity of the game increases even further.”
You can read more details about exactly what this means for StickWars in this forum post. There are a lot of new features and brand new game modes coming to StickWars. These new features will require a small purchase in game, but give access to many social features that allow you to compete against your friends in many different challenges.
Expect these features ready at the start of July, and for them to be live anywhere between 1-4 weeks after that when Apple approves it.
Create multi-line labels with cocos 2d iphone
Posted by Eric in cocos2d iPhone, iPhone Development on June 20th, 2009
Update: This code is obsolete now. You can just do
Label *messageLabel = [Label labelWithString:message dimensions:CGSizeMake(380, 120) alignment:UITextAlignmentCenter fontName:@"your_custom_font" fontSize:26];
by using the new FontManager class. For example, run this once in your app delegate when your program first loads
[[FontManager sharedManager] loadFont:@"your_custom_font"];
It can take a long NSString and create multiple labels without breaking up a word. I’ll eventually use this for my help screen, replacing the current 6 different 480×320 png images that I load for each one
.
The code is simple, but hopefully it might save somebody the time it took me to write it. I had to look up some very basic elements of ObjC here, so if there is a much easier way to do this, please let me know but don’t make too much fun of me.
You can easily switch out the BitmapFontAtlas for just a normal Label and it would work just fine.
(void) setTipString:(NSString*)str { NSInteger lineChars = 0; BOOL isSpace = NO; NSInteger index = 0; NSInteger numLines = 0; NSMutableString *line = [NSMutableString stringWithCapacity:LINE_LENGTH]; while (index <= [str length]) { if(index == [str length]) { BitmapFontAtlas *tip = [[BitmapFontAtlas bitmapFontAtlasWithString:[NSString stringWithString:line] fntFile:@"text.fnt" alignment:UITextAlignmentLeft] retain]; [tip setPosition: cpv(30,210 - 20 * numLines)]; [self addChild:tip]; return; } NSString *tmp = [str substringWithRange:NSMakeRange(index, 1)]; [line appendString:tmp]; if([tmp isEqual:@" "]) isSpace = YES; else isSpace = NO; if(lineChars >= LINE_LENGTH && isSpace) { BitmapFontAtlas *tip = [[BitmapFontAtlas bitmapFontAtlasWithString:[NSString stringWithString:line] fntFile:@"text.fnt" alignment:UITextAlignmentLeft] retain]; [tip setPosition: cpv(30,210 - 20 * numLines)]; [self addChild:tip]; lineChars = -1; [line setString:@""]; numLines++; } lineChars++; index++; } }
The Developer-Friendly AppStore
Posted by Eric in Distribution, StickWars on June 18th, 2009
Lacking a better medium to communicate to all my users how difficult Apple is making it to provide them with the level of service I’d want to, I’ll get my rant out here on my blog where it’s safe.
I had a bugfix version of StickWars ready the same day the buggy version 1.4 was released. That day was May 27th. Is is June 18th and I’m still waiting patiently for Apple take a look at the update. It has been two weeks since I submitted a corrected update, and I haven’t got a lick of feedback back. In the mean time, my app has dropped from #3 to #9, and my users are sitting there wondering what I’m doing when in fact I have two more full-content updates ready for release, completed in the time it took Apple to approve a bugfix release. Keep in mind that my app has been in the top 10 paid apps for months now–this is one of Apple’s moneymaking apps. I would think they would want to work quickly to approve an update which would improve the game experience for hundreds of thousands of players.
Now I am not absolving myself of the responsibility of releasing a version with some problems. The fault for that lies solely on me, and was due to a somewhat chaotic situation as I was moving homes and starting a new full-time job that led to me not having as much time as I expected to test the release. This is no excuse, however, as I should have just rejected the update and resubmitted later after more testing. But the problem was magnified greatly by the simple fact that Apple has no agreement or desire to protect the ability of developers to have control over the applications they are producing. We can’t respond directly to user complaints or support requests posted on the AppStore. Most importantly, our update schedule is 100% subject to the whims and scheduling of Apple, so an update that we kill ourselves to complete on a rapid basis could be live in 3 days, or in 30. And we never know what to expect until after the fact.
Before a couple weeks ago when Apple first started emailing me to let me know that they approved my update, do you know how I would find out my update was live? My as-it-happens google alert would send me an email that a new cracked version of StickWars was available on some blog somewhere. I also rely 100% on third-party tools to tell me how people are responding to my applications. Check out www.appfigures.com to see what I’m talking about–from that site, you can read reviews from all countries. I received a lot of valueable feedback reading through the reviews of other english-speaking nations. Apple provides no way (that I know of) for developers to see the status of their application in the AppStore (if it is featured anywhere, it’s rank in other countries, etc…).
In addition, I’ve only received two of the seven payments for April 2009 that I should have been paid by now. Granted, one of those received was from the US, and therefore was the largest one, but they are sitting there wasting interest on a lot money and I still have not recieved a single response as to why. Three weeks ago I dug through their contact form and sent a polite email asking them why I got the financial report for a few thousands but never received the deposit, but I have yet to receive even as much as an automated reply confirming receipt of my message. I have sent more inquries weekly, and just started sending them to a more broad range of email address, and will update this at some point when I get a response.
Don’t get me wrong, I think Apple has done an amazing job with the AppStore, and building a system like this from scratch is bound to be difficult, with problems like this sprouting up and slowly being fixed month by month. But right now, I seem to find myself trapped at a criticial point, where if another week goes by without Apple looking at my update and StickWars drops off the top 10, and then the many more features and improvements I’ve made in the meantime won’t matter as all the exposure in the AppStore revolves around the top 10 and top 100 lists. I understand that applications have a limited shelf life at the top of the lists, but I didn’t expect mine to be cut short at the end by random and unpredictable update approval practices by the iTunes Store.
I believe that complaining without recommending a course of action to fix the problem is a waste of words, so here are some ideas that I would love to see put into the AppStore.
- A priority queue for update approval based on popularity. The apps that are getting constant updates pushed out to hundreds of thousands of players should have more personal attention from Apple staffers. On the other hand, it should be balanced so that even an unpopular app which has gone two weeks with an update in the queue is bumped up to the top priority.
- A developer portal into iTunes Store “Insights” that gives you access to any information related to the status of your applications, including reviews and rankings (in all categories) from all nations, along with any places where the application is featured.
- More analytic information, so I can see how many users download my Lite version first, and then download the Paid version.
- Allow developers to post short ‘responses’ to user reviews. This way, when somebody complains “the game was too easy wish i could make it harder!!@#” a developer who cares can go through and add a response such as “just hit the options button and set the difficulty to hard” (real example).
Update: I did eventually get paid by Apple like 1 or 2 months late, and about 2 weeks after I got paid I got responses to my multiple emails (sent over a month before) saying “we show your account as paid in full…”. To be fair to Apple, my banking situation was a little complex and I understand that it takes time to go through all that, but I would have appreciated a brief response (or even an automated confirmation) to my inquiries long before almost 2 months have gone by.

Recent Comments