January 7, 2016

Android 2D RPG - Part 4: The Game Loop

Hello my peeps! This is part 4 of the Android 2D RPG series!!! Let's start with our app, first order of business is to finally draw something on it! Lets get on with it!

First of all, we need Android Studio opened and check that all we've got on MainActivity.java (or whatever name you gave your main activity) looks like this, which it should if you've followed this guides so far.


MainActivity.java
package ve.com.biocraft.biocraft;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

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

And now we're gonna change some things, I'll be updating the code little by little. Let's begin by adding a new View, a View is a class that allows event handling (like touch events) and provides a space to draw on. For our purposes, we'll be extending Android's SurfaceView. We'll also need to implement a Callback to gain access to changes in our SurfaceView, such as when it is destroyed or the orientation changes.

So, we click on File > New and select Java Class, I'll be naming it GamePanel.java. This is the code.

GamePanel.java
package ve.com.biocraft.biocraft;

import android.content.Context;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class GamePanel extends SurfaceView implements SurfaceHolder.Callback {

    public GamePanel(Context context) {
        super(context);
        // Adding callback (this) to surface holder to catch events
        getHolder().addCallback(this);
        // Make GamePanel able to focus so it can handle events
        setFocusable(true);
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {

    }

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

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

This is a simple code to override the methods we'll be needing. Not really much to explain. Now let's create another Java Class that we'll call MainThread.java and will extend Android's Thread class. This will be our Game Loop.

MainThread.java

package ve.com.biocraft.biocraft;

import android.util.Log;
import android.view.SurfaceHolder;

public class MainThread extends Thread {

    // Constant for logging
    private static final String TAG = MainThread.class.getSimpleName();

    private SurfaceHolder surfaceHolder;
    private GamePanel gamePanel;

    // This is the flag to check if the game is running
    private boolean running;

    // This is the void that sets the flag to either true or false
    public void setRunning(boolean running) {
        this.running = running;
    }

    public MainThread(SurfaceHolder surfaceHolder, GamePanel gamePanel) {
        super();
        this.surfaceHolder = surfaceHolder;
        this.gamePanel = gamePanel;
    }

    @Override
    public void run() {
        Log.d(TAG, "Starting Game Loop");
        while (running) {
            // Update Game State
            // Render
        }
    }

}

Not much to say here either, we simply override the run() method and create a running flag, which while true will do an infinite loop, of nothing, for now xD. We also created a constant string named TAG which we'll be using for logging purposes.

As it stands, we still have to start this thread, so let's make some changes in our GamePanel class. We'll also need a way to end the game, we'll go 100% simple and end it whenever the user touches the screen. Since we'll need to change multiple methods, here's the full code.

GamePanel.java

package ve.com.biocraft.biocraft;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class GamePanel extends SurfaceView implements SurfaceHolder.Callback {

    private MainThread thread;

    public GamePanel(Context context) {
        super(context);
        // Adding callback (this) to surface holder to catch events
        getHolder().addCallback(this);

        // Create the Game Loop (the thread)
        thread = new MainThread(getHolder(), this);

        // Make GamePanel able to focus so it can handle events
        setFocusable(true);
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {

    }

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        // When the surface is created we set the running flag to true
        thread.setRunning(true);
        // And we start the Game Loop
        thread.start();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
        boolean retry = true;
        while (retry) {
            try {
                thread.join();
                retry = false;
            } catch (InterruptedException e) {
                // Try again to shut down the thread
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            thread.setRunning(false);
            ((Activity)getContext()).finish();
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {

    }
}

We made a couple changes, first we declared the thread as a private attribute of our GamePanel, then we instantiated it. On the surfaceCreated() method we set the running flag to true and start the thread. We also added some code to the surfaceDestroyed() method that will ensure the thread shuts down cleanly. Finally, we changed the onTouchEvent(event) method, what it does right now is check if the event is the start of a pressed gesture, if it is, it ends the game loop.

Ok, by now we have our View and our Game Loop, but we still need to tell our MainActivity to use that view! Let's get to it!

MainActivity.java
package ve.com.biocraft.biocraft;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;

public class MainActivity extends AppCompatActivity {

    // Constant for logging
    private static final String TAG = MainActivity.class.getSimpleName();

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

        // Title OFF, we don't want it
        getSupportActionBar().hide();

        // Making App fullscreen
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

        // Set our GamePanel as the View
        setContentView(new GamePanel(this));
        Log.d(TAG, "View added!");
    }

    @Override
    protected void onDestroy() {
        Log.d(TAG, "Destroying!!!");
        super.onDestroy();
    }

    @Override
    protected void onStop() {
        Log.d(TAG, "Stopping...");
        super.onStop();
    }
}

Ok, here we added a logging constant again, we will always use the name TAG for those. We set the app title off, put on fullscreen mode and set our GamePanel as the app view. We also override the methods onStop() and onDestroy() and make sure we log them. We could run this app right now to check the logs, but it won't display anything but a black screen, where's the fun in that? xD

Displaying an image using Android is quite simple, really. To dumb it down even more, we'll draw in on the top left corner, coordinates (0, 0). I'm gonna draw a guy from my game, you can use this fella from our Game Idea post, here it is:


First thing is adding it to our app. Just copy the image into the /res/drawable directory. (You should be able to drag-and-drop it directly on Android Studio). Now let's make some changes. First we're going to add a new method to GamePanel.java.

GamePanel.java
public void render(Canvas canvas) {
    canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.lyon), 0, 0, null);
}

This new method we'll be using to draw our image on the top left corner.

Note: R.drawable.lyon is MY image, if yours is, say, "image.png", you need to change it to R.drawable.image.

And at long last, we'll update the run() method from MainThread.java so that it calls for our render().

MainThread.java
@Override
public void run() {
    Canvas canvas;
    Log.d(TAG, "Starting Game Loop");
    while (running) {
        canvas = null;
        // Try to lock the canvas for pixel editing
        try {
            canvas = this.surfaceHolder.lockCanvas();
            synchronized (surfaceHolder) {
                // Update Game State
                // Render
                this.gamePanel.render(canvas);
            }
        } finally {
            // Catching exceptions
            if (canvas != null) {
                surfaceHolder.unlockCanvasAndPost(canvas);
            }
        }
    }
}

Alright, here we are declaring the canvas we'll be using for drawing. A canvas is the surface's bitmap onto which we can draw. This is very simple, every time the game loop executes itself, we get hold of the canvas and draw our image on the left corner. Time to run the code and see our guy displayed endless times until we touch the screen. It should look like this (on an emulator).


What have we done so far?
  • Create a fullscreen app
  • Have a thread to control the Game Loop
  • Listening to basic events
  • Shutting down cleanly
  • Displaying a simple image

Well, displaying something endless times is probably not the best idea, so the next part in our game building tutorial will be discussing FPS! Don't miss it in the next post in the series: Part 5- FPS. I'll try my best to have it done by tomorrow, because I probably won't have time to do much during the weekend. Have a great day and thank you all very much for reading!!!

Remember, sharing is caring! Use those social buttons, it's free! :P

2 comments:

Akya singh said...

very nice blog, i have also found one good link here.
Loop Statements In Java

David Lyon said...

Thanks! :) I'm glad you like it.

Good find there! If someone has any doubts about loops, they should absolutely check that link!

It's important to know the very basics before going in a big project :)