Bermula dari pertanyaan mahasiswa, ternyata setelah cukup lama saya mencari belum ada penjelasan yang menurut saya enak mengenai loop untuk animasi atau game berbasis canvas (game loop). Akhirnya saya coba sendiri menggunakan dasar contoh LunarLander yang merupakan bagian dari SDK samples dengan lumayan banyak modifikasi.
Game loop pada intinya adalah:
while (true) { updatePosisiObjek(); gambarObjek(); }
Contoh berikut merupakan implementasi loop untuk animasi sederhana.
Pada app ini terdapat tiga class, pertama GameRunnable yang berfungsi untuk mengatur gerakan dan menggambar di canvas. Kedua adalah GameView yang merupakan turunan dari SurfaceView yang menyediakan canvas yang akan digambar. GameView ini juga bertugas membuat dan mematikan thread (thread ini diisi GameRunnable). Terakhir adalah activity utama (MainActivity). Penjelasan lebih detil dapat dilihat di source codenya langsung.
Source code untuk class GameRunnable:
package edu.upi.cs.yudiwbs.animasicanvas2; import com.example.animasicanvas2.R; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.Log; import android.view.SurfaceHolder; //menggunakan runnable, karena hanya run yang dibutuhkan untuk dioverride //baca: http://manikandanmv.wordpress.com/tag/extends-thread-vs-implements-runnable/ //tentang bedanya extends thread vs runnable public class GameRunnable implements Runnable { //jika true, loop di run() berakhir boolean mRun=false; float posX=10; float posY=100; Bitmap bmp; private Paint cat = new Paint(); //akses ke surface dan canvas private SurfaceHolder mSurfaceHolder; //akses ke context (untuk ambil resource) Context mContext; private void updatePosisi() { //geser ke kiri dan kalau sudah max, kembali ke awal posX= posX+10; if (posX>200) { posX=10; } //tidur 0.2 detik, agar animasi tdk terlalu cepat try { Thread.sleep((long)(1000*0.2)); } catch (InterruptedException e) { e.printStackTrace(); Log.e("yw","error saat mencoba sleep"+e.getMessage()); } } //siapkan resources berupa bitmap, ambil bitmap icon launcher supaya mudah public GameRunnable(SurfaceHolder vSurfaceHolder, Context vContext) { mSurfaceHolder = vSurfaceHolder; mContext = vContext; Resources res = mContext.getResources(); bmp = BitmapFactory.decodeResource(res, R.drawable.ic_launcher); } //bersihkan layar dan gambar objek public void doDraw(Canvas c) { //canvas perlu dicek apakah null, karena pada saat //user pindah ke activity lain (user tekan home), c bisa //berisi null (proses shutdown belum selesai dan //thread masih jalan, tapi canvas sudah didestroy) if (c!=null) { //clear screen c.drawColor(Color.WHITE); //gambar bitmap c.drawBitmap(bmp,posX,posY,cat); } } @Override public void run() { //loop forever selama tidak dishutdown (mRun diset false) while (mRun) { Log.i("yw","looping...."); //hapus log ini kalau tidak mendebug Canvas c = null; try { c = mSurfaceHolder.lockCanvas(null); //lock surface agar tidak diakses thread lain dulu synchronized (mSurfaceHolder) { updatePosisi(); doDraw(c); } } finally { //pastikan diunlock if (c != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } } } //jika diset false maka akan shutdown (lihat method run()) public void setRunning(boolean b) { mRun = b; } }
Sedangkan code untuk class GameView adalah
package edu.upi.cs.yudiwbs.animasicanvas2; import android.content.Context; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; //surfaceView digunakan karena GUI akan seriung diupdate //dan cepat untuk animasi public class GameView extends SurfaceView implements SurfaceHolder.Callback { public Thread mThread; private GameRunnable gr; //buat thread dan jalankan public void startThread() { //objek GameRunnable jadi parameter mThread = new Thread(gr); gr.setRunning(true); mThread.start(); } public void shutDownThread() { Log.i("yw","shutdown thread mulai"); boolean retry = true; gr.setRunning(false); //matikan, //loop terus sampai thread berhenti karena //setRunning(false) tidak menghentikan seketika //coba lihat method GameThread.run() while (retry) { try { mThread.join(); retry = false; } catch (InterruptedException e) { } } Log.i("yw","shutdown selesai"); } public GameView(Context context) { super(context); SurfaceHolder holder = getHolder(); holder.addCallback(this); gr = new GameRunnable(holder,context); } @Override public void surfaceCreated(SurfaceHolder holder) { Log.i("yw","jalankan thread !!"); startThread(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { shutDownThread(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // bisa digunakan untuk memberi tahu GameRunnable ada perubahan dimensi // surface (misal dirotate) } }
Dan terakhir adalah MainActivity
package edu.upi.cs.yudiwbs.animasicanvas2; import com.example.animasicanvas2.R; import android.os.Bundle; import android.app.Activity; public class MainActivity extends Activity { GameView gv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); gv= new GameView(this); setContentView(gv); } @Override protected void onPause() { super.onPause(); //nantinya perlu diisi dengan penyimpanan state //melalui bundle } @Override protected void onResume() { super.onResume(); //nantinya perlu diisi dengan load state } }
Kode diatas masih memiliki kelemahan, perhatikan penggunaan delay 0.2 ms pada updatePosisi() di GameRunnable. Jika suatu saat app ini diinstall pada smartphone yang lambat dan memiliki ratusan objek yang harus diupdate posisinya maka animasi akan berjalan semakin lambat. Pada kasus sepeert itu seharusnya delay dikurangi. Posting berikutnya akan membahas cara untuk mengatasi hal ini.
Salah satu hal yang perlu ditangani adalah saat pengguna pindah ke app lain dan kembali. Misalnya dengan menekan home, pindah ke app lain lalu kembali ke app animasi ini. Setiap pengguna pindah ke app lain, maka thread harus distop agar tidak memakan resources. Setelah mencoba beberapa kali dan memperhatikan hasil log, saat home ditekan, maka canvas pada doDraw dapat bernilai null. Jadi saat thread masih berjalan canvas dapat bernilai null. Ini sebabnya perlu ada pengecekan kondisi if (c!=null) pada method GameRunnable.doDraw (baris 63). Pada app LunarLander yang asli, tidak ada pengecekan ini dan terjadi error saat user menekan home.
Hal yang lain, saat home ditekan, maka GameView.surfaceDestroyed pasti akan dipanggil. Tadinya saya mengira sistem tidak akan men-destroy kecuali kalau membutuhkan. Ini menyebabkan proses mematikan thread dilakukan bukan di activity.onPause tapi cukup di method surfaceDestroyed. Tentu dalam kondisi sebenarnya onPause dan onResume perlu ditangani untuk menyimpan state. Sebagai contoh, saat app ini dijalankan dan kemudian device dirotasi maka app akan mulai dari awal lagi (nilai GameRunnable.posX hilang).