Kendama – A game analysis

I’ve been playing Kendama for over 6 years, and I’ve seen many people getting really hooked into it. As game designer / developer I was curious about it and analysed the game from the game design point of view. Let me share the results.

Let’s get to the basics: kendama is a traditional Japanese skill toy similar to ball and cup, except that it has 3 cups and a spike. This is how a kendama looks like:

Royal-Kendama-The-Void-Pink

The toy itself is several hundreds years old. Not only it is still played today, but its popularity is on the rise and there is a thriving community behind it.

When a game this old survives, it surely has some good game mechanics built in. The ones I found more interesting are onboarding, flow, appeal to player types and metagame.

Onboarding

Kendama is really straight forward to understand. There is a ball with a hole and there is a spike. Everybody gets that (although spike is not the first trick you should learn). Since there are 3 cups as well, people naturally gets into using those and not just the spike.

As players gets more familiar with the kendama, they usually realise of the other rims which you can also use that are not obvious on first sight.

There is an official list of tricks and levels that you can follow as a guided onboarding. These skill levels have been created by the Japanese Kendama Association. You can check the list of tricks and skills in the BKA page.

Most kendamas include some information of the basic tricks, sometimes the skill levels and even some websites with tutorials in the packaging.

While the skill levels are a great way to do your user journey right, they are limited.

The Internet in general and youtube in particular are key factors of the growth Kendama has seen in the recent years.

On one side, there is the amount of video tutorials but on the other side is exploring what is possible beyond the basic tricks by watching edits from players. Once you master the basics, you want new challenges and those are very easy to find online. Just in case, there are over 100 tricks with videos included in KendamApp.

In summary, onboarding for kendama was always easy, but the internet has played a large role in pushing the limits and helping people realise what is possible making the path to mastery easier to discover.

Flow

Flow is a state when the challenge you are trying to achieve is within your reach, but requires some effort. If it is to difficult, it generates anxiety, if too easy, boredom. When the difficulty is right you enter flow, and being in the flow feels great.

It is important to note that to stay on the flow the challenges must adapt to your skill as it improves.

flowYou can read more about flow in the article: Cognitive Flow: The Psychology of Great Game Design.

Kendama is really good at getting you in the flow and keeping you in. In this aspect it is not different from most juggling disciplines as it is self-driven and presents a continuous challenge.

Flow is the best way to improve any skill since while your skills improve and you are always presented with a challenge that is within your reach but requires you some effort to master it. Motivation and sense of progression are always there.

Let’s get into the 4 aspects that a game must have to provide flow:

Concrete goals with manageable rules

When you are trying to learn a new trick the goal is very specific. Tricks have very simple descriptions and are easy to understand, even the advanced ones.

There are many simple tricks. As you advance, tricks can be a composition of other tricks or a sequence or a modification, still concrete, still manageable rules.

Only demand actions that fit within a player’s capabilities

It is up to the player to choose the trick to practice. If a trick is too hard, the player can step down and try a simpler version, when a trick becomes too easy you can step up for something harder.

There is always a new trick or a variation that you can try. As it is all player-driven -unless training for a skill level- the risk of getting into the anxiety or boredom zones is very small.

There is a caveat that comes with being player driven: it needs motivation. This is actually solved by the game itself. Hitting a trick gives a dopamine high and the sense of progression helps making it addictive. On top of that, there is some exercise, so there are endorphins as well.

As we already covered on the onboarding section, there are hundreds of tricks, providing an almost unlimited challenge.

There are also different types of tricks so you can choose based on your preferences, aerial, string, balance, etc.

Clear and timely feedback on player performance

When you try a trick you either hit or you miss, the feedback is instantaneous and extremely clear (as happens with most real world games).

There are some important effects in the way you practice kendama too:

  • Each try is very quick.
    • It is easy to try again.
    • There is low impact of failure.
  • Anticipation of success: When you see the kendama getting right into place your brain anticipates the possibility of success and generates dopamine. This happens before the outcome is determined, so you get it even if you miss.
  • It is based on intrinsic motivation: you want to land the trick.

Remove any extraneous information that inhibits concentration

This is meant mainly for video games, but you’ll be surprised how much focus you can get when playing kendama for a trick you haven’t managed yet.

Types of Players

Bartle taxonomy of player types is a  well known classification of the different player types and I think kendama appeals to all of them.

385px-Character_theory_chart.svg

This classification is generally used to determine how a game is targeted to a certain type of players regarding how the interact with the game and with other players.

Achievers

These are players who prefer to gain “points”, levels, equipment and other concrete measurements of succeeding in a game.

For these players, the skill levels is a great fit. You have a level that you can improve and when you do so in front of an examiner, you get a diploma. You start from 10 Kyu to 1 Kyu (beginner levels) and then from 1st Dan up to 6th Dan.

Dave Mateo is a good example of a player of this type. He keeps pushing himself to add more spins on the tricks, like a 11-turn jumping stick.

The main difference with killers is that killer want to beat other players while achievers battle themselves to improve.

Explorers

Explorers prefer discovering areas, creating maps and learning about hidden places.

When it comes to kendama, explorers naturally go into freestyle, trying new combinations of tricks and pushing towards finding their own personal style, many times inventing new tricks.

Iijima Hiroki is an explorer, he went on and created his own style. You can learn more about him on: Portrait of a Kendama Samurai.

Socializers

There are a multitude of gamers who choose to play games for the social aspect, rather than the actual game itself. They gain the most enjoyment from a game by interacting with other players.

The kendama community is quite large and attending real life events usually means meeting other players in person whom you have only interacted with online. There is a lot of people that go to those just to be with other players, no need to even do a trick.

Jake Wiens is a socializer. He runs a weekly meetup in San Francisco just to meet people and expand the kendama love.

Killers

They thrive on competition with other players.

While this is the least common part of kendama, there are competitions like the Japanese Kendama Open, Kendama World Cup, European Kendama Open, and many more where you can compete against very good players and win prizes.

In addition to that, there are small competitive games that you can play on a group.

A good example of a killer is Tomoya Mukai. He is at his best when competing on stage.

Metagame

Kendama is not just about doing tricks. There are other aspects of it that also appeal to players.

  • Collecting: There are many kendama brands, some of them are discontinued and considered collector pieces. Players start getting more and more kendamas to try the differnet brands and before you know, you have started a collection.
  • Customizing: There is a large community of people that paint/tune their own kendamas to make them special, some of them sell them afterwards.
  • Modding: A kendama is composed of 3 parts, each one of them has a weight. How the weight is distributed impacts how a kendama plays. Some players mix and match several kendamas until they have one with the perfect distribution for their taste.

Putting all this into place, I’d say kendama fits very well into modern game design and it is only logical that its popularity has been on the rise and that players get hooked easily.

Putting Platty Soft on hold

This post is part announcement, part post-mortem and part rant, but which post-mortem does not have its share of ranting?

The announcement

I’ll have started working for Daqri recently, and since Platty Soft is a one man company, it will be put on hold. This means that all the projects will receive only maintenance releases which I’ll do in my spare time, instead of as part of my daily job as I’ve been doing until now.

For MTG Tracker, this means that new sets will be added, but most likely there won’t be any new features (maybe some small ones, but don’t hold your breath).

The post-mortem

I’ve been running Platty Soft for 4 years. My original idea was to do contractor work to guarantee an income and then dedicate the rest of the time to my own projects, namely apps and games.

Ideally, those apps and games would gradually generate more income so I could do less contracting work and more of my own projects until I could just work on my projects full time.

Obviously, it has not worked as expected.

727446

Along these years I’ve improved MTG Tracker a lot, released Codemon and updated it, co-created AppAffinity (now discontinued) and also published a few small apps like KendamApp, Learn To Hoop and Spin Off.

A while ago it was already clear for me that the plan was not going to work. In fact I started advising people to not go indie (or at least be aware of the difficulties) already at the end of 2013 with the talk The (mobile) Indie Game Developer Survival Guide. You can watch the video from the talk at DroidConNL.

All in all, the indie road is a very tough one. There has been a perfect storm forming for some time now which I’ve been calling “The Appocalypse: the apocalypse of the apps”. Funny enough, people are now talking about Zombie apps, which fits into the theme. I already talked about the Gold (but not the rush) being over also in 2013.

However, things have gone from bad to worse. Over the past year, the monthly gross income I was getting from all my apps and games combined has been decreasing at an alarming rate. To the point of it being 1/3 of what it was a year ago. That is, going from 1200 EUR a month to just 400.

With the figures of one year ago, it was hard to consider that I was making “a living” out of it, but with the current ones it is definitely impossible.

Obviously all my projects are in long tail now, but that long tail is getting thinner in mobile.

To put it in perspective, there was only one time when the income was lower: the first month when I released MTG Tracker Pro. Yes, back to 2011, and at that time MTG Tracker was only one app I had.

But I know what you are asking. Why did I kept doing it? Well, there were several reasons:

  • I had an international move planned from Amsterdam to Dublin, so I didn’t want to get a full time job to just quit short after.
  • I had a nice contractor work with Squla, which is an awesome startup, that allowed me great flexibility.
  • Contractor work pays well, so I didn’t need to change that. I just stopped making new projects.
  • Once I was already decided to get a full time job, I got contacted by Packt to write a book about Game Development for Android, which had kept me busy for a few extra months.
  • I hadn’t found a really interesting full-time job until past week.

Now, let’s get ranting

The income from Google Play comes mostly from MTG Tracker, and then a little bit from Codemon.

MTG Tracker has never recovered the top search spot it had a few years ago (after it was taken down by WoTC), and that is the largest factor in its income decrease. I am not “demanding” to be at the top of the search, but right now the app is around position 20 (depending of the search criteria) and for at least 15 of the apps that appear before it MTG Tracker has:

  • Better average rating
  • More downloads
  • More ratings
  • Better ratings per download ratio
  • Updates more often

This rant about the crap search results on Google Play is not new. It was even one of the reasons I made AppAffinity back in 2012! And it has not improved. If something, it has become worse.

I have talked about this with people that works at Google Play and they seem to agree with me, I rank good in all the factors that are considered important, yet I am appearing very low in the search. Not being one of the top 5 apps is close to not existing. I am sure I got most of my downloads from word of mouth.

In the case of Codemon, while it has a decent conversion rate and ARPPU, the volume has been quite small to pay the time I invested in building it, but at least it paid all the other people that I hired to make it happen. Still, the past 2 months have seen a huge drop in In-App Purchases.

I have one more project in progress: GeoDefense. It is a cooperative GPS based tower defense, currently in private beta, and given the current situation, it may never get out of that state.

Now, talking about new projects. I considered for a while making games based on third party IP. I talked with the webcomics Spindrift and El Sistema D13, but ultimately I was not sure any of those projects was going to even cover expenses, and I was feeling tired.

The thing is, unless you put serious effort (and money) in promotion, your app is going to disappear in the swarm that Play Store is (and the App Store, for what is worth). Your only chance is to go for a very niche market, like Codemon does for barcode scanning games, and then you still need to be lucky and worry about many things.

So, it is clear to me that Google and Apple need to take action and fix the search of their app stores or you’d keep reading this stories from more and more indie developers. It has been a winner takes all for a long while, but it is evolving to have less winners and they take even more.

Let’s put everything together: Increasing user acquisition cost, flooded market, huge marketing budget for the ones on top to remain on top, almost non-existing visibility for the rest, a race to bottom in prices… Do I need to continue?

It is just not worth it.

Ultimately the reason why I decided to move on was my feelings each time I thought about a new possible project on the last year.

When thinking about a new project I was not feeling excitement or joy, but despair. Despair about how I was going to deal with visibility, discoverability and promotion. And that was not even considering the headaches of monetization. Sadly, this is a quite common feeling among indie developers today.

The only time I have not felt that way is when I worked on the  CounterClockWise Watchface I made for Android Wear a few weeks ago. The reason is that I built it for myself and didn’t care about monetization, downloads, reach and so on. I just did it because I wanted to and it was merely 2 days of work. It got 50 downloads in a week, actually, more than I thought it would.

That’s not how I want to feel about a new project. And if what it needs to be done to get interested in them again is to make them side projects and don’t give a shit about who finds them, so be it.

keep-calm-and-don-t-give-a-shit-46

But now, for the foreseeable future I’ll be doing some innovation at Daqri, and I’m really excited about it.

Book: Mastering Android Game Development

I’ve been busy the past few months writing a book titled Mastering Android Game Development with focuses on using the Android SDK for building games. The book is now available online.

4757_MockupCover_Normal

If you are an intermediate-level Android developer who wants to create highly interactive and amazing games with the Android SDK, then this book is for you.

This book is a progressive, hands-on guide to developing highly interactive and complex Android games from scratch. You will learn all the aspects of developing a game using a space shooter game as the example that will evolve with you through the chapters. You will learn all about frame-by-frame animations and resource animations. You will also create beautiful and responsive menus and dialogs and explore the different options for playing sound effects and music in Android, the basics of creating a particle system, how to configure and use Google Play Game Services on the developer console and port our game to the big screen.

Snapping items on a horizontal list

The Problem

We want to have a horizontal scrolling view that holds several items and that snaps to the selected one once the scroll stops, placing it centered on the screen.

Essentially something that works like this:

ezgif.com-crop

In old versions of Android we had the Gallery class that allowed us to do something similar, but it has been deprecated. It is also similar to what you can do with a ViewPager, but those are normally designed for full width Fragments.

The Solution (TL;DR)

What we will do is to listen for the event of scrolling stop on the list. Then we will check which item is currently in the middle of the screen and make the list scroll until it is positioned on the center.

The code for this example is available on GitHub as the project SnappingList

The detailed solution

You probably want something more detailed, so let’s get into it.

Some considerations

We are going to use a RecyclerView because the OnScrollListener allows us to know the pixels that have been scrolled, and that is not possible with a standard ListView. Its OnScrollListener does not provide enough information. Besides, a RecyclerView is more fancy.

We will know the with of the items on the list beforehand. This is very important to be able to calculate the center of the screen and which item is the selected one.

We are going to have two extra items in the list, one at the beginning and one at the end, to be able to position the first and last items centered on the screen. The width of these extra items need to be large enough for it.

The list items

As we mentioned, we will have two different items in the list. The normal ones and the ones we will add at the beginning and the end.

The layout for the extra items is defined in the layout file list_item_padding.xml and looks like this:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/padding_item_width"
    android:layout_height="match_parent">
</FrameLayout>

On the other hand, the normal items we are going to use are just a square shape in the background and a number in the center. They are defined in the layout list_item.xml which looks like this:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/item_width"
    android:background="@drawable/bgr"
    android:layout_height="match_parent">

    <TextView
        style="@android:style/TextAppearance.DeviceDefault.Large"
        android:id="@+id/item_text"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</FrameLayout>

These layouts have the width defined as a dimension. This allows us to tweak it for different screen sizes and also to be able to read the width in code without the need of waiting for the views to be measured. The values we are using for the example are:

<resources>
    <dimen name="item_width">200dp</dimen>
    <dimen name="padding_item_width">300dp</dimen>
</resources>

The Adapter

Then, we are going to create an adapter that just adds the two extra items. We call it ExtraItemsAdapter and the code is like this:

public class ExtraItemsAdapter
        extends RecyclerView.Adapter<ViewHolder> {

  private static final int VIEW_TYPE_PADDING = 1;
  private static final int VIEW_TYPE_ITEM = 2;

  private final int mNumItems;

  public ExtraItemsAdapter(int numItems) {
    mNumItems = numItems;
  }

  @Override
  public int getItemCount() {
    return mNumItems+2; // We have to add 2 paddings
  }

  @Override
  public int getItemViewType(int position) {
    if (position == 0 || position == getItemCount()-1) {
      return VIEW_TYPE_PADDING;
    }
    return VIEW_TYPE_ITEM;
  }

  @Override
  public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    // create a new view
    if (viewType == VIEW_TYPE_ITEM) {
      View v = LayoutInflater.from(parent.getContext())
        .inflate(R.layout.list_item, parent, false);
      return new ViewHolder(v);
    }
    else {
      View v = LayoutInflater.from(parent.getContext())
        .inflate(R.layout.list_item_padding, parent, false);
      return new ViewHolder(v);
    }
  }

  @Override
  public void onBindViewHolder(ViewHolder holder, int pos) {
    if (getItemViewType(pos) == VIEW_TYPE_ITEM) {
      // We bind the item to the view
      holder.text.setText(String.valueOf(pos));
    }
  }
}

The adapter receives a number that says how many elements to display as a parameter of the constructor.When asked for the item count it will return that number+2.

When asked to create a view, it uses the item view type to check if it is either a padding or a normal item.

To use this on a real world example, you should pass a list of items instead of just an integer and do proper binding inside onBindViewHolder.

Controlling the RecyclerView

Now that the basics are set, we can move into the really interesting part: handling the RecyclerView.

As we mentioned before, the idea is that when the scroll is stopped, we calculate which item is the current position, then, make the list scroll to that position.

To be able to calculate all that, we need some initialization, mainly to know the width of the items (both normal and extra ones) and also the padding needed, which is calculated based on the screen width and the item width.

We also keep track of the current amount of scroll -in pixels- of the RecyclerView in the allPixels variable.

All this can be done inside the onCreate method of the Activity.

Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
firstItemWidth = getResources().getDimension(R.dimen.padding_item_width);
itemWidth = getResources().getDimension(R.dimen.item_width);
padding = (size.x - itemWidth) / 2;

allPixels = 0;

We also need to initialize the RecyclerView by creating and setting a LayoutManager.

final RecyclerView items = (RecyclerView) findViewById(R.id.item_list);
LinearLayoutManager itemslayoutManager = new LinearLayoutManager(getApplicationContext());
itemslayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
items.setLayoutManager(itemslayoutManager);

Finally, we set the OnScrollListener to the RecyclerView:

items.setOnScrollListener(new RecyclerView.OnScrollListener() {

  @Override
  public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    super.onScrollStateChanged(recyclerView, newState);
    synchronized (this) {
      if (newState == RecyclerView.SCROLL_STATE_IDLE) {
       calculatePositionAndScroll(recyclerView);
      }
    }
  }

  @Override
  public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);
    allPixels += dx;
  }
});

This listener does two things:

  • When the scroll stops (the scroll state changes to SCROLL_STATE_IDLE), it calls calculatePositionAndScroll to make the list snap.
  • When the list scrolls, it updates the value of allPixels.

Then we have the utility method calculatePositionAndScroll which is where the action is:

private void calculatePositionAndScroll(RecyclerView recyclerView) {
  int expectedPosition = Math.round((allPixels + padding - firstItemWidth) / itemWidth);
  // Special cases for the padding items
  if (expectedPosition == -1) {
    expectedPosition = 0;
  }
  else if (expectedPosition >= recyclerView.getAdapter().getItemCount() - 2) {
    expectedPosition--;
  }
  scrollListToPosition(recyclerView, expectedPosition);
}

private void scrollListToPosition(RecyclerView recyclerView, int expectedPosition) {
  float targetScrollPos = expectedPosition * itemWidth + firstItemWidth - padding;
  float missingPx = targetScrollPos - allPixels;
  if (missingPx != 0) {
    recyclerView.smoothScrollBy((int) missingPx, 0);
  }
}

To calculate the position we use the values of allPixels, padding,  firstItemWidth and itemWith. Note that we are rounding the result.

We check for the special cases of the first and last items -the extra ones- and then tell the list to scroll to that position.

Scrolling to the position calculates the point in which the list needs to be positioned for a specific item, subtracts the value of allPixels from it and tells the list to do a smooth scroll.

Finally, we initialize the adapter with the number of items we want to be displayed.

ExtraItemsAdapter adapter = new ExtraItemsAdapter(NUM_ITEMS);
items.setAdapter(adapter);

While the code works, there are a couple of situations we want to fix to make it nicer.

Finishing touches

The first problem is that rotation does not work properly. RecyclerView does remember the state, but the value of allPixels gets reset when the Activity is destroyed. This is very easy to fix, we just need to save and restore it using the methods from the Activity.

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  allPixels = savedInstanceState.getFloat(BUNDLE_LIST_PIXELS);
}

@Override
protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  outState.putFloat(BUNDLE_LIST_PIXELS, allPixels);
}

The other finishing touch is to make the list start in a selected position, since now it starts at the beginning of the list, where an extra item is.

If we call calculatePositionAndScroll directly it will create a wrong state where the value of allPixels is incorrect because the view has not been completely measured yet. To fix that we have to call that method once the layout has been completed and we do that via the OnGlobalLayoutListener class.

ViewTreeObserver vto = items.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
  @Override
  public void onGlobalLayout() {
    items.getViewTreeObserver().removeOnGlobalLayoutListener(this);
    calculatePositionAndScroll(items);
  }
});

I recommend that you place this code inside onResume (as oposed to onCreate) to ensure that it is called after onRestoreInstanceState. Although the layout is most likely never completed before onResume is called, the logic of positioning the list to the right position makes more sense inside onResume.

Final considerations

There are some considerations for this to be used on real world applications. Nothing major, but still worth commenting:

  • We chose the size of the items for an optimal view on a Nexus 5 in landscape. You should consider qualifying the dimensions based on the screen smallestWidth.
  • Removing the ViewTreeObserver has two different methods, one that is valid for minSDK < 16 and another one for SDK 16 on. This example only considers minSDK 16.

You can see all the code in the SnappingList project on GitHub.

Google Developer Expert

I can proudly say that since January, I have become a Google Developer Expert for Android (GDE in short). The first -and at the moment only- one in Ireland.

Google Experts are a global network of experienced product strategists, designers, developers and marketing professionals actively supporting developers, startups and companies changing the world through web and mobile applications.

As part of the activities of the GDE program I spent some time at the Office Hours during Google I/O helping people and I also gave a 20 minutes talk in the sandbox about Playful Design (slides coming shortly).

Leonids Lib 1.2 – Meteor Shower

I just released an update of Leonids Lib. It includes feedback from previous versions both from github and the workshop I gave at GDGFestDublin.

Changes

From this version on, speed and acceleration are using dips. This makes a lot of sense, since the library should work together with the standard concepts of the Android framework. Animations should me much more consistent among different pixel density screens now.

Previous versions were using pixels, so you may want to review your parameters when you update.

New Features

There are some new configuration options:

  • emitWithGravity: Allows you to emit from a specific side of the View and particles will be emitted along all the edge of the View. i.e. Gravity.BOTTOM will create a rain-like effect. Default is Gravity.CENTER.
  • updateEmitPoint: Allows to dynamically change the point of emission for the particle system (useful to follow the touch along the screen)
  • stopEmitting: Will stop creating new particles, but the existing ones will keep animating (this is different from cancel, which also stops the already spanned ones)
  • emit now has methods to emit from a specific point on the screen given x and y instead of from a View.

New Examples

Also, new examples are being included to showcase the new features:

  • Emit with Gravity: Simulates a rain-like effect using Gravity.BOTTOM.
  • Follow touch: Creates a trail of stars following the touch on the screen.

libdemo_rain

You can also check Leonids Lib github page and/or get Leonids Demo from google play.

KendamApp 2.0

There is a new version of KendamApp – The Kendama App out there. It includes 32 new videos and my favourite feature so far: Self Certification.

MTG Tracker on Google Play

Self Certification

Until now you could track your accuracy per trick, and then see how close you were to pass an exam, but an actual exam… that is another story.

This new feature allows you to try for a self certification. It tracks all the attempts of all the tricks and it tells you if you passed or not the grading.

As in a real exam, you don’t have to keep going with a trick after you’ve completed the required hits, and as in a real exam, you stop as soon as you miss one trick.

It also keeps track of your attempts to the grading. Note that the tricks performed during the self certification are not added to the trick tracking feature, it is a different feature.

An image is worth a thousand words, so here’s a screenshot of the self certification in action.

KendamApp Self Certification

Happy clicking!

Accessing expansion patch files bug (and solution)

You may have used expansion files on Android. They are very handy when your app goes over 50Mb which, for games, happens pretty soon. In my case I have KendamApp – The Kendama App which has an extensive library of videos included.

The expansion files are organized on a main and a patch files. Each of them can be up to 2Gb. Since uploading them can be a pain, you can reuse them from one version to the next.

There are 2 libraries provided with android SDK to help managing expansion files:

  • downloader_library: Which purpose should be obvious.
  • zip_file: Which is a nice utility to manage expansion files via a content provider. You should be using it if you are using videos.

Also, if you are going to use videos, do not compress them when zipping the obb file.

Updating only with a patch file

Essentially, if you are going to add a few assets, it is better to use a patch file and reuse the main file. It will save lots of bandwidth and time.

You don’t have to re-upload the main file and users do not need to re-download it. Everyone wins.

Except that it may not be too straight forward when using the zip_file library.

The first pitfall (a.k.a. the undocumented feature)

First thing first, you should know that if you do not add some meta-data to the AndroidManifest, the library is going to look for the versions that match with the version number of the app. I don’t recall reading this in the documentation. I actually figured it out by reading the code.

So, the content provider on your AndroidManifest should look like this:

<provider android:authorities="com.plattysoft.zipprovider" android:name="com.plattysoft.provider.ZipFileContentProvider" >
   <meta-data android:name="mainVersion" android:value="17"/>
   <meta-data android:name="patchVersion" android:value="18"/>
</provider>

The second pitfall (a.k.a. the terrible bug)

Then, I realized that every time I tried to watch one of the new videos the app was crashing. I checked for the main and patch files and they were properly downloaded and inside the obb directory.

Something weird was going on.

Digging with the debugger, I ended up in a function that is supposed to return an string array with the name of the available expansion files based on the version numbers and after checking that the files do exist.

static String[] getAPKExpansionFiles(Context ctx, int mainVersion, int patchVersion) {
   String packageName = ctx.getPackageName();
   Vector<String> ret = new Vector<String>();
      if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
         // Build the full path to the app's expansion files
         File root = Environment.getExternalStorageDirectory();
         File expPath = new File(root.toString() + EXP_PATH + packageName);

         // Check that expansion file path exists
         if (expPath.exists()) {
            if ( mainVersion > 0 ) {
               String strMainPath = expPath + File.separator + "main." + mainVersion + "." + packageName + ".obb";
               File main = new File(strMainPath);
               if ( main.isFile() ) {
                  ret.add(strMainPath);
               }
            }
            if ( patchVersion > 0 ) {
               String strPatchPath = expPath + File.separator + "patch." + mainVersion + "." + packageName + ".obb";
               File main = new File(strPatchPath);
               if ( main.isFile() ) {
                  ret.add(strPatchPath);
               }
            }
         }
      }
      String[] retArray = new String[ret.size()];
      ret.toArray(retArray);
      return retArray;
}

Except that it was returning a single string and not two. So, paying extra attention I noticed this specific piece of code.

if ( patchVersion > 0 ) {
   String strPatchPath = expPath + File.separator + "patch." + mainVersion + "." + packageName + ".obb";
   File main = new File(strPatchPath);
   if ( main.isFile() ) {
      ret.add(strPatchPath);
   }
}

Of course it couldn’t find the patch expansion file, it is constructing the name using the mainVersion number instead of the patchVersion. Event the File variable is called main!

Copy and paste is an anti-pattern.

I have checked the latest version of this library that comes with the android SDK and the bug is still there. I’d check where the code is and send a push request. In the meantime, you know what you have to change to fix it.

Safe scenarios

You would not encounter this bug if:

  • You are not using the zip_file library.
  • You are not using a patch file.
  • Your main and patch files have the same version number (you update both).

GDGFest Dublin & Playful Design

Playful design is a term that comes from the games industry, it means juiciness, it means maximum feedback for minimum input, it means delight the user.

In my talk at GDGFest Dublin,  I talked about animations, transitions and how playful design fits into material design.

Playful design works at subconscious levels on the users, they prefer to use your app to another one, but they won’t be able to tell why.

This has been one of the big differences between Android and iOS: How the default components managed the transitions. It seems minor, but devil is in the details. With material design Google has made a big statement of just how important playful design is.

It is very easy to improve the playfulness of any app with very simple animations that are available on the Android framework. That will make your users happier, without them being able to tell you why.

The slides of “Playful Design: What, Why and How” are available on Google Docs.

IMG_20141101_122952