Android Studio做超好玩的拼圖游戲 附送詳細(xì)注釋源碼
一、項(xiàng)目概述
之前有不少粉絲私信我說,能不能用Android原生的語言開發(fā)一款在手機(jī)上運(yùn)行的游戲呢?
說實(shí)話,使用java語言直接開發(fā)游戲這個需求有點(diǎn)難,因?yàn)橐恍┍容^復(fù)雜的游戲都是通過cocos2D或者Unity3D等游戲引擎開發(fā)出來的,然后再移植到Android手機(jī)當(dāng)中,使用完整的游戲引擎開發(fā)的過程比較簡單,而且界面比較流暢,觀感和體驗(yàn)度都很好。
所以直接使用java開發(fā)的游戲并不多。當(dāng)然,雖說不多但也有。簡單些的比如:2048、拼圖游戲、貪吃蛇、推箱子等,復(fù)雜點(diǎn)的比如:斗地主,這些都可以用java語言開發(fā)。因?yàn)檫@些游戲刷新界面次數(shù)比較少,是可以用java開發(fā)出來的。
所以在這篇博客里面,我們就來開發(fā)一款簡單的拼圖游戲,這款拼圖游戲就和我們小時候玩的游戲是一樣的,這里面的涉及到的算法不多,可以很容易學(xué)會,是作為入門Android的一個非常好的實(shí)例。
二、開發(fā)環(huán)境
三、需求分析
我們先來看下最終要實(shí)現(xiàn)的效果:
可以看到游戲開始后,開始計(jì)時,然后下面是被打亂的九宮格圖片,最后一塊是空白的,因?yàn)橐舫隹臻g移動,中間是重新開始按鈕,點(diǎn)擊就會重新計(jì)時而且拼圖碎片重新打亂,最底下是原圖,方便大家對照著進(jìn)行拼湊。當(dāng)你拼圖完成后,上面的第九塊拼圖會立刻顯示出來補(bǔ)齊整張圖片,然后彈出對話框,告訴你拼圖成功,用時為多少多少秒,點(diǎn)擊確認(rèn)即可。
所以我們分為六個步驟來實(shí)現(xiàn):
- 拼圖游戲布局繪制
- 拼圖游戲時間計(jì)時
- 拼圖游戲打亂顯示
- 拼圖游戲碎片位置切換
- 拼圖游戲成功的條件
- 拼圖游戲重新開始
我們來看下需要準(zhǔn)備的圖片素材:
這里先是一張小熊的樣圖,命名就是yangtu。然后就是將它按九宮格裁剪成的九張圖片,命名格式我來解釋下:我們看第八張我選中的圖片,它的名字為img_xiaoxiong_02x01。這里解釋下為什么是02x01,這就可以看做一個三行三列的二維數(shù)組,排列方式就和下面一樣。數(shù)組行和列下標(biāo)都是從0開始,所以第八張就是在第2行第1列,所以就是02x01,其他的也以此類推。
大家可以自己選圖片進(jìn)行裁剪命名,當(dāng)然也可以直接下載我的源碼,里面就有這些圖片。
下面我們就一起來實(shí)現(xiàn)這個拼圖游戲吧~
四、實(shí)現(xiàn)過程
1、拼圖游戲布局繪制
我們首先來分析下游戲的layout布局
再來看下最終實(shí)現(xiàn)的效果圖,先分析一下怎么繪制布局,實(shí)現(xiàn)一個項(xiàng)目的第一步是將布局按照自己期望的樣子完成。
因?yàn)檫@是一個上下結(jié)構(gòu),所以我們用一個線性布局(LinearLayout)來實(shí)現(xiàn)最合適,方向(orientation)設(shè)置為豎直方向(vertical)??梢钥吹竭@個拼圖分為三行三列,所以我們直接將每一行分為一個小的LinearLayout,一共三個,然后在每個小的LinearLayout里面水平放三個圖片按鈕,這樣就實(shí)現(xiàn)了,思路有了,我們來繪制吧。
我們來繪制游戲的layout布局
從上至下的第一個布局是顯示時間的TextView,我們將它的id設(shè)置為pt_tv_time,layout_width和layout_height都設(shè)置為wrap_content,就是適應(yīng)內(nèi)容大小,然后text文本內(nèi)容設(shè)為“時間:0”,這個是方便測試寫上文本的,因?yàn)檫厡懘a可以邊看旁邊的效果變化。
然后layout_gravity設(shè)置為"center",就是設(shè)置自己在父容器(頂層的LinearLayout)中居中,這里補(bǔ)充下知識點(diǎn):
- gravity是設(shè)置自身內(nèi)部元素的對齊方式。比如一個TextView,則是設(shè)置內(nèi)部文字的對齊方式。如果是ViewGroup組件如LinearLayout的話,則為設(shè)置它內(nèi)部view組件的對齊方式。
- layout_gravity是設(shè)置自身相當(dāng)于父容器的對齊方式。比如,一個TextView設(shè)置layout_gravity屬性,則表示這TextView相對于父容器的對齊方式。
再來改變下字體大小,設(shè)置textSize為20sp,sp是像素,補(bǔ)充下單位的知識點(diǎn):
- dp: device independent pixels(設(shè)備獨(dú)立像素),不同設(shè)備有不同的顯示效果,和設(shè)備硬件有關(guān)。
- px: pixels(像素).,不同設(shè)備顯示效果相同,這個用的比較多。
- pt: point,是一個標(biāo)準(zhǔn)的長度單位,1pt=1/72英寸,用于印刷業(yè),非常簡單易用。
- sp: scaled pixels(放大像素),主要用于字體顯示best for textsize。
最后設(shè)置字體顏色為#FF0000,即紅色。一般是通過colors.xml資源來引用,這里因?yàn)榧t色比較好表示就直接設(shè)置了。
TextView代碼如下:
<TextView android:id="@+id/pt_tv_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="時間 : 0" android:layout_gravity="center" android:textSize="20sp" android:textColor="#FF0000"/>
設(shè)置完成后,我們來看下效果圖:
接著我們來繪制九宮格拼圖,先設(shè)置第一行這三個小圖片的外布局,依然是LinearLayout,設(shè)置它的id="@+id/pt_line1",就表示第一行。
orientation選擇的是水平方向,因?yàn)槊恳恍惺撬椒胖玫?,layout_gravity設(shè)置為"center",表示居中,代碼如下。
<LinearLayout android:id="@+id/pt_line1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> </LinearLayout>
設(shè)置第一張圖片,選擇的控件是ImageButton,顧名思義:圖片按鈕,正常按鈕就規(guī)規(guī)矩矩的,而圖片按鈕就很好看,一張圖片也可以進(jìn)行點(diǎn)擊,這里設(shè)置它的id="@+id/pt_ib_00x00",方便在MainActivity里面調(diào)用。
00x00不用我多說了吧,上面解釋過了,將九宮格看成3X3的二維數(shù)組,那么行列下標(biāo)就是0行0列,這里每行數(shù)和列數(shù)都用2位數(shù)字表示而已。
設(shè)置src="@mipmap/img_xiaoxiong_00x00",就是將我們剛剛準(zhǔn)備的圖片資源復(fù)制到這個mipmap文件夾中進(jìn)行引用,每個id編號和圖片的名稱是對應(yīng)的。
再設(shè)置個onClick方法,方法名為"onClick",我們后面會在MainActivity里面進(jìn)行編寫點(diǎn)擊事件。第一張圖片的代碼如下:
<ImageButton android:id="@+id/pt_ib_00x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x00" android:padding="0dp" android:onClick="onClick"/>
依次類推,第二張和第三張圖片,我只要改下id和src就可以了,所以直接放上第一個小LinearLayout的代碼:
<LinearLayout android:id="@+id/pt_line1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <ImageButton android:id="@+id/pt_ib_00x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x00" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_00x01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x01" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_00x02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x02" android:padding="0dp" android:onClick="onClick"/> </LinearLayout>
來看下顯示效果:
那第二行和第三行是不是也一樣照葫蘆畫瓢,沒錯,直接復(fù)制第一行的代碼,然后修改id和src就行。這里直接給出三個LinearLayout的代碼:
<LinearLayout android:id="@+id/pt_line1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <ImageButton android:id="@+id/pt_ib_00x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x00" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_00x01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x01" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_00x02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_00x02" android:padding="0dp" android:onClick="onClick"/> </LinearLayout> <LinearLayout android:id="@+id/pt_line2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <ImageButton android:id="@+id/pt_ib_01x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_01x00" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_01x01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_01x01" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_01x02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_01x02" android:padding="0dp" android:onClick="onClick"/> </LinearLayout> <LinearLayout android:id="@+id/pt_line3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <ImageButton android:id="@+id/pt_ib_02x00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_02x00" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_02x01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_02x01" android:padding="0dp" android:onClick="onClick"/> <ImageButton android:id="@+id/pt_ib_02x02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/img_xiaoxiong_02x02" android:padding="0dp" android:onClick="onClick" android:visibility="invisible"/> </LinearLayout>
有一點(diǎn)需要注意的,不知道有沒有同學(xué)發(fā)現(xiàn)——第三行的第三張圖片,也就是右下角的那張圖片,它有個屬性,其他的圖片都沒有:visibility=“invisible”,這是干什么的呢?
這個其實(shí)就是設(shè)置控件是否可見,默認(rèn)情況下控件都是可見的(visible),只有設(shè)置visibility="invisible"后,這個控件才不顯示出來,我們來看下整體效果:
OK,九宮格完成后,下面是一個重新開始的Button。
這個比較簡單了,主要設(shè)置了onClick=“restart”,這個后面會在MainActivity里面編寫重新開始游戲的邏輯,還設(shè)置了android:layout_marginTop=“20dp”,這是設(shè)置此控件與上面控件邊距相隔20dp,為了和九宮格保持一定間距,代碼如下:
<Button android:id="@+id/pt_btn_restart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="restart" android:layout_gravity="center" android:text="重新開始" android:layout_marginTop="20dp"/>
顯示效果:
最后就是我們的樣圖了,有了我們上面的經(jīng)驗(yàn),這個應(yīng)該很容易就畫出來了,放置圖片的控件我們一般使用ImageView,然后設(shè)置src="@mipmap/yangtu",就顯示了我們的樣圖,最后為了保持距離美,設(shè)置layout_marginTop=“20dp”,代碼如下:
<ImageView android:id="@+id/pt_iv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:src="@mipmap/yangtu" android:layout_marginTop="20dp"/>
好了,我們來看下效果圖:
至此,我們的布局就繪制完成了!
我們來編寫下MainActivity的基本框架
可以先來看下什么都沒有的MainActivity。里面只有onClick()和restart()兩個新的方法,這是在上面布局中設(shè)置的方法,onClick是圖片按鈕的點(diǎn)擊事件,restart是重新開始按鈕的點(diǎn)擊事件,這兩個方法的具體實(shí)現(xiàn)邏輯會在下面講到。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 設(shè)置要顯示的視圖 setContentView(R.layout.activity_main); } // 圖片按鈕的點(diǎn)擊事件 public void onClick(View view) { } /* 重新開始按鈕的點(diǎn)擊事件*/ public void restart(View view) { } }
這里我們要做的是把所有在布局中用到的控件定義好,然后初始化這些控件
先來定義九個圖片按鈕,命名方法也是00,01這樣的橫縱坐標(biāo),一個重啟按鈕和一個顯示時間的文本框
// 定義九個圖片按鈕,命名方法也是00,01這樣的橫縱坐標(biāo) ImageButton ib00,ib01,ib02,ib10,ib11,ib12,ib20,ib21,ib22; // 一個重啟按鈕 Button restartBtn; // 一個顯示時間的文本框 TextView timeTv;
然后我們在onCreate中定義一個initView()方法,這個方法是用來初始化控件的
// 初始化layout控件的方法 initView();
然后創(chuàng)建該方法,在該方法里面初始化定義的控件,通過findViewById()進(jìn)行綁定控件,將聲明的變量和layout中對應(yīng)的控件進(jìn)行綁定,實(shí)現(xiàn)引用的效果,代碼如下:
/* 初始化控件:綁定9個圖片按鈕,1個顯示時間的文本框,1個重啟按鈕*/ private void initView() { ib00 = findViewById(R.id.pt_ib_00x00); ib01 = findViewById(R.id.pt_ib_00x01); ib02 = findViewById(R.id.pt_ib_00x02); ib10 = findViewById(R.id.pt_ib_01x00); ib11 = findViewById(R.id.pt_ib_01x01); ib12 = findViewById(R.id.pt_ib_01x02); ib20 = findViewById(R.id.pt_ib_02x00); ib21 = findViewById(R.id.pt_ib_02x01); ib22 = findViewById(R.id.pt_ib_02x02); timeTv = findViewById(R.id.pt_tv_time); restartBtn = findViewById(R.id.pt_btn_restart); }
初始化的完整代碼,可以作為模板:
public class MainActivity extends AppCompatActivity { // 定義九個圖片按鈕,命名方法也是00,01這樣的橫縱坐標(biāo) ImageButton ib00,ib01,ib02,ib10,ib11,ib12,ib20,ib21,ib22; // 一個重啟按鈕 Button restartBtn; // 一個顯示時間的文本框 TextView timeTv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 設(shè)置要顯示的視圖 setContentView(R.layout.activity_main); initView(); } private void initView() { ib00 = findViewById(R.id.pt_ib_00x00); ib01 = findViewById(R.id.pt_ib_00x01); ib02 = findViewById(R.id.pt_ib_00x02); ib10 = findViewById(R.id.pt_ib_01x00); ib11 = findViewById(R.id.pt_ib_01x01); ib12 = findViewById(R.id.pt_ib_01x02); ib20 = findViewById(R.id.pt_ib_02x00); ib21 = findViewById(R.id.pt_ib_02x01); ib22 = findViewById(R.id.pt_ib_02x02); timeTv = findViewById(R.id.pt_tv_time); restartBtn = findViewById(R.id.pt_btn_restart); } // 圖片按鈕的點(diǎn)擊事件 public void onClick(View view) { } /* 重新開始按鈕的點(diǎn)擊事件*/ public void restart(View view) { } }
2、拼圖游戲時間計(jì)時
完成基本工作后,我們思考下——如何實(shí)現(xiàn)時間的計(jì)時操作,這就相當(dāng)于計(jì)時器的功能。這里我們可以用Handler消息機(jī)制來實(shí)現(xiàn),補(bǔ)充下知識點(diǎn):
- Handler:作用就是發(fā)送與處理信息
- Message:Handler接收與處理的消息對象
當(dāng)我們的子線程想修改Activity中的UI組件時,我們可以新建一個Handler對象,通過這個對象向主線程發(fā)送信息;而我們發(fā)送的信息會先到主線程的MessageQueue進(jìn)行等待,由Looper按先入先出順序取出,再根據(jù)message對象的what屬性分發(fā)給對應(yīng)的Handler進(jìn)行處理!
簡單來說:Handler就是用來發(fā)送消息和處理消息的一種機(jī)制,上面這段話可能聽起來有些懵,不過沒關(guān)系,其實(shí)沒有這么深奧,下面會讓大家明白怎么使用它來實(shí)現(xiàn)計(jì)時的。
先定義個時間變量,初值為0,因?yàn)閺?開始計(jì)時
// 定義計(jì)數(shù)時間的變量 int time = 0;
然后定義發(fā)送和處理消息的對象handler,我們來重寫handleMessage方法,在方法里面我們進(jìn)行了if判斷,如果這條消息的what值為1,那么時間time就+1,然后timeTv顯示時間為time秒,然后繼續(xù)向自己發(fā)送消息。
handler.sendEmptyMessageDelayed(1,1000)這句話的意思就是:延時1000毫秒后發(fā)送參數(shù)what為1的空信息,這樣它自己就能循環(huán)接收自己發(fā)的消息,實(shí)現(xiàn)計(jì)時的功能了,就這么簡單。
當(dāng)然最開始要發(fā)送它一條消息,讓它這個方法運(yùn)轉(zhuǎn)起來,我們在onCreate這個方法里面加上了一條
handler.sendEmptyMessageDelayed(1,1000); 這樣在游戲一開始過了1s,handler就發(fā)送了一條what為1的空消息。然后它自己又立馬接收到了,進(jìn)行時間加1,又自己發(fā)送給自己消息,實(shí)現(xiàn)計(jì)時!
這是定義的handler的代碼:
// 定義發(fā)送和處理消息的對象handler Handler handler = new Handler(){ @Override // 重寫handleMessage方法,根據(jù)msg中what的值判斷是否執(zhí)行后續(xù)操作 public void handleMessage(Message msg) { if (msg.what==1) { time++; timeTv.setText("時間 : "+time+" 秒"); // 指定延時1000毫秒后發(fā)送參數(shù)what為1的空信息 handler.sendEmptyMessageDelayed(1,1000); } } };
這是在onCreate方法里面定義的一條消息
handler.sendEmptyMessageDelayed(1,1000);
我們來看下運(yùn)行效果:
除此之外,我們還需要在重新開始游戲后進(jìn)行重新計(jì)時,這里又要怎么實(shí)現(xiàn)呢?
這里我們只需要在restart方法里面先停止handler的消息發(fā)送,保證時間不會再繼續(xù)+1了,然后將時間重新歸0,顯示當(dāng)前時間,最后每隔1s發(fā)送參數(shù)what為1的消息msg,這樣就實(shí)現(xiàn)了重新開始計(jì)時,代碼如下:
/* 重新開始按鈕的點(diǎn)擊事件*/ public void restart(View view) { // 停止handler的消息發(fā)送 handler.removeMessages(1); // 將時間重新歸0,并且重新開始計(jì)時 time = 0; timeTv.setText("時間 : "+time+" 秒"); // 每隔1s發(fā)送參數(shù)what為1的消息msg handler.sendEmptyMessageDelayed(1,1000); }
點(diǎn)擊重新開始后的實(shí)現(xiàn)效果:
至此,我們的計(jì)時功能就實(shí)現(xiàn)了!
3、拼圖游戲打亂顯示
首先定義一個image數(shù)組,里面存放每張碎片(九宮格圖片)的id,int型數(shù)組是可以存放圖片的id的,但是不能存放圖片,注意這個區(qū)別。
// 將每張碎片的id存放到數(shù)組中,便于進(jìn)行統(tǒng)一的管理,int型數(shù)組存放的肯定是int型變量 private int[]image = {R.mipmap.img_xiaoxiong_00x00,R.mipmap.img_xiaoxiong_00x01,R.mipmap.img_xiaoxiong_00x02, R.mipmap.img_xiaoxiong_01x00,R.mipmap.img_xiaoxiong_01x01,R.mipmap.img_xiaoxiong_01x02, R.mipmap.img_xiaoxiong_02x00,R.mipmap.img_xiaoxiong_02x01,R.mipmap.img_xiaoxiong_02x02};
再聲明一個imageIndex數(shù)組,它來存放上面圖片數(shù)組的下標(biāo),一共九張圖片,所以下標(biāo)為0-8,它存儲的也就是0-8。我們?yōu)榱俗屔厦婢艔垐D片被打亂,所以,這里的下標(biāo)等下會被打亂。
// 聲明上面圖片數(shù)組下標(biāo)的數(shù)組,隨機(jī)排列這個數(shù)組,九張圖片,下標(biāo)為0-8 private int[]imageIndex = new int[image.length];
下面我們寫一個函數(shù)disruptRandom( ),來實(shí)現(xiàn)進(jìn)入游戲拼圖就打亂顯示的效果
先給下標(biāo)數(shù)組每個元素賦值,下標(biāo)是i,值就為i,就是imageIndex[i] = i。
// 給下標(biāo)數(shù)組每個元素賦值,下標(biāo)是i,值就為i for (int i = 0; i < imageIndex.length; i++) { imageIndex[i] = i; }
然后進(jìn)行20次for循環(huán),隨機(jī)選擇兩個角標(biāo)對應(yīng)的值進(jìn)行交換。先定義兩個角標(biāo)rand1和rand2,
rand1 = (int)(Math.random()*(imageIndex.length-1));這里我來重點(diǎn)解釋一下:
Math.random()產(chǎn)生的隨機(jī)數(shù)為0~1之間的小數(shù) 此處說的0~1是包含左不包含右,即包含0不包含1!
ps:我在這里卡了2h至少,因?yàn)檫@個小細(xì)節(jié)點(diǎn)沒注意到,所以一定不能想當(dāng)然,要查資料以求準(zhǔn)確。
Math.random()的值域?yàn)閇0,1),然后imageIndex.length-1就是8其實(shí),*8那就是[0,8),再int取整最終值域?yàn)閧0,1,2,3,4,5,6,7},因?yàn)閕nt取整只會取整數(shù)位,不會四舍五入!
再用do-while循環(huán)實(shí)現(xiàn)了rand2的生成,之所以在do-while里面生成rand2,是為了判斷二次生成的角標(biāo)和第一次是否相同,不同則break立刻跳出循環(huán),執(zhí)行swap交換;若第二次生成的與第一次相同,則重新進(jìn)入do-while循環(huán)生成rand2,這部分代碼如下:
// 規(guī)定20次,隨機(jī)選擇兩個角標(biāo)對應(yīng)的值進(jìn)行交換 int rand1,rand2; for (int j = 0; j < 20; j++) { // 隨機(jī)生成第一個角標(biāo) // Math.random()產(chǎn)生的隨機(jī)數(shù)為0~1之間的小數(shù) 此處說的0~1是包含左不包含右,即包含0不包含1 // Math.random()的值域?yàn)閇0,1),然后*8就是[0,8),再int取整最終值域?yàn)閧0,1,2,3,4,5,,6,7} rand1 = (int)(Math.random()*(imageIndex.length-1)); // 第二次隨機(jī)生成的角標(biāo),不能和第一次隨機(jī)生成的角標(biāo)相同,如果相同,就不方便交換了 do { rand2 = (int)(Math.random()*(imageIndex.length-1)); // 判斷第一次和第二次生成的角標(biāo)是否相同,不同則break立刻跳出循環(huán),執(zhí)行swap交換 if (rand1!=rand2) { break; } // 若第二次生成的與第一次相同,則重新進(jìn)入do-while循環(huán)生成rand2 }while (true); swap(rand1,rand2); }
這里的swap方法很簡單,就是交換兩個數(shù)的值,只不過這里參數(shù)是數(shù)組的下標(biāo):
// 交換數(shù)組指定角標(biāo)(0-7這八個自然數(shù))上的數(shù)據(jù) private void swap(int rand1, int rand2) { int temp = imageIndex[rand1]; imageIndex[rand1] = imageIndex[rand2]; imageIndex[rand2] = temp; }
這里有個整個游戲的一個核心點(diǎn):我們打亂的拼圖下標(biāo)是{0,1,2,3,4,5,6,7}這八個,第九張拼圖的下標(biāo)是不參與打亂的,有同學(xué)問為什么?是因?yàn)榈诰艔垐D片是不顯示出來的,而且不會參與到拼圖中,所以我們是將第九個圖片按鈕就設(shè)置成第九張圖片,然后invisible。
最后我們將每個圖片按鈕設(shè)置圖片,這時候 imageIndex[i]就是被打亂的下標(biāo),有可能是這樣的順序:{2,6,5,4,1,7,0,3,8},也有可能是這樣的順序{1,3,0,5,2,7,4,6,8}等等,不管怎么樣, imageIndex[8]一直是8,上面解釋過。代碼如下:
// ib00是綁定的第一塊圖片按鈕,設(shè)置圖片資源, // imageIndex[i]就是被打亂的下標(biāo),然后image[x]就表示對應(yīng)下標(biāo)為x的圖片的id ib00.setImageResource(image[imageIndex[0]]); ib01.setImageResource(image[imageIndex[1]]); ib02.setImageResource(image[imageIndex[2]]); ib10.setImageResource(image[imageIndex[3]]); ib11.setImageResource(image[imageIndex[4]]); ib12.setImageResource(image[imageIndex[5]]); ib20.setImageResource(image[imageIndex[6]]); ib21.setImageResource(image[imageIndex[7]]); ib22.setImageResource(image[imageIndex[8]]);
綜上,disruptRandom()的整體邏輯代碼如下:
// 隨機(jī)打亂數(shù)組當(dāng)中元素,以不規(guī)則的形式進(jìn)行圖片顯示 private void disruptRandom() { // 給下標(biāo)數(shù)組每個元素賦值,下標(biāo)是i,值就為i for (int i = 0; i < imageIndex.length; i++) { imageIndex[i] = i; } // 規(guī)定20次,隨機(jī)選擇兩個角標(biāo)對應(yīng)的值進(jìn)行交換 int rand1,rand2; for (int j = 0; j < 20; j++) { // 隨機(jī)生成第一個角標(biāo) // Math.random()產(chǎn)生的隨機(jī)數(shù)為0~1之間的小數(shù) 此處說的0~1是包含左不包含右,即包含0不包含1 // Math.random()的值域?yàn)閇0,1),然后*8就是[0,8),再int取整最終值域?yàn)閧0,1,2,3,4,5,,6,7} rand1 = (int)(Math.random()*(imageIndex.length-1)); // 第二次隨機(jī)生成的角標(biāo),不能和第一次隨機(jī)生成的角標(biāo)相同,如果相同,就不方便交換了 do { rand2 = (int)(Math.random()*(imageIndex.length-1)); // 判斷第一次和第二次生成的角標(biāo)是否相同,不同則break立刻跳出循環(huán),執(zhí)行swap交換 if (rand1!=rand2) { break; } // 若第二次生成的與第一次相同,則重新進(jìn)入do-while循環(huán)生成rand2 }while (true); // 交換兩個角標(biāo)上對應(yīng)的值 swap(rand1,rand2); } // 隨機(jī)排列到指定的控件上 // ib00是綁定的第一塊圖片按鈕,設(shè)置圖片資源,imageIndex[i]就是被打亂的圖片數(shù)組下標(biāo),然后image[x]就表示對應(yīng)下標(biāo)為x的圖片的id ib00.setImageResource(image[imageIndex[0]]); ib01.setImageResource(image[imageIndex[1]]); ib02.setImageResource(image[imageIndex[2]]); ib10.setImageResource(image[imageIndex[3]]); ib11.setImageResource(image[imageIndex[4]]); ib12.setImageResource(image[imageIndex[5]]); ib20.setImageResource(image[imageIndex[6]]); ib21.setImageResource(image[imageIndex[7]]); ib22.setImageResource(image[imageIndex[8]]); }
實(shí)現(xiàn)效果:
4、拼圖游戲碎片位置切換
我們完成亂序后,這時候拼圖碎片還不能移動,所以我們要設(shè)置點(diǎn)擊事件,來移動拼圖。
拼圖移動的規(guī)則也要注意一下:只有和空白區(qū)域在同一行或者同一列相鄰的拼圖才能移動,只要知道了這個邏輯,實(shí)現(xiàn)起來就不難了。
我們來編寫九個圖片按鈕的onClick()方法
這里因?yàn)榫艂€id不同的imagebutton點(diǎn)擊事件的邏輯相同,所以我們使用switch 語句來編寫,根據(jù)它們的id來執(zhí)行移動,按照從左到右、從上到下的順序進(jìn)行了case設(shè)置。移動我們定義了move()函數(shù),將它單獨(dú)封裝成了一個方法,下面就會講到。點(diǎn)擊事件的代碼如下:
public void onClick(View view) { int id = view.getId(); // 九個按鈕執(zhí)行的點(diǎn)擊事件的邏輯應(yīng)該是相同的,如果有空格在周圍,可以改變圖片顯示的位置,否則點(diǎn)擊事件不響應(yīng) switch (id) { case R.id.pt_ib_00x00: move(R.id.pt_ib_00x00,0); break; case R.id.pt_ib_00x01: move(R.id.pt_ib_00x01,1); break; case R.id.pt_ib_00x02: move(R.id.pt_ib_00x02,2); break; case R.id.pt_ib_01x00: move(R.id.pt_ib_01x00,3); break; case R.id.pt_ib_01x01: move(R.id.pt_ib_01x01,4); break; case R.id.pt_ib_01x02: move(R.id.pt_ib_01x02,5); break; case R.id.pt_ib_02x00: move(R.id.pt_ib_02x00,6); break; case R.id.pt_ib_02x01: move(R.id.pt_ib_02x01,7); break; case R.id.pt_ib_02x02: move(R.id.pt_ib_02x02,8); break; } }
我們來編寫九個圖片按鈕的move()方法
先定義變量,imageX是每行的圖片個數(shù),imageY是每列的圖片個數(shù),imgCount是圖片的總數(shù)目,也就是9個。blankSwap是空白區(qū)域的位置,就是8,這里的位置我們還是按照從左到右、從上到下的順序排列的,第一張圖片的位置是0,對照九宮格應(yīng)該理解了吧。
blankImgid就是空白區(qū)域的按鈕id,我們這里直接固定了R.id.pt_ib_02x02,就是第九個圖片按鈕,它一直是空白區(qū)域!
// 每行的圖片個數(shù) private int imageX = 3; // 每列的圖片個數(shù) private int imageY = 3; // 圖片的總數(shù)目 private int imgCount = imageX*imageY; // 空白區(qū)域的位置 private int blankSwap = imgCount-1; // 初始化空白區(qū)域的按鈕id private int blankImgid = R.id.pt_ib_02x02;
定義完要用到的變量,我們來寫move方法,這里我每句都寫上了注釋,這里就不再贅述了。
強(qiáng)調(diào)幾點(diǎn):
1.可以移動的條件有兩個:
- 在同一行,列數(shù)相減,絕對值為1,可移動
- 在同一列,行數(shù)相減,絕對值為1,可以移動
2.兩個參數(shù): imagebuttonId是被選中的圖片的id,site是該圖片在9宮格的位置(0-8)
3.將移動后的圖片按鈕設(shè)為不可見的,即顯示為空白區(qū)域
4.移動之前是不可見的,移動之后將圖標(biāo)按鈕設(shè)置為可見
5.進(jìn)行移動后將改變角標(biāo)的過程記錄到存儲圖片位置的數(shù)組當(dāng)中
/*表示移動指定位置的按鈕的函數(shù),將圖片和空白區(qū)域進(jìn)行交換*/ //imagebuttonId是被選中的圖片的id,site是該圖片在9宮格的位置(0-8) private void move(int imagebuttonId, int site) { // 判斷選中的圖片在第幾行,imageX為3,所以進(jìn)行取整運(yùn)算 int sitex = site / imageX; // 判斷選中的圖片在第幾列,imageY為3,所以進(jìn)行取模運(yùn)算 int sitey = site % imageY; // 獲取空白區(qū)域的坐標(biāo),blankx為行坐標(biāo),blanky為列坐標(biāo) int blankx = blankSwap / imageX; int blanky = blankSwap % imageY; // 可以移動的條件有兩個 // 1.在同一行,列數(shù)相減,絕對值為1,可移動 2.在同一列,行數(shù)相減,絕對值為1,可以移動 int x = Math.abs(sitex-blankx); int y = Math.abs(sitey-blanky); if ((x==0&&y==1)||(y==0&&x==1)){ // 通過id,查找到這個可以移動的按鈕 ImageButton clickButton = findViewById(imagebuttonId); // 將這個選中的圖片設(shè)為不可見的,即顯示為空白區(qū)域 clickButton.setVisibility(View.INVISIBLE); // 查找到空白區(qū)域的按鈕 ImageButton blankButton = findViewById(blankImgid); // 將空白區(qū)域的按鈕設(shè)置為圖片,image[imageIndex[site]就是剛剛選中的圖片,因?yàn)檫@在上面disruptRandom()設(shè)置過 blankButton.setImageResource(image[imageIndex[site]]); // 移動之前是不可見的,移動之后將控件設(shè)置為可見 blankButton.setVisibility(View.VISIBLE); // 將改變角標(biāo)的過程記錄到存儲圖片位置的數(shù)組當(dāng)中 swap(site,blankSwap); // 新的空白區(qū)域位置更新等于傳入的點(diǎn)擊按鈕的位置 blankSwap = site; // 新的空白圖片id更新等于傳入的點(diǎn)擊按鈕的id blankImgid = imagebuttonId; } }
運(yùn)行效果:
5、拼圖游戲成功的條件
上面我們已經(jīng)實(shí)現(xiàn)了拼圖碎片進(jìn)行移動的效果,但是并沒有拼圖游戲成功的效果和提示,所以,我們要在剛剛的move方法的最后加上一個判斷的方法judgeGameOver();顧名思義:判斷游戲結(jié)束。
我們來實(shí)現(xiàn)一下判斷游戲結(jié)束的邏輯
在方法里面先定義一個loop標(biāo)志位,然后要遍歷下標(biāo)數(shù)組,判斷是否它的imageIndex[i]==i,就是說所有拼圖的下標(biāo)全部對應(yīng)正確的位置。比如:第1張圖片的下標(biāo)是0,imageIndex[0]的值也是0,顯示第一張圖片。所有圖片都滿足,也就是說此時拼圖成功。如果一個不滿足,則未成功,所有l(wèi)oop置為false,繼續(xù)判斷。
boolean loop = true; //定義標(biāo)志位loop for (int i = 0; i < imageIndex.length; i++) { if (imageIndex[i]!=i) { loop = false; break; } }
如果拼圖成功了,則handler.removeMessages(1)進(jìn)行停止計(jì)時,
而且設(shè)置ib00.setClickable(false)禁止玩家繼續(xù)移動按鈕,
還有就是第九塊空白區(qū)域顯示出圖片,即下標(biāo)為8的第九張拼圖。
if (loop) { // 拼圖成功了 // 停止計(jì)時 handler.removeMessages(1); // 拼圖成功后,禁止玩家繼續(xù)移動按鈕 ib00.setClickable(false); ib01.setClickable(false); ib02.setClickable(false); ib10.setClickable(false); ib11.setClickable(false); ib12.setClickable(false); ib20.setClickable(false); ib21.setClickable(false); ib22.setClickable(false); // 拼圖成功后,第九塊空白顯示出圖片,即下標(biāo)為8的第九張圖片 ib22.setImageResource(image[8]); ib22.setVisibility(View.VISIBLE);
我們再來實(shí)現(xiàn)一下游戲結(jié)束時的對話框
對話框要用到AlertDialog.Builder對象,它的使用就是固定套路,我來補(bǔ)充知識點(diǎn):
- 第一步:創(chuàng)建AlertDialog.Builder對象
- 第二步:設(shè)置對話框的內(nèi)容:setMessage()方法來指定顯示的內(nèi)容
- 第三步:調(diào)用setPositive/Negative/NeutralButton()設(shè)置:確定,取消,中立按鈕
- 第四歩:調(diào)用create()方法創(chuàng)建這個對象
- 第五歩:調(diào)用show()方法來顯示我們的AlertDialog對話框
非常簡單,按照上面的流程,我們來設(shè)置下對話框:
// 彈出提示用戶成功的對話框,并且設(shè)置確實(shí)的按鈕 // 第一步:創(chuàng)建AlertDialog.Builder對象 AlertDialog.Builder builder = new AlertDialog.Builder(this); // 調(diào)用setIcon()設(shè)置圖標(biāo),setTitle()或setCustomTitle()設(shè)置標(biāo)題 // 第二步:設(shè)置對話框的內(nèi)容:setMessage()方法來指定顯示的內(nèi)容 builder.setMessage("恭喜,拼圖成功!您用的時間為"+time+"秒") // 第三步:調(diào)用setPositive/Negative/NeutralButton()設(shè)置:確定,取消,中立按鈕 .setPositiveButton("確認(rèn)",null); // 第四歩:調(diào)用create()方法創(chuàng)建這個對象 AlertDialog dialog = builder.create(); // 第五歩:調(diào)用show()方法來顯示我們的AlertDialog對話框 dialog.show();
實(shí)現(xiàn)效果:
6、拼圖游戲重新開始
我們在上面實(shí)現(xiàn)了拼圖游戲成功的條件和提示了,現(xiàn)在到了最后一步——如何讓游戲重新開始?
我們來看下拼圖成功后,點(diǎn)擊重新開始,目前只能重新計(jì)時,拼圖并沒有打亂,而且第九塊還沒有隱藏,所以,接下來我們的思路很明確,在重新開始的restart方法中編寫打亂和隱藏圖片的邏輯。
我們來實(shí)現(xiàn)重新開始游戲時的按鈕狀態(tài)還原
首先,這些按鈕已經(jīng)被設(shè)置成不可點(diǎn)擊了,所以我們先要將它們設(shè)置為可以點(diǎn)擊,就是設(shè)置ib00.setClickable(true),因?yàn)檫@部分代碼都是一樣的,所以我們將它單獨(dú)封裝成一個restore方法。
另外,還要還原被點(diǎn)擊的圖片按鈕變成初始化的模樣, ImageButton clickBtn = findViewById(blankImgid)其實(shí)就是綁定最后一次被隱藏的那塊拼圖,然后clickBtn.setVisibility(View.VISIBLE)將它顯示出來。ImageButton blankBtn = findViewById(R.id.pt_ib_02x02)就是綁定的第九塊拼圖,blankBtn.setVisibility(View.INVISIBLE)設(shè)置為不可見。最后blankImgid = R.id.pt_ib_02x02來初始化空白區(qū)域的按鈕id。
restore()的代碼如下:
// 狀態(tài)還原函數(shù),我們把它封裝起來 private void restore() { // 拼圖游戲重新開始,允許移動碎片按鈕 ib00.setClickable(true); ib01.setClickable(true); ib02.setClickable(true); ib10.setClickable(true); ib11.setClickable(true); ib12.setClickable(true); ib20.setClickable(true); ib21.setClickable(true); ib22.setClickable(true); // 還原被點(diǎn)擊的圖片按鈕變成初始化的模樣 ImageButton clickBtn = findViewById(blankImgid); clickBtn.setVisibility(View.VISIBLE); // 默認(rèn)隱藏第九張圖片 ImageButton blankBtn = findViewById(R.id.pt_ib_02x02); blankBtn.setVisibility(View.INVISIBLE); // 初始化空白區(qū)域的按鈕id blankImgid = R.id.pt_ib_02x02; blankSwap = imgCount - 1; }
最后,我們在restart()中實(shí)現(xiàn)重新開始的邏輯
- 將狀態(tài)還原 將拼圖重新打亂
- 停止handler的消息發(fā)送
- 將時間重新歸0,并且重新開始計(jì)時
- 每隔1s發(fā)送參數(shù)what為1的消息msg
/* 重新開始按鈕的點(diǎn)擊事件*/ public void restart(View view) { // 將狀態(tài)還原 restore(); // 將拼圖重新打亂 disruptRandom(); // 停止handler的消息發(fā)送 handler.removeMessages(1); // 將時間重新歸0,并且重新開始計(jì)時 time = 0; timeTv.setText("時間 : "+time+" 秒"); // 每隔1s發(fā)送參數(shù)what為1的消息msg handler.sendEmptyMessageDelayed(1,1000); }
重新開始游戲后的效果:
至此,拼圖游戲的所有功能已經(jīng)實(shí)現(xiàn)完畢,我先休息下,手腕已經(jīng)打酸了。如果你看到這里,我真的很欣慰,說明你是個很有耐心而且熱愛Android的學(xué)生,有熱情有耐心,再困難的東西都可以學(xué)會。
五、運(yùn)行效果
Android Studio實(shí)現(xiàn)拼圖游戲
六、項(xiàng)目總結(jié)
這次實(shí)現(xiàn)的拼圖游戲,說它簡單,其實(shí)它實(shí)現(xiàn)起來也并不是那么簡單,還是會有很多比較難的邏輯點(diǎn),需要思考才能寫出來;說它難,其實(shí)也不算難,比起來我前面發(fā)的那些項(xiàng)目【天氣預(yù)報】、【飲食搭配】來說邏輯實(shí)現(xiàn)還是比較簡單的,畢竟它只有一個MainActivity和一個layout。所以,說一個項(xiàng)目的難易得看你選的參照物了。
這篇文章一共25000多個字,820行,我寫這篇文章,不連上寫代碼時間,前后一共11個小時,前面構(gòu)思和注釋了4個小時,然后具體寫了7個小時,中間只有喝水a(chǎn)nd上廁所??梢哉f我完全是按照開發(fā)這款拼圖游戲的邏輯順序來寫下這篇教程。就是我們平時怎么開發(fā)Android項(xiàng)目,這篇博客就是怎么寫的。
我之所以寫的這么詳細(xì),也是因?yàn)楝F(xiàn)在網(wǎng)上缺少一個從頭到尾講實(shí)現(xiàn)過程的Android項(xiàng)目的教程,因?yàn)檫@實(shí)在太花時間了,我深有體會,極少有人一步一步地去把實(shí)現(xiàn)過程寫出來,但是我還是決定寫下這篇教程,為了讓更多的人喜歡上Android,讓更多的人對Android不再陌生,讓小白們不再望而卻步,讓小白們有個很好的實(shí)現(xiàn)案例,這是我的想法。
當(dāng)然,我也是正在學(xué)習(xí)Android的選手之一,才疏學(xué)淺,知識淺薄,文章中難免會有紕漏和錯誤,還希望大佬們批評指正。
七、項(xiàng)目源碼
這次的拼圖游戲項(xiàng)目是一個非常好的Android實(shí)現(xiàn)案例,涉及到很多常用的控件和知識點(diǎn),希望大家拿到源碼后,能對照著教程和注釋好好學(xué)習(xí)掌握。
源碼幾乎每條語句我都加上了注釋,這么良心的博主,點(diǎn)個三連支持下吧,源碼就送你啦,祝大家身體健康,學(xué)習(xí)愉快~
鏈接:拼圖游戲源碼
面向陽光時,陰影在你背后。背向陽光時,陰影在你眼前。世界從未改變,改變的只是我們面對世界的方向!加油!你值得更好!
到此這篇關(guān)于帶你用Android Studio做超好玩的拼圖游戲,0基礎(chǔ)小白也能包你學(xué)會,附送帶有超詳細(xì)注釋源碼的文章就介紹到這了,更多相關(guān)Android拼圖游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Android實(shí)現(xiàn)拼圖小游戲
- 基于Android平臺實(shí)現(xiàn)拼圖小游戲
- Android實(shí)現(xiàn)美女拼圖游戲詳解
- Android實(shí)現(xiàn)九宮格拼圖游戲
- Android自定義View實(shí)現(xiàn)拼圖小游戲
- Android利用ViewDragHelper輕松實(shí)現(xiàn)拼圖游戲的示例
- Android拼圖游戲 玩轉(zhuǎn)從基礎(chǔ)到應(yīng)用手勢變化
- Android實(shí)現(xiàn)滑塊拼圖驗(yàn)證碼功能
- Android 簡單的實(shí)現(xiàn)滑塊拼圖驗(yàn)證碼功能
- Android實(shí)現(xiàn)九格智能拼圖算法
相關(guān)文章
android tv列表焦點(diǎn)記憶實(shí)現(xiàn)的方法
本篇文章主要介紹了android tv列表焦點(diǎn)記憶實(shí)現(xiàn)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04在RecyclerView中實(shí)現(xiàn)button的跳轉(zhuǎn)功能
本次實(shí)驗(yàn)就是在RecyclerView中添加一個button控件并實(shí)現(xiàn)監(jiān)聽,使鼠標(biāo)點(diǎn)擊時可以跳轉(zhuǎn)到另外一個設(shè)計(jì)好的界面,對RecyclerView實(shí)現(xiàn)button跳轉(zhuǎn)功能感興趣的朋友一起看看吧2021-10-10利用DrawerLayout和觸摸事件分發(fā)實(shí)現(xiàn)抽屜側(cè)滑效果
這篇文章主要為大家詳細(xì)介紹了利用DrawerLayout和觸摸事件分發(fā)實(shí)現(xiàn)抽屜側(cè)滑效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10詳解基于Android的Appium+Python自動化腳本編寫
這篇文章主要介紹了詳解基于Android的Appium+Python自動化腳本編寫,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Android實(shí)現(xiàn)簡單的文件下載與上傳
今天小編就為大家分享一篇關(guān)于Android實(shí)現(xiàn)簡單的文件下載與上傳,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12Android NavigationView頭部設(shè)置監(jiān)聽事件
這篇文章主要為大家詳細(xì)介紹了Android NavigationView頭部設(shè)置監(jiān)聽事件,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10