Click here to Skip to main content
15,867,453 members
Articles / Internet of Things / Wearables

Bob's Quest (Conquering Android Wear, Part 2)

Rate me:
Please Sign up or sign in to vote.
4.93/5 (4 votes)
14 Oct 2015CPOL6 min read 16.8K   55   1   6
Introducing Bob's Quest, a Flappy Bird clone (of sorts), in which Bob must avoid crashing into pillars as he leaps through space.

Get it on Google Play

Image 2

Image 3 Image 4 Image 5 Image 6

Introduction

Hello and welcome to my latest CP article!

In Part 1 of this series, I released Wearable Chess, the world's first free & open-source chess game for Android Wear. I also included a FAQ for beginners and a guide to setting up your Android Wear Dev Environment. Click here to get Wearable Chess on Google Play.

In this article (Part 2), I am going to show you from start-to-finish how I built Bob's Quest. (Originally I wanted to do this for Part 1 as well, but the complexity of Wearable Chess meant that a complete, start-to-finish walkthrough wasn't feasible. Instead, I focused more on the general principles behind the app's design.)

Anyway, that's enough introduction. Let's get coding! :bob:

If you have any comments or suggestions while reading my code, feel free to post them in the comments section at the bottom of the page. :D

The Game Loop

There is one common concept that underlies almost all computer games today - the game loop.

A simple game loop, in pseudo-code, looks like this:

while (true) {
    GetUserInput(); //Get input from keyboard, mouse, touch, etc
    DoGameLogic(); //Use the above input to do explosions, physics calculations, etc, for this frame
    RefreshGUI(); //Draw the next frame
}

In Bob's Quest, our game loop takes the following form:

Java
package com.orangutandevelopment.bobsquest;

import ...

public class BobView extends View {
    final int refresh_interval = 40;
    private double delta_time = 0;
    private Date last_updated;
    private Handler h;

    public BobView(Context context) {
        super(context);
        init();
    }

    public void init() {
        setWillNotDraw(false); //Ensures that drawing occurs immediately when requested.

        //User input
        this.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //Handle touch event
				...
                return false;
            }
        });

        //Game loop
        last_updated = new Date();
        h = new Handler();
        h.postDelayed(new Runnable() {
            @Override
            public void run() {
                Update();
                h.postDelayed(this, refresh_interval);
            }
        }, refresh_interval);
    }

    public void Update() {
        //How long since last update?
        delta_time = (new Date()).getTime() - last_updated.getTime();
        
		...
		//Make changes to game data
		...

        //Update GUI!
        this.postInvalidate();
        last_updated = new Date();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        ...
		//Render the GUI
		...
    }
}

Creating Game Objects

Bob's Quest uses the following class to create the parallax background, the randomly generated walls that Bob needs to avoid, and even Bob himself!

Java
public class GameObject {
    public double X = 0;
    public double Y = 0;
    public double Horizontal_Speed = 0;
    public double Vertical_Speed = 0;
    public double scaling = 1;

    public GameObject() {

    }
}

In BobView.java, the GameObjects are declared like this:

Java
GameObject Bob = new GameObject();
ArrayList<GameObject> walls = new ArrayList<>();
ArrayList<GameObject> stars = new ArrayList<>();
ArrayList<GameObject> clouds = new ArrayList<>();

To draw a game object, we declare a few Bitmaps...

Java
public Bitmap Bob_Image;
public Bitmap Background_Image;
public Bitmap Star_Image;
public Bitmap Cloud_Image;

private Paint mPaint; //Used for drawing Bitmaps

public void init() {
    ...

    //Decode resources
    Bob_Image = BitmapFactory.decodeResource(getResources(), R.drawable.bob_w);
    Background_Image = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
    Cloud_Image = BitmapFactory.decodeResource(getResources(), R.drawable.cloud_t);
    Star_Image = BitmapFactory.decodeResource(getResources(), R.drawable.star_t);

    mPaint = new Paint();
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(Color.BLACK);
    mPaint.setTypeface(tf);
	
	...
}

...and then we use these voids to draw our object:

Java
private void drawGameObject(Canvas canvas, Paint paint, GameObject object, Bitmap bitmap) {
    canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), getGameObjectRect(object, bitmap), paint);
}

private RectF getGameObjectRect(GameObject object, Bitmap bitmap) {
    return new RectF((float) object.X, (float) object.Y, (float) object.X + (float)(object.scaling * bitmap.getWidth()), (float) object.Y + (float)(object.scaling * bitmap.getHeight()));
}

One thing that may catch C# developers like me by surprise is that Java seems to define Rectangles by (x1, y1, x2. y2), rather than (x, y, width, height).

Image 7

Creating the Parallax Background

Image 8

(Animated .GIF above, may take a while to load)

It took a few hours to implement this animation correctly - in other words, to make it enhance the visual experience without distracting the user. The hardest part was simply creating the images themselves and experimenting to get the colors right. Unfortunately - because images and colors will be different for every game - I can't give you much help with this, other than advising you to experiment, pay attention to details, and have patience.

Once the images were done, though, creating parallax was fairly straightforward. This is how the background, clouds, and stars were drawn:

Java
@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.argb(255, 0, 0, 0));

    //Background first
    canvas.drawBitmap(Background_Image, new Rect(0, 0, Background_Image.getWidth(), Background_Image.getHeight()), new Rect(0, 0, canvas.getWidth(), canvas.getHeight()), mPaint);

    //Clouds and stars next
    for (int j = 0; j < stars.size(); ++j) {
        drawGameObject(canvas, mPaint, stars.get(j), Star_Image);
    }
    for (int j = 0; j < clouds.size(); ++j) {
        drawGameObject(canvas, mPaint, clouds.get(j), Cloud_Image);
    }
    
    ...
}

To move the clouds and stars, I did this:

Java
for (int j = 0; j < stars.size(); ++j) {
    stars.get(j).X -= 1;
    if (stars.get(j).X < -30)
        stars.remove(j);
}
for (int j = 0; j < clouds.size(); ++j) {
    //Clouds move twice as fast as stars.
    clouds.get(j).X -= 2;

    //Removes them once they pass off the screen
    if (clouds.get(j).X < -1 * clouds.get(j).scaling * Cloud_Image.getWidth())
        clouds.remove(j);
}

New stars and clouds are generated at random like this:

Java
if (times == 50) {
    times = 0;
    GameObject c = new GameObject();
    c.Y = r.nextInt(180) + 120;
    c.X = 340;
    c.scaling = r.nextDouble();
    clouds.add(c);
}
if (times == 10 || times == 20 || times == 30 || times == 40 || times == 50 || times == 0) {
    GameObject s = new GameObject();
    s.Y = r.nextInt(320);
    s.X = 340;
    s.scaling = r.nextDouble() * .25;
    stars.add(s);
}

Drawing the Walls

Image 9

These required some fancy canvas work, Here's how I drew them, explained heavily in comments:

for (int j = 0; j < walls.size(); ++j) {
    GameObject w = walls.get(j);

    //Draw the black center top part
    canvas.drawRect((float) w.X + 3, 0, (float) w.X + 14, (float) w.Y - 60, mPaint);
 
    //Change color for the white outline
    mPaint.setColor(Color.argb(255, 230, 250, 252));
    
    //Top left white line
    canvas.drawRect((float) w.X, 0, (float) w.X + 2, (float) w.Y - 60, mPaint);
    
    //Top right white line
    canvas.drawRect((float)w.X + 15, 0, (float)w.X + 17, (float)w.Y - 60, mPaint);
    
    //Top white block
    canvas.drawRect((float) w.X - 4, (float) w.Y - 60, (float) w.X + 21, (float) w.Y - 54, mPaint);
    
    //Change color to draw bottom half
    mPaint.setColor(Color.argb(255, 0, 0, 0));

    //Bottom black region
    canvas.drawRect((float) w.X, (float) w.Y + 60, (float) w.X + 17, 320, mPaint);
    
    //Outline color
    mPaint.setColor(Color.argb(255, 230, 250, 252));
    
    //Bottom left white line
    canvas.drawRect((float) w.X, (float) w.Y + 60, (float) w.X + 2, 320, mPaint);
    
    //Bottom right white line
    canvas.drawRect((float) w.X + 15, (float)w.Y + 60, (float) w.X + 17, 320, mPaint);
    
    //Bottom white block
    canvas.drawRect((float) w.X - 4, (float) w.Y + 60, (float) w.X + 21, (float) w.Y + 66, mPaint);
    
    //Change color for repeat
    mPaint.setColor(Color.argb(255, 0, 0, 0));
}

The walls were animated just like the clouds and stars were, except that they move twice as fast as the clouds and four times as fast as the stars. Scoring is also implemented by checking for when we pass Bob.

Java
for (int j = 0; j < walls.size(); ++j) {
    walls.get(j).X -= 4;
    if (walls.get(j).X < -30)
        walls.remove(j);
        
    //Did we score?
    if (Math.abs(walls.get(j).X - Bob.X) < 2)
        ++score;
	
	...
}

Animating Bob

To make Bob "bob" up and down like a real object in gravity, we need to define a low gravitational constant and change Bob's rate of descent in accordance with this constant.

We also need to prevent him from falling down before the game starts, instead making him gently float on the screen, waiting for the user to start with a tap.

Java
if (game_started) {
    Bob.Y -= Bob.Vertical_Speed * delta_time;
    Bob.Vertical_Speed -= .00075 * delta_time; //.00075 is the gravitational constant here

    //Don't let him go through the roof or fall through the floor!
    if (Bob.Y > 320 - Bob_Space.height())
        Bob.Y = 320 - Bob_Space.height();
    if (Bob.Y < 0)
        EndGame();
} else {
    Bob.Y += bobbing ? .5 : -.5;
}

The bobbing boolean is changed every few cycles using the same times integer used to control when new clouds and stars are created.

To make Bob jump up when the screen is tapped, we need to implement an OnTouchListener in the init() function:

Java
this.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (!game_over) {
            game_started = true;
            Bob.Vertical_Speed = .3;
        }
        return false;
    }
});

Setting Bob's Vertical Speed to .3 means that he is going up at a rate of .3 pixels per frame. Over the next few frames, the gravitational constant will pull Bob's speed below 0 and make him start falling down again.

Drawing Bob is very easy, we just use the same drawGameObject() void from earlier:

Java
drawGameObject(canvas, mPaint, Bob, Bob_Image);

Detecting Collisions

Detecting collisions is quite simple. All we need to do is create a void that find the Rectangles occupied by a given wall, like this:

Java
private RectF[] getWallSpaceRect(GameObject wall) {
    RectF top = new RectF((float) wall.X, 0, (float) wall.X + 17, (float) wall.Y - 54);
    RectF bottom = new RectF((float) wall.X, (float)wall.Y + 60, (float) wall.X + 17, 320);
    return new RectF[] {top, bottom};
}

Then, we add this code to the end of the for loop that moves the walls - Java conveniently has a pre-built intersects() method!

Java
RectF Bob_Space = getGameObjectRect(Bob, Bob_Image);

for (int j = 0; j < walls.size(); ++j) {
    ...
    
    RectF[] wall_space = getWallSpaceRect(walls.get(j));
    for (RectF r : wall_space) {
        if (RectF.intersects(r, Bob_Space)) {
            EndGame(); //We have a hit!
            break;
        }
    }
}

Using a Custom Font

To add a custom font to an Android Wear project, create a new folder on the same level as "java" & "res" called "assets".

Image 10

Next, place your font files in that folder, as seen above. For this game, I used a cool font called Munro. To enable drawing on a canvas using this font, I used the below code in BobView.java:

Java
public void init() {
    ...
    Typeface tf = Typeface.createFromAsset(getContext().getAssets(), "Munro.ttf");
    mPaint.setTypeface(tf);
    ...
}

@Override
protected void onDraw(Canvas canvas) {
    //Just an example
    canvas.drawText("Cool Font", 10, 10, mPaint);
}

Additionally, because I needed to use this font in XML-based GUI, I created a new class called CoolFontTextView that extended the default TextView class:

Java
public class CoolFontTextView extends TextView {
    public CoolFontTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.setTypeface(Typeface.createFromAsset(context.getAssets(), "Munro.ttf"));
    }
}

Creating the GUI in XML

Image 11

Let's start with the root elements of the XML document:

XML
<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WearableFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:id="@+id/container" tools:context=".MainActivity"
    tools:deviceIds="wear">

    <com.orangutandevelopment.bobsquest.BobView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/bob_view" />

    ...

</android.support.wearable.view.WearableFrameLayout>

The BobView is the most important GUI element. On top of the BobView element is a FrameLayout, which provides the dark, semi-transparent overlay of the "Game Over" sign:

XML
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/overlay_view"
    android:background="#aa000000"
    android:visibility="gone">

    ...

</FrameLayout>

Withing that FrameLayout is a vertically-aligned LinearLayout, within which are two CoolFontTextView's and an ImageButton:

XML
<com.orangutandevelopment.bobsquest.CoolFontTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/view"
    android:layout_gravity="center_horizontal"
    android:text="Game Over"
    android:textColor="#ffffff"
    android:textSize="32sp" />

<com.orangutandevelopment.bobsquest.CoolFontTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/tx_score"
    android:layout_gravity="center_horizontal"
    android:text="Score: 2 | Top: 13"
    android:textColor="#e6fafc"
    android:textSize="22sp"
    android:layout_marginTop="3dp"
    android:layout_marginBottom="7dp" />

<ImageButton
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:id="@+id/btn_new"
    android:layout_gravity="center_horizontal"
    android:src="@drawable/ic_undo_white_24dp"
    android:layout_marginLeft="15dp"
    android:layout_marginRight="15dp"
    android:background="#0B81FF"
    android:padding="7dp" />

Adding Event Handlers

We're almost done! Now we need to implement a way to react when Bob crashes into a wall. I did this by creating the following interface:

Java
public interface OnGameFinishedListener {
    void onEvent(int score);
}

Then, in BobView.java, I wrote the following, which allows multiple listeners of the same type to be attached to the same event. Bob's Quest only needs one, but supporting many is good practice, because you may need to add more in the future.

Java
ArrayList<OnGameFinishedListener> mListeners = new ArrayList<>();
public void addListener(OnGameFinishedListener listener) {
    mListeners.add(listener);
}

Whenever I needed to fire this event from within BobView,java, I used the following code:

Java
for (OnGameFinishedListener hl : mListeners)
    hl.onEvent(score);

The Main Activity jumps on the receiving side and calls the addListener() method in MainActivity.java. This code shows the "Game Over" sign when Bob crashes.

Java
mBobView.addListener(new OnGameFinishedListener() {
    @Override
    public void onEvent(int score) {
        if (score > TopScore)
            TopScore = score;

        mTxScore.setText("Score: " + score + " | Top: " + TopScore);
        mGameOver.setVisibility(View.VISIBLE);
    }
});

On a similar note, we also handle the New Game button in MainActivity.java like this:

Java
mNewGame.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mGameOver.setVisibility(View.GONE);
        mBobView.NewGame();
    }
});

Remembering the Top Score

Remembering the Top Score is easy. All we need is the following implementation in MainActivity.java:

Java
private int TopScore = 0;
public static final String PREFS_NAME = "BobsQuestPrefs";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    TopScore = this.getSharedPreferences(PREFS_NAME, 0).getInt("Top_Score", 0);
    ...
}

@Override
protected void onStop() {
    super.onStop();
    SharedPreferences settings = this.getSharedPreferences(PREFS_NAME, 0);
    SharedPreferences.Editor editor = settings.edit();
    editor.putInt("Top_Score", TopScore);
    editor.commit();
}

That's it! :cool:

Implementing Hold-to-Exit

Image 12

I covered this in my last article (Part 1) - but for the sake of completeness, I'll cover this again here.

The first step is creating a custom resource called hold_to_exit.xml:

XML
<resources>
    <style name="HoldToExit" parent="@android:style/Theme.DeviceDefault.Light">
        <item name="android:windowSwipeToDismiss">false</item>
    </style>
</resources>

Next, in AndroidManifest.xml, change the style of the relevant Activity to HoldToExit:

XML
...
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/HoldToExit" >
...

Then, add this element to your XML layout file (preferably as the last child of the root element):

XML
<android.support.wearable.view.DismissOverlayView
    android:id="@+id/dismiss_overlay"
    android:layout_height="match_parent"
    android:layout_width="match_parent"/>

Lastly, in MainActivity.java, implement the following:

Java
private DismissOverlayView mDismissOverlay;
private GestureDetector mDetector;
...

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    mDismissOverlay = (DismissOverlayView) findViewById(R.id.dismiss_overlay);
    mDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
        public void onLongPress(MotionEvent ev) {
            mDismissOverlay.show();
        }
    });
    ...
}
    
@Override
public boolean dispatchTouchEvent (MotionEvent e) {
    return mDetector.onTouchEvent(e) || super.dispatchTouchEvent(e);
}

Supporting Round Screens

When developing for Android Wear, care must be taken to support round screens.

Round screens were supported in Bob's Quest by:

  • Placing the points-scored counter in the center-top of the screen, where it can't be cropped off.
  • Offsetting Bob towards the center of the screen by default, so that his movement is not cropped.
  • Restricting the positions of the holes in the walls to a range within that of a circular screen's field of view.
  • Keeping buttons above the 290px vertical mark (for the Moto 360).

Wrapping Up

Thanks for reading to the end! :D

I really enjoyed writing this article, and I hope that it inspires other developers to see what they can do with Android Wear. There still is a lot of untapped potential in wearable technology.

As always - If you have any comments, questions, or suggestions of any kind, please post them below. If you liked this article, don't forget to give it a 5! :cool:

If you have an Android Wear Smartwatch, but you'd prefer not to download the source & compile Bob's Quest for yourself, then you can get the pre-compiled app for a small fee on Google Play:

Get it on Google Play

History

  • 14/10/15 First Version Published
  • 15/10/15 Added the links to Google Play and reworded/rearranged a few sections

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Founder
Australia Australia
I'm a university student with a passion for stunning UI design and flawless code.

I fell in love with computers when I was 10 years old, and today I am fluent in C#, Python, Java, Javascript, HTML5, and CSS3. I know a little MATLAB, PHP, and SQL.

In my spare time, I build all sorts of cool things, like this Rubik's Cube Robot.

Away from the keyboard, I enjoy quality time with friends and family, as well as reading, painting, playing the piano, teaching chess & karate, and volunteering within my community.

I have learnt that success comes through hard work and dedication, and that giving back to the same people and communities that have helped me is both important and very rewarding.

Follow me on GitHub

Comments and Discussions

 
QuestionMy Vote +5 Pin
D V L6-Nov-15 22:57
professionalD V L6-Nov-15 22:57 
AnswerRe: My Vote +5 Pin
Mitchell J.7-Nov-15 21:19
professionalMitchell J.7-Nov-15 21:19 
GeneralMy vote of 5 Pin
Chris Maunder18-Oct-15 13:30
cofounderChris Maunder18-Oct-15 13:30 
GeneralRe: My vote of 5 Pin
Mitchell J.19-Oct-15 0:10
professionalMitchell J.19-Oct-15 0:10 
GeneralThanks for the contest entry Pin
Kevin Priddle14-Oct-15 10:22
professionalKevin Priddle14-Oct-15 10:22 
GeneralRe: Thanks for the contest entry Pin
Mitchell J.14-Oct-15 12:13
professionalMitchell J.14-Oct-15 12:13 
Thanks Kevin!
Up, Up, Down, Down, Left, Right, Left, Right, B, A.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.