News

Pebble game with Android connectivity

Pebble OS-based smart watches are social creatures. Let’s teach Flappy Tux to get in touch with Android smartphones

Flappy Tux now talks to your phone

Previously, we taught you how to create a version of Flappy Bird for your Pebble smartwatch. Due to the complexity of the code, our game had to make do without a wall system: we simply didn’t have enough space to go over its implementation.

Fortunately, a new issue of this magazine brings four new pages of Tam-generated goodness. In addition to a wall system, we will implement a form to display the current session’s high score. Finally, a little conduit will be hacked up; it will connect Flappy Tux to your Android smartphone, opening up all kinds of fascinating possibilities for interaction.

Fine-tuning the difficulty of a game is one of the hardest design challenges. Simply increasing the speed of everything is easy – modifying the environment and/or artificial intelligence leads to more satisfying outcomes.

We use this approach by creating an ‘evil’ wall generation algorithm. Whenever a wall is created, the current position and speed of the player is taken into account. As we can compute the ‘maximal’ position which can be reached via Newtonian physics, a wall can be made higher or lower in order to modify the reaction time given.

Flappy Tux now talks to your phone
Flappy Tux now talks to your phone

Give me… a predictor

New walls will appear on the right-hand side of the screen when the last wall is more than 60 pixels away. This means that the player has about 60 pixels worth of space in order to climb or sink. Since we assume a constantly accelerated motion, the distance travelled can be determined by the formula s = (0.5 * a * t2) + (v0 * t). ‘v0’ stands for the initial speed, while ‘a’ stands for the acceleration that is applied. In Pebble C, this can be implemented as below:

static float getMaxClimb(int t)
{
   return 0.5 * -0.06 * t * t + moves_per_frame * t;
}

static float getMaxDroop(int t)
{
   return 0.5 * 0.04 * t * t + moves_per_frame * t;
}

getMaxClimb and getMaxDroop differ insofar as they return the maximum value valid for raising and falling. Performing the power of two involves using advanced mathematical functions, so its value is instead determined by transforming the operation into a series of multiplications: t2 becomes t*t (t3 would be t*t*t).

We use these values in order to create new walls. Wall positions get updated in the same pass, thereby creating an illusion of movement directed towards the player character:

static void checkWalls(GContext *ctx)
{
   if(wall1Alive==false && wall2x<60)
   {//Build a wall
      wall1x=140;

      wall1y= wall2y + getMaxDroop(60)/4;

      if(wall1y<20) wall1y=20;
      if(wall1y>100) wall1y=100;
      wall1Alive=true;
}
//Second wall omitted
//Move walls
wall1x--;
wall2x--;
if(wall1x==0) wall1Alive=0;
if(wall2x==0) wall2Alive=0;

Walls must not be too close to one another. We accomplish this via mutual exclusivity: a new wall spawns only if the previous one has passed across half of the screen. A generous allowance is deducted to give the player ample reaction time. Furthermore, values are clamped in order to prevent the algorithm from going berserk.

Developers working on real games should offer different difficulty values. The actual amount of pixels to be subtracted should be determined by play testing. Coders working on existing games could also resort to analysing the behaviour of their current customers.

Drawing the actual walls is simple. We forego the use of sprites and render rectangles instead. You are, of course, free to change this if your application is to be released commercially:

   //Draw wall
   if(wall1Alive)
   {
   graphics_fill_rect(ctx,GRect(wall1x, 0, 10, wall1y), 0, GCornerNone);
   }
   if(wall2Alive)
   {
      graphics_fill_rect(ctx,GRect(wall2x, 159-wall2y+32, 10, wall2y), 0, GCornerNone);
   }
}

graphics_fill_rect is interesting insofar as it permits the creation of rectangles with rounded corners. This can be achieved by passing in a radius in lieu of zero; one or more GCorner flags can be ORred together in order to select the affected corners.

Stylish collisions

Flappy Tux should display the player’s current high score once the game has ended. CloudPebble’s recently released GUI editor makes creating new forms really easy. Click the Add New button next to the Source Files section in order to open the creation wizard. Set File Type to Window Layout, and proceed to creating a new window called GameOverView.

The GUI editor is divided into two parts. Clicking control headers in the Toolkit section adds a corresponding widget to the form, while the properties of the currently selected widget can be modified in the aptly-named Properties pane. Add a group of controls in order to end up with the layout shown in the figure.

In the next step, proceed to clicking the ruler symbol on the right-hand side of your screen. CloudPebble will respond by showing you the generated code of the form, which will have a structure similar to:

// BEGIN AUTO-GENERATED UI CODE; DO NOT MODIFY
static TextLayer *s_you;
static TextLayer *s_high;
...
static void initialise_ui(void) {
...
}

static void destroy_ui(void) {
   ...
}
// END AUTO-GENERATED UI CODE

static void handle_window_unload(Window* window) {
   destroy_ui();
}
...

Qt developers will immediately recognise how the GUI editor works. The parts inside the UI comments are generated automatically whenever the layout of the form changes. Your code should confine itself to the methods outside of the comment – they are not regenerated as time passes by.

show_gameoverview activates the form for display. We can modify it in order to display the high and current scores, which are stored in global variables:

static char buf[] = “123456”;
static char buf1[] = “123456”;
void show_gameoverview(void)
{
   ...

   snprintf(buf, sizeof(buf), “%d”, highScore);
   text_layer_set_text(s_high, buf);

   snprintf(buf1, sizeof(buf1), “%d”, myScore);
   text_layer_set_text(s_you, buf1);

   ...
}

handle_window_unload gets called when form is removed from the screen. It is the ideal place to resume the game loop with a fresh start:

static void handle_window_unload(Window* window) {
   destroy_ui();
   goverFlag=false;
   flownWay=0;
   totalPos=50;
   moves_per_frame=0;
   wall1Alive=wall2Alive=false;
   wall1x=wall1y=wall2x=wall2y=0;
   app_timer_register(34, timer_handler, NULL);
}

With that, the game loop must be updated one more time. Add the following snippet to the bottom in order to invoke the checkWalls function. Collisions with walls are handled by setting the game_over flag:

static void updateGame(Layer *layer, GContext *ctx)
{
   ...
   checkWalls(ctx);
   if(wall1x<30 && wall1x>10 && wall1Alive==true)
   {
      if(totalPos<wall1y)
      {
         goverFlag=true;
         myScore=flownWay;
         if(highScore<myScore)highScore=myScore;
      }
   }
   //Second wall omitted
}

Our timer event handler does not preserve the AppTimer reference returned to it, which makes cancelling it a bit difficult. We solve this problem by parsing the GameOver-Flag in timer_handler:

void timer_handler(void *context)
{
   if(goverFlag==false)
   {
      layer_mark_dirty(myCanvas);
      app_timer_register(34, timer_handler, NULL);
   }
   else
   {
      show_gameoverview();
   }
}

If goverFlag is set to true, no further frames are fed into the game engine. Instead, show_ gameoverview is invoked in order to show the screen of doom.

Android, ahoy!

Pebble supports Android and iOS. The Android SDK is available via a dedicated GitHub repository. We will use version 2.6 in the following steps, so simply click the link bearing its number to get it. Extract the archive and import the AndroidManifest file into Eclipse via Import>Android>Existing code. The SDK will show up as a project called main. Open the Properties dialog and navigate to the Android subsection: the checkbox “Is Library” must be enabled. Finally, drag and drop the contents of the java folder into src.

In the next step, the actual application is to be right-clicked. Open the Properties dialog and select the Android subsection. Click the Add button in the Library area. Eclipse will display a popup permitting you to select the “main” library created in the preceding step.

Pebble applications are identified via their globally unique UUID. Find yours in the Settings tab, and simplify access by creating a constant in your MainActivity:

public class MainActivity extends
ActionBarActivity {
   private final static UUID PEBBLE_APP_ UUID = UUID.fromString(“56f93cf8-1ab7-48c0- 9859-d3c2f631c1db”);

Pebble applications communicate with their companion applications via so-called dictionaries. A dictionary is best described as a key-value store – pass in an ID in order to retrieve the value associated with it.

For simplicity’s sake, our Android conduit consists of one method. OnCreate starts by trying to find if a Pebble is currently connected to the smartphone. If that is the case, our application is brought to the foreground:

myConnected = PebbleKit.isWatchConnected(getApplicationContext());
if(myConnected)
{
      PebbleKit.startAppOnPebble(getApplicationContext(), PEBBLE_APP_UUID);
      PebbleDictionary data = new PebbleDictionary();
      data.addUint8(0, (byte) 1);

      PebbleKit.sendDataToPebble(getApplicationContext(), PEBBLE_APP_UUID, data);
}

A button must be pressed in order to transmit an artificial high score to the watch. Its implementation is interesting, mainly due to the way the OnClickListener is declared:

Button aButton=(Button) findViewById(R.id.button1);
aButton.setOnClickListener(new OnClickListener() {
   @Override
   public void onClick(View v)
   {
      if(myConnected)
      {
         PebbleDictionary data = new PebbleDictionary();
         data.addUint8(0, (byte) 2);
         data.addInt16(1, (short)9000);
         PebbleKit.sendDataToPebble(getApplicationContext(), PEBBLE_APP_UUID, data);
      }
   }
   });

Both methods create an empty PebbleDictionary, which is then populated with one or more values. The individual tuples don’t need to be stored in ascending order – a dictionary consisting of the values 1 and 50 would be perfectly legal.

Receiving information is a bit more difficult due to the way the interaction between watch and app is configured. PebbleKit is but a thin wrapper which fires intents into the driver, thereby saving your application from needing Bluetooth permissions. Harvesting data requires the use of a handler class. Our example combines this with a thread dispatch, which permits you to update the user interface:

final Handler handler = new Handler();
PebbleKit.registerReceivedDataHandler(this, new PebbleKit. PebbleDataReceiver(PEBBLE_APP_UUID) {

   @Override
   public void receiveData(final Context context, final int transactionId, final PebbleDictionary data)
   {
      handler.post(new Runnable() {
         @Override
         public void run() {
            TextView myView=(TextView)findViewById(R.id.textView1);
            myView.setText(String.valueOf(data.getInteger(0)));
         }
      });
      PebbleKit.sendAckToPebble(getApplicationContext(), transactionId);
   }
});
}

Pebble applications communicating via AppMessage should declare a total of four event handlers in main(). app_message_open informs the operating system about the “chattivity” of your app, and furthermore permits you to specify the maximum size of incoming and outgoing dictionaries:

int main(void) {
   handle_init();
   app_message_register_inbox_received(inbox_received_callback);
   app_message_register_inbox_dropped(inbox_dropped_callback);
   app_message_register_outbox_failed(outbox_failed_callback);
   app_message_register_outbox_sent(outbox_sent_callback);
   app_message_open(app_message_inbox_size_maximum(), app_message_outbox_
size_maximum());
   app_event_loop();
   handle_deinit();
}

Space constraints force us to omit an explanation of the dropped/failed/sent handlers – their fairly primitive code can be seen in the example code on FileSilo.co.uk. inbox_ received_callback is more interesting due to a unique constraint of Pebble OS – developers cannot provide a dictionary instance with an ID in order to receive the value in question. Instead, all tuples must be parsed one after another using a method like:

static void inbox_received_callback(DictionaryIterator *iterator, void *context)
{
      Tuple *t = dict_read_first(iterator);
   while(t != NULL)
   {
      switch (t->key)
      {
         case 1:
            highScore = (int)t->value->int16;
            break;
      }
      // Get next pair, if any
      t = dict_read_next(iterator);
   }
}

Finally, the current highscore is sent to the smartphone via the timer_handler function. app_message_outbox_begin opens the outbox dictionary, which is then populated with the user data. app_message_outbox_send transmits the data to the smartphone, where it should be acknowledged by the client application:

void timer_handler(void *context)
{
   if(goverFlag==false)
   ...
   else
   {
      DictionaryIterator *iter;
      app_message_outbox_begin (&iter);
      dict_write_int16(iter, 0, (int16_t)highScore);
      uint32_t final_size = dict_write_end(iter);
      app_message_outbox_send ();
      show_gameoverview();
   }
}

Learn some more

Pebble OS is, rather refreshingly, really not that difficult to work with; even after spending just a little time with it, you should find it easy to use. Developers who are used to classic PDA and smartphone operating systems tend to be impressed by the simplicity of the API. Sadly, this does not mean that two tutorials of four pages each can cover the entirety of the features that are available to developers. Our treatment of the GUI stack, for example, is necessarily quite introductory.

However, Pebble itself has recently worked over its developer documentation. Simply open the Pebble Developers site in your browser of choice in order to start learning more about what you can do; content found in Guides tends to provide more detailed information on specific topics, while the syntax and parameter roles of individual functions can be studied by selecting Documentation.

×