Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现

上一篇
Eclipse RCP详解(02):Eclipse的Runtime和UI初探

  我在前面讲过:如果讲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类的代码:
Spirit.java
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、视图类的代码:
GameView.java
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(null24011970);
                        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类的代码,算法的实现都在这里面了,有详细的注释:
GameAlgorithmUtil.java
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 == 1return 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 == 1return 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:
plugin.xml
<?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类的代码,点击工具栏左边那个带旗子的图标,开始游戏:
GameStartHandler.java
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类的代码,也就是点击右边那个放大镜图标时,自动寻找一对能够连通的方块:
SpiritSearchHandler.java
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中运行的截图:
Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现_第1张图片

  该游戏在Ubuntu中运行很流畅,但是在Windows7有点闪烁,要解决这个问题需要用到double buffer。另外,由于不想增加额外的复杂性,我没有使用多线程,所以方块的消除是在下一次点击鼠标时完成的,用户体验略差。
  如果想用多线程,就得更改程序的结构,不能直接在MouseListener中处理鼠标点击事件,而是应该另外建立一个队列,将所有的操作,包括定时器到期的操作,都发送到队列中,然后在队列另一端使用一个消费者消费这些事件。由于我不是在讲并发编程,这里就不详细展开了。我以前用MFC做了一个俄罗斯方块小游戏,就是把所有的操作都发送到队列中,大家可以参考,博客在这里: 写个小游戏练一练手

下一篇:
   Eclipse RCP详解(04):Eclipse RCP相关的学习资料及国内相关图书点评

你可能感兴趣的:(Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现)