2048游戲最近很火,想看下源碼,卻不會JavaScript。網上搜了搜安卓版的源碼,嘗試下來學習。
uberspot
在https://github.com/uberspot/2048-android 上面發現了一個安卓版的2048代碼,於是下載准備閱讀。卻發現源文件中只有一個Java類,MainActivity.java。打開大致看了一下:
// If there is a previous instance restore it in the webview if (savedInstanceState != null) { mWebView.restoreState(savedInstanceState); } else { mWebView.loadUrl("file:///android_asset/2048/index.html"); }
原來是用一個webview對原本的JavaScript進行了封裝,使用安卓內部webkit瀏覽器進行了加載。相當於用手機瀏覽器玩網頁版的游戲,只能再搜索了。
極客學院
另外找到一個極客學院版本的源碼,在網站上面還有視頻教程。本文分析的主要內容就是極客學院版本的源代碼了,作者是ime。
源碼鏈接:https://github.com/plter/Android2048GameLesson
分析的目標為code\ide\ADT\Game2048Publish目錄中的源碼版本
1 界面
界面比較簡單了,打開activity_main.xml看看,幾個TextView,一個按鈕,還有三個自定義的控件GameView,AnimLayer,Card。游戲的截圖如下:
標准控件就不介紹了,介紹一下系統三個自定義的控件。
1.1 Card(后文混用Card 卡片 方塊三個詞語)
類Card繼承了FrameLayout,目的是作為游戲中的卡片。卡片數字和樣式的實現:
public void setNum(int num) { this.num = num; if (num<=0) { label.setText(""); }else{ label.setText(num+""); } switch (num) { case 0: label.setBackgroundColor(0x00000000);//透明色 break; case 2: label.setBackgroundColor(0xffeee4da); break; case 4: label.setBackgroundColor(0xffede0c8); break; case 8: label.setBackgroundColor(0xfff2b179); break; case 16: label.setBackgroundColor(0xfff59563); break; …… default: label.setBackgroundColor(0xff3c3a32); break; } }
num<=0表明是空白方格。當前位置上如果沒有card,則使用num<=0的card進行替代。card 0沒有label,同時底色為透明。除了card 0之外,card 2之后的卡片都有對應的顏色和數字。
1.2 AnimLayer
類AnimLayer繼承了FramLayout,用於動畫展示。在極客學院安卓2048最主要由兩個動畫:卡片移動和卡片出現。
a) 對於卡片出現動畫:
//目標卡片 public void createScaleTo1(Card target){ //縮放 ScaleAnimation sa = new ScaleAnimation(0.1f, 1, 0.1f, 1, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); sa.setDuration(100); target.setAnimation(null); target.getLabel().startAnimation(sa); }
b) 對於卡片移動動畫:
使用ArrayList<Card> cards用於管理臨時卡片的創建和回收(避免每次創建臨時卡片時創建新的對象)
創建一個臨時卡片,從卡片from移動到卡片to,當完成動畫之后將臨時卡片設為不可見,並使用cards回收該卡片。
創建卡片:
private Card getCard(int num){ Card c; if (cards.size()>0) { c = cards.remove(0); }else{ c = new Card(getContext()); addView(c); } c.setVisibility(View.VISIBLE); c.setNum(num); return c; }
創建卡片時,如果cards不為空,則從cards隊首取出一張臨時卡片。(這里認為使用LinkedList<Card>更加適合臨時卡片管理隊列)
回收卡片:
private void recycleCard(Card c){ c.setVisibility(View.INVISIBLE); c.setAnimation(null); cards.add(c); }
回收卡片將當前卡片設為不可見,並加入到cards中。
public void createMoveAnim(final Card from,final Card to,int fromX,int toX,int fromY,int toY){ //臨時卡片 final Card c = getCard(from.getNum()); //設置布局 LayoutParams lp = new LayoutParams(Config.CARD_WIDTH, Config.CARD_WIDTH); lp.leftMargin = fromX*Config.CARD_WIDTH; lp.topMargin = fromY*Config.CARD_WIDTH; c.setLayoutParams(lp); if (to.getNum()<=0) { to.getLabel().setVisibility(View.INVISIBLE); } //從from卡片位置移動到to卡片 TranslateAnimation ta = new TranslateAnimation(0, Config.CARD_WIDTH*(toX-fromX), 0, Config.CARD_WIDTH*(toY-fromY)); ta.setDuration(25); ta.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {} @Override public void onAnimationRepeat(Animation animation) {} //動畫結束,將臨時卡片回收 @Override public void onAnimationEnd(Animation animation) { to.getLabel().setVisibility(View.VISIBLE); recycleCard(c); } }); c.startAnimation(ta); }
1.3 GameView
GameView繼承了GridLayout,包含了界面和游戲邏輯兩個部分。這里介紹界面。
界面中比較重要的內容就是手勢識別,用於操控格子的移動:
private void initGameView(){ setColumnCount(Config.LINES); setBackgroundColor(0xffbbada0); setOnTouchListener(new View.OnTouchListener() { private float startX,startY,offsetX,offsetY; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN://按下坐標 startX = event.getX(); startY = event.getY(); break; case MotionEvent.ACTION_UP: offsetX = event.getX()-startX; offsetY = event.getY()-startY; if (Math.abs(offsetX)>Math.abs(offsetY)) { if (offsetX<-5) { swipeLeft(); }else if (offsetX>5) { swipeRight(); } }else{ if (offsetY<-5) { swipeUp(); }else if (offsetY>5) { swipeDown(); } } break; } return true;//listener已經處理了事件 } }); }
使用了View.OnTouchListener來偵聽觸摸事件:計算按下和抬起來時offsetX和offsetY,預測手勢的移動。
2 游戲邏輯
上一節介紹了基本的界面展現,本節介紹業務邏輯,即游戲實現原理。
2.1 游戲初始化
調用函數initGameView()完成游戲初始化:
private void initGameView(){ setColumnCount(Config.LINES);//設置行數量 setBackgroundColor(0xffbbada0); setOnTouchListener(new View.OnTouchListener() { } }); }
設置控件的方格數量,隨后設置了控件北京,最后注冊了剛才分析過的觸摸事件監聽器。此時游戲已經准備好了,正式開始。
2.2 開始游戲
函數startGame();正式開始游戲,首先向方格內隨機寫入兩個方塊:
public void startGame(){ MainActivity aty = MainActivity.getMainActivity(); aty.clearScore(); aty.showBestScore(aty.getBestScore()); for (int y = 0; y < Config.LINES; y++) { for (int x = 0; x < Config.LINES; x++) { cardsMap[x][y].setNum(0); } } addRandomNum(); addRandomNum(); }
這個函數addRandomNum()向游戲面板內隨機加入兩個方塊,開始游戲:
private void addRandomNum(){ //private List<Point> emptyPoints = new ArrayList<Point>(); emptyPoints.clear(); //將所有空格子搜集起來 for (int y = 0; y < Config.LINES; y++) { for (int x = 0; x < Config.LINES; x++) { if (cardsMap[x][y].getNum()<=0) { emptyPoints.add(new Point(x, y)); } } } if (emptyPoints.size()>0) { //隨機位置生成一個card Point p = emptyPoints.remove((int)(Math.random()*emptyPoints.size())); int num = Math.random()>0.1?2:4; cardsMap[p.x][p.y].setNum(num); MainActivity.getMainActivity().getAnimLayer().createScaleTo1(cardsMap[p.x][p.y]); } }
函數addRandomNum()向面板中空的格子中隨機生成一個卡片。首先搜集面板中所有空的位置,搜集到一個List中,最后生成隨機數,隨機生成一個數字,並完成生成動畫。
2.3 移動
2048游戲通過游戲中所有的方格朝某個方向移動,合並相同數字的方塊。有四個函數負責移動,分別是上下左右,這里只分析一個方向。
private void swipeLeft(){ boolean merge = false;//是否合並卡片, 1空卡片和已有卡片合並 2兩個數字相同的卡片合並 for (int y = 0; y < Config.LINES; y++) {//對所有列 for (int x = 0; x < Config.LINES; x++) { //檢查當前點的右側是否有非空卡片(非空:num>=2) for (int x1 = x+1; x1 < Config.LINES; x1++) { if (cardsMap[x1][y].getNum()>0) {//如果右邊有非空卡片 if (cardsMap[x][y].getNum()<=0) {//當前坐標上沒有格子(空卡片和已有卡片合並) MainActivity.getMainActivity().getAnimLayer().createMoveAnim(cardsMap[x1][y],cardsMap[x][y], x1, x, y, y); cardsMap[x][y].setNum(cardsMap[x1][y].getNum()); cardsMap[x1][y].setNum(0); x--;//和空卡片合並,還需要從當前位置計算(否則:|0|2|2|2|左移之后變為|2|2|2|0|) merge = true; }else if (cardsMap[x][y].equals(cardsMap[x1][y])) { MainActivity.getMainActivity().getAnimLayer().createMoveAnim(cardsMap[x1][y], cardsMap[x][y],x1, x, y, y); cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2); cardsMap[x1][y].setNum(0); MainActivity.getMainActivity().addScore(cardsMap[x][y].getNum()); merge = true; } break; } } } } //只要有任意一行發生過卡片移動,則需要產生新的卡片 if (merge) { addRandomNum(); checkComplete();//判斷當前游戲是否失敗 } }
左移,針對面板中所有列,將每行的方塊向左移動。在兩種情況發生卡片合並:
1 當前位置為空卡片,右側為非空卡片,合並后當前位置卡片Num為右側卡片,右側卡片清零。
2 當前位置為非空卡片,右側卡片數值和它相等,合並后當前位置卡片數量翻倍,右側卡片清零。
從游戲角度來講:1 對應卡片單純的移動,2 對應兩張相同卡片的合並。因此,只要發生卡片實質上的移動,就應該隨機再生產一個卡片,調用addRandomNum()。
2.4 游戲結束的判斷
每次發生卡片移動,都要檢查游戲還能否繼續,是否已經結束。函數checkComplete()完成游戲失敗(感覺叫做checkFailure()更好)的檢查:
private void checkComplete(){ boolean complete = true; ALL: for (int y = 0; y < Config.LINES; y++) { for (int x = 0; x < Config.LINES; x++) { //滿足任意兩個條件,游戲就可以繼續:1 有空的格子,2 有可以合並的卡片 if (cardsMap[x][y].getNum()==0||//1 有多余空間 (x>0&&cardsMap[x][y].equals(cardsMap[x-1][y]))||//2 和左面相等 (x<Config.LINES-1&&cardsMap[x][y].equals(cardsMap[x+1][y]))|//2 和右面相等 (y>0&&cardsMap[x][y].equals(cardsMap[x][y-1]))||//2 和上面相等 (y<Config.LINES-1&&cardsMap[x][y].equals(cardsMap[x][y+1]))) {//2 和下面相等 complete = false; break ALL; } } if (complete) { new AlertDialog.Builder(getContext()).setTitle("你好").setMessage("游戲結束").setPositiveButton("重新開始", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startGame(); } }).show(); } }
游戲可以繼續的兩個條件:有空的格子,或者還有能夠合並的卡片。