2013-02-25

動態物件的產生與捕捉

有段時間沒有 PO 文了 ~ 漫長的學習過程常會有望不到終點的感覺,能持續支撐下去的原因大概就是那股澆不熄的熱情吧 ! 不知您是否與我一樣 ? ^_^


如果要開發動態遊戲類別的 Apps,勢必要運用不少動態圖片,雖然 ImageView 元件已經內建了一些特效,使用起來也很方便,但是 ~ 若程式於執行期間需在極短的時序中處理較多圖片時,在速度與互動的表現上可能會不如預期,因此還是建議您使用 SurfaceView 類別比較妥當。

這個範例主要結合了三個重要的技法 :
1. 執行緒的運用 (Thread)    
2. 自訂類別物件架構的建立 (class Object)
3. 觸碰事件方法的運用 (onTouchEvent)

程式功能介紹 :
執行時會產生 10 隻 Android 小綠人在畫面上四處亂晃,每一隻上方都附有一血條(血量值),每隻的移動速度可能會不一樣,而移動方向則有 8 個方位,小綠人碰到螢幕邊緣會自動改變方向,當使用者以手觸碰 Android 小綠人時 (於模擬器執行時則是以滑鼠游標配合點擊) Android 小綠人的血條則會縮短,當血量歸零時,該隻小綠人也會跟著消失,當所有 Android 小綠人都消失後,程式結束。(執行初期的畫面如下)

圖片使用系統預設的Android圖示

※雖然程式碼多已附上註解,不過筆者仍假設讀者已經具備 Java 執行緒與物件導向及 Android 相關程式設計基礎,其餘語法或指令請恕筆者不另做詳解。筆者設計功力仍屬拙劣,欠妥之處尚請先進們不吝指正。

程式碼如下 :

MainActivity.java (主 Activity)
public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
     
        //設定全螢幕顯示
        requestWindowFeature(Window.FEATURE_NO_TITLE);
                getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
      
               setContentView(new DrawBitmap(this));
    }
}


AndroidUnit.java (自訂類別物件架構)
//自訂類別物件 AndroidUnit -- (附加實作執行緒 Runnable 介面)
public class AndroidUnit implements Runnable {
   
    private boolean flag = true;          //控制執行緒開啟與關閉 (預設為開啟)
    private int x, y;                     //顯示物件的座標
    private int direction;                //前進的方向 (0 ~ 7)
    private int speed;                    //移動的速度(移動一步的距離) 
    private int step;                     //移動的步數
    private int maxHp = 100;              //最大血條值
    private int currentHp;                //目前的血條值 
    private int unit_Width, unit_Height;  //物件圖片的寬、高
    private Bitmap unit_bmp = null;       //代表該物件的圖片
    private Paint paint;                  //畫筆 (此參數在此僅用來繪製血條之用)

    //矩形框變數,與觸碰事件比對座標,看是否點在此物件圖片範圍內 
    Rect unit_rect = new Rect(); 
 
    //==== 建構子 ==== (參數 unit_bmp 為圖片來源) 
    public AndroidUnit(Bitmap unit_bmp){

        //指定圖片來源
        this.unit_bmp = unit_bmp;
        
        //此物件參數的初始設定
        UnitInitial();      
      
        //AndroidUnit 類別實作了 Runnable 介面
        //仍需以 Thread 類別來建立執行緒,如下 :
        new Thread(this).start();
        
        //將 AndroidUnit 類別本身(this)當做參數並實體化為執行緒
        //啟動 .start() 方法會自動執行 run() 函式內的程式內容
        //當 AndroidUnit 此類別 被實體化時,便會同時啟動此類別的執行緒
        //例如 : AndroidUnit au = new AndroidUnit(bmp);
    }
        
    //==== 此物件參數的初始設定 ====
    private void UnitInitial() {
        // TODO Auto-generated method stub

        //取得物件圖片的高、寬
        unit_Height = unit_bmp.getHeight(); 
        unit_Width = unit_bmp.getWidth(); 
      
        //血條值
        currentHp = maxHp;
      
        //以亂數決定此物件的初始座標
        x = (int)(Math.random() * (Constant.monitor_Width - unit_Width));
        y = (int)((Math.random() * (Constant.monitor_Height - unit_Height - 5)) + 5);
       
        //以亂數決定此物件的速度(speed)、前進的方向(direction)、步數(step)
        speed = (int)(Math.random() * 10 + 3);  //數值範圍 : 3 ~ 12 
      
        //產生新的步數(step) 與 移動方向(direction)
        StepAndDirection();
      
        //設定畫筆的參數 (paint 參數在此僅用來繪製血條之用)
        paint = new Paint();
        paint.setColor(Color.RED); //設定畫筆顏色
        paint.setStrokeWidth(3);     //畫筆的寬度
    }
    
    //==== 產生新的步數(step) 與 移動方向(direction) ====
    private void StepAndDirection() {

        //產生新的步數:數值範圍 : 5 ~ 15
        step = (int)(Math.random() * 11 + 5);
        
        //產生新的移動方向:數值範圍 : 0 ~ 7
        direction = (int)(Math.random() * 8);
    }
  
    //==== 將圖 PO 到 canvas(畫布)上 ====
    protected void PostUnit(Canvas canvas) {

        //在 canvas 上繪出物件本體
        canvas.drawBitmap(this.unit_bmp, x, y, null);
       
        //計算應繪出的血條值長度
        int hpWidth = (int)( ((float)currentHp/(float)maxHp) * (float)unit_Width );
        if (hpWidth <= 0) hpWidth = 0;
      
        //繪出血條 (血條繪於物件圖片的上方)
        canvas.drawLine(x, y - 5, x + hpWidth, y - 5, paint);
    }
   
    //==== 改變物件座標的運算函式 ====
    private void PositionChange() {

        //造成物件改變方向的兩個因素 :
        // 1. 步數用完
        // 2. 碰到邊界
        
        //判斷步數是否用完
        if (step <= 0) {
            //產生新的步數(step)與 移動方向(direction)
            StepAndDirection();
        }
        
        //按照移動的方向來改變物件的座標位置
        if (direction == 3 || direction == 4 || direction == 5){
            // y 值增加
            y += speed;
        }
        if (direction == 0 || direction == 1 || direction == 7){
            // y 值減少
            y -= speed;
        }
        if (direction == 1 || direction == 2 || direction == 3){
            // x 值增加
            x += speed;
        }
        if (direction == 5 || direction == 6 || direction == 7){
            // x 值減少
            x -= speed;
        }
      
        //判斷是否超出螢幕範圍(碰到邊界)
        //一旦碰到邊界就改變物件的移動方向,重新產生新的步數及方向
        if (x <= 0) {
            x = 0;
            StepAndDirection();  //重新產生步數與方向
        }
        if (x >= Constant.monitor_Width - unit_Width) {
            x = Constant.monitor_Width - unit_Width;
            StepAndDirection();
        }
        if (y <= 6) {
            //由於物件圖片的上方顯示血條
            //因此血條的高度必須納入圖片啟始 y 座標考量
            y = 6;
            StepAndDirection();
        }
        if (y >= Constant.monitor_Height - unit_Height) {
            y = Constant.monitor_Height - unit_Height;
            StepAndDirection();
        }
        
        //設定矩形框範圍,與觸碰事件比對是否觸碰到此物件範圍內
        unit_rect.set(x, y, x + unit_Width, y + unit_Height) ;
        step--;  //每執行此函式一次,步數就遞減一次
    }
   
    //==== 檢查是否被碰觸到 ====
    protected void IsTouch(int touch_x, int touch_y) {

        //將觸碰點的座標 touch_x 與 touch_y 傳入到
        //矩形框類別變數 unit_rect 的 contains(x, y) 方法中去判別
        //如果觸碰點的座標位於矩形框範圍內則contains(x, y)方法會傳回 true
        //否則傳回 false

        if (unit_rect.contains(touch_x, touch_y)) {
            //進行血條損傷計算
            currentHp -= 20;  //碰一次,血量減 20
            //進一步檢查血量值是否歸零
            if (currentHp <= 0) {  //血條值歸零,表示該物件生命結束
                flag = false;           //flag 設為 false,結束執行緒運作
            }
        }
    }
  
    //==== 檢視物件是否仍存在著(活著) ====
    protected boolean IsAlive() {
        return flag;
    }
  
    //==== 執行緒的工作內容 ====
    //不斷改變物件的座標值,如此才能讓物件在螢幕中四處遊走
    @Override
    public void run() {
        // TODO Auto-generated method stub
        
        while(flag) {
            PositionChange(); //改變物件座標的運算函式

            try {
                //暫停0.15秒,等同每隔 0.15 秒就進行一次改變物件座標的運算
                Thread.sleep(150);    
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}


DrawThread.java (繪製畫面的執行緒)
//繪製畫面的類別 DrawBitmap
//繼承了 SurfaceView 類別 及 實作了 SurfaceHolder.Callback 與
//Runnable 兩個介面
public class DrawBitmap extends SurfaceView
    implements SurfaceHolder.Callback, Runnable {

    private Resources res;
    private Bitmap bmp;
    private boolean flag = true;
    private Canvas canvas = null;
    private SurfaceHolder holder;
    private ArrayList<AndroidUnit> Au;  //AndroidUnit 類別型態的物件陣列
    private Thread db_thread;

    //==== 建構子 ==== (傳入的參數為 MainActivity 本身)
    public DrawBitmap(Context context) {
        super(context);
        // TODO Auto-generated constructor stub
        getHolder().addCallback(this);
        holder = getHolder();
      
        //指定圖片來源
        res = getResources();
        bmp = BitmapFactory.decodeResource(res, R.drawable.ic_launcher);
      
        //初始設定
        InitialSet();
       
        //建立執行緒
        db_thread = new Thread(this);
    }
   
    //==== 初始設定 ====
    private void InitialSet() {
        //建立 AndroidUnit 物件陣列實體 
        Au = new ArrayList<AndroidUnit>();
        Au.clear();  //先清除 Au 物件陣列
       
        //建立 AndroidUnit 物件 10 隻
        for(int i=0; i<10; i++) {
            //產生 AndroidUnit 實體 au
            AndroidUnit au = new AndroidUnit(bmp);
            //陸續將 au 放入 Au 物件陣列中
            Au.add(au);
        }
    }
 
    //==== 加入觸碰事件方法 ====
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO Auto-generated method stub
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            int x = (int)event.getX();
            int y = (int)event.getY();
           
            //巡覽 Au 物件陣列一遍,逐一比對是否碰觸到物件圖片
            for (AndroidUnit a: Au) {
                a.IsTouch(x, y);
            }
        }
        return true;
    }
  
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        // TODO Auto-generated method stub
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // TODO Auto-generated method stub
        db_thread.start();  //啟動執行緒
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // TODO Auto-generated method stub
    }

    //==== 此執行緒的工作內容 ====
    //逐一將物件陣列裡的物件貼至 canvas 上,並顯示至螢幕上
    //且每隔 0.05 秒更新畫面一次

    @Override
    public void run() {
        // TODO Auto-generated method stub

        while(flag){
            //當 Au 物件陣列沒有任何物件存在時,結束執行緒運作
            if (Au.isEmpty()) {
                flag = false;        //停止執行緒 
                System.exit(0);  //直接結束程式
            }
                  
            //將物件顯示到螢幕上
            try {
                //暫停 0.05 秒(每隔 0.05 秒更新畫面一次)
                Thread.sleep(50);
               
                //取得並鎖住畫布(canvas)
                canvas = holder.lockCanvas();

                //以黑色當背景 (清除畫面)
                canvas.drawColor(Color.BLACK);

                //巡覽 Au 物件陣列中的所有物件

                for (AndroidUnit a: Au) {
                    //若該物件還活著,則呼叫 AndroidUnit 物件的 PostUnit() 方法
                    //將物件圖片繪至 canvas 上 
                    if (a.IsAlive()) a.PostUnit(canvas);
                }
               
                //從 Au 物件陣列中移除已經停止活動的物件
                for (AndroidUnit b: Au) {
                    if (!b.IsAlive()) Au.remove(b);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            finally {
                if (canvas != null) {
                    //解鎖畫布(canvas)並顯示到螢幕上 
                    holder.unlockCanvasAndPost(canvas);
                }
            }
        } //while
    }
}


Constant.java (常數類別)
//只設定螢幕的寬、高規格
//如果要在其他不同規格的螢幕執行,只需更改此處即可
public class Constant {
    final static int monitor_Width = 480;
    final static int monitor_Height = 800;
}

16 則留言:

  1. 偶組有第一段看得懂,其他的...都牡颯颯XD~~
    澆不熄的熱情...我完全能體會^-*
    一個人如果有意願去做一件事情,那麼那件事剛開始不是那麼順利,只要慢慢來就會越來越順
    如果意願加上喜歡,那麼除了事情順利外,有朝一日可能還會帶給自己一份意想不到的驚喜,如果熱情不澆熄的話
    我和你是在不同領域上踏著漫長的學習階梯,雖然爬不到盡頭,但學而無涯,只要我們用心認真在學習,心靈上的富足相信是飽滿的
    給你無限的祝福喔^_^~~~來自另一個柯瑞克特斯的祝福...呵呵

    回覆刪除
    回覆
    1. 哈哈哈哈 ~ 牡颯颯 這段好笑 XDD

      這番鼓勵猶如一劑強心針 ~ 再度令人充滿鬥志
      創作這條路最最最令人苦惱的就是 ---- 靈感 ---- 了吧
      我目前最缺的就是靈感 T皿T
      偏偏嚴謹的程式語言 與 天馬行空的創作思維是兩個極端不同的領域
      或許還需要具備分裂人格的特質才能將這兩者完美結合吧 ^_^

      能收到柯瑞克特斯的祝福 真是滿心歡喜 ^++++^

      刪除
  2. 達仔真是貼心,兩則回覆都以超連結讓阿記咻一下就來到了這裡^_^ 你訴不訴怕偶年紀大了不認得路,走到別人家去~~呵呵
    雖然創作大家都認為是天馬行空,但也要有一定的邏輯和可信度在裡頭,否則要以角色去抓住讀者的心緒強度可就很難了,阿記一直在努力中呢!
    至於靈感...其實要自己多方面充實找柴火,我的領域是醬,不知你那讓偶牡颯颯的領域是否也如此?^_*
    不過有時候想不出東西來的時候,就讓腦子休息休息,進進『補』,再度出發才會有更強的能量,我們一起加油喔~~~

    回覆刪除
    回覆
    1. 由於這個部落格無法為匿名留言者提供 Email 回覆通知
      所以才會提供連結給阿記個方便 ^_^
      不過阿記倒是給了我一個惡搞的好點子,下次回覆時我可能會附上一個會迷路的網址 XDD
      文字創作的確有其限制,遊戲創作亦是,遊戲雖然可以無厘頭,但規則仍需定在 "人類" 能夠理解的範圍內。上圖書館是我找靈感的方法之一,可惜 ~ 每每越是刻意追尋,繆思女神卻似乎離得越遠 Q_Q
      "進補" ~ 經您這麼一提醒,突然想起有本書一直忘了去借來看 : 『創意姚言』

      刪除
  3. 達仔 你要不換蟑螂看看
    我想很多人都很願意下載來測試看看的XDDD

    回覆刪除
    回覆
    1. 這程式的功能太簡單了 !
      我不好意思上傳到 Google Play 去 (只會多增加一個垃圾 App 而已) XD
      這樣會毀了我工作室的招牌,不妥 !
      不過老哥還是非常感謝你的建議 ^_^
      把圖換成你不爽的人的頭像 用力給他巴下去吧 ! XDDD

      刪除
    2. 我之前就看過有人把一隻 紮小人的程式放上去 好像也是有人下載XDD

      刪除
    3. 這很正常 ~
      外面教 Android 補習班的萬年範例 --- 計算 BMI 值 都嘛塞爆 Google Play 了
      Google Play 的自由市場機制就是來者不拒 ~
      這種情況絕不可能發生在要收開發者年費 US$ 99 元的 Apple 或 微軟 身上,亂作的 Apps 光是審核那關就會被打回票 ! 這兩種市場機制各有利弊啦 ! 像我這種半調子的 ... 能沾沾 Google Play 的邊就很滿足了 ! XD

      刪除
    4. 期待哪天達仔的APP會在第一名XDDD

      刪除
    5. 感謝你 ~ 先收下你的祝福囉 !
      志不在第一 僅希望能做出好玩有趣的遊戲

      刪除
  4. 作者已經移除這則留言。

    回覆刪除
  5. 你好
    我想請問在點擊物件時要怎麼才可以改變一個TextView內的文字呢
    (我在主Activity分兩個layout,畫面新增在第一個layout,TextView在第二個)

    回覆刪除
    回覆
    1. 嗯 ...
      僅由您的提問中還是無法得知問題的癥結所在
      可否詳述 或 附上您的程式碼

      刪除
  6. 您好
    想請問是否可以在AndroidUnit中直接取得螢幕尺寸呢?
    就是可以在任何的螢幕尺寸中直接使用,不必去修改Constant中的數值這樣

    感謝您的回答>"<

    回覆刪除
    回覆
    1. 請參考 DisplayMtrics 類別的用法 : (官網如下)
      http://developer.android.com/reference/android/util/DisplayMetrics.html


      大致用法如下 : ( 2 個步驟 )

      1 . 先宣告並取得該物件 此即 --> metrics

      DisplayMetrics metrics = new DisplayMetrics();
      getWindowManager().getDefaultDisplay().getMetrics(metrics);


      2 . 之後 ~ 你可以用 Fields 欄位中的方法來取得你要的數據 :
      (Fields 欄位的各式方法請參考官網上的說明)

      例如 :

      int height = metrics.heightPixels; //取得螢幕的高度 (以像素單位為主)
      int width = metrics.widthPixels; //取得螢幕的寬度 (以像素單位為主)

      其他詳細用法請自行參考官網說明‧

      刪除
    2. 更正 :
      Fields 欄位裡頭的並非 "方法" 應稱為 "成員" 或 "參數" .... (某些名稱用中文表達時,容易亂掉 !@#$ ) XD

      刪除

搜尋此網誌