1. 程式人生 > >Eclipse SWT開發教程以及一個連連看遊戲的程式碼實現下載

Eclipse SWT開發教程以及一個連連看遊戲的程式碼實現下載

  我在前面講過:如果講GUI程式設計一味只講各個控制元件的使用方法,那麼純粹是浪費大家時間,如果出書,那絕對是騙錢的。所以我並不會詳細地講解SWT各個控制元件的具體使用方法。然而的眾所周知,Eclipse的UI介面是建立在SWT基礎之上的,如果一字不提SWT,似乎也不大可能。SWT是一個優秀的GUI程式設計框架,即使不要Eclipse的其它部分,SWT也可以單獨使用。單獨使用SWT編寫GUI程式的最簡單示例如下:

 public static void main (String [] args) {
      Display display = new Display ();
      Shell shell = new Shell (display);
      Label label = new Label (shell, SWT.CENTER);
      label.setText ("Hello_world");
      label.setBounds (shell.getClientArea ());
      shell.open ();
      while (!shell.isDisposed ()) {
         if (!display.readAndDispatch ()) display.sleep ();
      }
      display.dispose ();
   }


  從上面的程式碼可以看出,使用SWT比使用其它GUI框架要多出一步,那就是先要建立一個Display。SWT與其它GUI框架的另外一個不同之處就是它的主視窗不叫視窗,而是叫Shell。除此之外,如果大傢俱有其它GUI框架的程式設計基礎,使用SWT就沒什麼難度了。
  我在這裡簡單比較一下SWT和Swing的區別。在使用Swing時,一般會建立一個JFrame作為主視窗,然後向裡面新增控制元件,而且不需要Display。Swing新增控制元件的時候是parent.add(child)。SWT需要先建立一個Display,然後再建立一個Shell作為主視窗,然後再新增控制元件。SWT新增控制元件的方法是把parent作為引數傳遞到child的建構函式中。SWT的主視窗關閉後,還得dispose掉Display。而它們對於事件的處理基本上是一樣的,都是通過control.addXXXListener新增一個事件處理器來進行。
  SWT為什麼要這麼一個額外的Display呢?而這個Display又代表著什麼呢?SWT底層究竟有著什麼樣的設計哲學呢?
  Eclipse自己的文件說Display是SWT和作業系統本地GUI之間的橋樑,Display的主要用途就是建立一個訊息迴圈並進行訊息的派發,另外一個用途就是幫助GUI執行緒和非GUI執行緒進行通訊。我認為,Display還有一個理解,那就是Display是計算機顯示系統的一個抽象。看一下下面這個Drawable介面的繼承關係:



  可以看到,Display實現了Drawable介面,說明我們可以隨意在Display上進行繪圖。另外,Display還是Device的子類,說明Display代表著一種裝置,而另外一個同樣也屬於裝置的是Printer。我們可以這樣理解:如果要把影象顯示在電腦螢幕上,就可以使用Display進行畫圖,如果要把什麼內容列印到紙上,使用Printer就好,如果有其它的另類的顯示裝置,就只好自己實現Device了。
  SWT的widgets中包含很多Control,具體用法我就不講了,大家可以自己參考SWT的文件,或者直接上SWT Designer或WindowBuilder pro。通過Control的addXXXListener方法可以為該控制元件新增事件處理器,從而處理響應的事件。下面的截圖可以看到Control可以新增的一系列事件處理器:


  除了一般的滑鼠鍵盤事件,每一個實現了Drawable介面的控制元件都可以隨意進行繪製,只需要使用addPaintListener來新增一個PaintListener即可。繪圖離不開下面這些東西:


  按我自己的理解,我一般將GUI程式分成三類。
  第一類就是一個對話方塊裡面有許多文字框和按鈕的那種,使用者依次將各個文字框填滿,點選一個按鈕,然後就等著程式處理這些資料了。這種型別的GUI系統非常的簡單,它的重點並不是GUI,而是GUI背後的業務邏輯。比如超市、銀行用的終端,即使是使用全由字元介面組成的只顯示黑白灰色的GUI系統,依然能夠處理複雜的業務。還有現在和很多ERP系統、HIS系統等等,無非就是資料的採集、儲存、顯示,用幾個文字框和按鈕控制元件足以,沒有什麼技術含量。
  第二類是涉及到自己繪圖的那種,比如繪製個什麼波形圖啊,繪製個什麼2D遊戲啊。這種型別的GUI程式不需要複雜的控制元件,重點在於繪圖的操作。比如我後面即將展示的連連看遊戲,就僅僅使用了一個Canvas而已。當然,要寫個像Photoshop之類的巨型軟體,難度還是很大的。
  第三類就是可以用來處理文件的那種。比如各種字處理程式、表格程式、瀏覽器程式,還有我們程式設計師最常用的IDE。在這些處理程式中,什麼字型變大變小、顏色變紅變白什麼的,說到底還是畫圖,和第二類的畫圖差不多,但是其背後掩藏著複雜的演算法。就拿我們常見的IDE來說,語法高亮、自動提示這樣的功能怎麼也離不開對文字內容的解析,還要計算每一個字元究竟顯示在什麼位置。要做得漂亮,難度也是很大的。
  如果以上三類GUI程式都能熟練編寫,就可以算是GUI程式設計的達人了。今天,我先展示一個連連看遊戲的例子。直到目前為止,我所講述的Eclipse RCP都還只是建立在檢視上。怎麼寫編輯器(Editor)還沒有涉及。以後肯定會講到編輯器的編寫,當然是可以分析文字的編輯器,對於那種只靠文字框和按鈕收集和編輯資料的,我覺得不好意思叫編輯器。今天的連連看程式仍然是一個單檢視的程式。

  連連看遊戲執行效果圖:


  如果碰到困難,點選工具欄的放大鏡圖示,可以自動推薦兩個可以匹配的方塊,如下圖:


  下面來談談這個程式的實現。首先是準備素材。大家可以看到這個遊戲中用到的圖片都是Java世界一些比較著名的開源專案,如Spring、Tomcat、Glassfish、MySQL等。我從網上找到它們的Logo,然後用GIMP稍微處理了一下,就成了遊戲中需要用到的方塊。從上面兩個截圖可以看到,每一個方塊都有三個狀態,分別是正常狀態、被選擇狀態和被推薦狀態,所以每張圖片有三個樣式,如下圖:


程式的GUI結構:

  程式的GUI結構非常簡單,就是一個單檢視的Eclipse RCP程式。這個檢視的標題是Game View。和前兩篇博文講的不一樣的地方是這個程式沒有用選單,而是使用了檢視的工具欄。使用檢視的工具欄和使用選單的流程是一樣的:1、先新增一個org.eclipse.ui.commands擴充套件,定義兩個Command;2、再新增一個org.eclipse.ui.menus擴充套件,定義一個MenuContribution,該MenuContribution可以控制我們新增的Command顯示在選單中還是顯示在工具欄中;3、寫兩個Handler,分別處理兩個Command命令即可。
  對MenuContribution設定不同的locationURI屬性可以控制Command的顯示位置。在前面一篇博文中,我們設定locationURI為menu:org.eclipse.ui.main.menu,所以就在主選單中添加了一個選單。在這個連連看遊戲中,我將locationURI設定為toolbar:JavaLinkGame.views.game,也就是冒號前是toolbar,冒號後是Game View的Id,所以在Game View中添加了一個工具欄和兩個按鈕。
  在Game View中只使用了一個Canvas控制元件,Canvas控制元件用來畫圖和響應使用者的操作。所以只需要設定Canvas的PaintListener和MouseListener即可。程式的框架非常簡單,如下圖:


  上面圖片中顯示的程式碼只是為了顯示程式的結構,後面都經過了大量的更改和擴充。在上面的程式碼中,我在註釋中問了兩個一樣的問題:這裡用什麼?如果是C或C++,就可以在這裡傳遞一個函式指標,如果是函數語言程式設計語言,就可以在這裡直接傳遞一個函式。但是,我們用的是Java,所以canvas.addPaintListener()的引數只能是一個物件。要獲得一個物件,就必須定義一個類,這正是Java語言的麻煩之處。
  好在這個麻煩有許多的解決辦法。如果不想另外寫一個.java檔案,我們可以定義一個內部類;如果連類名都懶得想,可以用一個匿名類;如果真的不想定義一個類,那就用lambda表示式吧。在這裡我使用的是匿名類。如下圖:
  

  使用Java語言,除了時時刻刻要考慮定義一個類的問題外,還有一個問題,那就是field的可見性。一個類要如何才能訪問到另一個類中的field?如上圖,我們定義的匿名類中可以直接訪問外面類中的canvas和spirits,也就是說內部類可以直接訪問外部類中的field。是不是相當於閉包?假如我們不是用的匿名類,也不是用的內部類,而是另外定義一個類,那該如何訪問到這裡的canvas和spirits呢?只能通過建構函式把這兩個field當引數傳進去吧。

程式的資料組織:

  在這個程式中,除了Eclipse RCP必須的一個View和兩個Handler類之外,我只額外寫了兩個類,一個類是Spirit,用它表示遊戲中的一個方塊。每一個Spirit物件儲存了它需要的三幅圖片、狀態(是正常還是被選擇還是被推薦)、座標(在陣列中的座標,而不是畫素的座標)以及一個imageId,使用imageId的目的是為了方便判斷使用者選擇的兩個方塊是否是相同的,相同的方塊如果在轉兩個彎之內連得上的話就可以消去。另外一個類是GameAlgorithmUtil,它主要處理遊戲中的演算法。
  我用了一個Spirit[][]二維陣列來儲存方塊,這個二維陣列的邊緣一圈填充的Spirit的imageId為0,中間的Spirit的imageId不為0。imageId為0的Spirit不會顯示,所以邊緣這一圈顯示為空白,也是最開始時候連線方塊的通道。如下圖:


  另外,當兩個方塊可以消去時,我們還要顯示他們之間的連線,所以需要儲存他們之間連線的路徑。這個簡單,不需要另外寫一個類,用一個LinkedList<Spirit>即可。

遊戲中的演算法:

  遊戲中涉及的演算法不多,只有兩個。 第一個,是遊戲開始時,需要把所有方塊的位置打亂,所以需要一個洗牌演算法。第二個,在使用者選擇了兩個圖片相同的方塊時,需要判斷它們是否可以連通,所以需要一個尋找路徑的演算法。
  洗牌演算法:先在這n個方塊中隨機選擇一個方塊和第n個方塊交換,再在前n-1個方塊中隨機選擇一個和第n-1個交換,再在前n-2個方塊中隨機選擇一個和第n-2個交換……一直遞迴到第一個。
  尋找路徑演算法:我沒有用《程式設計之美》中講到的廣度優先搜素演算法,而是用了另外一個分類掃描演算法。將兩個方塊可以連通的情況分為三類。第1類,兩個方塊可以通過一條直線連通,沒有轉角;第2類,兩個方塊需要通過兩條直線連通,有一個轉角,第2中情況可以遞迴為判斷這個轉角處的元素可以和這兩個方塊分別通過一條直線連通;第3類,兩個方塊需要通過三條直線連通,有兩個轉角,很顯然其中一個轉角肯定要麼和第一個方塊同行,要麼同列,然後遞迴為判斷這個轉角是否能夠和第二個方塊通過兩條直線連線。

下面開始貼程式碼:
1、Spirit類的程式碼:

package com.xkland.examples.rcp.javalinkgame;

import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Canvas;

public class Spirit {

    public static final int NORMAL = 0;
    public static final int SELECTED = 1;
    public static final int RECOMMENDED = 2;
    
    public int i;
    public int j;    
    public int imageId;
    
    private int state;
    private Image[] images = new Image[3];
    
    public Spirit(Image normal, Image selected, Image recommended,
            int imageId,int i, int j) {
        this.images[0] = normal;
        this.images[1] = selected;
        this.images[2] = recommended;
        this.imageId = imageId;
        this.i = i;
        this.j = j;
    }

    public void draw(Canvas canvas) {
        GC gc = new GC(canvas);
        if(images[state]!=null && imageId != 0)gc.drawImage(images[state], j*80, i*80);
    }
    
    public void changeState(){
        if(state == NORMAL || state == RECOMMENDED){
            state = SELECTED;
        }else{
            state = NORMAL;
        }
    }
    
    public void recommend(){
        state = RECOMMENDED;
    }
    
    public void setPosition(int i, int j){
        this.i = i;
        this.j = j;
    }
}


2、檢視類的程式碼:

package com.xkland.examples.rcp.javalinkgame;

import java.util.LinkedList;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.part.ViewPart;

public class GameView extends ViewPart {
    
    private Canvas canvas;
    private final int M = GameAlgorithmUtil.M;
    private final int N = GameAlgorithmUtil.N;
    private Spirit[][] spirits = new Spirit[M][N];
    private List<Spirit> linkPath = new LinkedList<Spirit>();

    @Override
    public void createPartControl(Composite parent) {
        parent.setLayout(null);
        canvas = new Canvas(parent, SWT.NONE);
        canvas.setBounds(0,0,1120,720);
        
        canvas.addPaintListener(new PaintListener(){
            @Override
            public void paintControl(PaintEvent e) {
                //顯示已選擇的兩個Spirit之間的連結路徑
                if(!linkPath.isEmpty() && linkPath.size() <= 4){
                    for(int i=1; i<linkPath.size(); i++){
                        Color color = new Color(null, 240, 119, 70);
                        GC gc = new GC(canvas);
                        gc.setForeground(color);
                        gc.setLineWidth(2);
                        gc.drawLine(linkPath.get(i-1).j*80+36, linkPath.get(i-1).i*80+36, 
                                linkPath.get(i).j*80+36, linkPath.get(i).i*80+36);
                    }
                }
                //顯示陣列中的Spirit
                for(int i=0; i<M; i++){
                    for(int j=0; j<N; j++){
                        if(spirits[i][j] !=null ){
                            spirits[i][j].draw(canvas);
                        }
                    }
                }
            }            
        });
        
        canvas.addMouseListener(new MouseListener(){
            //下面兩個field儲存當前選中的Spirit和上一次點選選中的Spirit
            private Spirit oldSelect;
            private Spirit curSelect;
            //下面兩個field儲存已經匹配過了但是還沒有消除的兩個Spirit
            private Spirit matchedSpirit1;
            private Spirit matchedSpirit2;
            @Override
            public void mouseDown(MouseEvent e){
                //只要使用者點選滑鼠,就把之前已經匹配好的Spirit和它們之間的LinkPath消掉
                if(matchedSpirit1 != null && matchedSpirit2 != null && !linkPath.isEmpty()){
                    matchedSpirit1.imageId = 0;
                    matchedSpirit2.imageId = 0;
                    linkPath.clear();
                }
                
                //獲取當前選擇的Spirit
                for(int i=0; i<M; i++){
                    for(int j=0; j<N; j++){
                        if((i*80 + 4) < e.y && (i*80 + 68) > e.y && (j*80+4) < e.x && (j*80+68) > e.x){
                            if(curSelect != null){
                                oldSelect = curSelect;
                                curSelect = null;
                            }
                            curSelect = spirits[i][j];
                            if(curSelect.imageId !=0 )curSelect.changeState();
                            //如果選擇的兩個Spirit中的圖片是一樣的,則尋找它們之間的路徑
                            if(oldSelect != null  && curSelect != null && oldSelect != curSelect
                                    && oldSelect.imageId == curSelect.imageId
                                    && curSelect.imageId != 0){
                                if(!GameAlgorithmUtil.findPath(spirits, curSelect, oldSelect, linkPath)){
                                    oldSelect.changeState();
                                }else{
                                    //如果找得到路徑,就把它們儲存起來,下一次點選滑鼠時消掉
                                    matchedSpirit1 = oldSelect;
                                    matchedSpirit2 = curSelect;
                                    oldSelect = null;
                                    curSelect = null;                                    
                                }
                            }else{
                                if(oldSelect !=null)oldSelect.changeState();
                            }
                        }
                    }
                }
                //最後重新整理顯示
                canvas.redraw();
            }

            @Override
            public void mouseDoubleClick(MouseEvent arg0) {
                
            }

            @Override
            public void mouseUp(MouseEvent arg0) {
                
            }
        });
    }

    @Override
    public void setFocus() {
        // TODO Auto-generated method stub

    }

    public Canvas getCanvas() {
        return canvas;
    }

    public Spirit[][] getSpirits() {
        return spirits;
    }

    public List<Spirit> getLinkPath() {
        return linkPath;
    }

}


3、GameAlgorithmUtil類的程式碼,演算法的實現都在這裡面了,有詳細的註釋:

package com.xkland.examples.rcp.javalinkgame;

import java.util.List;
import java.util.Random;

import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.PlatformUI;

public class GameAlgorithmUtil {
    public static final int M = 9;
    public static final int N = 14;
    public static final int C = 7;
    
    private static Image[] normalImages = new Image[C];
    private static Image[] selectedImages = new Image[C];
    private static Image[] recommendedImages = new Image[C];
    private static boolean isImagesLoaded = false;
    
    private static void loadImages(Spirit[][] spirits){
        for (int i = 1; i <= C; i++) {
            normalImages[i-1] = new Image(PlatformUI.getWorkbench().getDisplay(),
                    spirits.getClass().getResourceAsStream("/images/0" + Integer.toString(i) + "o.png"));
            selectedImages[i-1] = new Image(PlatformUI.getWorkbench().getDisplay(), 
                    spirits.getClass().getResourceAsStream("/images/0" + Integer.toString(i) + "s.png"));
            recommendedImages[i-1] = new Image(PlatformUI.getWorkbench().getDisplay(), 
                    spirits.getClass().getResourceAsStream("/images/0" + Integer.toString(i) + "r.png"));
        }
    }
    
    public static void fillSpirits(Spirit[][] spirits){
        if(!isImagesLoaded){
            loadImages(spirits);
            isImagesLoaded = true;
        }

        for(int i=0; i<M; i++){
            for(int j=0; j<N; j++){
                int n = ((i-1)*(N-2) + (j-1)) % C;
                if(i==0 || i==M-1 || j==0 || j==N-1){
                    spirits[i][j] = new Spirit(null,null,null,0,i,j);
                }else{
                    spirits[i][j] = new Spirit(normalImages[n],selectedImages[n],recommendedImages[n],n+1,i,j);
                }
                
            }
        }
    }

    public static void shuffle(Spirit[][] spirits){
        //使用網上流傳的隨機抽牌和最後一張交換的洗牌演算法,演算法複雜度為O(n)
        Random rand = new Random();
        int count = (M-2)*(N-2);
        for(int i=M-2; i>=1; i--){
            for(int j=N-2; j>=1; j--){
                int n = rand.nextInt(count);
                Spirit temp = spirits[i][j];
                spirits[i][j] = spirits[n/(N-2)+1][n%(N-2)+1];
                spirits[n/(N-2)+1][n%(N-2)+1] = temp;
                //不僅要調整Spirit在陣列中的位置,還要更改Spirit中自身儲存的位置
                spirits[i][j].setPosition(i, j);
                spirits[n/(N-2)+1][n%(N-2)+1].setPosition(n/(N-2)+1, n%(N-2)+1);
            }
        }
    }
    
    //判斷兩個Spirit能否用一條線連結起來
    //如果能夠用一條線連結起來,它們肯定在同一行或同一列
    private static boolean isLinkedByOneLine(Spirit[][] spirits, Spirit curSelect, Spirit oldSelect){
        boolean result = true;
        //如果curSelect和oldSelect在同一行
        if(curSelect.i == oldSelect.i){
            int p = curSelect.j > oldSelect.j ? oldSelect.j : curSelect.j;
            int q = curSelect.j > oldSelect.j ? curSelect.j : oldSelect.j;
            if (q - p == 1) return result;
            for (int j = p + 1; j < q; j++) {
                if (spirits[curSelect.i][j].imageId != 0)
                    result = false;
            }
        }
        //如果curSelect和oldSelect在同一列
        if(curSelect.j == oldSelect.j){
            int p = curSelect.i>oldSelect.i ? oldSelect.i : curSelect.i;
            int q = curSelect.i>oldSelect.i ? curSelect.i : oldSelect.i;
            if(q - p == 1) return result;
            for(int i = p+1; i<q; i++){
                if(spirits[i][curSelect.j].imageId != 0) result = false;
            }
        }
        //如果curSelect和oldSelect不再同一行同一列,當然不可能用一條線連結起來
        if(curSelect.i != oldSelect.i && curSelect.j != oldSelect.j){
            result = false;
        }
        return result;
    }
    
    //判斷兩個Spirit能否用兩條線連線起來,如果能夠用兩條線連結起來,它們之間肯定只有一箇中間結點
    //這個中間結點只能是spirits[curSelect.i][oldSelect.j]或者spirits[oldSelect.i][curSelect.j]
    //然後可以遞迴為分別判斷這兩個中間結點能不能用一條線和curSelect以及oldSelect連結起來
    //如果能,返回值為中間結點,如果不能,返回值為null
    private static Spirit isLinkedByTwoLine(Spirit[][] spirits, Spirit curSelect, Spirit oldSelect){
        if(isLinkedByOneLine(spirits, curSelect, spirits[curSelect.i][oldSelect.j]) 
                && isLinkedByOneLine(spirits, oldSelect, spirits[curSelect.i][oldSelect.j])
                && spirits[curSelect.i][oldSelect.j].imageId == 0){
            return spirits[curSelect.i][oldSelect.j];
        }
        if(isLinkedByOneLine(spirits, curSelect, spirits[oldSelect.i][curSelect.j]) 
                && isLinkedByOneLine(spirits, oldSelect, spirits[oldSelect.i][curSelect.j])
                && spirits[oldSelect.i][curSelect.j].imageId == 0){
            return spirits[oldSelect.i][curSelect.j];
        }
        return null;
    }
    
    //判斷兩個Spirit能否用三條線連線起來
    //可以遞迴為判斷在curSelect的同一行或同一列的所有空元素能否用兩條線和oldSelect連線起來
    //如果能,返回值為兩個中間結點,如果不能,返回值為空
    private static Spirit[] isLinkedByThreeLine(Spirit[][] spirits, Spirit curSelect, Spirit oldSelect){
        Spirit[] nodes = new Spirit[2];
        //為了儘量找到短一點的路徑,所以從curSelect和oldSelect的中間開始向上下左右四個方向分別掃描
        //向上
        for(int i=(curSelect.i+oldSelect.i)/2; i>=0; i--){
            nodes[0] = spirits[i][curSelect.j];
            nodes[1] = isLinkedByTwoLine(spirits, nodes[0], oldSelect);
            if(isLinkedByOneLine(spirits, curSelect, nodes[0])
                    && nodes[1] != null && nodes[0].imageId == 0){
                return nodes;
            }
        }
        //向下
        for(int i=(curSelect.i+oldSelect.i)/2; i<M; i++){
            nodes[0] = spirits[i][curSelect.j];
            nodes[1] = isLinkedByTwoLine(spirits, nodes[0], oldSelect);
            if(isLinkedByOneLine(spirits, curSelect, nodes[0])
                    && nodes[1] != null && nodes[0].imageId == 0){
                return nodes;
            }
        }
        //向左
        for(int j=(curSelect.j+oldSelect.j)/2; j>=0; j--){
            nodes[0] = spirits[curSelect.i][j];
            nodes[1] = isLinkedByTwoLine(spirits, nodes[0], oldSelect);
            if(isLinkedByOneLine(spirits, curSelect, nodes[0])
                    && nodes[1] != null && nodes[0].imageId == 0){
                return nodes;
            }
        }
        //向右
        for(int j=(curSelect.j+oldSelect.j)/2; j<N; j++){
            nodes[0] = spirits[curSelect.i][j];
            nodes[1] = isLinkedByTwoLine(spirits, nodes[0], oldSelect);
            if(isLinkedByOneLine(spirits, curSelect, nodes[0])
                    && nodes[1] != null && nodes[0].imageId == 0){
                return nodes;
            }
        }
        return null;
    }
    
    public static boolean findPath(Spirit[][] spirits,Spirit curSelect, Spirit oldSelect, List<Spirit> path){
        //情況一,兩個Spirit能用一條線連線起來
        if(isLinkedByOneLine(spirits, curSelect, oldSelect)){
            path.add(curSelect);
            path.add(oldSelect);
            return true;
        }
        
        //情況二,兩個Spirit能用兩條線連線起來
        Spirit temp = isLinkedByTwoLine(spirits, curSelect, oldSelect);
        if(temp != null){
            path.add(curSelect);
            path.add(temp);
            path.add(oldSelect);
            return true;
        }
        
        //情況三,兩個Spirit能用三條線連線起來
        Spirit[] temps = isLinkedByThreeLine(spirits, curSelect, oldSelect);
        if(temps != null){
            path.add(curSelect);
            path.add(temps[0]);
            path.add(temps[1]);
            path.add(oldSelect);
            return true;
        }
        
        return false;
    }
}


上面三個就是這個遊戲的主要實現了。另外三個程式碼如下:
4、plugin.xml,就只定義了一個檢視,兩個Command及其menuContributions和Handler:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>

   <extension
         id="application"
         point="org.eclipse.core.runtime.applications">
      <application>
         <run
               class="com.xkland.examples.rcp.javalinkgame.Application">
         </run>
      </application>
   </extension>
   <extension
         point="org.eclipse.ui.perspectives">
      <perspective
            name="RCP Perspective"
            class="com.xkland.examples.rcp.javalinkgame.Perspective"
            id="com.xkland.examples.rcp.javalinkgame.perspective">
      </perspective>
   </extension>
   <extension
         point="org.eclipse.ui.views">
      <view
            class="com.xkland.examples.rcp.javalinkgame.GameView"
            id="JavaLinkGame.views.game"
            name="Game View"
            restorable="true">
      </view>
   </extension>
   <extension
         point="org.eclipse.ui.perspectiveExtensions">
      <perspectiveExtension
            targetID="*">
         <view
               id="JavaLinkGame.views.game"
               minimized="false"
               relationship="left"
               relative="org.eclipse.ui.editorss">
         </view>
      </perspectiveExtension>
   </extension>
   <extension
         point="org.eclipse.ui.commands">
      <command
            id="JavaLinkGame.commands.startGame"
            name="Start Game">
      </command>
      <command
            id="JavaLinkGame.commands.search"
            name="Search">
      </command>
   </extension>
   <extension
         point="org.eclipse.ui.menus">
      <menuContribution
            allPopups="false"
            locationURI="toolbar:JavaLinkGame.views.game">
         <command
               commandId="JavaLinkGame.commands.startGame"
               icon="icons/StartIcon.png"
               style="push"
               tooltip="開始遊戲">
         </command>
         <command
               commandId="JavaLinkGame.commands.search"
               icon="icons/SearchIcon.png"
               style="push"
               tooltip="給點提示">
         </command>
      </menuContribution>
   </extension>
   <extension
         point="org.eclipse.ui.handlers">
      <handler
            class="com.xkland.examples.rcp.javalinkgame.GameStartHandler"
            commandId="JavaLinkGame.commands.startGame">
      </handler>
      <handler
            class="com.xkland.examples.rcp.javalinkgame.SpiritSearchHandler"
            commandId="JavaLinkGame.commands.search">
      </handler>
   </extension>

</plugin>


5、GameStartHandler類的程式碼,點選工具欄左邊那個帶旗子的圖示,開始遊戲:

package com.xkland.examples.rcp.javalinkgame;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.ui.PlatformUI;

public class GameStartHandler extends AbstractHandler {

    @Override
    public Object execute(ExecutionEvent event) throws ExecutionException {
        GameView view = (GameView)PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
                .findView("JavaLinkGame.views.game");
        GameAlgorithmUtil.fillSpirits(view.getSpirits());
        GameAlgorithmUtil.shuffle(view.getSpirits());
        view.getCanvas().redraw();
        return null;
    }

}


6、SpiritSearchHandler類的程式碼,也就是點選右邊那個放大鏡圖示時,自動尋找一對能夠連通的方塊:

package com.xkland.examples.rcp.javalinkgame;

import java.util.LinkedList;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.ui.PlatformUI;

public class SpiritSearchHandler extends AbstractHandler {

    @Override
    public Object execute(ExecutionEvent event) throws ExecutionException {
        GameView view = (GameView)PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
                .findView("JavaLinkGame.views.game");
        Spirit[][] spirits = view.getSpirits();
        Spirit first;
        Spirit second;
        for(int i=0; i<GameAlgorithmUtil.M; i++){
            for(int j=0; j<GameAlgorithmUtil.N; j++){
                first = spirits[i][j];
                //再用兩層迴圈找second
                for(int m=0; m<GameAlgorithmUtil.M; m++){
                    for(int n=0; n<GameAlgorithmUtil.N; n++){
                        second = spirits[m][n];
                        //如果first和second之間有路徑相連,則推薦它們
                        if(first.imageId != 0 && second.imageId != 0 && first != second
                                && first.imageId == second.imageId
                                && GameAlgorithmUtil.findPath(spirits, first, second, new LinkedList<Spirit>())){
                            first.recommend();
                            second.recommend();
                            view.getCanvas().redraw();
                            return null;
                        }
                    }
                }
            }
        }
        return null;
    }
}



完整的專案壓縮檔案如下:
JavaLinkGame.zip

該專案是在Ubuntu下寫的,下載後使用Eclipse可以直接匯入。如果是在Windows下使用的話,一定記得在專案的屬性中將字元編碼改成UTF-8,換行風格改成Unix風格。否則出現亂碼。

在Windows 7中執行的截圖:


  該遊戲在Ubuntu中執行很流暢,但是在Windows7有點閃爍,要解決這個問題需要用到double buffer。另外,由於不想增加額外的複雜性,我沒有使用多執行緒,所以方塊的消除是在下一次點選滑鼠時完成的,使用者體驗略差。
  如果想用多執行緒,就得更改程式的結構,不能直接在MouseListener中處理滑鼠點選事件,而是應該另外建立一個佇列,將所有的操作,包括定時器到期的操作,都發送到佇列中,然後在佇列另一端使用一個消費者消費這些事件。由於我不是在講併發程式設計,這裡就不詳細展開了。我以前用MFC做了一個俄羅斯方塊小遊戲,就是把所有的操作都發送到佇列中,大家可以參考,部落格在這裡:寫個小遊戲練一練手

相關資料

官方驗證:

專案截圖:

專案截圖

執行時需要用Run As ..Eclipse Application,但是沒有執行成功,估計是和我本地的jdk7有關

java.lang.UnsatisfiedLinkError: Cannot load 32-bit SWT libraries on 64-bit JVM

大家有誰驗證過的可以提供下說明。 

相關推薦

Eclipse SWT開發教程以及一個連連遊戲程式碼實現下載

  我在前面講過:如果講GUI程式設計一味只講各個控制元件的使用方法,那麼純粹是浪費大家時間,如果出書,那絕對是騙錢的。所以我並不會詳細地講解SWT各個控制元件的具體使用方法。然而的眾所周知,Eclipse的UI介面是建立在SWT基礎之上的,如果一字不提SWT,似乎也不

Eclipse swt開發環境搭建

swt1.下載swt,格式為.zip,<p>swt<a href="http://archive.eclipse.org/eclipse/downloads/drops4/R-4.4.2-201502041700/index.php#SWT" target="_blank"><s

詳解連連遊戲開發流程

遊戲介紹:       “連連看”是一款來源於我國臺灣的桌面小遊戲,主要考驗的是玩家們的眼力,在有限的時間內,只要能把所有能連線的相同圖案,兩個兩個的找出來,每找到一對,它們就會自動消失,只要能把所有的圖案全部消完即可獲得勝利。所謂能夠連線,是指無論橫向還是縱向,從

win32遊戲開發(2) --連連遊戲製作(vc++版)

工程目錄一覽 檔案功能及關係圖: GameEngine類 成員名 作用 static GameEngine * m_pGameEngine 指向自身的指標,供外界程式訪問 HINSTANCE m_hInstance 例項控制

利用Python制作一個連連遊戲,邊學邊玩!

ffffff 生成 判斷 fab 精靈類 簡介 用戶 之間 自帶 導語 今天我們將制作一個連連看小遊戲,讓我們愉快地開始吧~ 開發工具 Python版本:3.6.4 相關模塊: pygame模塊; 以及一些Python自帶的模塊 環境搭建 安裝Python並添加到環境變量,

jfinalQ開發教程01-表命名規範和程式碼生成

mysql 1.選擇 java可以配套各種資料庫,oracle,mysql,sqlserver等等, 但是就個人開發或者中小公司開發,無疑mysql是最好的推薦, 所以jfinalQ預設使用mysql資料庫,其他資料庫的支援有待完善。 2.版本 5.6.17

JAVA類的屬性以及一個簡單的Java程式碼

寫在前面 最近著手學習Java,根據網課以及課本自學,網課是中國大學MOOC上邊有許多關於Java的教程,以及一個網站—>網課網址,而課本就是比較有名的《Head first Java》,這個在Java入門裡面算是比較經典的教材,網上有推薦《think in Java

[原始碼和文件分享]基於C#的單機版連連遊戲設計與實現

摘 要 遊戲“連連看”,只要將相同花色的兩張牌用三根以內的直線連在一起就可以消除,規則簡單容易上手。遊戲速度節奏快,畫面清晰可愛,老少皆宜。豐富的道具和公共模式的加入,增強遊戲的競爭性。多樣式的地圖,使玩家在各個遊戲水平都可以尋找到挑戰的目標,長期地保持遊戲的新鮮感。使用新穎的連擊積分規則,使遊

連連遊戲核心程式碼(C++實現

         這兩天研究了一下連連看遊戲的原始碼,感覺它挺簡單的,主要就是判斷選中的兩張圖片能否消去。我參考了網上的原始碼(抱歉的是,不記得當時下載的網址了,在此對原作者表示深深的歉意!),然後自己把核心程式碼整理如下,與大家共享。需要說明的是,這只是核心演算法的程式碼

連連遊戲消除演算法

今天在收到一道的面試題,覺得比較有意思,決定記錄下來,整個題目與解答過程大概如下。 連連看是一種很受大家歡迎的小遊戲。下面四張圖給出了最基本的消除規則: 圖 A 中出現在同一直線上無障礙的圈圈可以消除;圖 B 中兩個圈圈可以通過一次轉彎消除;圖 C 和圖 D 中,兩個

Delphi容器類之---TList、TStringList、TObjectList,以及一個例程的程式碼分析

  看了這裡標題,大家可能以為我會談TListBox控制元件,那就錯了。我要談的是Delphi提供給我們的具有列表性質的類:TStringList、TList和TObjectList。TStringList用來存放字串,TList存放指標,而TObjectList則存

JavaScript 一個文字跑馬燈程式碼實現 可以設定文字內容、左右滾動方向、文字大小和顏色、滾動速度

JavaScript 一個文字跑馬燈程式碼實現,可以設定文字內容、左右滾動方向、文字大小及顏色、滾動速度,暫停和繼續播放。這是一個比較簡單,甚至完全可以用css實現,其實就是滿足小朋友興趣愛好寫的這麼一個demo。看似簡單,其實這裡還是有些實現細節需要注意,所以就跟大家分享下。 1、頁面

mysql資料庫的連線以及增刪改查Java程式碼實現(Statement版)

資料庫: create table t1(id int primary key not null auto_increment,name varchar(32),password varchar(32)); insert into t1(name,password) va

mysql資料庫的連線以及增刪改查Java程式碼實現(PreparedStatement版)

資料庫: create table t1(id int primary key not null auto_increment,name varchar(32),password varchar(32)); insert into t1(name,password) v

一個 11 行 Python 程式碼實現的神經網路

概要:直接上程式碼是最有效的學習方式。這篇教程通過由一段簡短的 python 程式碼實現的非常簡單的例項來講解 BP 反向傳播演算法。 程式碼如下: Python 1234567891011X=np.array([

Window XP驅動開發(十七) 晶片韌體程式設計 (程式碼實現,針對USB2.0 晶片CY7C68013A)

 一、韌體的修改 1、修改VID、PID 1、1   修改hex檔案中的VID與PID 在韌體工程下的dscr.a51檔案中修改。 1、2  修改EEPROM中的VID、PID 其中IIC_Hdr結構體中存放預設的VID、PID:    IIC_HDR IIC_

關於LDA學習的一些有用的部落格以及大牛寫的程式碼實現

1、Blei的LDA程式碼(C):http://www.cs.princeton.edu/~blei/lda-c/index.html 2、D.Bei的主頁:http://www.cs.princeton.edu/~blei/publications.html 3、Gibb

就懂的手機APP開發教程

軟件開發 移動開發 界面 服務端 平臺 android 教程 效率 疑問 現在的移動互聯網屬於全民的狂歡時代,是每個人、每個用戶、每個企業的歡暢淋漓的時代,所以APP正在勢如破竹地開拓廣闊的市場。手機APP開發指的是專註於手機應用軟件開發與服務,是當前最為迫切的需求。無獨有

初學安卓開發隨筆之 Menu、toast 用法、活動的四種啟動模式 以及 一個方便的Base活動類使用方法

pro 一點 cte edi standard oid nal xtend 解釋 Toast toast 是安卓系統的一種非常棒的提醒方式 首先定義一個彈出Toast的觸發點,比如可以是按鈕之類 其中 Toast.LENGTH_SHORT是指顯示時長 還有一個內置變量為To

PythonWeb開發教程(二),搭建第一個django項目

translate -s 分享圖片 ble show main tab table python 這篇寫怎麽創建django項目,以及把django項目運行起來。 1、創建django項目 a.使用命令創建,安裝完django之後就有djang