如果要開發動態遊戲類別的 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; }
偶組有第一段看得懂,其他的...都牡颯颯XD~~
回覆刪除澆不熄的熱情...我完全能體會^-*
一個人如果有意願去做一件事情,那麼那件事剛開始不是那麼順利,只要慢慢來就會越來越順
如果意願加上喜歡,那麼除了事情順利外,有朝一日可能還會帶給自己一份意想不到的驚喜,如果熱情不澆熄的話
我和你是在不同領域上踏著漫長的學習階梯,雖然爬不到盡頭,但學而無涯,只要我們用心認真在學習,心靈上的富足相信是飽滿的
給你無限的祝福喔^_^~~~來自另一個柯瑞克特斯的祝福...呵呵
哈哈哈哈 ~ 牡颯颯 這段好笑 XDD
刪除這番鼓勵猶如一劑強心針 ~ 再度令人充滿鬥志
創作這條路最最最令人苦惱的就是 ---- 靈感 ---- 了吧
我目前最缺的就是靈感 T皿T
偏偏嚴謹的程式語言 與 天馬行空的創作思維是兩個極端不同的領域
或許還需要具備分裂人格的特質才能將這兩者完美結合吧 ^_^
能收到柯瑞克特斯的祝福 真是滿心歡喜 ^++++^
達仔真是貼心,兩則回覆都以超連結讓阿記咻一下就來到了這裡^_^ 你訴不訴怕偶年紀大了不認得路,走到別人家去~~呵呵
回覆刪除雖然創作大家都認為是天馬行空,但也要有一定的邏輯和可信度在裡頭,否則要以角色去抓住讀者的心緒強度可就很難了,阿記一直在努力中呢!
至於靈感...其實要自己多方面充實找柴火,我的領域是醬,不知你那讓偶牡颯颯的領域是否也如此?^_*
不過有時候想不出東西來的時候,就讓腦子休息休息,進進『補』,再度出發才會有更強的能量,我們一起加油喔~~~
由於這個部落格無法為匿名留言者提供 Email 回覆通知
刪除所以才會提供連結給阿記個方便 ^_^
不過阿記倒是給了我一個惡搞的好點子,下次回覆時我可能會附上一個會迷路的網址 XDD
文字創作的確有其限制,遊戲創作亦是,遊戲雖然可以無厘頭,但規則仍需定在 "人類" 能夠理解的範圍內。上圖書館是我找靈感的方法之一,可惜 ~ 每每越是刻意追尋,繆思女神卻似乎離得越遠 Q_Q
"進補" ~ 經您這麼一提醒,突然想起有本書一直忘了去借來看 : 『創意姚言』
達仔 你要不換蟑螂看看
回覆刪除我想很多人都很願意下載來測試看看的XDDD
這程式的功能太簡單了 !
刪除我不好意思上傳到 Google Play 去 (只會多增加一個垃圾 App 而已) XD
這樣會毀了我工作室的招牌,不妥 !
不過老哥還是非常感謝你的建議 ^_^
把圖換成你不爽的人的頭像 用力給他巴下去吧 ! XDDD
我之前就看過有人把一隻 紮小人的程式放上去 好像也是有人下載XDD
刪除這很正常 ~
刪除外面教 Android 補習班的萬年範例 --- 計算 BMI 值 都嘛塞爆 Google Play 了
Google Play 的自由市場機制就是來者不拒 ~
這種情況絕不可能發生在要收開發者年費 US$ 99 元的 Apple 或 微軟 身上,亂作的 Apps 光是審核那關就會被打回票 ! 這兩種市場機制各有利弊啦 ! 像我這種半調子的 ... 能沾沾 Google Play 的邊就很滿足了 ! XD
期待哪天達仔的APP會在第一名XDDD
刪除感謝你 ~ 先收下你的祝福囉 !
刪除志不在第一 僅希望能做出好玩有趣的遊戲
作者已經移除這則留言。
回覆刪除你好
回覆刪除我想請問在點擊物件時要怎麼才可以改變一個TextView內的文字呢
(我在主Activity分兩個layout,畫面新增在第一個layout,TextView在第二個)
嗯 ...
刪除僅由您的提問中還是無法得知問題的癥結所在
可否詳述 或 附上您的程式碼
您好
回覆刪除想請問是否可以在AndroidUnit中直接取得螢幕尺寸呢?
就是可以在任何的螢幕尺寸中直接使用,不必去修改Constant中的數值這樣
感謝您的回答>"<
請參考 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; //取得螢幕的寬度 (以像素單位為主)
其他詳細用法請自行參考官網說明‧
更正 :
刪除Fields 欄位裡頭的並非 "方法" 應稱為 "成員" 或 "參數" .... (某些名稱用中文表達時,容易亂掉 !@#$ ) XD