Feb 22

Seems this past week I’ve been slammed with a ton of stuff. Had to take a breather, step back and get a bit more organized to gain some perspective. Tax season is in full swing, so that’s always biting at my ankles as this is my first year filing as a self-employed tax payer. Tilt to Live Viva la Coop is close to being live on the app store (hopefully), and HD will be followed up shortly after. In the meantime, I’m tasked with starting a brand new project, and it’s an exciting one indeed. The biggest hurdle so far is trying to mitigate the second-system effect. With all the problems encountered developing Tilt to Live, I want to try to get rid as many of them as possible the 2nd go around. Yet, trying to eliminate all of them doesn’t seem like a wise choice because even though there were a few ‘snags’ in developing TTL, they weren’t big enough problems that would justify going out of my way writing custom tools, scripts, or whatever to relieve them.

One of the bigger problems (or a possible non-problem depending on where we go in the future) was the realization of how locked into the app store and iOS system we were. Tilt to Live and it’s HD counterpart were pretty much written in pure Objective-C. Very little constructs of C or C++ were used. There were only a handful of structs for networking, and vector functions, but that was about it. When faced with the decision of wanting to try out the game on different platforms, it was a non-starter as all the gameplay code would have to be rewritten in C or C++ (of course Android is a beast on it’s own using Java). It wasn’t a problem at the time because we were focused on simply trying to succeed in the quickest and easiest way possible. But looking forward, I’d like to have the option to go to another platform without the pain of starting from scratch.

GDC is looming around the corner, and it’s really exciting to think after so many years of following GDC in the news I’ll finally be going to this awesome event not as just a spectator, but as a game developer. Exciting times indeed.

Feb 14

Tilt to Live HD's Viva la Turret mode is now live and people seem to be enjoying it. One of the bigger hurdles in this prior update was getting in app purchases to work the way we wanted them to without violating the standard user experience of IAP. I figured I'd share how I went about solving this for Tilt to Live HD specifically. We had a few business goals in mind:

  • We wanted to maintain parity with the upgrade model we went with on the iPhone, which was a simple in-app purchase to get the new mode.
  • We still wanted to maintain the 'demo then upgrade to full version' packaging of Tilt to Live HD for a single price, as we still believe it appeals to a certain type of gamer that wants a more traditional model for buying a game.

In a way, it seems our two goals were at odds with each other. We wanted players to have an optional upgrade, but at the same time we didn't want the 'full version' to break down into "meh..it's kind of the full version..but not really". Our solution was to treat it as a paid upgrade for existing users, but bundle the new mode in with any new customers who bought the full version of Tilt to Live. The caveat being we did not want to break it down into some sort of 'store' list, where a user could inadvertently end up buying the wrong version and be double charged for overlapping content. We were pretty proud of the IAP integration in HD where it was a relatively seamless experience that didn't involve any UI that required a "store" button. We simply showed you the gametype option and it was locked if you didn't have it, and unlocked if you did.

With the additional new mode, we now had 3 basic states you could possible be in.

1. You have the demo version of Tilt to Live HD, so the gametype select screen is pretty standard and you see 1 in app purchase that includes everything:

2. You have the full version of Tilt to Live HD prior to February 8th. The gametype select screen should show you just the small IAP for the single Viva la Turret game mode:

3. You have the brand new full version of Tilt to Live HD, so everything is unlocked:

Now this sounds all well and good and pretty straight forward when I first considered it. Then came the technical details:

  • The behavior of IAP is such that it mirrors functionality like App Store purchases. If you've ever bought that IAP before, you can buy it again, but a pop up will notify you that you already own it and are downloading it again for free.
  • If a person has never restored, then simply checking what modes in the client are unlocked is sufficient and is the path of least resistance.
  • If a person has restored/re-installed, there is no API from Apple to determine if a person has already bought a piece of content before they try to buy it again, which could result in angry customers being double charged if they were in state #2 (pictured above) but have re-installed and ended up buying the full version again as if it was in state #1 (pictured above).

Essentially, when a prior customer re-installs our game for whatever reason, there was no way for me to determine whether they never bought the old full version and we should upsell the entire new full version, bought the old full verison and we should upsell just the game mode, or if they already have the entire thing unlocked (either by buying the old full version + the turret IAP or by buying the new full version that includes everything). Confused yet?

The riskiest thing was that I did not want to have a situation where a person could inadvertently buy the entire brand new 'full version' IAP when all they needed to buy was the small turret IAP. With the StoreKit API lacking any sort of method to determine a user's purchase history, and with Tilt to Live HD not using any sort of server solution to track receipts,  I was running out of ideas. Then I got the idea of experimenting with a little "hack" to simulate the same IAP experience.

When a user taps on the 'buy' button for any IAP in the game I decided to instead force it to restore purchases all the time. Using a small collection of state variables when a restore was finished I would check to see if what they were purchasing was in line with what was restored:

  • If so, do nothing and give a standard dialog showing they restored successfully.
  • If it was in conflict, let them know nothing was purchased but their previous items restored. I would then refresh the upsell screen showing what they could really purchase.
  • If there was no conflict and they, in fact, don't own any part of the IAP, then I would immediately push the purchase through. The great thing here is since it happens right after the restore, the user is never prompted for their password again since they already did so during the initial tap of the buy button to trigger the restore.

When going about implementing this I had a few false starts and dead ends, but after re-reading the documentation and a bit of experimentation I finally grokked most of the restoration API from StoreKit.

There are two methods involving the end of restoring tranactions. There is one that is called at the end of each IAP item restored:

-(void)restoreTransaction:(SKPaymentTransaction*)transaction

And then there's a method that is called when none, or all IAP items have been restored:

-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue

The former I left alone to act as it normally would under any restoration of transactions. The latter method, I modified to behave as an ending point if the user really tapped on 'restore purchases' or a beginning point to analyze what was restored and to continue with the purchase if needed. Below is the method in full in case you're curious:

-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
    if(iapTransactionState == kIAPTransStateRestoring) // restore as is.
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseRestoreSucceededNotification object:self userInfo:nil];
    }
    else if(iapTransactionState == kIAPTransStatePurchasing)
    {
        if([self isContentUnlocked:purchaseItemID])
        {
            // if it's been restored or unlocked already then no need to do it again.
            [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseTransactionAlreadyHaveItemNotification object:self userInfo:nil];
        } else {
            //...unless we're already in some half-bought state (full -> full+turret)
            bool makePurchase = true;
            if([purchaseItemID isEqualToString:kIAPFullAndVivaLaTurretID])
            {
                // if after restoring we found the player purchased the full and viva separately
                // don't go forward with payment for the bundle.
                if([self isContentUnlocked:kIAPVivaLaTurretID] || [self isContentUnlocked:kIAPFullVersionID])
                {
                    makePurchase = false;
#ifdef DEBUG
                    NSLog(@"bundling purchase error");
#endif
                }
            }
           
            if(makePurchase)
            {
                // continue with the purchase
                SKPayment *payment = [SKPayment paymentWithProductIdentifier:purchaseItemID];
                [[SKPaymentQueue defaultQueue] addPayment:payment];
            }
            else {
                // send a bundling error message
                // or a restore complete if they have entire bundle
                // because they purchased it separately (upgraders)
                bool hasBundle = [self isContentUnlocked:kIAPVivaLaTurretID] && [self isContentUnlocked:kIAPFullVersionID];
                if(hasBundle)
                {
#ifdef DEBUG
                    NSLog(@"has complete bundle, no purchase made");
#endif
                    [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseTransactionAlreadyHaveItemNotification object:self userInfo:nil];
                }
                else
                {
                    [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseTransactionAlreadyHavePartialBundleNotification object:self userInfo:nil]
                }

               
            }

        }
    }
#ifdef DEBUG
    NSLog(@"restore done");
#endif
//  [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseRestoreSucceededNotification object:self userInfo:nil];
}

I can't say this is the most ideal solution since I feel it's using the StoreKit API in a slightly unorthodox way, but it works well. So this is one way of implementing a 'paid upgrade' for IAP items. If we were selling more items in a more 'freemium' type model it might've been a good idea to generalize the above code to handle upgrades for any item, but since Tilt to Live HD's model is a one-off IAP I didn't bother generalizing.

Feb 2

I'm still in the middle of finishing up our Co-op mode for Tilt to Live and coding in the last odds and ends to polish up the UI. The gameplay rules have be finalized and it's been a hoot to play. Yes, I said hoot. We'll be playtesting the mode in the coming weeks to do any minor tweaking to the physics and balance.

The wireless P2P GameKit API has been pretty cool to work with. It certainly takes a bit longer to get something tangible out of it because it's a relatively 'invisible' piece of the iOS SDK  (as opposed to Core Animation, UIKit, and the like). Yet there's always something inside me that goes "that is so damn cool" when I finally see a game running on two separate devices and are interacting with each other.

I'd like to write up a mini post-mortem after the co-op update is done looking back at adding real-time networked play to an iphone game. But for now, the biggest hurdle was realizing that running with the GKPeerPickerController  limited you to only bluetooth. The docs weren't 100% clear on this and I was beating my head on my desk seeing people claiming GameKit worked over Wi-fi with no issues and I couldn't get any device to talk to each other without bluetooth. Couple that with a faulty router setting keeping my iDevices from connecting to each other and it was a painful couple of days. In any case, let it be known, that if you wish to support local Wi-Fi and Bluetooth transparently you'll need your own custom UI outside of GKPeerPickerController for clients connecting a host.

Oh, and the utter joy I experienced when I realized I had to change zero networking code to have it work over w-fi was indescribable. I think I fell in love with GameKit at that point since I was anticipating  a day of refactoring, integrating bonjour directly, and whatever else I had in mind to keep the game-level networking code intact.

For anyone thinking about doing some real-time multiplayer game on the iPhone, the biggest problem you'll probably face is interference, followed by throughput (when dealing with bluetooth anyway). iPhones are jam packed with so much wireless technology that it seems if the wifi or 3G antenna so much as thinks about sending or receiving data, bluetooth communication suffers horribly.  I've had many a game end in death when I received an e-mail or text during gameplay. We originally intended on supporting nothing but bluetooth, but given this type of wildly varying performance we needed to have a fall back solution that was a bit more reliable. Thankfully, GameKit made it easy. While I was hoping I wouldn't have to code up a custom UI (pictured above) because of GKPeerPickerController's magic, in the end it added more robust functionality, as well as felt a lot more integrated into the game.