Shared hosting and the holiday season bump

It seems that every time I leave town to visit family, some issue comes up with my research games that is difficult to deal with while travelling. This Christmas holiday was no exception, as I noticed on Christmas day that I had an email with the subject heading “URGENT: Account Suspension.” The automated message was not encouraging:

System administration was forced to suspend your site in an emergency to prevent server and system overloads. We should have more information forthcoming and will attempt to reach out to you shortly…

CPUusageDecember

CPU usage as reported in cPanel with our webhost.

Scary stuff, but when I attempted to load the site, it seemed to be running fine, and I did not see any unusual activity (though Resource Usage graph provided by my webhost showed a spike in activity).  Since the site appeared to have not been suspended, I assumed there were no major issues, but I did sent a reply to the tech support at my webhost.  I received a reply fairly quickly, and it turned out my most recent research game, Taxi Dash, was generating a lot of POST requests to a page on my site (in excess of 70k per day at the worst).

Eventually, my site was suspended (twice) as the resource use triggered automated suspensions.  The tech support staff in general were very helpful, and although they did want to move me up to a more expensive plan, they did reactivate my site, and have given me some time to get the resource use under control.  For behaviorgames.com, I use a shared hosting package, which allows me to run the site for a modest cost. The shared hosting package I use has limits, though, in how much of the server CPU it should be using in a day. Usually this CPU cap isn’t a problem, since the research games that I am working with are distributed mostly through mobile app stores (on iOS and Android), and I use the website primarily to provide information about the games and to collect data. However, the version of Taxi Dash that was live on two stores (Barnes and Noble, and Amazon), was set up to communicate with the server too often, it turns out.  Each installation sends high scores, information on how much of the game a player has completed, and more detailed analytics that help us test our research hypotheses, and improve the game.

Number of installs of Taxi Dash on the Barnes and Noble Nook in December.

Number of installs of Taxi Dash on the Barnes and Noble Nook in December.

And, in the way that I had structured these requests, each installation was generating a LOT of POST requests.  Especially around Christmas, when the number of installations from the Barnes and Noble app store shot up dramatically.  I had expected a lot of new installs, as there is typically a holiday season bump around Christmas in the U.S., but in the past this has not caused problems with my website.  The difference this year was that as new users downloaded Taxi Dash, the demand on my site was increasing dramatically for each installation.

Even now, after the Christmas rush has died down, I am still seeing a lot of traffic generated by the game.  I have had only 7,581 unique visitors in the first 6 days of January, but those visitors have generated 289k page requests.  Each of those requests attempts to connect to the mySQL database that I use for saving data, and that combination of traffic with the processing required to connect to the database seems to be the main issue.

For now, I have disabled the script that handles communicating with Taxi Dash, and I hope this will be enough to keep the site from being suspended again.  My overall CPU usage is down in the past few days (as you can see in the graph above), but it’s difficult for me to say if that is from a change in the script, or if it is just due to a decrease in traffic to the page (though, I am still seeing 50-75k page requests in most days so far in January, this is still lower than I saw at the peak around Christmas (of 80-95k)).  In any case, a new version of Taxi Dash has been submitted to both Barnes and Noble and Amazon, and I hope they will both go live this week.  In the meantime, now I can get back to doing some research and planning for courses for next semester!

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