The (Mobile) Indie Game Dev Survival Guide

The (Mobile) Indie Game Dev Survival Guide is the talk I presented at GDC China 2013.

Can an indie survive in the mobile world?

It may look like games for mobile is a paradise for indie developers, but truth is that it is quite hard to be noticed among the swarm of apps.

In this presentation, Raul shares the tips & tricks he has learned about how to survive as a mobile indie game developer for the past 3 years, including a post-mortem of all the games of The Pill Tree and also a post-mortem of The Pill Tree as a company.

MTG Tracker 5.3 Released

Another update for MTG Tracker, just in time for another pre-release.

First things first, the changelog:

  • Added Theros
  • Fixed crash filtering collection / wish list
  • Improved deck view with card details
  • Improved deck list with color and format
  • Improved flow for adding cards to deck
  • Price for a list now applies on the selection
  • Taping on a card on a list selects the proper set image

Now, apart from a few bugs on the collection / wish list section, all this one does is to improve the screens and the flow on the decks area. Because it is one of the most used features of the app, and I want to provide the best experience for the users.

To notice how much of an improvement on the UI this version is, I have to show two screenshots. This is how it was until this version:

decks_deck_framed

This is quite a nice screen with good information. Being able to see the deck list together with the details is nice, and it has been good for a long time, but with today’s release it looks like this:
decks_deck_new_framed (1)Now, each card on a deck has type information and casting cost… I can’t believe I had not added that before, now I can’t imagine that screen without them.

But also, for each deck, you can now see the format of the deck and the colors it uses. Because it is always nice to know that stuff.

cardshark_header

Speaking at GDC China

The talk “The Indie Game Developer Survival Guide” is part of the Mobile and Smartphone Games Summit of GDC China.

The talk is inspired by other guides like “The Hitchhikers’ Guide to the Galaxy” and “Zombie Survival Guide” and it is a compendium of the post-mortem of the games of The Pill Tree and a post-mortem of The Pill Tree as well.

The talk goes over the current mobile gaming landscape, the different business models, how they worked for us, lessons learned and tips so you can increase your chances of survival as an Indie Game Developer.

GDC is a great conference for game developers, be it USA, Europe or China, and I am really humbled that they invited me to speak there.

However, China is very far away, but for the ones in the Netherlands, I will be presenting a slightly different talk at DroidConNL this year: “The Mobile Indie Developer Survival Guide” which includes some more details about how apps do.

The slides will be available on Slideshare soon after the conference.

Displaying items in a grid with a header

The problem

Let’s say you want a layout that is a grid of items with a header. Something like this:

fashiolista profile

Displaying items in a grid is easy, we have GridView for that.

Displaying a list of items with a header is easy, we have setHeaderView on ListView for that.

The problem is when we want to show items in a grid with a header, since GridView does not support headers and ListView does not support columns.

This is a common problem and there are a few suggestion on StackOverflow, but none of them goes further than some guidelines. I did implement it and I want to share it so you don’t need to reinvent the wheel

From the architectural point of view, the solution is to use a ListView with a special adapter that displays the entries as separated columns.

How the code should look like

The code at activity level when you configure the view is like this:

ListView listView = (ListView) findViewById(R.id.listView);
listView.addHeaderView(createHeaderView());

adapter = new GidViewWithHeaderExampleAdapter(this);
adapter.setNumColumns(2);
listView.setAdapter (adapter);

Note that you add the header to the list view as a normal header, but set the number of  columns to the Adapter.

Extending from the right adapter

The adapter itself has to extend from GridViewWithHeaderBaseAdapter and implement some methods that are slightly different from the ones in a normal adapter.

Integer[] mArray = new Integer[] {1,2,3,4,5,6,7,8,9,10,11,12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};

private LayoutInflater mInflater;

public GridViewWithHeaderExampleAdapter(Context context) {
    super(context);
    mInflater = LayoutInflater.from(context);
}

@Override
public Integer getItem(int position) {
    return mArray[position];
}

@Override
public long getItemId(int position) {
    return position;
}

@Override
public int getItemCount() {
    return mArray.length;
}

@Override
protected View getView(int position, View v) {
    if (v == null) {
        v = mInflater.inflate(R.layout.simple_list_item, null);
    }
    TextView tv = (TextView) v.findViewById(R.id.text);
    tv.setText(String.valueOf(getItem(position)));
    return v;
}

So getItem and getItemId are the same as for normal adapters.

On the other hand getItemCount is a replacement method for getCount and getItemView is a replacement for getView. both are implemented on the GridViewWithHeaderBaseAdapter and are the methods that do the magic.

Handling click on items

The adapter creates a LinearLayout and adds child views to it for each row, creating a single list item for each row.
Then, of course, OnItemClickListener is broken, because each list item is a row. You have to use GridItemClickListener instead.

@Override
public void onGridItemClicked(View v, int position, long itemId) {
    // TODO: Handle item click here
}

Advantages versus TableLayout

What is the advantage of this solution versus just a TableLayout on a ScrollView?

The advantage is that the Adapter recycles the views and it is a lot more efficient in memory and in execution.
The other advantage is that the Adapter is dynamic. You can not hardcode a TableView without knowing the items beforehand. Sure, you can build it programatically, but if you are getting that far, you probably want to get one step further and build an adapter, which is what I made.

GridViewWithHeaderBaseAdapter is about 150 lines of code, so feel free to look at the source for inspiration and/or modify it for your own purposes, it is under BSD license.

You can check the project on github. It includes the example.

MTG Tracker 5.2 Released

A new set of Magic the Gathering is out, and a new version of MTG Tracker comes with it. This time it is Magic 2014 Core Set.

With this version the playtesting feature includes Library manipulation, the key part that was missing (and that many people kept asking on the reviews on Google Play)

Also, a subtle change in the UI of the advanced search. It was like this:

Advanced search example

The icons for color selection were bad, it was not trivial to see when they were selected and they had a very old design and did not fit with the new tendency towards flat design, so I updated them.

The sort of change that is barely noticeable, but it works. It looks like this now:

new search

Full ChangeLog of MTG Tracker 5.2:

  • Magic M14 Core Set
  • Library manipulation on playtest
  • Fixed reset of playtest on device rotation
  • Fixed problems with format search on Spanish / German / French
  • UI update on Advanced search

cardshark_header

Codemon 3.0 – The Arcanes

It’s been a long time without updates on Codemon, finally, the wait is over.

Players asked for more Codemons and evolutions… well, players always ask for more creatures and evolutions in games like this.

There were a few limitations to give them what they were asking for.

In first place, the barcodes always generate the same result, by design. That makes impossible to make more creatures from scans.

Secondly, Codemons were already able to reach level 100 without any evolution, so that discards the easy way. In fact making the Codemons evolve will make it look more like a copy of Pokemon, and the idea is not to copy, but to just be inspired.

framed_new_home_screen_with_summon

We found a quire interesting solution: The Arcane Codemons.

Those were the original Codemons and they are extinct. That is why they can’t be scanned. Arcane Codemons need to be summoned.

To perform a summon, you need to use 2 Pure (a.k.a. having only one element) Codemons, they will be sacrificed and the Arcane will be summoned. Well, it is not that simple, there are several types of summoning and it depends on the Codemons involved.

framed_arcane_summon

This is a great mechanic, it puts more Codemons into the game, and it does it in something that “resembles” an evolution and at the same time does not break the existing game mechanics and is also different from other games.

The other big improvement of this version is cloud sync of accounts. That sounds easy, but it has been quite a nightmare. I needed to rewrite all the communication layer, the server side and the client cache… pretty much half of the game has been rewritten to make it work.

But all the coding and testing is done and it is available on Google Play, so grab it and play!

Speaking at GOTO Amsterdam 2013

GOTO Amsterdam is a conference designed for software developers, IT architects, agilists, product owners and project managers, it includes a total of 8 tracks with over 40 presentations.

As part of the mobile track I will be presenting “The Road to Publishing” on Wednesday (14:30-15:20).

This talk has received excellent feedback when I presented at DroidConNL, Appsterdam WWLL, AgileCyL and Delft University.

Abstract (as on GOTO website): “The process of building an App is slightly different from just Software Engineering, it is Product Engineering. In this talk I present several topics about Product Engineering that are relevant to anyone that is making or planning to make apps”.

Hope to see you there.

MTG Tracker 5.1 Released

Today the version 5.1 of MTG Tracker is available on Google Play.

For a very long time, people have been complaining, and with good reason, that the Advanced search needed more options.

Although the previous version allowed to search for green enchantments that cost 3 or less, or green cards that had “gain life” in the text that are valid on Modern, It was impossible to search for green enchantments that have “gain life” in the rules text.

This was specially noticeable when searching key pieces for combos or when building tribal decks. Another example is to search for humans with first strike.

For one reason or another, that feature has dropped from the list from version to version, but given that it was arguably the biggest flaw of the app, I am happy to have it solved.

The new advanced search looks like this:

Advanced search example

The complete list of changes is:

  • Improved Advanced search
  • Includes latest set: Modern Masters
  • Adds “Exiled” section to Playtest

And in case you were wondering, there are 7 cards that are green enchantments that cost 3 or less, valid in modern which have “gain life” in the rules text.

Advanced search results

App Gold Rush: The gold is (almost) over

While the feeling of Gold Rush in the field of mobile apps and games still exist, my impression is that the gold has been mostly depleted for a while -at least for indie developers-. This post is the summary of the journey of MTG Tracker, and how it has performed over time.

Note: MTG stands for Magic the Gathering, a very popular collectable card game. MTG Tracker is a tool for players of this game, not a game on itself.

Not so long ago, in January 2012

Back then, the free version of MTG Tracker just won 3rd place on BestAppEver Awards 2011 and was by far the most popular mtg app on Android.  In a surprising twist of events,  WotC (the makers of the game) suspended the app via a copyright C&D letter. At that very moment it was over 220,000 downloads, averaging 400-500 downloads per day.

That attack on 3rd party mtg apps took down 3 other apps, leaving Android mtg players with crap apps and the paid version of MTG Tracker (why it was not targeted by the C&D letter is something I don’t know).

The side effect of that suspension was a significant increase on the downloads of the paid version, and also the growth of some apps that were not popular before.

Fast Forward to June 2012

As WotC seemed to not be targeting 3rd party apps anymore and they even removed their official app from Android and iOS App Stores, I decided to resume development of MTG Tracker, which was on maintenance mode since January.

The new features made the gap between my app and the rest even bigger.

The re-publishing: January 2013

With no news from WotC in 1 year, and 6 months since I resumed active development of MTG Tracker, I decided to publish the free version again, under a new package name (after removing all copyrighted material, of course).

The app is -as it was before- the best Android MTG app, since it has even more features than before and competition has not improved that much (one of the other apps has improved quite a lot, but still misses 2 key features)

The downside of using a new package is that it enters Google Play as a new app, starting with no downloads and no ratings. Only the name of the paid app to back it up.

One would expect that given the popularity of the old free version, the growth of the paid version and the lack of quality competition, this new release will quickly catch up with the pace of downloads it had before, specially given that there are much more Android smartphones out there.

Wrong.

Looking at the figures

It has been 4 months since the initial release and a few weeks since an important update. The paid app is still keeping the same pace, but the fee app has barely passed 10,000 downloads in this period of time.

That is less than 100 downloads a day.

That is 20% of the downloads it had before, per day.

That is why I say that the gold is over.

Wait! What? It doesn’t make sense

Disclaimer: I don’t have any hard data to back the conclusions, they are just my hypothesis after analyzing the figures.

I think there are 2 main problems.

The first problem is lack of discoverability. Right now, doing a search on Google Play returns all sorts of apps, and getting in the top 5 of a search by the keywords you are targeting is quite difficult. Why my app is behind other apps with the same relevant keywords, even thou MTG Tracker has more ratings, more downloads and better average ratings… and it is also newer. It puzzles me.

The other problem is user weariness. Let me explain it: All the early adopters were eager to install and try each and every app. That eagerness does not last forever, I am actually surprised it has lasted that long, and the new users are nowhere close in activity as the early adopters.

TL;DR;

MTG Tracker was suspended from Google Play on Jan 2012, being the leader on its niche. When an improved version was republished in Jan 2013, the traction it got was significantly lower. One year of difference and a better app results in less downloads.

Playing music with an intro and a loop in Android

I just added background music for Codemon. It consists on 2 tracks: On for the Codiseum and one for everything else. Each track consists of an intro and a loop.

The main track plays the intro only once and the one for the Codiseum restarts from the beginning each time you enter the battle arena.

It turns out, this is not as trivial as it should be, but I won’t spoil the fun.

Initial solution (almost good)

To play long music tracks on Android, we have the class MediaPlayer, and the easiest way to do an intro+loop is to use 2 MediaPlayers and play one after the other. We will create a utility class IntroAndLoopMusicPlayer to handle it for us.

Below is the code for creation, load and unload for the utility class, which has an intro player and a loop player as members. This class loads the music from the assets directory given the path of the 2 files.

public IntroAndLoopMusicPlayer(AssetManager assets,
   String introMusicPath, String loopMusicPath)
{
   mAssets = assets;
   mIntroMusicPath = introMusicPath;
   mLoopMusicPath = loopMusicPath;

   mIntroPlayer = new MediaPlayer();
   mLoopPlayer = new MediaPlayer();

   load();
}

private void load()
{
   mLoopHasStarted = false;
   AssetFileDescriptor afd;
   try {
      afd = mAssets.openFd(mIntroMusicPath);
      mIntroPlayer.setDataSource(afd.getFileDescriptor(),
         introfd.getStartOffset(),afd.getLength());
      mIntroPlayer.setLooping(false);
      mIntroPlayer.prepare();

      afd = mAssets.openFd(mLoopMusicPath);
      mLoopPlayer.setDataSource(afd.getFileDescriptor(),
         afd.getStartOffset(),afd.getLength());
      mLoopPlayer.setLooping(true);
      mLoopPlayer.prepare();

      mIntroPlayer.setOnCompletionListener(this);
   }
   catch (IOException e) {}
}

@Override
public void onCompletion(MediaPlayer mp) {
    mLoopPlayer.start();
    mLoopHasStarted = true;
}

public void unload() {
    mIntroPlayer.stop();
    mIntroPlayer.release();
    mLoopPlayer.stop();
    mLoopPlayer.release();
}

Simple enough, the intro player is not looped, and when it completes, the loop player starts, which is looped. Only missing part is to just call start on the intro player to get the music started.

Next step: Since we want 2 music tracks, we need to be able to pause and resume each one of them, so let’s add some code to handle this

public void pause() {
    if (mIntroPlayer.isPlaying()) {
        mIntroPlayer.pause();
    }
    if (mLoopPlayer.isPlaying()) {
        mLoopPlayer.pause();
    }
}

public void start() {
    if (!mLoopHasStarted) {
        mIntroPlayer.start();
    }
    else {
        mLoopPlayer.start();
    }
}

Note: It is important to check if the player is playing before calling pause. In case you try to pause the intro after it has finished, or the loop before it has started the MediaPlayer will yield an error and the music will stop. Yes, seriously.

As mentioned, we want to restore the tracks either to the beginning of the loop or to the beginning of the track, so we need 2 more methods

public void restoreLoop() {
    mLoopPlayer.seekTo(0);
}

public void restoreIntroAndLoop() {
    if (mLoopHasStarted) {
        mLoopPlayer.seekTo(0);
    }
    else {
        mIntroPlayer.seekTo(0);
    }
    mLoopHasStarted= false;
}

And finally, from the sound manager, we can pause or start the music. Whenever we start/resume one track we will restore the other its the initial point.

public void pauseBgMusic() {
    mMainPlayer.pause();
    mCodiseumPlayer.pause();
}

public void resumeBgMusic(BGMusic bgMusic) {
    if (bgMusic == BGMusic.Main) {
        mMainPlayer.start();
        mCodiseumPlayer.restoreIntroAndLoop();
    }
    else {
        mCodiseumPlayer.start();
        mMainPlayer.restoreLoop();
    }
}

Note: I call pauseBgMusic inside onPause of all activities and call resumeBgMusic on both onCreate and onResume. This is the seamless way of playing music among several activities I have found so far, including pausing the music when the game goes to the background.

So this all looks fine. What’s the problem then?

The problem: Forward compatibility

This solution works… until you try it on a phone with Jelly Bean on it. Then you will notice that the music stops for almost a second after the intro finishes and before the loop starts. Note that this does not happen on older versions such as Gingerbread. Yes, I was quite puzzled.

It turns out that JellyBean has introduced a new method to have a seamless continuation of playing: setNextMediaPlayer. Which, as a side effect, makes the previous method useless.

So, forward compatibility, here we go. We have to replace this:

      mIntroPlayer.setOnCompletionListener(this);

With this:

      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
         mIntroPlayer.setNextMediaPlayer(mLoopPlayer);
      }
      mIntroPlayer.setOnCompletionListener(this);

And then, onCompletion has to be updated to look like this:

@Override
public void onCompletion(MediaPlayer mp) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
    {
        mLoopPlayer.start();
    }
    mLoopHasStarted = true;
}

We have yet another problem: When the loop player starts via setNextMediaPlayer, the intro player gets released automatically.

The solution is to reset and reload the intro player, but then another weird side effect happens: When you reset the intro player, the next media player gets reset as well.

The problem is that MediaPlayer is very hard to debug:

  • Documentation is extensive, but incomplete.
  • It just yields cryptic errors when you call an invalid method for the current state.
  • There is no way to get the current state.
  • Whenever there is an error, all music stops.

It took me a lot of trial an error to discover in which state the players were after each call. Once you know that, the fix is fairly straight forward: Reset and reload both of them.

public void restoreIntroAndLoop()  {
    if (mLoopHasStarted) {
        // For JB this also resets the next player...
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
        {
            mIntroPlayer.reset();
            mLoopPlayer.reset();
            load();
        }
        else {
            mLoopPlayer.seekTo(0);
        }
    }
    else {
        mIntroPlayer.seekTo(0);
    }
    mLoopHasStarted= false;
}

I also tried using a single MediaPlayer that seeks to the beginning of the loop whenever it completes, but has the same ‘glitching’ problem as the original solution.

I hope you find this useful and that it saves you the headaches I had to go through.