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.

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 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!

Adding in a Facebook “Like Us” Button using CoronaSDK

We’re going there. We’re moving a little bit closer to this newfangled social media fad the kids are talking about these days. I decided to add in a Facebook icon on the title screen of Brok, just for the purposes of giving our players an easy way to keep up with our scuttlebutt.

The idea is to offer the most painless way possible for players to “like” us on FB (btw, we have a FB page, you should check it out). I began by looking at the various Corona tutorials and examples to see how the integration should work. Corona has a Facebook api baked in, giving the developer an easy way to get a user logged in and make requests against the FB social graph. Below are a handful of the many posts that could help you get up and running.

Continue reading