Senin, 01 Juli 2019

Cara Membuat Game Space Shooter Berbasis Android Studio

Membuat Game Space Shooter Berbasis Android




Tugas Kelompok 3

Nama Anggota:
Satrio Yudho Utomo ( 56415435 )
Andre Syafrudin ( 50415719 )
Baruna Adiharto ( 51415269 )










Membuat Game Space Shooter Berbasis Android


Space Shooter merupakan game klasik yang dari dulu sudah ada dan terus berkembang dengan gameplay yang lebih menantang, musuh yang beraneka ragam dan tentunya dengan grafis yang lebih bagus. Berbagai tools jg bisa digunakan untuk membuatnya seperti Unity, Unreal Engine dan untuk tutorial kali ini kita akan membuatnya menggunakan Android Studio.
Game ini bisa dibilang termasuk game legenda yang masih eksis hingga sekarang. Saya pertama kali memainkan game ini waktu itu masih di console Nintendo dan bagi saya waktu itu cukup seru untuk dimainkan. Lalu dari waktu ke waktu dengan munculnya berbagai console baru, game ini berkembang dengan berbagai variasi didalamnya.
Saya cukup senang memainkan game ini, lalu saya penasaran bagaimana jika game ini kita sendiri yang buat? Apakah rasa senang itu akan terasa lebih jika dibandingkan dengan bermain dari game orang lain. Menjawab pertanyaan itu saya coba untuk membuat game SpaceShooter ini dengan gameplay yang sangat simple dan tidak memerlukan waktu yang lama.
Sekaligus mengungkap rasa penasaran saya, apakah bisa kita membuat game dengan android studio? Dimana android studio ini biasa digunakan untuk membuat aplikasi android bukan game. Hasil googling sebentar, ternyata banyak tutorial yang menunjukkan bahwa kita dapat membuatnya lewat android studio, bahkan mungkin lewat IDE lain seperti Eclipse dan Netbeans pun bisa.
Dalam pembuatan game pada umumnya dibutuhkan beberapa orang dengan talenta masing-masing, karena dalam pembuatan game tidak hanya melibatkan orang yang jago dalam programming saja namun melibatkan orang-orang yang ahli dalam grafis, pembuatan musik, perancangan gameplay dan masih banyak lagi. Namun ditutorial kali ini kita akan fokus pada programmingnya saja karena kita akan menggunakan asset dari luar yang banyak orang sudah menyediakannya secara free.
Berikut adalah daftar asset yang bisa diunduh untuk nantinya digunakan ditutorial ini:
1.      http://www.kenney.nl/assets/space-shooter-redux, sebagian besar asset didapatkan dari sini
Lalu dari kumpulan asset diatas kita akan menggunakan beberapa diantaranya yaitu:
1.      SpaceShip warna biru sebagai player
2.      Enemy warna oranye dengan 3 model berbeda
3.      Meteor dengan 3 model berbeda
4.      Star dengan 3 model berbeda
5.    Laser sebagai senjata dari player dengan warna biru



 





1 hal lagi sebelum masuk ke programnya yaitu koordinat di android, ini penting karena nanti dalam pembuatan objek atau pembaruan posisi objek akan selalu menggunakan posisi koordinat. Jadi di Android mempunyai sistem koordinat seperti ini:
1.      0,0 itu letaknya di sudut kiri atas
2.      maxX, 0 itu letaknya di sudut kanan atas
3.      0, maxY itu letaknya di sudut kiri bawah
4.      maxX, maxY itu letaknya di sudut kanan bawah
Jadi untuk bergerak dari kiri ke kanan maka nilai xnya ditambah dan kebalikannya jika ingin bergerak dari kanan ke kiri maka nilai xnya dikurangin. Berlaku jg untuk axis y, jika ingin bergerak turun maka nilai ynya ditambah, jika ingin bergerak naik maka nilai ynya dikurangin.
Objek yang digambarpun juga mempunyai sistem koordinat yang sama, nilai posisi 0,0 itu berada dikiri atas. Jika asset sudah siap dan koordinat sudah paham kita masuk ke programnnya. Sekarang kita buka Android Studio terlebih dahulu. Kemudian buat project baru.
Untuk membuat SpaceShooter simple ini hanya dibutuhkan 1 Activity yaitu MainActivity. Terdapat beberapa settingan di MainActivity ini:
MainActivity.java
package com.example.satrioyudhoutomo.spaceshooter;

import android.content.Context;
import android.graphics.Paint;
import android.graphics.Point;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;

public class MainActivity extends AppCompatActivity implements SensorEventListener{

    private GameView mGameView;
    private float mXTemp;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Membuat tampilan menjadi full screen
        
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

        //Membuat tampilan selalu menyala jika activity aktif
       
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        //Mendapatkan ukuran layar
       
Display display = getWindowManager().getDefaultDisplay();
        Point point = new Point();
        display.getSize(point);
        Log.d("X and Y size", "X = " + point.x + ", Y = " + point.y);

        mGameView = new GameView(this, point.x, point.y);
        setContentView(mGameView);

        //Sensor Accelerometer digunakan untuk menggerakan player ke kanan dan ke kiri
       
SensorManager manager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        Sensor accelerometer = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0);
        manager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mGameView.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mGameView.pause();
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        mXTemp = event.values[0];

        if (event.values[0] > 1){
            mGameView.steerLeft(event.values[0]);
        }
        else if (event.values[0] < -1){
            mGameView.steerRight(event.values[0]);
        }else{
            mGameView.stay();
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }
}


Terlihat dari kode diatas beberapa settingan diperlukan agar game ini bisa berjalan dengan baik.

·        FLAG_FULLSCREEN akan membuat tampilan Activity menjadi full screen.

·        FLAG_KEEP_SCREEN_ON akan membuat smartphone on, kenapa diperlukan ini karena controller dari game ini berupa sensor Accelerometer yang membuat kita tidak menyentuh layar dan hanya menggoyangkan hp saja. Ini akan membuat layar off jika tidak ada interaksi sentuhan selama beberapa detik atau menit tergantung setingan hp.
·        getWindowManager().getDefaultDisplay() ini akan mengembalikan ukuran layar yang nantinya akan digunakan untuk menentukan koordinat berbagai objek.
·        Sensor Accelerometer digunakan untuk menggerakkan player kekanan dan kekiri.
Tampilan dari MainActivity ini menggunakan sebuah class yang me-extends SurfaceView.
Kemudian kita akan membuat GameView.java, codingannya seperti dibawah.
GameView.java
package com.example.satrioyudhoutomo.spaceshooter;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.util.ArrayList;
import java.util.Random;

public class GameView extends SurfaceView implements Runnable {

    private Thread mGameThread;
    private volatile boolean mIsPlaying;
    private Player mPlayer;
    private Paint mPaint;
    private Canvas mCanvas;
    private SurfaceHolder mSurfaceHolder;
    private ArrayList<Laser> mLasers;
    private ArrayList<Meteor> mMeteors;
    private ArrayList<Enemy> mEnemies;
    private ArrayList<Star> mStars;
    private int mScreenSizeX, mScreenSizeY;
    private int mCounter = 0;
    private SoundPlayer mSoundPlayer;
    private SharedPreferencesManager mSP;
    public static int SCORE = 0;
    public static int METEOR_DESTROYED = 0;
    public static int ENEMY_DESTROYED = 0;
    private volatile boolean mIsGameOver;
    private volatile boolean mNewHighScore;

    public GameView(Context context, int screenSizeX, int screenSizeY) {
        super(context);

        mScreenSizeX = screenSizeX;
        mScreenSizeY = screenSizeY;
        mSP = new SharedPreferencesManager(context);

        mSoundPlayer = new SoundPlayer(context);
        mPaint = new Paint();
        mSurfaceHolder = getHolder();

        reset();
    }

    void reset() {
        SCORE = 0;
        mPlayer = new Player(getContext(), mScreenSizeX, mScreenSizeY, mSoundPlayer);
        mLasers = new ArrayList<>();
        mMeteors = new ArrayList<>();
        mEnemies = new ArrayList<>();
        mStars = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            mStars.add(new Star(getContext(), mScreenSizeX, mScreenSizeY, true));
        }
        mIsGameOver = false;
        mNewHighScore = false;
    }

    @Override
    public void run() {
        while (mIsPlaying) {
            if (!mIsGameOver) {
                update();
                draw();
                control();
            }
        }
        Log.d("GameThread", "Run stopped");
    }

    public void update() {
        mPlayer.update();
        if (mCounter % 200 == 0) {
            mPlayer.fire();
        }

        for (Meteor m : mMeteors) {
            m.update();

            if (Rect.intersects(m.getCollision(), mPlayer.getCollision())) {
                m.destroy();
                mIsGameOver = true;
                if (SCORE>mSP.getHighScore()){
                    mNewHighScore = true;
                    mSP.saveHighScore(SCORE, METEOR_DESTROYED, ENEMY_DESTROYED);
                }
            }

            for (Laser l : mPlayer.getLasers()) {
                if (Rect.intersects(m.getCollision(), l.getCollision())) {
                    m.hit();
                    l.destroy();
                }
            }
        }

        boolean deleting = true;
        while (deleting) {
            if (mMeteors.size() != 0) {
                if (mMeteors.get(0).getY() > mScreenSizeY) {
                    mMeteors.remove(0);
                }
            }

            if (mMeteors.size() == 0 || mMeteors.get(0).getY() <= mScreenSizeY) {
                deleting = false;
            }
        }
        if (mCounter % 1000 == 0) {
            mMeteors.add(new Meteor(getContext(), mScreenSizeX, mScreenSizeY, mSoundPlayer));
        }

        for (Enemy e : mEnemies) {
            e.update();
            if (Rect.intersects(e.getCollision(), mPlayer.getCollision())) {
                e.destroy();
                mIsGameOver = true;
                if (SCORE>=mSP.getHighScore()){
                    mSP.saveHighScore(SCORE, METEOR_DESTROYED, ENEMY_DESTROYED);
                }
            }

            for (Laser l : mPlayer.getLasers()) {
                if (Rect.intersects(e.getCollision(), l.getCollision())) {
                    e.hit();
                    l.destroy();
                }
            }
        }
        deleting = true;
        while (deleting) {
            if (mEnemies.size() != 0) {
                if (mEnemies.get(0).getY() > mScreenSizeY) {
                    mEnemies.remove(0);
                }
            }

            if (mEnemies.size() == 0 || mEnemies.get(0).getY() <= mScreenSizeY) {
                deleting = false;
            }
        }
        if (mCounter % 2000 == 0) {
            mEnemies.add(new Enemy(getContext(), mScreenSizeX, mScreenSizeY, mSoundPlayer));
        }

        for (Star s : mStars) {
            s.update();
        }
        deleting = true;
        while (deleting) {
            if (mStars.size() != 0) {
                if (mStars.get(0).getY() > mScreenSizeY) {
                    mStars.remove(0);
                }
            }

            if (mStars.size() == 0 || mStars.get(0).getY() <= mScreenSizeY) {
                deleting = false;
            }
        }

        if (mCounter % 250 == 0) {
            Random random = new Random();
            for (int i = 0; i < random.nextInt(3) + 1; i++) {
                mStars.add(new Star(getContext(), mScreenSizeX, mScreenSizeY, false));
            }

        }


    }

    public void draw() {
        if (mSurfaceHolder.getSurface().isValid()) {
            mCanvas = mSurfaceHolder.lockCanvas();
            mCanvas.drawColor(Color.BLACK);
            mCanvas.drawBitmap(mPlayer.getBitmap(), mPlayer.getX(), mPlayer.getY(), mPaint);
            for (Star s : mStars) {
                mCanvas.drawBitmap(s.getBitmap(), s.getX(), s.getY(), mPaint);
            }
            for (Laser l : mPlayer.getLasers()) {
                mCanvas.drawBitmap(l.getBitmap(), l.getX(), l.getY(), mPaint);
            }
            for (Meteor m : mMeteors) {
                mCanvas.drawBitmap(m.getBitmap(), m.getX(), m.getY(), mPaint);
            }
            for (Enemy e : mEnemies) {
                mCanvas.drawBitmap(e.getBitmap(), e.getX(), e.getY(), mPaint);
            }
            drawScore();
            if (mIsGameOver) {
                drawGameOver();
            }
            mSurfaceHolder.unlockCanvasAndPost(mCanvas);
        }
    }

    void drawScore() {
        Paint score = new Paint();
        score.setTextSize(30);
        score.setColor(Color.WHITE);
        mCanvas.drawText("Score : " + SCORE, 100, 50, score);
    }

    void drawGameOver() {
        Paint gameOver = new Paint();
        gameOver.setTextSize(100);
        gameOver.setTextAlign(Paint.Align.CENTER);
        gameOver.setColor(Color.WHITE);
        mCanvas.drawText("GAME OVER", mScreenSizeX / 2, mScreenSizeY / 2, gameOver);
        Paint highScore = new Paint();
        highScore.setTextSize(50);
        highScore.setTextAlign(Paint.Align.CENTER);
        highScore.setColor(Color.WHITE);
        if (mNewHighScore){
            mCanvas.drawText("New High Score : " + mSP.getHighScore(), mScreenSizeX / 2, (mScreenSizeY / 2) + 60, highScore);
            Paint enemyDestroyed = new Paint();
            enemyDestroyed.setTextSize(50);
            enemyDestroyed.setTextAlign(Paint.Align.CENTER);
            enemyDestroyed.setColor(Color.WHITE);
            mCanvas.drawText("Enemy Destroyed : " + mSP.getEnemyDestroyed(), mScreenSizeX / 2, (mScreenSizeY / 2) + 120, enemyDestroyed);
            Paint meteorDestroyed = new Paint();
            meteorDestroyed.setTextSize(50);
            meteorDestroyed.setTextAlign(Paint.Align.CENTER);
            meteorDestroyed.setColor(Color.WHITE);
            mCanvas.drawText("Meteor Destroyed : " + mSP.getMeteorDestroyed(), mScreenSizeX / 2, (mScreenSizeY / 2) + 180, meteorDestroyed);
        }

    }

    public void steerLeft(float speed) {
        mPlayer.steerLeft(speed);
    }

    public void steerRight(float speed) {
        mPlayer.steerRight(speed);
    }

    public void stay() {
        mPlayer.stay();
    }

    public void control() {
        try {
            if (mCounter == 10000) {
                mCounter = 0;
            }
            mGameThread.sleep(20);
            mCounter += 20;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void pause() {
        Log.d("GameThread", "Main");
        mIsPlaying = false;
        try {
            mGameThread.join();
            mSoundPlayer.pause();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void resume() {
        mIsPlaying = true;
        mSoundPlayer.resume();
        mGameThread = new Thread(this);
        mGameThread.start();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mIsGameOver){
                    ((Activity) getContext()).finish();
                    getContext().startActivity(new Intent(getContext(), MainMenuActivity.class));
                }
                break;
        }
        return super.onTouchEvent(event);
    }
}

Cukup banyak kode diclass ini, saya coba untuk menjelaskannya satu persatu.
·        Class ini extends SurfaceView dimana nantinya sebagai media untuk menempatkan beberapa objek diatasnya. Untuk lebih detailnya tentang SurfaceView bisa dibaca di https://developer.android.com/reference/android/view/SurfaceView.html
·        Class ini jua implements Runnable karena didalam class ini akan dibuat sebuah thread baru yang nantinya digunakan untuk menggambarkan objek dengan posisi dan kondisi yang berbeda-beda setiap framenya.
·        Perlu dicermati pada method run(), didalamnya memanggil method update(), draw(), control() secara berulang-ulang. update() digunakan untuk melakukan perubahan disemua objek yang dibentuk ataupun bisa melakukan pembuatan dan penghapusan objek. draw() ini digunakan untuk menggambarkan objek dengan keadaan yang telah diupdate sebelumnya. Lalu didalam method control() terdapat Thread.sleep(20) itu artinya Thread akan berhenti selama 20 miliseconds.
·        Dari yang saya amati control() digunakan untuk mengatur jumlah frame per secondnya. Di kasus ini digunakan Thread.sleep(20) itu artinya dalam 1000 miliseconds atau 1 detik akan terdapat 1000/20 = 50 pemanggilan update() dan draw(), jadi ada 50 frame/second.
·        Lalu ada method pause() dan resume(), kedua method ini dipanggil dari MainActivity yang berfungsi untuk menghentikan Thread dan membuat Thread baru.
·        Setelah itu ada method steerLeft(), steerRight() dan stay(), ini juga dipanggil dari MainActivity ketika sensor berubah nilainya.
·        reset() ini membuat tampilan seperti pada awal sebelum game dimulai.
·        onTouchEvent() merupakan Listener yang menangkap response sentuhan dari user di smartphone. Method ini digunakan ketika sudah gameover dan ingin memulai game kembali yaitu dengan menyentuhnya sekali.
·        mNewHighScore digunakan sebagai kondisi ketika game over, jika ada high score baru, maka tampilkan tulisan NewHighScore dan beberapa data lainnya.
·        Beberapa drawText baru untuk menampilkan jumlah enemy dan meteor yang berhasil dihancurkan.
·        Ketika game over dan user menekan layar, maka akan berpindah ke MainMenuActivity.

Kemudian kita akan membuat SoundPlayer.java agar pada saat kita main game nya ada suara yg keluar. Codingannya seperti dibawah ini.
SoundPlayer.java
package com.example.satrioyudhoutomo.spaceshooter;

  

  import android.content.Context;

  import android.media.AudioAttributes;

  import android.media.AudioManager;

  import android.media.SoundPool;

  import android.os.Build;

  import android.util.Log;

  

  public class SoundPlayer implements Runnable {

  

    private Thread mSoundThread;

    private volatile boolean mIsPlaying;

    private SoundPool mSoundPool;

    private int mExplodeId, mLaserId, mCrashId;

    private boolean mIsLaserPlaying, mIsExplodePlaying, mIsCrashPlaying;

  

    public SoundPlayer(Context context){

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

            AudioAttributes attributes = new AudioAttributes.Builder()

                    .setUsage(AudioAttributes.USAGE_GAME)

                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)

                    .build();

            mSoundPool = new SoundPool.Builder()

                    .setMaxStreams(10)

                    .setAudioAttributes(attributes)

                    .build();

        } else {

            mSoundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 1);

        }

        mExplodeId = mSoundPool.load(context, R.raw.rock_explode_1, 1);

        mLaserId = mSoundPool.load(context, R.raw.laser_1, 1);

        mCrashId = mSoundPool.load(context, R.raw.rock_explode_2, 1);

    }

  

    @Override

    public void run() {

        while (mIsPlaying){

            if (mIsLaserPlaying){

                mSoundPool.play(mLaserId, 1, 1, 1, 0, 1f);

                mIsLaserPlaying = false;

            }

  

            if (mIsExplodePlaying){

                mSoundPool.play(mExplodeId, 1, 1, 1, 0, 1);

                mIsExplodePlaying = false;

            }

  

            if (mIsCrashPlaying){

                mSoundPool.play(mCrashId, 1, 1, 1, 0, 1);

                mIsCrashPlaying = false;

            }

        }

    }

    public void playCrash(){

        mIsCrashPlaying = true;

    }

  

    public void playLaser(){

        mIsLaserPlaying = true;

    }

  

    public void playExplode(){

        mIsExplodePlaying = true;

    }

  

    public void resume(){

        mIsPlaying = true;

        mSoundThread = new Thread(this);

        mSoundThread.start();

    }

  

    public void pause() throws InterruptedException {

        Log.d("GameThread", "Sound");

        mIsPlaying = false;

        mSoundThread.join();

    }

}

Class ini intinya digunakan untuk memainkan suara. Class ini juga menggunakan thread baru karena waktu pertama saya coba tanpa menggunakan thread baru terkadang ada lag didalam game.
SharedPreferencesManager.java
package com.example.satrioyudhoutomo.spaceshooter;

  

  import android.content.Context;

  import android.content.SharedPreferences;

  

  public class SharedPreferencesManager {

  

    private String mName = "SpaceShooter";

    private Context mContext;

  

    public SharedPreferencesManager(Context context) {

        mContext = context;

    }

  

    public void saveHighScore(int score, int meteorDestroyed, int enemyDestroyed){

        SharedPreferences sp = mContext.getSharedPreferences(mName, Context.MODE_PRIVATE);

        SharedPreferences.Editor e = sp.edit();

        e.putInt("high_score", score);

        e.putInt("meteor", meteorDestroyed);

        e.putInt("enemy", enemyDestroyed);

        e.commit();

    }

  

    public int getHighScore(){

        SharedPreferences sp = mContext.getSharedPreferences(mName, Context.MODE_PRIVATE);

        return sp.getInt("high_score", 0);

    }

  

    public int getMeteorDestroyed(){

        SharedPreferences sp = mContext.getSharedPreferences(mName, Context.MODE_PRIVATE);

        return sp.getInt("meteor", 0);

    }

  

    public int getEnemyDestroyed(){

        SharedPreferences sp = mContext.getSharedPreferences(mName, Context.MODE_PRIVATE);

        return sp.getInt("enemy", 0);

    }

}

Class ini digunakan untuk memudahkan dalam menyimpan dan memanggil nilai high score.
Lalu kemudian tersisa class-class yang berperan sebagai :
1.      Player
2.      Meteor
3.      Laser
4.      Star
5.      Enemy
Didalam class-class tersebut semuanya terdapat method udpate() yang fungsinya untuk memperbarui posisi dan kondisi objek yang dibuat dari class tersebut. Saya ambil contoh untuk class Player:
Player.java
package com.example.satrioyudhoutomo.spaceshooter;

  

  import android.content.Context;

  import android.graphics.Bitmap;

  import android.graphics.BitmapFactory;

  import android.graphics.Rect;

  

  import java.util.ArrayList;

  

  public class Player {

  

    private Bitmap mBitmap;

  

    private int mX;

    private int mY;

    private int mSpeed;

    private int mMaxX;

    private int mMinX;

    private int mMaxY;

    private int mMinY;

    private int mMargin = 16;

    private boolean mIsSteerLeft, mIsSteerRight;

    private float mSteerSpeed;

    private Rect mCollision;

    private ArrayList<Laser> mLasers;

    private SoundPlayer mSoundPlayer;

    private Context mContext;

    private int mScreenSizeX, mScreenSizeY;

  

    public Player(Context context, int screenSizeX, int screenSizeY, SoundPlayer soundPlayer) {

        mScreenSizeX = screenSizeX;

        mScreenSizeY = screenSizeY;

        mContext = context;

  

        mSpeed = 1;

        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.spaceship_1_blue);

        mBitmap = Bitmap.createScaledBitmap(mBitmap, mBitmap.getWidth() * 3/5, mBitmap.getHeight() * 3/5, false);

  

        mMaxX = screenSizeX - mBitmap.getWidth();

        mMaxY = screenSizeY - mBitmap.getHeight();

        mMinX = 0;

        mMinY = 0;

  

        mX = screenSizeX/2 - mBitmap.getWidth()/2;

        mY = screenSizeY - mBitmap.getHeight() - mMargin;

  

        mLasers = new ArrayList<>();

        mSoundPlayer = soundPlayer;

  

        mCollision = new Rect(mX, mY, mX + mBitmap.getWidth(), mY + mBitmap.getHeight());

    }

  

    public void update(){

        if (mIsSteerLeft){

            mX -= 10 * mSteerSpeed;

            if (mX<mMinX){

                mX = mMinX;

            }

        }else if (mIsSteerRight){

            mX += 10 * mSteerSpeed;

            if (mX>mMaxX){

                mX = mMaxX;

            }

        }

  

        mCollision.left = mX;

        mCollision.top = mY;

        mCollision.right = mX + mBitmap.getWidth();

        mCollision.bottom = mY + mBitmap.getHeight();

  

        for (Laser l : mLasers) {

            l.update();

        }

  

        boolean deleting = true;

        while (deleting) {

            if (mLasers.size() != 0) {

                if (mLasers.get(0).getY() < 0) {

                    mLasers.remove(0);

                }

            }

  

            if (mLasers.size() == 0 || mLasers.get(0).getY() >= 0) {

                deleting = false;

            }

        }

    }

  

    public ArrayList<Laser> getLasers() {

        return mLasers;

    }

  

    public void fire(){

        mLasers.add(new Laser(mContext, mScreenSizeX, mScreenSizeY, mX, mY, mBitmap, false));

        mSoundPlayer.playLaser();

    }

  

    public Rect getCollision() {

        return mCollision;

    }

  

    public void steerRight(float speed){

        mIsSteerLeft = false;

        mIsSteerRight = true;

        mSteerSpeed = Math.abs(speed);

    }

  

    public void steerLeft(float speed){

        mIsSteerRight = false;

        mIsSteerLeft = true;

        mSteerSpeed = Math.abs(speed);

    }

  

    public void stay(){

        mIsSteerLeft = false;

        mIsSteerRight = false;

        mSteerSpeed = 0;

    }

  

    public Bitmap getBitmap() {

        return mBitmap;

    }

  

    public int getX() {

        return mX;

    }

  

    public int getY() {

        return mY;

    }

  

    public int getSpeed() {

        return mSpeed;

    }

}

Sedikit penjelasan mengenai kode diatas, terdapat pengecekan apakah mIsSteerLeft == true, jika true maka mX -= 10 * mSteerSpeed akan dipanggil yang artinya posisi player dalam axis x akan bergeser ke kiri dengan nilai 10 * mSteerSpeed. mSteerSpeed ini digunakan untuk menentukan seberapa besar perpindahan objek. Biasanya dalam pembuatan game jika menggerakkan sesuatu, speednya lah yang akan diubah nilainya.
Didalam method update() di class Player ini, juga terdapat kode lain seperti mCollision. Ini adalah sebuah variabel Rect yang mempunyai nilai pada masing-masing sisinya. Ini digunakan untuk mendeteksi tabrakan antar objek. Seperti meteor menabrak player atau laser mengenai meteor.
Lalu juga ada kode untuk memperbarui posisi dan kondisi laser yang dikeluarkan dari player. Laser akan dibuat didalam class player ini ketika method fire() dipanggil dari class GameView. Laser juga akan dihancurkan jika nilai Ynya kurang dari 0, ini dilakukan agar memori tidak penuh.
Untuk class-class lain kurang lebih sama konsepnya jadi lebih detailnya bisa dipelajari sendiri. Kodenya akan saya share digithub dengan harapan game ini bisa terus dikembangkan.
Kita memerlukan1 activity baru, yaitu MainMenuActivity.
Di MainMenuActivity akan terdapat 3 button sesuai yang saya jelaskan sebelumnya, kemudian ada judul game berupa gambar dan background yang juga berupa gambar. Untuk buttonnya sendiri saya juga buat custom menggunakan gambar dari https://kenney.nl/assets/ui-pack, kemudian saya konversi ke 9-patch image. Untuk tutorial konversi gambar ke 9-patch image bisa dilihat di https://blog.andevindo.com/memahami-9-patch-image-dan-cara-menggunakannya/.
Berikut gambar untuk judul dan backgroundnya:

Tampilan akhir dari MainMenuActivity:


Kode untuk custom background buttonnya:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/blue_button01" android:state_pressed="true"/>
    <item android:drawable="@drawable/blue_button01" android:state_focused="true"/>
    <item android:drawable="@drawable/blue_button00"/>
</selector>

blue_button00 dan blue_button01 harus dibuat terlebih dahulu, tutorialnya bisa dilihat di https://blog.andevindo.com/memahami-9-patch-image-dan-cara-menggunakannya/
Kode untuk tampilan MainMenuActivity:
<?xml version="1.0" encoding="utf-8"?>

  <RelativeLayout 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"

    tools:context=".MainMenuActivity">

  

    <ImageView

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:scaleType="centerCrop"

        android:src="@drawable/space_shooter_main_menu_background" />

  

    <LinearLayout

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:layout_centerInParent="true"

        android:layout_marginLeft="32dp"

        android:layout_marginRight="32dp"

        android:orientation="vertical"

        android:gravity="center_horizontal">

  

        <ImageView

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:layout_marginBottom="80dp"

            android:adjustViewBounds="true"

            android:src="@drawable/logo" />

  

        <LinearLayout

            android:layout_width="match_parent"

            android:layout_height="wrap_content"

            android:orientation="vertical"

            android:paddingLeft="48dp"

            android:paddingRight="48dp">

  

            <Button

                android:id="@+id/play"

                android:layout_width="match_parent"

                android:layout_height="wrap_content"

                android:layout_marginBottom="8dp"

                android:background="@drawable/custom_button_background"

                android:text="PLAY"

                android:textColor="@android:color/white"

                android:textSize="20sp"

                android:textStyle="bold" />

  

            <Button

                android:id="@+id/high_score"

                android:layout_width="match_parent"

                android:layout_height="wrap_content"

                android:layout_marginBottom="8dp"

                android:background="@drawable/custom_button_background"

                android:text="HIGH SCORE"

                android:textColor="@android:color/white"

                android:textSize="20sp"

                android:textStyle="bold" />

  

            <Button

                android:id="@+id/exit"

                android:layout_width="match_parent"

                android:layout_height="wrap_content"

                android:background="@drawable/custom_button_background"

                android:text="EXIT"

                android:textColor="@android:color/white"

                android:textSize="20sp"

                android:textStyle="bold" />

  

        </LinearLayout>

    </LinearLayout>

  

</RelativeLayout>
Kode untuk MainMenuActivity
package com.example.satrioyudhoutomo.spaceshooter;

  

  import android.content.Intent;

  import android.support.v7.app.AppCompatActivity;

  import android.os.Bundle;

  import android.view.View;

  import android.view.WindowManager;

  import android.widget.Button;

  

  public class MainMenuActivity extends AppCompatActivity implements View.OnClickListener {

  

    private Button mPlay, mHighScore, mExit;

  

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main_menu);

  

        //Membuat tampilan menjadi full screen

        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

  

        //Membuat tampilan selalu menyala jika activity aktif

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

  

        mPlay = findViewById(R.id.play);

        mHighScore = findViewById(R.id.high_score);

        mExit = findViewById(R.id.exit);

  

        mPlay.setOnClickListener(this);

        mHighScore.setOnClickListener(this);

        mExit.setOnClickListener(this);

    }

  

    @Override

    public void onClick(View v) {

        switch (v.getId()){

            case R.id.play:

                startActivity(new Intent(this, MainActivity.class));

                finish();

                break;

            case R.id.high_score:

                startActivity(new Intent(this, HighScoreActivity.class));

                break;

            case R.id.exit:

                finish();

                break;

        }

    }

}

Membuat High Score Menu
Kita perlu membuat activity baru yaitu HighScoreActivity dan berikut adalah tampilannya:



Terdapat back button, judul, keterangan kalau belum pernah main dan juga keterangan jumlah score, meteor dan pesawat musuh yang dihancurkan ketika mendapatkan high score.
Untuk gambar back button bisa didownload di https://materialdesignicons.com/
Kode untuk tampilan HighScoreActivity:
<?xml version="1.0" encoding="utf-8"?>

  <RelativeLayout 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"

    tools:context=".HighScoreActivity">

  

    <ImageView

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:scaleType="centerCrop"

        android:src="@drawable/space_shooter_main_menu_background" />

  

    <ImageView

        android:layout_margin="16dp"

        android:id="@+id/back"

        android:src="@drawable/arrow_left_circle"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />

  

    <LinearLayout

        android:layout_marginLeft="32dp"

        android:layout_marginRight="32dp"

        android:layout_marginTop="80dp"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:gravity="center_horizontal"

        android:orientation="vertical">

        <TextView

            android:text="HIGH SCORE"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:textColor="@android:color/white"

            android:fontFamily="@font/iceberg_regular"

            android:textSize="32dp"/>

        <TextView

            android:id="@+id/null_high_score"

            android:layout_marginTop="16dp"

            android:text="Belum ada high score"

            android:textColor="@android:color/white"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content" />

        <LinearLayout

            android:id="@+id/high_score_container"

            android:layout_marginTop="16dp"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:orientation="vertical"

            android:background="@android:color/white"

            android:paddingLeft="48dp"

            android:paddingRight="48dp"

            android:paddingTop="16dp"

            android:paddingBottom="16dp"

            android:gravity="center_horizontal">

            <LinearLayout

                android:layout_width="wrap_content"

                android:layout_height="wrap_content"

                android:orientation="horizontal"

                android:layout_marginBottom="16dp">

                <TextView

                    android:textStyle="bold"

                    android:text="SCORE: "

                    android:layout_width="wrap_content"

                    android:layout_height="wrap_content" />

                <TextView

                    android:textStyle="bold"

                    android:id="@+id/score"

                    android:text="100"

                    android:layout_width="wrap_content"

                    android:layout_height="wrap_content" />

            </LinearLayout>

            <LinearLayout

                android:layout_width="wrap_content"

                android:layout_height="wrap_content"

                android:orientation="horizontal"

                android:gravity="center_vertical"

                android:layout_marginBottom="16dp">

                <ImageView

                    android:src="@drawable/meteor_1"

                    android:layout_width="48dp"

                    android:layout_height="wrap_content"

                    android:adjustViewBounds="true"

                    android:layout_marginRight="8dp"/>

                <TextView

                    android:textStyle="bold"

                    android:id="@+id/meteor"

                    android:text="x100"

                    android:layout_width="wrap_content"

                    android:layout_height="wrap_content" />

            </LinearLayout>

            <LinearLayout

                android:layout_width="wrap_content"

                android:layout_height="wrap_content"

                android:orientation="horizontal"

                android:gravity="center_vertical">

                <ImageView

                    android:src="@drawable/enemy_red_1"

                    android:layout_width="48dp"

                    android:layout_height="wrap_content"

                    android:adjustViewBounds="true"

                    android:layout_marginRight="8dp"/>

                <TextView

                    android:textStyle="bold"

                    android:id="@+id/enemy"

                    android:text="x100"

                    android:layout_width="wrap_content"

                    android:layout_height="wrap_content" />

            </LinearLayout>

        </LinearLayout>

    </LinearLayout>

  

</RelativeLayout>

Kode untuk HighScoreActivity
package com.example.satrioyudhoutomo.spaceshooter;

  

  import android.support.v7.app.AppCompatActivity;

  import android.os.Bundle;

  import android.view.View;

  import android.widget.ImageView;

  import android.widget.LinearLayout;

  import android.widget.TextView;

  

  public class HighScoreActivity extends AppCompatActivity implements View.OnClickListener {

  

    private ImageView mBack;

    private TextView mScore, mMeteor, mEnemy, mNullHighScore;

    private LinearLayout mHighScoreContainer;

  

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_high_score);

  

        mBack = findViewById(R.id.back);

        mScore = findViewById(R.id.score);

        mMeteor = findViewById(R.id.meteor);

        mEnemy = findViewById(R.id.enemy);

        mNullHighScore = findViewById(R.id.null_high_score);

        mHighScoreContainer = findViewById(R.id.high_score_container);

        mBack.setOnClickListener(this);

  

        loadHighScore();

    }

  

    void loadHighScore(){

        SharedPreferencesManager spm = new SharedPreferencesManager(this);

        if (spm.getHighScore()!=-1){

            mNullHighScore.setVisibility(TextView.GONE);

            mHighScoreContainer.setVisibility(LinearLayout.VISIBLE);

            mScore.setText(spm.getHighScore() + "");

            mMeteor.setText(spm.getMeteorDestroyed() + "");

            mEnemy.setText(spm.getEnemyDestroyed() + "");

        }else{

            mNullHighScore.setVisibility(TextView.VISIBLE);

            mHighScoreContainer.setVisibility(LinearLayout.GONE);

        }

    }

  

    @Override

    public void onClick(View v) {

        switch (v.getId()){

            case R.id.back:

                finish();

                break;

        }

    }

}

Nah sekarang kita akan Run Game Space Shooter-nya.
Pada saat dijalankan, game akan menampilkan sebuah Splash Screen yang berisi tentang nama permainan ini. Tampilannya seperti ini:



Kemudian setelah Splash Screen, game akan masuk ke halaman Main Menu. Di dalam Main Menu terdapat tombol button Play, High Score dan Exit. Pada bagian High Score ini berisi tentang high score yang sudah di mainkan. Tampilannya seperti ini :


Selanjutnya ketika menekan tombol play maka akan tampil menu game dan dapat langsung dimainkan. Pada bagian ini terdapat pesawat luar angkasa sebagai pemain, terdapat musuh seperti pesawat alien dan meteor untuk membuat pemain Game Over dan tidak lupa juga dengan adanya score yang dapat bertambah dengan menghancurkan pesawat alien dan meteor.