模擬一個 CandyBox (糖果盒),裡面放了 100 顆糖果,然後由 13 個 kid (小孩) 隨意抓取,每次抓取的糖果數量為 5 ~ 10 顆不等 (以亂數決定),每個小孩抓取的次數不限,直到糖果盒裡面的糖果被取完為止,最後再以文字的方式將各個數據結果呈現在螢幕上。
◎此範例主要是模擬多個執行緒共同取用一個固定值的程式架構示範,並沒有穿插精彩的圖片動畫作為輔助說明。
程式說明 :
程式主要分為四個部份 :
1. MainActivity : 主要的 Activity (Activity 類別)
2. CandyBox : 糖果盒類別 (自訂類別)
3. Kid : 小孩類別 (自訂類別並實作 Runnable 介面)
4. ExecResult : 負責處理整個事件運作的核心類別 (View類別)
在 MainActivity 中僅簡單地設定了全螢幕顯示模式,而負責呈現畫面內容的 setContentView() 函式中則將 ExecResult 類別物件導入。其他部份則維持原始設定。
程式碼如下 :
MainActivity.java
package a.b.c.testsync;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.Window;
import android.view.WindowManager;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設定全螢幕
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(new ExecResult(this));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
CandyBox 類別基本上算是一個靜態類別,此類別並不會被拿來生成物件 (object),所以不會在其他程序中看到類似 CandyBox candyBox = new CandyBox(); 的敘述發生。
此類別的構造非常簡單,兩個 static 資料欄位 (candy 與 catchCount) 和一個 static 函式 (函式名稱 : takeout),candy 與 catchCount 分別記錄糖果剩餘數量與抓取次數的總合。
注意到 static 函式 takeout 使用了 synchronized (同步) 修飾詞,當函式宣告為 synchronized 後,於程式執行期間,若有一執行緒呼叫此函式,在該函式未完成此執行緒交付的工作之前,其他執行緒無法再進入執行此函式。
您可以想成這個糖果盒設定了一道規則,就是一次只能允許一個小孩來抓取裡面的糖果;而這個 takeout 函式就是糖果盒( CandyBox 類別 )所設定的規則,同一時間只能允許一個 kid 執行緒來抓取糖果。
在現實的世界中即使所有的小孩在同一時間中搶成一團,不管各自搶到多少糖果,在糖果還未被吃掉之前,糖果的總合並不會有所改變。但反觀在多執行緒程式運作的世界裡,若沒有針對數值資料作保護限制,最後呈現的結果將會出乎意料、錯誤百出。
◎synchronized 同步敘述還有其他用法,這裡僅示範以函式為施行對象。
程式碼如下 :
CandyBox.java
package a.b.c.testsync; public class CandyBox { static int candy; static int catchCount; //靜態(類別)初始函式 static { candy = 100; //預設有 100 顆糖果 catchCount = 0; //記錄糖果被抓取的次數總合 } //糖果抓取事件 protected synchronized static int takeout(int count) { //candy:糖果目前剩餘的數量 count:此次要取出的糖果數量 //當糖果的剩餘數量不足以應付要取出的數量時 if (candy <= count) { //剩多少就給多少 count = candy; candy = 0; } else { //扣除被取走的糖果 candy -= count; } catchCount++; //記錄糖果被抓取的次數總合 return count; //傳回該次糖果被抓取時所取出的數量 } }
Kid 類別的結構也很簡單,由於我們只需要這個 kid (小孩) 去做抓取糖果的動作,沒有要他們互相打架,所以不會發武器給他們 XD。
此類別設置了一個口袋 pocket 用來放置糖果,count 是這個小孩一次想抓取的糖果數量,因為此類別必須 "動起來" 所以我實作了 Runnable 介面,只要一生成此類別物件,其執行緒也會跟著啟動。
執行緒的工作內容 : public void run() { //工作內容 ........ }
小孩 (kid) 會先去檢查糖果盒 (CandyBox) 裡面是否還有糖果,接著以亂數值來模擬每次抓取一把糖果的數量 count,再透過呼叫 CandyBox.takeout(int count) 函式確定實際取得的糖果數量,最後再放入口袋 (pocket) 中。
在糖果還未被搶光以前,每個小孩都可能還有第二次 (甚至第三次) 機會由糖果盒中抓取糖果,程式中雖然設定執行緒每次抓取糖果後會稍停 0.2 秒空檔 -- Thread.sleep(200) 以便讓其它小孩也有空檔可以抓取糖果,但仍無法保證每個小孩都可以搶到糖果。
簡略來說,小孩 (kid) 的工作內容就是去糖果盒 (CandyBox) 抓取 (takeout) 糖果 (candy) 並放入口袋 (pocket),當小孩發現 CandyBox 裡的糖果 (candy <= 0) 都已經沒有的時候,該小孩才會停止動作 (即執行緒停止)。
最後我們可以藉由 checkpocket 函式來檢查這個小孩共拿了多少糖果。
程式碼如下 :
Kid.java
package a.b.c.testsync; public class Kid implements Runnable { private int pocket = 0; //口袋 private boolean flag = true; private int count = 0; //抓取一把糖果的數量 //建構函式 public Kid() { new Thread(this).start(); } //檢查口袋 protected int checkpocket() { return pocket; //傳回口袋裡的糖果數量 } @Override public void run() { // TODO Auto-generated method stub while(flag){ if (CandyBox.candy <= 0) { count = 0; flag = false; } else { count = (int)(Math.random()*6)+5; //count 亂數值介於 5 ~ 10 } int temp = 0; temp = CandyBox.takeout(count); //從CandyBox中 隨機抓取 5 ~ 10 顆糖果 pocket += temp; //把抓取到的糖果放入口袋 try { Thread.sleep(200); //延遲 0.2 秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
最後一個類別 ExecResult,它繼承了 View 類別,其主要工作內容如下 :
1. 生成 kid 物件
2. 隨時檢查 CandyBox 裡的 candy 是否已經歸零
3. 負責處理各項數據的畫面佈局
4. 加入螢幕觸碰函式 (這裡的作用是碰觸螢幕後就直接結束程式)
程式中宣告了一個物件陣列 ArrayList,用 ArrayList 來存放所產生的 kid 物件,以便後續的一些數據操作,程式中設定產生 13 個 kid 物件。
當這些 kid 物件被產生之後,每一個 kid 物件隨即啟動自身的執行緒進行指定的工作,kid 的工作細節請回顧上方 Kid 類別的內容。
在生成 13 個 kid 物件並放入 kid 物件陣列後,接著會執行 checkResult() 函式,這個函式裡面宣告了一個 check 執行緒,並在函式內啟動,check 執行緒會不斷地去檢查 CandyBox 裡的 candy 數量,一旦確定 candy 為零時,就會執行 invalidate() 函式,而這個 invalidate() 函式會自動去呼叫 onDraw() 函式,onDraw() 函式會依照我們所指定的內容將數據結果顯示到畫面上。
程式碼如下 :
ExecResult.java
package a.b.c.testsync; import java.util.ArrayList; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.view.MotionEvent; import android.view.View; public class ExecResult extends View { ArrayList<Kid> kid; Paint paint; boolean flag; //執行緒 check 旗標 //建構函式 public ExecResult(Context context) { super(context); // TODO Auto-generated constructor stub paint = new Paint(); //產生畫筆物件 kid = new ArrayList<Kid>(); //產生 Kid 物件陣列 initialSet(); //初始設定 } //初始設定函式 private void initialSet() { CandyBox.candy = 100; //設定糖果盒有 100 顆糖果 CandyBox.catchCount = 0; //清除抓取總合 //設定畫筆 paint.setColor(Color.WHITE); paint.setTextSize(30); paint.setStrokeWidth(2); paint.setAntiAlias(true); //checkResult 函式裡的 check 執行緒旗標 flag = true; kid.clear(); //先清除 kid 物件陣列 //產生 13 個 Kid 執行緒 for (int i=0; i<13; i++) { kid.add(new Kid()); } //檢查結果函式(呼叫後將同時啟動其內部的 check 執行緒) checkResult(); } protected void checkResult() { //宣告在函式內部的執行緒及工作內容 Runnable check = new Runnable() { //執行緒 check 的工作內容 @Override public void run() { // TODO Auto-generated method stub while(flag) { //檢查 CandyBox 內的 candy 數量是否歸零 if(CandyBox.candy <= 0) { flag = false; //設定 flag 為 false 準備結束執行緒 invalidate(); //invalidate()會執行 onDraw()函式,繪出結果 } } } }; //上方寫好了執行緒的工作內容之後 //接著把 check 套入 Thread 類別並將它啟動 Thread p; p = new Thread(check); p.start(); //啟動 check 執行緒 //如果於執行期間造成緒程方面的錯誤而導致程式被迫中斷 //請使用 join(),等待此 check 執行緒結束,即可解決此情形 try { p.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); canvas.drawColor(Color.BLACK); //清除畫面 canvas.drawText("糖果盒裡面共有 100 顆糖果", 10, 30, paint); //當所有的糖果都被取光時 印出結果 if (done){ int num = 0; int sum = 0; for (Kid k : kid) { num++; sum += k.checkpocket(); canvas.drawText("第 " + num + " 號 kid 拿了 " + k.checkpocket() + " 個糖果", 10, 60 + 30 * num, paint); } canvas.drawText("糖果盒剩 : " + CandyBox.candy + " 顆 ", 10, 30 + 30 * (num + 3, paint); canvas.drawText("共發生 " + CandyBox.catchCount + " 次抓取事件", 10, 30 + 30 * (num + 4), paint); canvas.drawText("所有 Kid 的糖果總合 : " + sum , 10, 30 + 30 * (num + 5), paint); } } }
執行結果 :
好複雜的程式,看了眼花。這跟FB的糖果遊戲是一樣的嗎?我玩這個遊戲很遜,應該是所有遊戲是都一樣
回覆刪除希維亞
ㄟ嘿 ~
刪除妳怎跑到這裡留言了 ~.~
這只是在描述某個語法的實際運用 .... 與 FB 的 Candy Crush 完全無關 XD
最近 Candy Crush 很熱門 ! 不曉得妳到第幾關了 ? XDDD
達仔, 您好。難得看到台灣有人這麼熱心寫這麼詳細的文章介紹Android開發。感謝!
回覆刪除Android應該會是最普及的平台。加油!