Wear OS Game Development

Hello reader!
On black Friday 2019 I bought myself a TicWatch Pro. I wanted to own a smart watch for a few years already but was actually expecting Google to release a pixel watch. But sadly to this date that didn’t happen. On black Friday I found a good deal for a TicWatch Pro and I immediately bought it.
(See here link to Amazon)

Disclaimer: the code shown in this post is not optimized or made for any commercial use, it is only written for self study.

I am really enjoying my smart watch and try to use it as much as possible. The maps navigation works great when I am on my motor bike, receiving notifications when to exercise is great and the watch faces are fun!
But I couldn’t find many games in the play store. At least, not as much as I expected to..
From the day I bought the watch I already planned to make a game for my watch, but seeing the lack of games around motivated me more and more to start building one!

Researching

When starting to develop for Wear OS I began doing research. My first try was YouTube! I was wondering if there are videos around explaining how to develop a game for Wear OS.
Sadly there were only 6 videos on YouTube about Wear OS development which of 4 are explaining what to and what not to do.
I ended up only following the “Android Wear Tutorial: Notes App” which I did not even finish because it did not match my requirements for making an actual game.

Trial and ERROR

because of my YouTube video research I only knew how to set up a Wear OS project in Android Studio I thought why not try to make my own update/ticker my self. I already did this once with my Library I made about 5 years ago! (See here my archived Library: http://justinbieshaar.com/archive/operator.php)
I added my run method to my MainActivity.java and it worked!!
….
But there was a problem.. My screen remained black.

The compiler was correct, it outputs the correct delta time for the FPS rate I set my ticker to. But sadly the while loop broke the renderer.. I tried many many things as refreshing the activity each tick, running a different thread but couldn’t communicate with the UI thread and last tried to invalidate the main_activity view in the while loop but the while loop was literally breaking the renderer.
Now it really began to feel like a good challenge. πŸ˜€
I tried reading the documentation and only update/draw/ticker methods I could find in the Wear OS documentation were about creating watch faces. But I want to make an application, not a watch face!

Thinking out of the box

I started to think out of the box but inside the Android Studio box. I tried searching how games for mobile were made using Android Studio. I came across this “Flying fish game tutorial, by Coding Cafe” and noticed he removed the activity_main.xml and scripted a view by its own. This was the eye opener! πŸ˜€ (I definitely recommend watching the series to learn some basic Android Studio game development)
Being able to set a different view which overrides a draw method was all I needed. I followed the series till episode 7, so I knew how to draw bitmaps and capture input.

Developing

After knowing how to draw/update and set up a project I quickly made a pipe and a bird. I will now show the codes:

MainActivity.java, creating new view and start timer to call ticker method.

package com.example.testgame;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.support.wearable.activity.WearableActivity;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends WearableActivity {

    private static final long INTERFAL = 1L;

    private GameView game;
    private Handler handler = new Handler();
    private Timer timer = new Timer();
    private Runner runner;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // creating game view and set to content.
        game = new GameView(this);
        setContentView(game);

        // runner is originally made for game loop, reused for handler as Runable 
        // class to separate logic.
        runner = new Runner(game);

        // start ticker/update
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                // run runnable
                handler.post(runner);
            }
        }, 0, INTERFAL);

        // Enables Always-on
        setAmbientEnabled();
    }
}

Runner.java, calls game view ticker.

package com.example.testgame;

public class Runner implements Runnable {
    private GameView gameView;

    public Runner(GameView gameView) {
        this.gameView = gameView;
    }

    @Override
    public void run() {
        // todo: calculate delta
        // ticking/updating game view
        gameView.tick(1);
    }
}

GameView.java, creates the bird and pipes and shows a little message. Handles input and sends to bird to move upwards.

In tick method I called super.invalidate(). this causes the view to redraw and calls the Override draw method. I call all other instantiations before this invalidation so we can do our calculations before we redraw.

package com.example.testgame;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.view.MotionEvent;
import android.view.View;

import com.example.testgame.drawables.*;

public class GameView extends View {

    private static final String MESSAGE_START = "Tap to Start";

    private Bird bird;
    private Pipes pipes[] = new Pipes[3];

    private boolean move = false;

    private String message;
    private Paint messagePaint = new Paint();

    public GameView(Context context) {
        super(context);

        // creating bird
        bird = new Bird(getResources());
        
        // creating pipes
        for (int i = 0; i < pipes.length; i++){
            pipes[i] = new Pipes(getResources());
        }
        // set pipe positions by resetting them
        resetPipes();

        // initialize message text paint
        messagePaint.setColor(Color.GREEN);
        messagePaint.setTextAlign(Paint.Align.CENTER);
        messagePaint.setTextSize(65);
        messagePaint.setTypeface(Typeface.DEFAULT);
        messagePaint.setAntiAlias(true);
        
        message = MESSAGE_START;
    }

    public void tick(double deltaTime){
        if(!move){
            return;
        }

        bird.tick(deltaTime);
        for (int i = 0; i < pipes.length; i++) {
            pipes[i].tick(deltaTime);
        }

        // invalidating this view will recall draw
        super.invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        bird.draw(canvas);
        for (int i = 0; i < pipes.length; i++) {
            pipes[i].draw(canvas);
        }

        if(!move){
            // draw text "Tap to Start"
            canvas.drawText(message, canvasWidth / 2, canvasHeight / 2, messagePaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            // communicate to the bird to move up.
            bird.moveUp();
            if(!move){
                // reset pipes if te player died
                resetPipes();
                move = true;
            }
        }
        return true;
    }

    private void resetPipes(){
        float maxX = pipes.length * 300;
        for (int i = 0; i < pipes.length; i++){
            // adding a padding of 300 for each pipe
            pipes[i].setX(800 + (300 * i), maxX);
        }
    }
}

IDrawable.java, interface for the Bird and Pipes. This makes it easy to set up the classes and adds consistency to the code.

package com.example.testgame.drawables;

import android.graphics.Canvas;

interface IDrawable {
    void tick(double deltaTime);
    void draw(Canvas canvas);

    float getX();
    float getY();
}

Bird.java, draws and updates the bird to move up or down.

package com.example.testgame.drawables;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;

import com.example.testgame.GameView;
import com.example.testgame.R;

public class Bird implements IDrawable {

    private Bitmap bird[] = new Bitmap[2];
    private Bitmap activeBird;

    private float x = 150;
    private float y = 200;

    private float direction = 1;
    private float speed = 0.4f;

    private float desiredY = 500;
    private float jumpPower = 50;

    public Bird(Resources resources) {
        bird[0] = BitmapFactory.decodeResource(resources, R.drawable.bird);
        bird[1] = BitmapFactory.decodeResource(resources, R.drawable.bird2);

        // set active bird to draw
        activeBird = bird[0];
    }

    @Override
    public void tick(double deltaTime) {
        // check if direction is up or down.
        direction = desiredY > y ? 1 : -1;
        if(y < desiredY){
            desiredY = 500;

            // set active bird
            activeBird = bird[0];
        }
        
        // move bird up/down
        y += direction * speed;
        
        if(y > groundY()){
            // stop bird from moving out the screen but staying on the ground.
            y = groundY();
            // todo: die
        }
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.drawBitmap(activeBird, x, y, null);
    }

    @Override
    public float getX() {
        return x;
    }

    @Override
    public float getY() {
        return y;
    }

    public float getWidth() {
        return activeBird.getWidth();
    }

    public float getHeight() {
        return activeBird.getHeight();
    }

    public void moveUp(){
        desiredY = y - jumpPower;

        // set active bird
        activeBird = bird[1];
    }

    private float groundY(){
        // todo: receive from game view
        return 350;
    }
}

Pipes.java, moves and re positions the pipes

package com.example.testgame.drawables;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;

import com.example.testgame.GameView;
import com.example.testgame.R;

public class Pipes implements IDrawable {

    private Bitmap pipes[] = new Bitmap[2];

    private float x;
    private float y;
    private float spacing = 325;
    private float resetX;

    private float speed = -0.4f;

    public Pipes(Resources resources){
        for (int i = 0; i < pipes.length; i++){
            pipes[i] = BitmapFactory.decodeResource(resources, R.drawable.tube);
        }

        randomY();
    }

    @Override
    public void tick(double deltaTime) {
        x += speed;

        if(x + (pipes[0].getWidth() / 2)< GameView.getInstance().getCanvasWidth() / 2f){
            // todo: add score
        }

        // reset pipe back and randomize Y position
        if(x <= -100){
            x = resetX - 100;
            randomY();
        }
        
        // todo: add collision detection.
    }

    @Override
    public void draw(Canvas canvas) {
        for (int i = 0; i < pipes.length; i++){
            // check if it is the pipe top or bottom.
            float y = (i + 1) % 2 == 0 ? this.y : this.y + spacing;
            canvas.drawBitmap(pipes[i], x, y, null);
        }
    }

    public void setX(float x, float resetX){
        this.x = x;
        this.resetX = resetX;
    }

    private void randomY(){
        y = (float)(Math.random() * 100f) - 110f;
    }

    @Override
    public float getX() {
        return x;
    }

    @Override
    public float getY() {
        return y;
    }
}

and……
Run πŸ™‚

It doesn’t happen that often but it worked on first compile! πŸ˜€
I was even very surprised with how smooth it worked on my watch.
But as the code shown I still needed to add several things..

Tweaking

I could have finished here and moved to another project, but I wanted to finish this little Flappy bird game. πŸ˜›

Parallax Scrolling:

I started with adding some parallax scrolling for some extra effects. Therefor I made a new class named ParallaxLayer which implements the IDrawable. To make my life easier I made each layer the size of the screen.

package com.example.testgame.drawables;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;

public class ParallaxLayer implements IDrawable{

    private Bitmap layer;

    private float speed;
    private int width;
    
    // two x positions to move two images
    private float x[] = new float[2];

    public ParallaxLayer(Resources resources, int layerId, float speed){
        layer = BitmapFactory.decodeResource(resources, layerId);
        this.speed = speed;
        width = layer.getWidth();
        // set position of first layer image to 0 and second image allign it to the right
        x[0] = 0;
        x[1] = width;
    }

    @Override
    public void tick(double deltaTime) {
        for (int i = 0; i < x.length; i++){
            x[i] -= speed;
            // if image i moved out of screen allign to other image to the right.
            if(x[i] <= -width) {
                x[i] += width * 2;
            }
        }
    }

    @Override
    public void draw(Canvas canvas) {
        // draw both layers
        for (int i = 0; i < x.length; i++){
            canvas.drawBitmap(layer, x[i], 0, null);
        }
    }

    @Override
    public float getX() {
        // returning 0 because we don't use this getter
        // for this instance
        return 0;
    }

    @Override
    public float getY() {
        // returning 0 because we don't use this getter
        // for this instance
        return 0;
    }
}

In GameView.java I instantiated these layers:

    // create list of layers
    private static final int PARRALAX_LAYERS[] = { R.drawable.layer1, R.drawable.layer2, R.drawable.layer3, R.drawable.layer4 };
    // create list of speeds
    private static final float PARRALAX_LAYERS_SPEED[] = { 0.1f, 0.25f, 0.3f, 0.4f };
    
    public GameView(Context context) {
        ...
        for (int i = 0; i < layers.length; i++){
            layers[i] = new ParallaxLayer(getResources(), PARRALAX_LAYERS[i], PARRALAX_LAYERS_SPEED[i]);
        }
        ...
    }

    public void tick(double deltaTime){
        ...
        for (int i = 0; i < layers.length; i++){
            layers[i].tick(deltaTime);
        }
        ...
    }

    @Override
    protected void onDraw(Canvas canvas) {
        ...
        for (int i = 0; i < layers.length; i++){
            layers[i].draw(canvas);
        }
        ...
    }

Collision and add Points

To add collision we have to make our own collision detection. I made an easy detection by checking if the current x/y of the bird are colliding with the pipe x/y and width/height.

To make my life easier and not really thinking about a good architecture of what so ever, I decided to quickly add a singleton to the GameView.java so other classes could axis it. I then added a few getters as getBird and addScore. Thereby I also visualize the score points as text with a smaller text to the right being the high score.

package com.example.testgame;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.view.MotionEvent;
import android.view.View;

import com.example.testgame.drawables.*;

public class GameView extends View {

    // start text
    private static final String MESSAGE_START = "Tap to Start";
    // died text
    private static final String MESSAGE_END = "Oh no..";

    private static final int PARRALAX_LAYERS[] = { R.drawable.layer1, R.drawable.layer2, R.drawable.layer3, R.drawable.layer4 };
    private static final float PARRALAX_LAYERS_SPEED[] = { 0.1f, 0.25f, 0.3f, 0.4f };

    // singleton
    private static GameView instance;

    private Bird bird;
    private Pipes pipes[] = new Pipes[3];
    private ParallaxLayer layers[] = new ParallaxLayer[4];

    // points text paint
    private Paint scorePaint = new Paint();
    private Paint messagePaint = new Paint();
    private Paint blackPaint = new Paint();

    private boolean move = false;
    private boolean died = false;

    // point variables
    private int score = 0;
    private int highScore = 0;

    private String message;

    public GameView(Context context) {
        super(context);
        
        ...

        // initialize texts
        scorePaint.setColor(Color.RED);
        scorePaint.setTextAlign(Paint.Align.CENTER);
        scorePaint.setTextSize(50);
        scorePaint.setTypeface(Typeface.DEFAULT);
        scorePaint.setAntiAlias(true);

        // black paint is only to add a small shadow to the texts
        blackPaint.setColor(Color.BLACK);
        blackPaint.setTextAlign(Paint.Align.CENTER);
        blackPaint.setTextSize(50);
        blackPaint.setTypeface(Typeface.DEFAULT);
        blackPaint.setAntiAlias(true);

        messagePaint.setColor(Color.GREEN);
        messagePaint.setTextAlign(Paint.Align.CENTER);
        messagePaint.setTextSize(65);
        messagePaint.setTypeface(Typeface.DEFAULT);
        messagePaint.setAntiAlias(true);
        
        ...
    }

    public void tick(double deltaTime){
        ...
    }

    @Override
    protected void onDraw(Canvas canvas) {
        ...

        // drawing texts
        scorePaint.setColor(Color.YELLOW);
        scorePaint.setTextSize(50);
        
        // set paint size for black to match so we can reuse it in other lines
        blackPaint.setTextSize(50);
        
        canvas.drawText(Integer.toString(score), (canvasWidth / 2) - 2, 50 + 2, blackPaint);
        canvas.drawText(Integer.toString(score), canvasWidth / 2, 50, scorePaint);
        if(highScore > 0){
            scorePaint.setColor(Color.RED);
            scorePaint.setTextSize(30);
            
            // set paint size for black to match so we can reuse it in other lines
            blackPaint.setTextSize(30);
            
            canvas.drawText(Integer.toString(highScore), canvasWidth / 2 + 38, 42, blackPaint);
            canvas.drawText(Integer.toString(highScore), canvasWidth / 2 + 40, 40, scorePaint);
        }

        if(!move){
            // set paint size for black to match so we can reuse it in other lines
            blackPaint.setTextSize(65);
            
            canvas.drawText(message, (canvasWidth / 2) - 2, (canvasHeight / 2) + 2, blackPaint);
            canvas.drawText(message, canvasWidth / 2, canvasHeight / 2, messagePaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            bird.moveUp();
            if(!move){
                resetPipes();
                move = true;
                score = 0;
            }
        }
        return true;
    }

    // get singleton
    public static GameView getInstance(){
        return instance;
    }

    public void addScore(){
        score++;
    }

    public void died(){
        // reset data
        died = true;
        move = false;
        message = MESSAGE_END;
        messagePaint.setColor(0x80008000);

        if(score > highScore){
            highScore = score;
            // todo: save highscore
        }
    }

    public Bird getBird(){
        return bird;
    }
    
    ...
}

I added collision detection in Pipes.java

package com.example.testgame.drawables;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;

import com.example.testgame.GameView;
import com.example.testgame.R;

public class Pipes implements IDrawable {

    private Bitmap pipes[] = new Bitmap[2];

    private float x;
    private float y;
    
    @Override
    public void tick(double deltaTime) {

        // if pipe is on the half of the screen, it will add a point.
        if(x + (pipes[0].getWidth() / 2)< GameView.getInstance().getCanvasWidth() / 2f){
            // canApplyScore is here to prevent this pipe to keep adding score.
            if(canApplyScore){
                GameView.getInstance().addScore();
                canApplyScore = false;
            }
        }
        
        checkCollision();
    }

    public void setX(float x, float resetX){
        ...
        // this pipe is reseted so it will be able to apply score again
        canApplyScore = true;
    }

    private void randomY(){
        // this pipe is reseted so it will be able to apply score again
        canApplyScore = true;
    }
    
    ...

    private void checkCollision(){
        Bird bird = GameView.getInstance().getBird();
        float bX = bird.getX() + bird.getWidth();
        float bY = bird.getY();
        for (int i = 0; i < pipes.length; i++){
            int width = pipes[i].getWidth();
            int height = pipes[i].getHeight();
            
            // receiving correct y position
            float y = (i + 1) % 2 == 0 ? this.y : this.y + spacing;
            
            // check if x position is inside pipe x and pipe x + pipe width
            if(x < bX && x + width > bX){
                // check if bird is inside pipe y and pipe y + pipe height
                // also check if bird y + bird height is inside pipe y and pipe y + pipe height
                if((bY > y && bY < y + height) || 
                    (bY + bird.getHeight() > y && bY + bird.getHeight() < y + height)){
                    // COLLIDED, died
                    GameView.getInstance().died();
                }
            }
        }
    }
}

In case you don’t understand my if statements in the collisions, here a quick explanation:

I drew an object A and B both comparing with the tube. The tube is 50 pixels wide and is 100 pixels tall. It is positioned on x 80 and y -10.
The positions are pivot to the left top corner. Because of the pivot we check X/X + Width == 80/130
and
Y/Y + Height == -10/90
Lets call it for now X/XW.
To check collision we need to check if number is bigger than X and smaller than XW. If this is the case, we are inside!

Obect A: we can clearly see it is not inside the box, but lets calculate:
A is 10 pixels wide, 10 pixels tall and located on x 50 and y 40.
this means we first check our X position: 50/60. We already see that 50 and 60 both are not bigger than 80 So, we already do not have a match. But if this continues on this height, we will collide because our Y position is already bigger than -10 and smaller than 90!

Object B: we can already see a collision, but a part is also outside the box. Lets calculate:
B is 10 pixels wide, 10 pixels tall and located on x 75 and y 65.
X position is not bigger than 80, but x + width is 85 and is bigger than 80 and smaller than 130! This means we now must check Y.
Y position 65 is bigger than -10 and smaller than 90! this means both positions are colliding so we have found a collision!

Disclaimer: as you can see in the code I ignored x + width collision in my example code. I did this because this last collision can be to annoying to play. So, I decided to give the feeling to the user you just did not hit while he actually might did. πŸ˜‰

Saving high score

To complete the game we can add saving of the high score. So, when you close the application and reopen it, it will remember your high score.
To save data we can use SharedPreferences from Android Studio. We can get this in our MainActivity.java. Again to make our lives easier, I decided to add a singleton to the MainActivity.java so we can access it from other classes.

package com.example.testgame;

import android.app.Activity;
import android.content.SharedPreferences;
...

public class MainActivity extends WearableActivity {

    ...
    private static final String HIGH_SCORE_PREF = "HighScore";

    // singleton
    public static MainActivity Instance;
    
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Instance = this;
        ...
    }

    public void setHighscore(int score){
        // get shared preference and put our score in
        SharedPreferences sp = getSharedPreferences(HIGH_SCORE_PREF, Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.putInt(HIGH_SCORE_PREF, score);
        editor.commit();
    }

    public int getHighScore(){
        // return high score we saved, if we did not save it
        // it returns 0.
        return getSharedPreferences(HIGH_SCORE_PREF, Activity.MODE_PRIVATE).getInt(HIGH_SCORE_PREF, 0);
    }
}

In GameView.java we already saved score and highscore locally, so we can call in there to save it in shared preferences.

package com.example.testgame;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.view.MotionEvent;
import android.view.View;

import com.example.testgame.drawables.*;

public class GameView extends View {

    private static final String MESSAGE_START = "Tap to Start";
    private static final String MESSAGE_END = "Oh no..";

    private static final int PARRALAX_LAYERS[] = { R.drawable.layer1, R.drawable.layer2, R.drawable.layer3, R.drawable.layer4 };
    private static final float PARRALAX_LAYERS_SPEED[] = { 0.1f, 0.25f, 0.3f, 0.4f };

    private static GameView instance;

    private Bird bird;
    private Pipes pipes[] = new Pipes[3];
    private ParallaxLayer layers[] = new ParallaxLayer[4];

    private Paint scorePaint = new Paint();
    private Paint messagePaint = new Paint();
    private Paint blackPaint = new Paint();

    private boolean move = false;

    private int score = 0;
    private int highScore = 0;
    private float canvasWidth = 400;
    private float canvasHeight = 400;

    private boolean died = false;

    private String message;

    public GameView(Context context) {
        ...
        // receiving last saved high score
        highScore = MainActivity.Instance.getHighScore();
    }
    
    ...

    public void died(){
        died = true;
        move = false;
        message = MESSAGE_END;
        messagePaint.setColor(0x80008000);

        if(score > highScore){
            highScore = score;
            
            // save high score to shared preference.
            MainActivity.Instance.setHighscore(score);
        }
    }

    ...
}

Voila! we did it. We now made a Flappy bird game for Wear OS!


That is it!

I hope you liked this post πŸ™‚
If you created something with this, please let me know! I am always happy to see what other people create. If you have any questions or remarks regarding this post, please feel free to let me know in the comments section below or send me a message throughΒ Instagram!

Thanks for reading! Have a great 2020! πŸ˜€

– Justin Scott

Follow Justin Scott:

I love to learn and share. - 01001010 01010011 01000010

5 Responses

  1. hans kuijs

    Je bent lekker professioneel bezig met een mooie website, die ik net gevonden heb. Begon zelf op de p.c. in 1976 en was een uurtje bezig om een lijntje te programmeren. groetjes Opa

Leave a Reply

Your email address will not be published. Required fields are marked *