構想:

構想是源自一個簡單的桌遊;就是桌面上覆蓋許多牌(數量為偶數),遊戲開始前,桌面上的牌都是翻開的讓玩家記憶;開始時,將所有卡牌覆蓋,玩家每次開啟2張牌,如果兩張卡牌是一樣的代表成功,否則是失敗,失敗時必須將卡牌覆蓋回去重新選2張牌,當檯面上所有卡牌皆開啟時,遊戲結束。

版面設計:

基本版面配置

因為素材取得不易,所以卡片背面就以安卓內建的機器小人圖案代替。
xml程式碼
基本排版是使用LinearLayout(vertical)為最外層,內由三塊LinearLayout(horizontal)分割為三個主要部分。
第一部分:

第一部分內容為兩個Button,功能分別為開始(START)以及離開(EXIT)的按鈕。

第二部分:

第二部分內容只使用了一個TextView,功能為顯示花費時間。

第三部分:

第三部分比較複雜一點,是由四個LinearLayout(vertical)等分成四個區塊,然後每個區塊內再由兩個LinearLayout(horizontal)平分成兩個小區塊,總共八個區塊,每個區塊內都有一個ImageView,功能為玩家的翻牌區。

程式碼:

宣告

為了控制XML的原件,我們宣告了相對應的物件,不過因為ImageView的數量較多,我使用陣列方式儲存。

設定ImageViewID,因為宣告ImageView時使用了陣列型態,所以為了方便,將ImageView的ID(R.id.~)也存成陣列型態,以利之後呼叫使用。

FindViewByID的設定,這時候因為我們把ImageView存成陣列,所以可以使用一個for迴圈完成8次的FindViewByID設定。

設定卡牌的ID,我在電腦裡找了10張圖片匯入,setPictureID這個方法是將圖片的ID的ID(R.id.~)存成陣列型態,以利之後呼叫使用。

NumberOfUsingImage這個變數是來標記使用圖片的數量,目前因為有8張牌,所以會使用4張圖片,每張圖片會出現2次。
isStart是來判斷遊戲是否進行中,按下Start按鈕,isStart將會變成true表示遊戲開始進行。
pNumber是用來存取使用圖片的代號,是由10張圖片中隨機選出不重複的4張;而position是隨機配置圖片位置;然後reverse_position是position的反向(例如:position[0] = 7 >>> reverse_position[7] = 0)用來逆向回推該位置的圖片。
isOpen則是用來判斷8個ImageView是否配點開,如果卡牌全數開啟表示遊戲結束。

Handler以及Runnable是用來定期重複執行某些指令用的,通常使用在偵測,而在這裡被我們用來計算花費的時間。而countTime就是用來儲存時間用的。
isClick是用來記錄玩家是否有點選了卡牌,因為每次玩家需要選擇2張卡牌來比較是否一樣,而isClick讓我們清楚玩家目前的狀況。
memberClickImg是用來紀錄玩家所選的第一張卡牌(位置)的代號,用來檢查玩家選擇的第二張卡牌是否跟第一張一至。
隨機選取並設置圖片(方法)

第一部分:設定pNumber

pNumber是用來存取要使用圖片的陣列,所以宣告它的長度為NumberOfUsingImage,也就是4張圖的意思。
為了從10張圖片中隨機選取4張並且不重複;所以寫了這段程式碼。首先我使用了一個for迴圈(從0跑到3),用來設置圖片的代號;「pNumber[i] = (int)(Math.random()*10);」是隨機生成一個圖片代號放入pNumber陣列中,這個代號可以對到10張圖片中的其中一張;再來我們設定一個boolean值叫做isRe來記錄圖片是否重複;然後我們進入do-while迴圈,首先我們假設圖片代號沒有重複(isRe = false;),然後進入下一個for迴圈來檢視圖片是否真的沒有重複;這個for迴圈是把之前的圖片代號拿來跟目前的圖片代號做比對,比對完畢之後,如果重複isRe就會設定成true,當isRe為true時,目前的pNumber[i]就會重新設定(就是隨機再選一張),然後while條件吻合,就會再次回到do-while迴圈裡做比對,直到圖片不重複為止;若isRe維持為false的狀態,表示選取的圖片代號不重複,就會順利的完成設定,然後接著設定下一張,直到圖片全數設定完畢。

第二部分:設定position

position是用來存取「圖片設置位置」的陣列,我們有8個ImageView,由ImageGroup[]這個ImageView陣列來控制,而position的功能主要是紀錄圖片分別配置到ImageGroup中的位置;不過position的實作方式比較像是生成一個隨機編排且不重複的數字陣列,由於position與pNumber組合成的隨機配置是單向的(例如: position[0] = 5 , position[1] = 7這兩個位置可能都是對應到pNumber[0]這張圖片)所以我們必須創造另一個陣列是position的反向reverse_position,當使用者點選時,我們就可以透過reverse_position陣列紀錄的點擊位置逆向推回圖片的代號。
為了生成一個長度為8且內容為整數0到7且不重複的陣列。首先我使用了一個for迴圈(從0跑到7),用來設置陣列內的整數;「position [i] = (int)(Math.random()*8);」是隨機生成一個0到7的整數,放入position陣列中;再來我們設定一個boolean值叫做isRe來記錄position中的整數是否重複;然後我們進入do-while迴圈,首先我們假設position中的整數沒有重複(isRe = false;),然後進入下一個for迴圈來檢視position中的整數是否真的沒有重複;這個for迴圈是把之前的position中的整數取出跟目前的position中的整數做比對,比對完畢之後,如果重複isRe就會設定成true,當isRe為true時,目前的position [i]就會重新設定(就是隨機再選一個數字),然後while條件吻合,就會再次回到do-while迴圈裡做比對,直到數字不重複為止;若isRe維持為false的狀態,表示選取的整數不重複,就會順利的完成設定,然後接著設定下一position的值,直到所有數字設定完畢。

第三部分:設定reverse_position

由於reverse_position是position的反向,所以在position已經完成的情況下,我們只要把position拿過來使用,即可順利完成reverse_position。
首先我使用了一個for迴圈(從0跑到7),用來設置reverse_position陣列;因為reverse_position是position的反向,所以我們只要將position的值代入reverse_position中,就能設定好reverse_position的值了。

第四部分:設定圖片

將圖片依據先前設定好的順序擺入ImageView。
因為圖片有4張,但是ImageView有8格,所以每張圖片必須出現2次。所以我設定position[i2]與position[i2+1]設定為同一張圖片pNumber[i];
ImageGroup[position[i2]]是選取第position[i2]個ImageView,而position[i*2]的值是之前設定的亂數,所以等同隨機取一格ImageView設置圖片,但因為之前過濾過position,所以就算是隨機選擇,也不會重複選取到同一個ImageView做設定。
而.setImageResource則是設定圖片的語法,將R.Id.~的圖片放進ImageView;而圖片的ID(R.Id.~)就是PictureID[pNumber[i]];PictureID[pNumber[i]]是從所有圖片的陣列中選出pNumber[i]這張圖,而pNumber[i]就是之前隨機產生的4張圖中的其中一張,同樣的,因為特別過濾過,這4張圖片不重複。
初步畫面

因為起始畫面要給玩家記憶圖片位置,所以一開始圖片是打開的,我們必須把isOpen都設定為true。

OnClickListener點擊功能

第一部分: Start

按下Start表示開始遊戲。OnClickListener中最外層的if判斷式「if(!isStart)」是用來避免例外事件,當遊戲開始時不可再按開始,所以只有在遊戲還沒開始的情況下點擊才有效果;如果遊戲進行中,isStart狀態為true,點擊Start將不會有任何效果。
如果遊戲還沒開始,點擊Start表示遊戲將開始,首先isStart將變成狀態為true(isStart=true);再來將所有卡牌覆蓋,我們使用一個for迴圈將ImageGroup中的ImageView全部設置成安卓機器人的圖案來代表卡牌覆蓋,再來將isOpen的狀態全部改成false;然後我們生成handler及runnable來計時,handler.postDelayed(runnable,1000);表示每1000毫秒(1秒)執行一次,執行計時,也就是每秒countTime + 1,並且將countTime的值用setText顯示在CostTime上來達到計算花費時間並且顯示的效果。

第二部分: Exit

按下Exit表示離開遊戲。內容比較簡單,OnClickListener中只有finish();來結束程式。

第三部分: ImageGroup

這部分比較複雜,內容是在寫按下圖片後的所有動作。

首先,因為我們有8張牌,所以OnClickListener也要設定8次;所以我用了一個for迴圈(從0跑到7)一次設定完成,但是因為遊戲過程中,程式必須知道玩家選擇的卡牌位置,才能判斷玩家所選擇的是否正確,所以在OnClickListener中必須存放一個卡牌的位置代號,但是因為setOnClickListener中的OnClickListener為新生的類別,無法輸入區域變數;所以我們將OnClickListener改寫成可以接收一個變數i的類別叫做「SendIToOnClickListener」。

SendIToOnClickListener,使用implement實作View.OnClickListener的介面,我們新宣告一個整數「I」;然後新增一個建構式,接收一個整數存到I;再寫一個方法叫做getI()來取得我們輸入的數字I。
(基本上這個類別除了多傳一個整數之外,其他所有功能都與OnClickListener相同)

在OnClickListener中,第一個判斷式「if(!isOpen[getI()])」是用來判斷這張卡牌是否被翻開,getI()就是這張牌的位置代號;而如果這張牌相對應的isOpen[]是true表示這張牌已經翻開了,點擊已經打開的牌將不會有任何作用;如果這張牌相對應的isOpen[]是false就可以翻開這張牌。

當玩家點擊覆蓋卡牌的時候,將會開啟此卡牌;首先,必須判斷玩家是否在該回合已經開啟過卡牌isClick。

如果isClick為false,表示玩家在本回合尚未選取過卡牌,此時僅需將卡牌翻開並記錄位置。

因為每回合只選擇2張卡片來比較是否相同,所以當選擇了一張卡牌之後,isClick將轉換成true;然後這張卡牌的位置isOpen[]將會變成true表示目前此卡牌已開啟不得再次點選;再來memberClickImg將紀錄玩家所選的卡牌位置的代號,getI()就是卡牌位置的代號;最後就是將此張卡牌翻開,也就是設置這張牌相對應位置的圖片。
reverse_position[getI()]是逆向推得該圖片在”圖片代號陣列(pNumber)”中的索引代號,判斷式中寫了reverse_position[getI()]%2==0是指,若reverse_position[getI()]為偶數,我們只需要將它除以2即可得到索引代號,若為奇數,我們則需要將reverse_position[getI()]先減去1再除以2才可以取得索引代號;因為我們使用的圖片的數量為4張,但是圖片出現的數次為每張出現2次,又因為我們的編排方式,

使得position[2i]以及position[2i+1]為相同的圖片,所以我們可以由reverse_position[getI()]經過一些運算推得點選到的圖片在pNumber中的索引代號。
得到pNumber的索引代號後,設置圖片也變得簡單,只要將點擊到的圖片ImageGroup[getI()]使用.setImageResoure將圖片放入即可呈現翻開的效果。而該放入的圖片的ID(R.id.~)藉由PictureID[pNumber[索引代號]]得知。

如果isClick為true,表示玩家在本回合已選取過卡牌,此時必須將上一張選取的卡牌拿來與這次選取的卡牌做比對,如果不同,必須將兩張卡牌都覆蓋;若相同則顯示成功選擇,並且檢查遊戲是否達到結束條件。

因為每回合只選擇2張卡片來比較是否相同,所以當已經選擇過一張卡牌之後,第二次的點選,無論失敗或是成功,該回合都將結束,所以isClick將轉換成false回歸起始狀態。
而比較兩張卡牌時,我們必須知道兩張卡牌上的圖片是什麼;所以與設定第一次點選的卡牌時相同,我們必須藉由reverse_position逆向推得兩張圖片在pNumber中的索引代號,第一張就是reverse_position[memberClickImg],而第二張就是這次點選的reverse_position[getI()]。

首先我們先找出第二張點選的卡牌,將它翻開「ImageGroup[getI()].setImageResoure(PictureID[pNumber[索引代號]])」。畢竟無論玩家選對選錯都要讓他知道自己選了什麼。

之後做的事情都是為了判定兩張卡牌是否相同做的判斷,只差在判斷式reverse_position[memberClickImg]以及reverse_position[getI()]的奇偶不同,除此之外,基本上內容都是一樣的,所以我就拿兩方皆是偶數的狀況作舉例。
如果判斷式成立,表示玩家選的兩張卡牌上的圖案是一樣的,此時確定本回合成功!所以將點選的第二張卡牌isOpen的狀態改為true保持卡牌開啟;並使用Toast顯示「Success!」的短訊;然後使用isGameOver()方法檢查遊戲是否達到結束條件。

isGameOver()就是檢視遊戲是否達到結束條件的方法;首先計算isOpen狀態為true的數量,因為isOpen = true表示卡牌已經翻開,而當全數卡牌都翻開(count = 8)時表示遊戲結束,否則表示遊戲尚未結束,將繼續進行。
如果遊戲結束(count = 8),將會關閉計時功能;也就是把runnable關閉,再將handler以及runnable清空;然後使用AlertDialog跳出視窗顯示該局遊戲所花費的時間,並且結束遊戲。在彈跳視窗按下確定時,將會使用setBase()方法重啟新的一局,並將遊戲狀態改為尚未開始(isStart=false),然後把時間歸零。

如果判斷式不成立,表示玩家選的兩張卡牌上的圖案是不同的,此時確定本回合失敗。所以將點選的第一張卡牌isOpen的狀態改為false;然後使用使用AlertDialog跳出視窗顯示「失敗!請重新選擇」。在彈跳視窗按下確定時,將會把本回合選的兩張卡都蓋上。蓋上卡牌的做法就是把ImageGroup[memberClickImg]跟
ImageGroup [getI()]用.setImageResoure將兩張牌都設定回原本的安卓機器小人圖案(R.mipmap.ic_launcher)。
成品

結語:

本篇程式碼是我用最原始的想法,使用原生碼程式勾勒出來的小遊戲,因為沒有很用心排版以及思考如何優化程式,所以畫面很醜,程式碼也非常攏長,我想如果優化應該能在100行左右完成,而且效能可能更好!我想~能夠看完、看懂這篇文章的你(妳),優化這支程式對你(妳)而言應該也不是太大的問題了!加油吧!