Amazing Brok Post-mortem: What $600 can teach you

It’s been over a month since we release Amazing Brok (available for iOS and Android). It’s been over a year since we started developing it. We took away several lessons that, hopefully, we will apply going forward. It is also our hope that this will be helpful to others.

The Ninety-ninety Rule Is Real

I have heard this cliche so many times, and still I was fooled into thinking we had dodged it somehow. I can be hopelessly optimistic at times. For the last 4 months of development, I was self-deluded into thinking that we were 2 weeks away from completion. I was even aware, at several different points during that time, that my previous 2 week estimate had expired. And yet I still thought 2 more weeks!

This wasn’t the end of the world for us. We both have day jobs, so we weren’t depending on sales to pay rent. If you happen to be in that (bad) position, I’d suggest making financial arrangements for at least 3 times as long as you think it’ll take. Well, first I’d suggest getting a job with a dependable income.

Finishing Is Hard

Know the Tools

Do the Math

 

Unity: Build a better ReadOnlyAttribute

(Skip the read, here’s the repo. Otherwise, read on.)

I tend to use the Header attribute to break up my component values in the inspector. I’ll write code like this:

public class TestScript : MonoBehaviour {
	[Header("Inspector Values")]
	public bool someBool = true;
	public float someFloat = 1f;
	public int someInt = 30;

	[Header("Debug")]
	public bool debugBool = false;
	public float debugFloat = 0f;
	public int debugInt = 0;

	private void Update() {
		debugInt++;
		if(debugInt == someInt) {
			debugBool = !debugBool;
			debugInt = 0;
		}

		debugFloat += someFloat * Time.deltaTime;

		if(someBool && Mathf.Sin(debugFloat) > .95) {
			debugInt = 0;
		}
	}
}

That code will get you results like this:

Continue reading

Unity Tip: Getting the actual object from a custom property drawer

While writing some editor scripts for my current project, I ran into a small problem. I’m using a property drawer for a class like this.

[Serializable]
public class MyDataClass
{
    public void DoSomething()
    {
        // ...doing something
    }
}

And let’s setup a property drawer for this class.

[CustomPropertyDrawer(typeof(MyDataClass))]
public class MyDataClassDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (GUI.Button(position, "Do Something"))
        {
            MyDataClass myDataClass = fieldInfo.GetValue(property.serializedObject.targetObject) as MyDataClass;
            myDataClass.DoSomething(); // normally this works fine
        }
    }
}

Now, this usually works. For example, if you have a MonoBehaviour that includes a field of type MyDataClass, then this will get the job done. However, if you have a MonoBehaviour with a field of type MyDataClass[] then it won’t. The reason is that fieldInfo will refer to the array field of the parent object, and not the individual elements being drawn. In my case, I had MonoBehaviour classes that used MyDataClass for both single instances and in arrays. Not all is lost, however, as Unity provides us with some other useful info.

Let’s revise our approach above to get at the actual object being drawn whether it’s in an array or not. We know that if it’s not an array, we’re already good. So all we need to change is how we handle the case of the array. We can figure out if it’s an array by using plain old reflection. Note that we can’t use property.isArray, since property only refers to a single serialized object of type MyDataClass. But fieldInfo is reflection data that refers to the array field on the parent. If you think that’s a bit annoying, I agree.

[CustomPropertyDrawer(typeof(MyDataClass))]
public class MyDataClassDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (GUI.Button(position, "Do Something"))
        {
            var obj = fieldInfo.GetValue(property.serializedObject.targetObject);
            MyDataClass myDataClass = obj as MyDataClass;
            if (obj.GetType().IsArray)
            {
                // ...Need some magic here
            }

            myDataClass.DoSomething();
        }
    }
}

The SerializedProperty parameter passed into your OnGUI method contains a propertyPath field, which is a string that describes how you get from the parent object to the object currently being drawn. In the case of an array, the string will look something like “myDataClassArray.Array.Data[X]” (where X will be the index of the element currently being drawn). We can use a fairly sketchy (ha!) hack extracting the index (you could definitely make this more robust, but it serves our purposes here).

[CustomPropertyDrawer(typeof(MyDataClass))]
public class MyDataClassDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (GUI.Button(position, "Do Something"))
        {
            var obj = fieldInfo.GetValue(property.serializedObject.targetObject);
            MyDataClass myDataClass = obj as MyDataClass;
            if (obj.GetType().IsArray)
            {
                var index = Convert.ToInt32(new string(property.propertyPath.Where(c => char.IsDigit(c)).ToArray()));
                myDataClass= ((MyDataClass[])obj)[index];
            }

            myDataClass.DoSomething();
        }
    }
}

And that should do it. If you want to make this a bit more generic we could pull that logic out into a utility class of some sort and make it a static function. (Although it’s not an editor script exactly, you still probably want to drop it in an editor folder to prevent it from getting included into your build).

public class PropertyDrawerUtility
{
    public static T GetActualObjectForSerializedProperty<T>(FieldInfo fieldInfo, SerializedProperty property) where T : class
    {
        var obj = fieldInfo.GetValue(property.serializedObject.targetObject);
        if (obj == null) { return null; }

        T actualObject = null;
        if (obj.GetType().IsArray)
        {
            var index = Convert.ToInt32(new string(property.propertyPath.Where(c => char.IsDigit(c)).ToArray()));
            actualObject = ((T[])obj)[index];
        }
        else
        {
            actualObject = obj as T;
        }
        return actualObject;
    }
}

Now, we modify our original OnGUI method like so:

[CustomPropertyDrawer(typeof(MyDataClass))]
public class MyDataClassDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (GUI.Button(position, "Do Something"))
        {
            MyDataClass myDataClass = PropertyDrawerUtility.GetActualObjectForSerializedProperty<MyDataClass>(fieldInfo, property);
            myDataClass.DoSomething();
        }
    }
}

And we’re done. Hope you find this helpful.

Quick Tip: Running Moai build from Sublime Text using OSX

I’m planning a longer post about setting up a dev environment for Moai using a Mac, but for now I thought I’d put out this quick tip about using Sublime. It took me a few minutes to figure this out, as I couldn’t find it in the documentation for the Sublime build command.

When setting up your build command in Sublime, you’ll need to add the shell property to build config object, and set the value to true. Here’s mine:

{
   "cmd": ["${project_path:${folder}}/run.sh"],
   "shell": true
}

Prototypes and Post-mortems

I was planning on doing a post-mortem for Amazing Brok tonight, but I can’t figure out what I want to say. I’m working on it, and will try to have it finished by Monday. In the meantime, you may be interested to know that we’ve come up with an idea for our next project that we think will be a lot of fun. We’ve worked out what we want to see in the prototype, and will hopefully have something for you guys to play with soon.

Monthly Report – July 2012

The month of July, 2012. The month we released our first project ever, Amazing Brok. The lessons learned are coming hard and fast, but that’s for another post. This will be all about the numbers. So let’s get to it, shall we?

To start, I should briefly mention the setup. We released Brok on the Apple AppStore as a free app with limited content, and an in-app purchase for more. Currently there is only one level pack you can buy, priced at $.99. On Google Play, we have a paid app with all the levels, also for $.99. How well did that work out?

Downloads

iOS Downloads for July 2012

The huge spike is the initial release day, and it’s been slowly going down ever since. Total downloads for the month landed at 6616. For our first attempt ever, we’ve been pretty pleased with this number. Sure, we had no frame of reference. But still, we were fairly pessimistic at how many people would even see the game.

Sales

Sales for July 2012

With such a small sample size, the graph isn’t exactly helpful. Other than showing that sales are going down roughly with downloads, not much can be gleaned. It’s maybe a bit interesting to note that a paid app for Android devices did much better than an in-app purchase for iOS. Total sales for both stores for the month were 122 units (76 for Android, 46 for iOS), for a total of $83.79.

Store Rankings

It may be relevant or interesting to some of you, so here are the ranking charts for the Apple and Play store separately.

Apple Store Rankings for US in July 2012

I shortened the time frame. After July 11, Brok doesn’t appear again in Apple’s store rankings. Here’s the Play store.

Play Store Rankings for US in July 2012

It seems like there is an issue with AppFigures collecting data from the Play store occasionally. It will sometimes go several hours without an update. I’m not sure if AppFigures fails to collect data, or if there’s an issue finding the rankings off the Play store. Either way, you can still see the plot points showing the declining curve after the 16th. That sudden spike at the end of the month is when I released an update (I’m still waiting for Apple to review that update for iOS devices).

Marketing

We’ve done practically zero marketing. I posted in a few subreddits, and told friends and family that the game was released. We haven’t spent any money on ads. We haven’t done any press releases. I haven’t emailed any review sites. It looks bad when I put it all together like that.

I do plan on emailing sites when I get the first update approved. That update really improves the levels, adds some graphical niceties, includes more free content, and more. The update gets the game closer to what we wanted to release. Once it’s approved and live, I’ll feel better about people reviewing the game.

That said, we have had 3 or 4 sites review the game already. They were small sites, but the reviews were favorable. I’m not sure if they helped with downloads or not.

Pirates

One fun thing to learn was the piracy rate. I knew it could be high; I’d heard numbers as high as 95%. Also, lots of posts had mentioned it being worse on Android than iOS. I’m not sure if that’s true or not. However, in this case it almost certainly has been. It’s a bit more difficult to set up the reasoning, but we saw a minimum of about 98% piracy of the Android version. Here’s my data.

We used CoronaSDK to build this app, which bakes in it’s own data collection code unless you explicitly turn it off. I decided to leave it in, since I could see the data via their dashboard and I was curious about what it would reveal (did I mention that this has been a major learning experience?). They collect a few pieces of data which interest me, but in this case I was looking at the number of unique daily users broken down by OS. (They claim it’s unique users, but my guess is that it’s really unique device IDs).

Unique Users by OS

As you can see, at a high point we had 4630 unique devices playing Brok in a single day. That establishes a minimum number of Android devices that Brok could be on. All the other days’ users could just be a subset of the users from July 12. Some (slightly oversimplified) math shows us that 76 (legit users) / 4630 (min # of devices with Brok) = 1.64% legitimate users.

Now obviously it’s not quite that simple. There could have been legit users with multiple devices. Or pirates with multiple devices. Maybe I should be counting the number of units sold by July 12th, instead of using the total for the month. Lots of different ways you may want to look at the data. (I’m happy to oblige, btw, if you want a specific piece of data that I haven’t given here. Let me know in the comments).

Looking Forward

I’m considering releasing a second version of Brok on Play as a free app, with ads and an in-app purchase to remove those ads. I’m also considering putting the app on the Amazon AppStore for Android and Kindle Fire, as well as the Barnes & Noble store for Nook Color and Tablet. We’ll see what happens.

Closing

Despite the terrible sales, we have both been really stoked about the whole process. This is something I do in off-hours, so I’m not depending on the income for anything. My first milestone goal for the company is to just break even after the CoronaSDK license and developer fees, and I have 5 more months for that.

I intend to post a report every month about how we’re doing. I think that’s something people would like to see, especially other developers. Also, I’d love to hear any comments or criticisms you may have, about our overall approach, the game, this report, whatever. And thanks for reading!

Edit: Added in rankings data and marketing info as per user onewayout in /r/gamedev

Amazing Brok v1.1: There were… complications

So, we’ve released an update for the Android version of Brok. Complete with the new levels, some bug fixes, graphical and performance improvements, etc. We also removed the permissions that Corona bakes into your app. We had a (small) boost to purchases after releasing the update, and at the time of writing this we’re are in the top 200 again (after dropping off the chart). So updating does appear to help. And based on an incredibly small sample size, I’m guessing the permissions thing was the cause of about half of the cancelled orders we were getting before.

But…

There’s this Apple AppStore thing, and it’s causing me some issues. Convoluted story time. When we originally released Brok, we were using CoronaSDK build version 704. I didn’t know it at the time (probably wasn’t paying attention), but there was a newer build, version 840. In 704, both armv6 and armv7 architectures were supported. To people who don’t know or care what that means, basically all generations of iPhones could “run” my game (although older phones, like 3Gs couldn’t render fast enough to actually let you play it).

After releasing, I set out to fix the terrible performance on the older devices. I noticed that the 840 (newest Corona version) build included some features that would improve performance in a huge way. As I said, the game was unplayable on 3Gs, and even iPhone 4’s struggled on a few of the levels. Using 840 and including the new features, I produced a build of Brok that ran smooth as butter on Adam’s iPhone 4. I was excited. Then I got another friend to test on his iPhone 3G. My excitement turned to fear, then anger, then hate.

My friend couldn’t get the game to install. We were using TestFlightApp.com, which we had used before. We figured out that the app itself was failing to install, since it was only built for armv7 architectures. The iPhone 3G is armv6. So this new udpate that I’ve been building, the one that would make the game playable on this older model of phone, couldn’t be actually be played on it. Awesome.

Well, after I deliberated for a while, I decided that dropping support of the 3G phones (supposedly only ~6% of the iOS market) was the way to go, since that still gave the noticeable improvement to those with iPhone 4 or better. This will make some people upset (possibly) but it seemed like the right thing to do. Ah, doing the right thing. That’s just not the world I live in, however.

It turns out, Apple has a policy that I’m not allowed to submit an update to an app that would eliminate support for a particular hardware spec. If the app previously worked on armv6, I will always have to support it. I can up the minimum OS requirement, but I can’t drop hardware.

Sorry this is getting so long. But here’s the current plan: submit the update anyway, upping the minimum OS version required, and see if they notice. If they do, I’ll have to figure out other ways to improve performance. If not, however, then yay me!

Amazing Brok v1.1 Nearing Completion

We’ve made all the bug fixes, changes, and improvements that we’re going to make. Starting tonight, Adam and I are just doing playtesting on the new levels, and adjusting the timings and scoring system to make it a bit harder to get gold.

I’m not allowed to show you all the visual changes we made. However, I can tell you that getting gold on all levels in any stage unlocks a surprise. Further, each stage holds it’s own surprise. Further still, getting golds on all levels will get you yet another surprise. Lots of surprises. Surprise!

On iOS, there will be a significant performance improvement. Several of the images have been touched up for higher resolution displays. We added a Facebook link on the title screen. We also added a periodic prompt for the user to rate the game. Finally, we added a banner in the level select menu to make it easier to buy the game. Some users mentioned not knowing how to buy the full game; we had previously hidden the buy button in the settings screen, or you had to complete all the levels before it prompted you to buy.

If all goes well, we’ll be submitting the update this weekend. Then it’s on to the next project!