Category Archives: Nook Development

Using player analytics for game design

Our new game/research project, Taxi Dash, was quietly released on the Nook and Amazon Appstore in early May.  Over the first two weeks, we released one update, mainly to add a new feature and some bug fixes.  As we were preparing the second update, we thought it would be useful to take a look at the game’s performance, and the player behavior, to see if we could make any improvements that would increase user retention.

Continue reading

Nook In-app integration for Unity

If you are a Nook developer, chances are that you’ve heard in-app purchases are launching on the Nook this month.  This week, Nook and Fortumo made the in-app SDK for Nook available and Barnes and Noble is offering special publicity for apps using the new IAP options, if they are submitted by May 3rd.

This sounds like a great opportunity, and if you have a lot of experience creating Android apps, integrating Fortumo into your game may be trivial.  But for many users of Unity, such as myself, the process may seem daunting (it did to me!).  For my other projects, I’ve relied on third-party plugins for Unity (such as prime31‘s, which are top-notch) to do in-app purchases, display advertisements, etc.

But, assuming a Fortumo plugin is not available by May 3rd, you may want to try to integrate Fortumo’s IAP on your own. That’s what I did this week, using my usual “trial -> error -> Google -> repeat” approach.  But, using the demo project provided by Fortumo, and a number of forum posts  by Unity and Android users, I was able to piece together an integration that seems to work, and I wanted to share my approach for anyone who is interested in building their own plugin for Fortumo.

For my game, I am using the free version of Unity and the Android Basic add-on, along with Eclipse to build the plugin, and it appears to be working now (in sandbox mode).   If you have any questions (or suggestions!), please let me know, and I’m hoping to continue to improve the integration.

Set up Eclipse:

  • Register with Fortumo (http://fortumo.com/nook) and create a service (one product that you want to sell in your game).
  • Set up an Eclipse project for your Unity game.  (I made one awhile back, and used this forum post, but in the new versions of Unity, you should be able to use “Create Eclipse project” though I have not tried that myself).
  • Get the FortumoInApp-android-9.0.7.jar after you register with Fortumo and add the jar to your ./Plugins/Android/ folder in your project. Import the .jar to your Eclipse project.
  • In Eclipse, create a new Android project, and add the following  three java classes shown below (AndroidFnc.java, FortumoFnc.java, PaymentStatusReceive.java).
  • Change the package name in the java classes to your package’s name.
  • Create a jar file (AndroidFnc.jar) and save it to your ./Plugins/Android/ folder in your Unity project.

Set up your Unity project:

  • In Unity, create a gameobject to handle your IAP purchases, and attach the AndroidFnc.cs script (below).
  • In AndroidFnc.cs, replace myID and mySecret with the appropriate values from the Fortumo dashboard (http://fortumo.com/services/).
  • Modify your AndroidManifest.xml (which should be in your project’s ./Plugins/Android/ folder) with the additions shown below.  Make sure that you change the main activity from com.unity3d.player.UnityPlayerActivity to com.mypackage.name.AndroidFnc.

 Other tips:

  • For other info on integration, see http://developers.fortumo.com/, and especially the information on testing (including sample credit card numbers you can use for test purchases).

The java classes listed below are used to communicate with the UnityPlayer, to launch the IAP process, and handle the result of the purchase.  The AndroidFnc class extends UnityPlayerActivity, and we will use this class to receive requests from the game and to send information back.  AndroidFnc will launch the purchase by starting an intent for FortumoFnc (and adding the purchase parameters as extra fields to the intent).  The final class, PaymentStatusReceive, will send the result of our purchase back to Unity.

Known issues:

  • On the Nook, if a user selects Cancel in the IAP screen, the user is not automatically returned to the UnityPlayer (and needs to press the soft key back button to do so).
  • If the user is not connected to Wi-Fi, the IAP screen displays “Unknown error” with no further information (it would be better to return with a failure, I think, but I’m working on that).
  • This version does not return details of the transaction (such as the user id, or other information you could use to verify the transaction if you are also logging transactions on your own server)

AndroidFnc.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.mypackage.identifier;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;
public class AndroidFnc extends UnityPlayerActivity {
  @Override
  protected void onCreate (Bundle icicle) {
    super.onCreate (icicle);
    Log.i("AndroidFnc", "Launched");
  }
   
  public static void logUnity(String msg) {
    UnityPlayer.UnitySendMessage("androidMng", "androidMsg", msg);
  }
 
  public static void purchaseFailed() {
    Log.i("AndroidFnc", "purchase failed.");
    UnityPlayer.UnitySendMessage("androidMng", "purchaseFailed","failed");
  }
 
  public static void purchaseSuccess(String product_name) {
    Log.i("AndroidFnc", "purchase successful.");
    UnityPlayer.UnitySendMessage("androidMng", "purchaseSuccess",product_name);
  }
 
  public void buy(final String ID, final String SECRET, final String product_id, final String product_description, final boolean isConsumable) {
    UnityPlayer.UnitySendMessage("androidMng", "androidMsg", "Starting purchase.");
 
    UnityPlayer.currentActivity.runOnUiThread(new Runnable() {
 
      public void run() {
        Intent i = new Intent(getApplicationContext(), FortumoFnc.class);
        // Pass in details about our item to be purchased.
        i.putExtra("ID", ID);
        i.putExtra("SECRET", SECRET);
        i.putExtra("product_id", product_id);
        i.putExtra("product_description", product_description);
        i.putExtra("isConsumable", isConsumable);
        UnityPlayer.currentActivity.startActivity(i);
      }
    });
  }
}

FortumoFnc.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.mypackage.identifier;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;

import com.fortumo.android.Fortumo;
import com.fortumo.android.PaymentActivity;
import com.fortumo.android.PaymentRequestBuilder;

public class FortumoFnc extends PaymentActivity {

  // Fortumo service information - http://fortumo.com/nook
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      Log.i("FortumoFnc", "create");
    AndroidFnc.logUnity("onCreate called");
    super.onCreate(savedInstanceState);
   
    Intent intent = getIntent();
    String ID = intent.getStringExtra("ID");
    String SECRET = intent.getStringExtra("SECRET");
    String product_id = intent.getStringExtra("product_id");
    String product_description = intent.getStringExtra("product_description");
    boolean isConsumable = intent.getBooleanExtra("isConsumable", false);
   
    buy(ID , SECRET, product_id, product_description, isConsumable);
  }
 
 
  public void buy(String itemID, String appSECRET, String product_id, String product_description, boolean isConsumable) {
      Log.i("FortumoFnc", "buy");

      Fortumo.enablePaymentBroadcast(this, Manifest.permission.PAYMENT_BROADCAST_PERMISSION);
     
          PaymentRequestBuilder builder = new PaymentRequestBuilder();
          builder.setService(itemID, appSECRET);
          // displayed on receipt sent to users
          builder.setDisplayString(product_description);
          // used to restore non-consumable purchases, also available in receipt verification requests
          builder.setProductName(product_id);
          // non-consumable items can only be purchased once and purchases can be restored using the product name value
          builder.setConsumable(isConsumable);
          makePayment(builder.build());

        IntentFilter filter = new IntentFilter("com.fortumo.android.PAYMENT_STATUS_CHANGED");
        registerReceiver(updateReceiver, filter);
  }
 
  @Override
  protected void onResume() {
    super.onResume();
   
    IntentFilter filter = new IntentFilter("com.fortumo.android.PAYMENT_STATUS_CHANGED");
    registerReceiver(updateReceiver, filter);
  }
 
  @Override
  protected void onPause() {
    super.onPause();
    unregisterReceiver(updateReceiver);
  }
 
  protected void onPaymentCanceled() {
    Log.i("FortumoFnc", "cancelled");
    // Does not seem to be reached
    finish();
  }
 
  private BroadcastReceiver updateReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      Log.i("FortumoFnc", "received");
      runOnUiThread(new Runnable() {
        public void run() {
          // updatePurchaseInfo();
        };
      });
      finish();
    }
  };
}

PaymentStatusReceive.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.mypackage.identifier;

import com.fortumo.android.Fortumo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

public class PaymentStatusReceiver extends BroadcastReceiver {
 
  @Override
  public void onReceive(Context context, Intent intent) {
    Bundle extras = intent.getExtras();

    if (extras.getInt("billing_status") == Fortumo.MESSAGE_STATUS_BILLED) {
      //String product_code = extras.getString("product_name");
      AndroidFnc.purchaseSuccess("success");
    }
   
    if (extras.getInt("billing_status") == Fortumo.MESSAGE_STATUS_FAILED) {
      AndroidFnc.purchaseFailed();
    }
   
  }

}

In my Unity project, I create a gameobject in my first scene to which I attach the script AndroidFnc.cs.  This gameobject will be named androidMng, and will handle communicating with my AndroidFnc.java class.

AndroidFnc.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
using UnityEngine;
using System.Collections;
using System.IO;

public class AndroidFnc : MonoBehaviour {
 
  public delegate void IAPevent(bool val);
  public static event IAPevent purchaseResult;
 
#if UNITY_ANDROID
 
  void Start() {
    GameObject androidM = GameObject.Find("androidMng");
    if (androidM == null) {  // This is the first load of the main menu
      DontDestroyOnLoad (transform.gameObject);
      this.gameObject.name = "androidMng";
    } else {
      Destroy(gameObject);
    }
  }
 
  // Used to track which purchase we are waiting to hear back about.
  public enum purchaseType {None, Coins2X , Coins5k};
  purchaseType pendingPurchase = purchaseType.None;
 
  public void getCoins(int val) {
    string ID = "";
    string SECRET = "";
   
    switch (val) {
     
    default: // 5000 coins
      pendingPurchase = purchaseType.Coins5k;
      ID = "myID";
      SECRET = "mySecret";
      break;
    }
   
    buy(ID,SECRET,val.ToString("N0") + " coins", val.ToString("N0")+ " coins", true);
  }
 
  public void get2XCoins() {   
    pendingPurchase = purchaseType.Coins2X;
    string ID = "myID";
    string SECRET = "mySecret";
   
    buy(ID,SECRET,"2X coins","2X coins",false);
  }
 
  public void buy(string ID, string SECRET, string product_id, string product_description, bool consumable) {  
    Debug.Log("Sending purchase request... " + product_id);
   
    object[] args = new object[]{ID, SECRET, product_id, product_description, consumable};
    using (AndroidJavaClass cls_AndroidFnc = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) {
      using (AndroidJavaObject obj_AndroidFnc = cls_AndroidFnc.GetStatic<AndroidJavaObject>("currentActivity")) {//
                obj_AndroidFnc.Call("buy",args);
          }
    }
  }
 
  public void purchaseFailed(string val) {
    Debug.Log("purchase failed.");
    pendingPurchase = purchaseType.None;
    if (purchaseResult != null)
      purchaseResult(false);
  }
 
  public void purchaseSuccess(string val) {
     
    Debug.Log("purchase succeeded: " + val);
    // handle purchase
   
    pendingPurchase = purchaseType.None;
    if (purchaseResult != null)
      purchaseResult(true);
  }
 
  public void androidMsg(string msg) {
    Debug.Log("Android message received: " + msg);
  }
#endif
 
}

To finish up, I made the following additions to my AndroidManifest.xml (which I placed in the ./Plugins/Android/ folder):

<application android:icon="@drawable/app_icon" android:label="@string/app_name" android:debuggable="false">

<activity android:name="com.mypackage.identifier.AndroidFnc" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name=".FortumoFnc" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" android:screenOrientation="landscape">
</activity>
<activity android:name="com.unity3d.player.UnityPlayerActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" android:screenOrientation="landscape">
</activity>
<activity android:name="com.unity3d.player.UnityPlayerNativeActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" android:screenOrientation="landscape">
<meta-data android:name="android.app.lib_name" android:value="unity" />
<meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="false" />
</activity>

<service android:name="com.fortumo.android.FortumoService" />
<service android:name="com.fortumo.android.StatusUpdateService" />
<activity android:name="com.fortumo.android.FortumoActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
<activity android:name="com.fortumo.android.PaymentActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>

<!-- Implement a BroadcastReceiver to track payment status -->
<!-- Should be protected with "signature" permission -->
<receiver android:name=".PaymentStatusReceiver"
android:permission="com.your.domain.PAYMENT_BROADCAST_PERMISSION">
<intent-filter>
<action android:name="com.fortumo.android.PAYMENT_STATUS_CHANGED" />
</intent-filter>
</receiver>
</application>

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Define your own permission to protect payment broadcast -->
<permission android:label="Read Fortumo payment status"
android:name="com.your.domain.PAYMENT_BROADCAST_PERMISSION"
android:protectionLevel="signature" />

<!-- "signature" permission granted automatically by system, without notifying user. -->
<uses-permission android:name="com.your.domain.PAYMENT_BROADCAST_PERMISSION" />

Nook Color, anyone?

In my last post, I mentioned that my last game, Amnesia Island, had done reasonably well on the Nook.  Over a 3 month period, the game had been downloaded over 50k times, which is good performance (in my opinion) for a game which is based on a set of memory tests for rats.

Currently, I’m working on a new game (titled Taxi Dash), which I’m hoping to release on the Nook in the next couple of weeks.  As I get closer to submitting the game to Barnes and Noble, I have become a bit concerned about how the game performs on my Android test devices.  According to my in-game counter, Taxi Dash is running at about 50 frames per second on my Samsung Player 4.0, and my first generation Kindle Fire.  But, that same version only runs at about 20 fps on my Nook Color (the original tablet released by Barnes and Noble).  And, at that frame rate, the game feels a bit laggy to me.

So, since the Nook Color is no longer sold by Barnes and Noble (though you can still get on on Amazon, ironically), I’m curious how many Nook Colors are still in use (because I don’t want to miss out on a large portion of the Nook market, and I don’t want users of the game to have a bad experience).  I haven’t found much information online on Nook device stats, so I turned to my own experience with Amnesia Island.  Of the 50k installs that Barnes and Noble reported, about 46,980 were recorded on my servers, so I would estimate that I have analytic data from most of the installations of the game.  And for each game session, I collect the device model, so I can get a sense of what types of devices/computers my players are using.

Installs of Amnesia Island received between December 20th 2012 and mid-March 2013

Installs of Amnesia Island received between December 20th 2012 and mid-March 2013

Of the sessions that were reported to my server, Continue reading