Halo teman-teman semuanya, sudah lama saya ngga update blog nih. Walaupun sudah lama ngga update blog, tapi saya tetap memantau blog ini kok. Dan dari sekian artikel yang sudah saya terbitkan, artikel tentang Buat Game Space Shooter dengan Android Studio menjadi artikel yang banyak dibaca dan banyak dikomentari. Oleh karena itu, saya memutuskan untuk mengembangkan aplikasinya dan tentunya menuliskan tutorialnya di blog ini.
Nah di tulisan kali ini, saya akan membagikan tutorial tentang membuat Main Menu dan Detail Record High Score. Main menu akan berisikan 3 button yaitu Play, High Score dan Exit. Sedangkan detail record high score itu artinya tidak hanya score yang disimpan, melainkan juga jumlah meteor dan pesawat yang berhasil dihancurkan.
Membuat Main Menu
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.andevindo.spaceshooter; import android.content.Intent; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; 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: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.andevindo.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); 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; } } }
Update Kode Sebelumnya
Karena kita ingin menambahkan data jumlah meteor dan enemy yang dihancurkan, maka ada perubahan sedikit di kodenya.
SharedPreferencesManager
package com.andevindo.spaceshooter; import android.content.Context; import android.content.SharedPreferences; /** * Created on : 8/12/2017 * Developed by : Hendrawan Adi Wijaya * Github : https://github.com/andevindo * Website : http://www.andevindo.com */ 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); } }
Dari kode di atas, ketika menyimpan highScore, juga sekalian menyimpan data jumlah meteor dan enemy.
GameView
package com.andevindo.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; /** * Created on : 8/11/2017 * Developed by : Hendrawan Adi Wijaya * Github : https://github.com/andevindo * Website : http://www.andevindo.com */ 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); } }
Beberapa kode baru dari kode di atas adalah:
- 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.
Enemy
... public void hit(){ if (--mHP ==0){ SCORE += 50; ENEMY_DESTROYED++; destroy(); }else{ mSoundPlayer.playExplode(); } } ...
Tambahkan kode ENEMY_DESTROYED++; pada class Enemy.
Meteor
... public void hit(){ if (--mHP ==0){ SCORE += 20; METEOR_DESTROYED++; destroy(); }else{ mSoundPlayer.playExplode(); } } ...
Tambahkan kode METEOR_DESTROYED++; pada class Meteor.
Setelah itu jangan lupa pindahkan intent filter Launcher dari MainActivity ke MainMenuActivity agar MainMenuActivity dipanggil pertama kali saat aplikasi dibuka. Dan juga tambahkan screenOrientation=”portrait” untuk semua activity agar semua activity tidak berubah orientasinya dan tetap portrait.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.andevindo.spaceshooter"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:theme="@style/AppTheme.NoActionBar" android:screenOrientation="portrait"></activity> <activity android:name=".MainMenuActivity" android:label="@string/title_activity_main_menu" android:theme="@style/AppTheme.NoActionBar" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".HighScoreActivity" android:theme="@style/AppTheme.NoActionBar" android:screenOrientation="portrait"></activity> </application> </manifest>
Itulah beberapa update dari game Space Shooter ini. Kode terbaru dari tutorial ini bisa dilihat di https://github.com/andevindo/space-shooter-as/tree/v1.1.0