“方块游戏”简介
“方块”游戏使用一个3x3的网格,其中每一个单元格要么显示一种颜色,要么什么都没有(表示为黑色)。游戏开始时一些单元格随机填充颜色,其他的都用默认黑色。只要你在30秒内清除所有单元格的颜色(全部变为黑色,没有其他颜色存在),你就获胜了。



你要么移动鼠标点击一个单元格,要么直接按小键盘的相应数字键,都可以清除那个单元格里的颜色。类似的,如果你所点击的单元格本身是黑色,那么那个单元格就会被填充一种其他颜色。也就是说会有这样的循环:黑色变彩色,彩色变黑色。如果仅仅这样游戏就太容易了,因此我设计的方块游戏是,你对单元格的点击/按键会影响他自己和他的周围单元格,如图1所示。




图1. (A) 游戏板布局;(B) 当单元格1改变而受到影响的单元格;(C) 当单元格2改变而受到影响的单元格;(D) 当单元格5改变而受到影响的单元格



图1根据数字小键盘的布局显示了相应的游戏板。例如,数字键7对应左上角的单元格。图1中还展示了当一个单元格改变而受到影响的相应单元格(B、C、D中)。如果改变的是角上的,周围三个单元格也会受到影响(B);如果你改变的是边上的,同一边的其他两个单元格也会受到影响(C);如果改变的是中心的,它东南西北的单元格也都会受影响(D)。



用Java重写



我最早是用C写的“方块”游戏。因为C和Java的语法很相似,所以用Java重写并不困难。在我展示我的第一个“方块”applet的代码之前,你大概想知道界面是怎样的。图2显示了你运行那个applet时的界面。




图2. 包含一个游戏板、两个按钮的“方块”游戏界面



游戏板控件是一个类似于“石头剪子”游戏的网格的区域,并且在它下边有一个白色的消息区域。这个控件还有一个边框,这个边框在空间失去焦点的时候是黑色的,在获得焦点时变成蓝色。“Change Square Color”按钮初始时无效,只有游戏开始以后才可用(如果游戏没有进行,也就没理由改变颜色了)。点击“Start”按钮可以开始游戏,如图3所示。




图3. “方块”游戏开始以后,在游戏板的消息区域会显示当前剩余的秒数



图3显示了游戏进行时的界面。消息区显示了把所有单元格变为黑色还剩余的秒数。如果这个数字到达0,你就输了。如果你能在此之前把所有单元格变为黑色,那你就赢了。在游戏进行时,你可以点击“Change Square Color”按钮以随机改变各单元的颜色。不过如果你输了或者赢了,那“Change Square Color”按钮会变成无效,而“Start”按钮会恢复有效,这样你就可以开始另一个游戏了。



下边是源代码:


Squares.java


// Squares.java

import java.awt.*;
import java.awt.event.*;
import java.util.Random;
import javax.swing.*;

public class Squares extends JApplet
{
   private void createGUI ()
   {
       // 设定界面
      getContentPane ().setLayout (new FlowLayout ());

      // 创建游戏板控件:每个单元格有40像素宽,默认绿色,并且在获得焦点时边框是蓝色,
   // 而失去焦点时变为黑色。把控件加到content pane里。
      final GameBoard gb;
      gb = new GameBoard (40, Color.green, Color.blue, Color.black);
      getContentPane ().add (gb);

      // 界面其他部分包括两个按钮,他们会被放到一个panel里以作为整体处理。例如,
      // 如果Applet的宽度变大了,两个按钮(而不是一个按钮)都会向游戏板的右侧对齐。
      JPanel p = new JPanel ();

      // 创建“Change Square Color”按钮并设置为无效。只有游戏进行中可以改变颜色。
    final JButton btnChangeSquareColor = new JButton ("Change Square Color");
      btnChangeSquareColor.setEnabled (false);

      // 建立“Change Square Color”按钮的action事件监听器,点击此按钮,会随机改变
   // 单元格的颜色
      ActionListener al;
      al = new ActionListener ()
           {
               public void actionPerformed (ActionEvent e)
               {
                  Random rnd = new Random ();

                  while (true)
                  {
                      int r = rnd.nextInt (256);
                      int g = rnd.nextInt (256);
                      int b = rnd.nextInt (256);

                      // 不使用所有组成原色(红、绿、蓝)都小于192的颜色,因为那不
                      // 容易和背景的黑色区分出来。
                      if (r < 192 && g < 192 && b < 192)
                          continue;

                      gb.changeSquareColor (new Color (r, g, b));

                      break;
                  }
               }
           };

      btnChangeSquareColor.addActionListener (al);

      p.add (btnChangeSquareColor);

      // 创建“Start”按钮
    final JButton btnStart = new JButton ("Start");

      // 建立“Start”按钮的action事件监听器。点击这个按钮时,它本身会变为无效(没
      // 理由开始一个正在进行的游戏),并使“Change Square Color”按钮有效(游戏进
      // 行时可以改变单元格颜色)。“done”事件监控器则用于在游戏结束时使“Start”按
      // 钮有效,以及使“Change Square Color”按钮无效。
      al = new ActionListener ()
           {
               public void actionPerformed (ActionEvent e)
               {
                  btnStart.setEnabled (false);
                  btnChangeSquareColor.setEnabled (true);

                  gb.start (new GameBoard.DoneListener ()
                            {
                                public void done ()
                                {
                                   btnStart.setEnabled (true);
                                   btnChangeSquareColor.setEnabled (false);
                                }
                            });
               }
           };

      btnStart.addActionListener (al);

      // 通过一个panel把两个按钮添加到content pane里边。
      p.add (btnStart);

      getContentPane ().add (p);

      // 在Java 1.4.0里,如果不设置JApplet为焦点循环根节点、并且新建一个焦点遍历
      // 规则的话,你就没有办法把焦点从一个控件切换到另一个。你可以在以下链接看到相关信
      // 息:http://bugs.sun.com/bugdatabase/view_bug.do;:YfiG?bug_id=4705205 
      if (System.getProperty ("java.version").equals ("1.4.0"))
      {
          setFocusCycleRoot (true);
          setFocusTraversalPolicy (new LayoutFocusTraversalPolicy ());
      }
   }

   public void init ()
   {
      // Sun的Java教程说Swing控件应该在事件处理线程里创建、查询、以及操作。由于大
      // 多数浏览器都不去调用Applet的主如init()的那些主要方法,我们在那个线程里调
      // 用SwingUtilities.invokeAndWait()以保证在事件处理线程里GUI被正确创建。
      // 我们用invokeAndWait()而不是invokeLater(),因为后者会导致在GUI创建之前
      // init()方法会返回;这会造成一些很难跟踪的applet问题。
      try
      {
          SwingUtilities.invokeAndWait (new Runnable ()
                                        {
                                            public void run ()
                                            {
                                               createGUI ();
                                            }
                                        });
      }
      catch (Exception e)
      {
          System.err.println ("Unable to create GUI");
      }
   }
}

class GameBoard extends JPanel
{
   // 游戏状态
   private final static int INITIAL = 0;
   private final static int INPLAY = 1;
   private final static int LOSE = 2;
   private final static int WIN = 3;

   // 边框尺寸
   private final static int BORDER_SIZE = 5;

   // 当前游戏状态
   private int state = INITIAL;

   // 在单元格边框之间的像素宽度
   private int cellSize;

   // 游戏板的宽度(包含边框)
   private int width;

   // 游戏板及消息区的总计高度(包含边框)
   private int height;

   // 每一个单元格的颜色
   private Color squareColor;

   // 在游戏板拥有焦点时的边框颜色
   private Color focusBorderColor;

   // 在游戏板是去焦点时的边框颜色
   private Color nonfocusBorderColor;

   // 游戏板当前的边框颜色
   private Color borderColor;

   // 单元格状态:true代表特定单元格包含一个有颜色的方块(非黑色)
   private boolean [] cells = new boolean [9];

   // 对游戏结束监听器的引用
   private GameBoard.DoneListener dl;

   // 对倒计时的计时器的引用;这个计数器判断玩家时候获胜/失败,并且通知当游戏结束时通
   // 知DoneListener
   private Timer timer;

   // 计时器的计时数字
   private int counter;

   // 游戏板构造函数
   GameBoard (int cellSize, Color squareColor, Color focusBorderColor,
              Color nonfocusBorderColor)
   {
      this.cellSize = cellSize;

      width = 3*cellSize+2+2*BORDER_SIZE;
      height = width + 50;

      setPreferredSize (new Dimension (width, height));

      this.squareColor = squareColor;


      this.focusBorderColor = focusBorderColor;


      this.nonfocusBorderColor = nonfocusBorderColor;

      this.borderColor = nonfocusBorderColor;

      addFocusListener (new FocusListener ()
                        {
                            public void focusGained (FocusEvent e)
                            {
                               borderColor = GameBoard.this.focusBorderColor;

                               repaint ();
                            }

                            public void focusLost (FocusEvent e)
                            {
                               borderColor = GameBoard.this.nonfocusBorderColor;

                               repaint ();
                            }
                        });

      addKeyListener (new KeyAdapter ()
                      {
                          public void keyTyped (KeyEvent e)
                          {
                             if (state != INPLAY)
                               return;

                             char key = e.getKeyChar ();

                             // 如果玩家通过数字小键盘输入,则将输入映射到相应的单
                             // 元格,并对此单元格及其周围的单元格做出相应变动。
                             if (Character.isDigit (key))
                                 switch (key)
                                 {
                                    case '1': GameBoard.this.toggle (6);
                                              break;

                                    case '2': GameBoard.this.toggle (7);
                                              break;

                                    case '3': GameBoard.this.toggle (8);
                                              break;


                                    case '4': GameBoard.this.toggle (3);
                                              break;

                                    case '5': GameBoard.this.toggle (4);
                                              break;

                                    case '6': GameBoard.this.toggle (5);
                                              break;

                                    case '7': GameBoard.this.toggle (0);
                                              break;

                                    case '8': GameBoard.this.toggle (1);
                                              break;

                                    case '9': GameBoard.this.toggle (2);
                                 }
                          }
                      });

      addMouseListener (new MouseAdapter ()
                        {
                            public void mouseClicked (MouseEvent e)
                            {
                               if (state != INPLAY)
                                 return;

                               // 当鼠标点击游戏板时,确保游戏板获得焦点,以便玩家
                               // 使用键盘作为替代输入方法。
                               GameBoard.this.requestFocusInWindow ();

                               // 哪一个单元格被点击?
                               int cell = GameBoard.this.
                                          mouseToCell (e.getX (), e.getY ());

                               // 如果一个单元格被点击(cell != -1),则翻转那个
                               // 单元格及其邻居的颜色。
                               if (cell != -1)
                                   GameBoard.this.toggle (cell);
                            }
                        });

      setFocusable (true);
   }

   // 修改当前单元格的颜色。注意:这个方法被事件处理线程调用
   void changeSquareColor (Color squareColor)
   {
      if (!SwingUtilities.isEventDispatchThread ())
          return;

      this.squareColor = squareColor;
      repaint ();
   }

   // 绘制组件:先画边框,对后画消息
   public void paintComponent (Graphics g)
   {
      // 推荐首先调用父类的paintComponent()
      super.paintComponent (g);

      // 用当前边框颜色绘制四边
      g.setColor (borderColor);
      for (int i = 0; i < BORDER_SIZE; i++)
           g.drawRect (i, i, width-2*i-1, height-2*i-1);

      // 将组件的游戏板画为黑色(除了边框及消息区)
      g.setColor (Color.black);
      g.fillRect (BORDER_SIZE, BORDER_SIZE, width-2*BORDER_SIZE,
                  width-2*BORDER_SIZE);

      // 画游戏板的水平线
      g.setColor (Color.white);
      g.drawLine (BORDER_SIZE, BORDER_SIZE+cellSize,
                  BORDER_SIZE+width-2*BORDER_SIZE-1, BORDER_SIZE+cellSize);

      g.drawLine (BORDER_SIZE, BORDER_SIZE+2*cellSize+1,
                  BORDER_SIZE+width-2*BORDER_SIZE-1, BORDER_SIZE+2*cellSize+1);

      // 画游戏板的垂直线
      g.drawLine (BORDER_SIZE+cellSize, BORDER_SIZE, BORDER_SIZE+cellSize,
                  BORDER_SIZE+width-2*BORDER_SIZE-1);

      g.drawLine (BORDER_SIZE+2*cellSize+1, BORDER_SIZE,
                  BORDER_SIZE+2*cellSize+1, BORDER_SIZE+width-2*BORDER_SIZE-1);

      // 画方格
      g.setColor (squareColor);
      for (int i = 0; i < cells.length; i++)
      {
           if (cells [i])
           {                           
               int x = BORDER_SIZE+(i%3)*(cellSize+1)+3;
               int y = BORDER_SIZE+(i/3)*(cellSize+1)+3;

               int w = cellSize-6;
               int h = w;

               g.fillRect (x, y, w, h);
           }
      }

      // 将消息区画为白色(在游戏板下方,边框之内)
      g.setColor (Color.white);
      g.fillRect (BORDER_SIZE, width-BORDER_SIZE, width-2*BORDER_SIZE,
                  height-width);

      // 如果游戏板不是初始化状态,则打印出相应消息
      if (state != INITIAL)
      {
          g.setColor (Color.black);

          String text;

          switch (state)
          {
             case LOSE:
                  text = "YOU LOSE!";
                  break;

             case WIN:
                  text = "YOU WIN!";

                  break;

             default:
                  text = "" + counter;
          }

          g.drawString (text, (width-g.getFontMetrics ().stringWidth (text))/2,
                        width-BORDER_SIZE+30);
      }
   }

   // 如果游戏不再进行中,则开始一个新游戏。注册游戏结束监听器,并且初始化一个方块颜色
   // 的图案,同时启动一个间隔为1秒的计时器。注意:这个方法将被事件处理线程调用。
   void start (GameBoard.DoneListener dl)
   {
      if (!SwingUtilities.isEventDispatchThread ())
          return;

      if (state == INPLAY)
          return;

      this.dl = dl;

      Random rnd = new Random ();

      while (true)
      {
         for (int i = 0; i < cells.length; i++)
              cells [i] = rnd.nextBoolean ();

         int counter = 0;
         for (int i = 0; i < cells.length; i++)
              if (cells [i])
                  counter++;

         if (counter != 0 && counter != cells.length)
             break;
      }

      ActionListener al;
      al = new ActionListener ()
           {
               public void actionPerformed (ActionEvent e)
               {
                  // 如果玩家赢了,则通知游戏结束监听器
                  if (state == WIN)
                  {
                      timer.stop ();
                      GameBoard.this.dl.done ();
                      return;
                  }

                  // 如果计时器到达0,则玩家输了;通知游戏结束监听器
                  if (--counter == 0)
                  {
                      state = LOSE;
                      timer.stop ();
                      GameBoard.this.dl.done ();
                  }

                  repaint ();
               }
           };

      timer = new Timer (1000, al);

      state = INPLAY;
      counter = 30;
                
      timer.start ();
   }

   // 将鼠标位置映射到单元格编号[0,8],如果鼠标坐标在任何单元格之外,则返回-1。
   private int mouseToCell (int x, int y)
   {
       // 检查第一列
       if (x >= BORDER_SIZE && x < BORDER_SIZE+cellSize)
       {
           if (y >= BORDER_SIZE && y < BORDER_SIZE+cellSize)
               return 0;

           if (y >= BORDER_SIZE+cellSize+1 && y < BORDER_SIZE+2*cellSize+1)
               return 3;

           if (y >= BORDER_SIZE+2*cellSize+2 && y < BORDER_SIZE+3*cellSize+2)
               return 6;
       }

       // Examine second column.
       // 检查第二列
       if (x >= BORDER_SIZE+cellSize+1 && x < BORDER_SIZE+2*cellSize+1)
       {
           if (y >= BORDER_SIZE && y < BORDER_SIZE+cellSize)
               return 1;

           if (y >= BORDER_SIZE+cellSize+1 && y < BORDER_SIZE+2*cellSize+1)
               return 4;

           if (y >= BORDER_SIZE+2*cellSize+2 && y < BORDER_SIZE+3*cellSize+2)
               return 7;
       }

       // 检查第三列
       if (x >= BORDER_SIZE+2*cellSize+2 && x < BORDER_SIZE+3*cellSize+2)
       {
           if (y >= BORDER_SIZE && y < BORDER_SIZE+cellSize)
               return 2;

           if (y >= BORDER_SIZE+cellSize+1 && y < BORDER_SIZE+2*cellSize+1)
               return 5;

           if (y >= BORDER_SIZE+2*cellSize+2 && y < BORDER_SIZE+3*cellSize+2)
               return 8;
       }

       return -1;
   }

   // 翻转一个单元格及其周围的颜色。文中图1A展示了如下遵循数字键盘布局的单元格映射表:
   // 7 8 9
   // 4 5 6
   // 1 2 3
   //
   // 由于单元格数组从0开始,更容易使用的映射方式如下图所示:
   // 0 1 2
   // 3 4 5
   // 6 7 8
   //
   // 当调用toggle(),调用的代码必须把数字键(1-9)转换为如上所示的索引(0-8)。
   private void toggle (int cell)
   {
      // 切换单元格颜色
      switch (cell)
      {
         case 0: cells [0] = !cells [0];
                 cells [1] = !cells [1];
                 cells [3] = !cells [3];
                 cells [4] = !cells [4];
                 break;

         case 1: cells [0] = !cells [0];
                 cells [1] = !cells [1];
                 cells [2] = !cells [2];
                 break;

         case 2: cells [1] = !cells [1];
                 cells [2] = !cells [2];
                 cells [4] = !cells [4];
                 cells [5] = !cells [5];
                 break;

         case 3: cells [0] = !cells [0];
                 cells [3] = !cells [3];
                 cells [6] = !cells [6];
                 break;

         case 4: cells [0] = !cells [0];
                 cells [2] = !cells [2];
                 cells [4] = !cells [4];
                 cells [6] = !cells [6];
                 cells [8] = !cells [8];
                 break;

         case 5: cells [2] = !cells [2];
                 cells [5] = !cells [5];
                 cells [8] = !cells [8];
                 break;

         case 6: cells [3] = !cells [3];
                 cells [4] = !cells [4];
                 cells [6] = !cells [6];
                 cells [7] = !cells [7];
                 break;

         case 7: cells [6] = !cells [6];
                 cells [7] = !cells [7];
                 cells [8] = !cells [8];
                 break;

         case 8: cells [4] = !cells [4];
                 cells [5] = !cells [5];
                 cells [7] = !cells [7];
                 cells [8] = !cells [8];
      }

      // 检测玩家是否获胜。这段代码放在这儿不和递减计时器及判断玩家是否失败的代码一块儿放到
//start()方法的事件监听器,否则如果玩家碰巧把所有方块都交换成黑色,而又立刻换成了其它颜色,
//结果本来该获胜的玩家却被判输了。这种办法不可取。
      int i;
      for (i = 0; i < cells.length; i++)
           if (cells [i])
               break;

      if (i == cells.length)
          state = WIN;

      // 绘制游戏板,以及单元的颜色
      repaint ();
   }

   // 游戏结束监听器的接口定义。Start()方法接受一个实现此接口的对象作为参数。
   interface DoneListener
   {
      void done ();
   }
}



由于已经包含了丰富的注释,我们不再重述。这里我要强调两点。


-我并没有用运行JApplet的public void init()方法的线程创建GUI,而是把创建过程延迟到Swing的事件处理线程;这正是Sun的Java教程里推荐的办法。我通过把所有applet的活动限制在事件处理线程里以避免同步问题。


-在J2SE 1.4(此专栏所使用的版本)之前的版本里,聚焦系统(控制你用TAB键在组件之间切换)有很多缺陷,并且具有平台差异。J2SE 1.4通过提供java.awt.KeyboardFocusManager类、焦点循环根节点、以及焦点遍历策略来修正了聚焦系统。由于J2SE 1.4的JApplet类依赖于Abstract Window Toolkit(AWT)的焦点遍历策略(AppletViewer及Java Plug-in都使用java.awt.Frame类作为JApplet的顶级父类,因此说他们依赖于AWT的焦点遍历策略),因此如果没有外在帮助,你无法在一个JApplet里同TAB键从一个组件切换到另一个。这样的外在帮助包括将J2SE 1.4的JApplet设为焦点循环根节点,以及设定一个焦点遍历策略。此外,我在GameBoard的构造函数里调用setFocusable(true),以保证游戏板组件可以获得焦点。(尽管我们做了这么多,在我们开始游戏的时候,游戏板及两个按钮都没有得到焦点。)这个却现在J2SE 1.4及以后的版本中已经得到纠正。



音效



到目前为止,“方块”游戏并没有想象中的那么有趣。不过我们可以通过增加音效来让游戏更有趣。我们至少可以有三种音效:当玩家切换单元格(及其周围)颜色时的音效,当玩家获胜时的音效,以及当玩家失败时的音效。



我为这些情形准备了一套适当的音效,分别是toggle.au,win.au,lose.au。(我决定使用Sun的声音文件,而不是Microsoft的wave文件,以增强可移植性。)在下边从第二版的Squares.java里摘录的代码片断里,音效文件被加载到声音剪辑里,并且在applet初始化时通过构造函数传递给GameBoard。


// 加载玩家切换单元格颜色、获胜、以及失败时播放的声音剪辑。

AudioClip acToggle;

acToggle = getAudioClip (getClass ().getResource ("toggle.au"));

AudioClip acWin = getAudioClip (getClass ().getResource ("win.au"));

AudioClip acLose = getAudioClip (getClass ().getResource ("lose.au"));


// 创建游戏板组件:每个单元格有40像素宽,方块颜色是绿色,并且游戏板在得到焦点时边框是蓝色,失

// 去焦点时边框是黑色。游戏板组件被添加到content pane里。

final GameBoard gb;

gb = new GameBoard (40, Color.green, Color.blue, Color.black, acToggle,acWin, acLose);

代码片断里使用了getClass().getResource(),以便那些声音文件可以和applet的class文件一并打包到一个Jar里边。

当玩家获胜或者失败时,会播放相应的声音片断;这是在下边从GameBoard的void start(GameBoard.DoneListener dl)里取出的代码片断里实现的:

// 如果玩家获胜,则通知游戏结束监听器。

if (state == WIN)

{

    acWin.play ();

    timer.stop ();

    GameBoard.this.dl.done ();

    return;

}


// 当计时器到达0,则玩家失败,并通知游戏结束监视器。

if (--counter == 0)

{

    state = LOSE;

    acLose.play ();

    timer.stop ();

    GameBoard.this.dl.done ();

}

最后,颜色切换的声音片断会在单元格颜色被切换时播放,这是在以下从GameBoard的
private void toggle(int cell)方法里取出的代码片断中实现的:


// 绘制游戏板,以及有颜色的单元格。

repaint ();


// 播放颜色切换的声音。如果你用早期的Java 1.5.0或后期的Java 1.4.x,那有一个bug会阻止很短

// 的声音文件播放出来,因此你可能听不到声音(或者只听到的一声)。你可以在以下链接了解到更多信息:

// http://bugs.sun.com/bugdatabase/view_bug.do;:YfiG?bug_id=6251460

acToggle.play ();



可能你没法克服这个bug,不过一般在J2SE 5.0以及5.x或J2SE 1.4不会需要播放很短的声音片断(你可能顶多只听到一声,仅此而已)。例如,如果你在J2SE5.0下运行第二个(或者第三、第四个)“方块”applet,toggle.au里的声音似乎只播放了一次(我没法播放更多次)。幸运的是,再J2SE 1.4里这个问题并不存在。



视觉特效



另一个增加“方块”有戏可玩性的办法是利用视觉特效。尝试了不同特效以后,我选择了简单的办法:在玩家获胜或失败的时候显示一个从右到左、通过applet中心的滚动消息。例如,当玩家获胜时,如图4所示的“祝贺你!”信息会再applet里水平滚动。




图4 当玩家获胜或失败时,一条消息会再applet中部水平滚动



下边从第三版的Squares.java的GameBoard的start()方法里取出的代码片断显示了如何再applet使用这个视觉特效。



// 如果玩家获胜,通知游戏结束监听器,并且动态显示祝贺信息。

if (state == WIN)

{

    acWin.play ();

    timer.stop ();

    GameBoard.this.dl.done ();


    animate ("Congratulations!", Color.red);

    return;

}



// 如果计时器到达0,则玩家失败,通知游戏结束监听器,并动态显示“下次好运”的消息。

if (--counter == 0)

{

    state = LOSE;

    acLose.play ();

    timer.stop ();

    GameBoard.this.dl.done ();

    animate ("Better luck next time!", Color.red);

}



当玩家获胜时,调用animate ("Congratulations!", Color.red);

会显示一条滚动的红色“祝贺你!”信息。类似的,调用animate ("Better luck next time!", Color.red);

会显示一条“下次好运”的红色滚动信息。接下来这些方法会调用GameBoard的

private void animate(String message, Color msgColor)方法来设置合适的动画:


// 通过从左到右滚动一条消息而在玻璃板上显示动画。

private void animate (String message, Color msgColor)

{

   ActionListener al;

   al = new ActionListener ()

        {

            public void actionPerformed (ActionEvent e)

            {

               if (gp.isDone ())

               {

                   timerAnim.stop ();

                   applet.getGlassPane ().setVisible (false);

               }

            }

        };


   timerAnim = new Timer (100, al);


   gp = new GlassPane (message, msgColor);

   applet.setGlassPane (gp);

   applet.getGlassPane ().setVisible (true);


   // 阻止鼠标事件被玻璃板之下的组件截获。

  applet.getGlassPane ().addMouseListener (new MouseAdapter () {});

   applet.getGlassPane ()

         .addMouseMotionListener (new MouseMotionAdapter () {});


   timerAnim.start ();

}

animate()方法通过以下步骤实现动画:创建一个计时器,一个定制的动画组件,在JApplet的新的玻璃板组件
(它覆盖了整个applet绘图区)里安装这个动画组件,显示这个新的玻璃板,并且起动计时器。这个定制的动画
组件是一个的GameBoard里GlassPane的内部类的实例。


// GlassPane组件类

private class GlassPane extends JPanel

{

   private String text;


   private Color msgColor;


   private boolean first = true;


   private boolean done;


   private int width, height;



   private int scrollTextHeight, scrollTextWidth;


   private int xOffset, yOffset;


   private Font font;


   GlassPane (String text, Color msgColor)

   {

      this.text = text;


      this.msgColor = msgColor;


      setOpaque (false);


      font = new Font ("Serif", Font.BOLD, 24);

   }


   boolean isDone ()

   {

      repaint ();


      return done;

   }


   public void paintComponent (Graphics g)

   {      

      super.paintComponent (g);


      g.setFont (font);


      // 在第一次调用paintComponent()方法时取得玻璃板的宽和高是最容易的途径。

      if (first)

      {

          width = getWidth ();

          height = getHeight ();


          FontMetrics fm = g.getFontMetrics ();


          scrollTextWidth = fm.stringWidth (text);

          scrollTextHeight = fm.getHeight ();


          xOffset = width;

          yOffset = (height-scrollTextHeight)/2;


          first = false;

      }


      // 显示文字的阴影。

      g.setColor (Color.gray);

      g.drawString (text, xOffset+1, yOffset+1);


      // 显示文字。

      g.setColor (msgColor);

      g.drawString (text, xOffset, yOffset);


      // 计算下一个最左边的位置。如果所有的文字都已滚动出显示框的左侧,设置完成标志。

      xOffset -= 10;

      if (xOffset < -scrollTextWidth)

          done = true;

   }

}

当整段消息滚动到applet窗口的左侧(原文是右侧??),一个done变量北设置为done。
在animate()方法的事件监听器里会探测这一改变,并且相应停止计时器,并隐藏玻璃板。

尽管我对动画基本满意,不过还有两处可以改进。这可以作为给你的练习:
-尽管你对玻璃板之下的组件屏蔽了鼠标事件,但却没有屏蔽TAB键或者其他键盘事件。
你可以修改第三和第四个applet,以使消息滚动时游戏无法开始或响应用户操作。
-如果在消息滚动时如果“Start”按钮可用,会让人产生疑惑。这是由于GameBoard.this.dl.done();
方法在动画完成之前被调用。修改第三个第四个“方块”applet,保证GameBoard.this.dl.done();
在动画完成之前不被调用。


扩展关卡


尽管音效和视觉效果可以增加“方块”游戏的可玩性,但是玩多了还是会让人觉得无聊。

一旦你发现了在单元格里适当的规律,你就可以长胜不败,从而失去了玩游戏的乐趣。为了避免这一点,

引入更多的游戏关卡是很必要的。


我做出了第四个“方块”applet,其中包含了四个关卡。第一关如同前三关一样,显示的图案是方块;

第二关显示的是立方体(看点不一样的东西)。第三、第四关继续分别显示方块和立方体,不过略有不同。

在这些关卡里,有一个单元格有意不被显示出来。因为要花时间才能发现不被显示的单元格,在30秒内要把

所有单元格变黑就不是那么容易了。你每赢一关,就可进入下一关;不过如果你输了,就会退回第一关重新开始。

图5展示了游戏的第二关。



图5. 当前关卡的名字显示在消息区里


除了加入多个游戏关卡之外,我还戏剧化的修改了游戏板组件。我用一个Swing的边框取代了原先的黑-兰边框,

并且在消息区里显示当前的游戏关卡。此外,当然还有立方体图案。


编写游戏关卡的支持代码很简单。下边从GameBoard的start()方法里取出的代码片断展示了如何在玩家点击“Start”

按钮时游戏如何选择下一关的。



timer = new Timer (1000, al);

// 基于前一关的输出状态,调整相应的当前关卡。如果是第一次进行游戏,则是用默认关卡level = 1。

if (state == WIN)

{

    if (++level > MAXLEVELS)

        level = 1;

}

else

if (state == LOSE)

    level = 1;


state = INPLAY;

counter = 30;

                

timer.start ();

这个被创建的计时器就是游戏秒数的倒计时器。其它大部分关卡相关的代码可以在GameBoard的public void paintComponent(Graphics g)方法里找到。

我为你提供了以下练习,你可以试着自己完成:
-加入一关,并显示不同的形状。
-设计一关来显示16(4 x 4),25(5 x 5),或者更多的方块/立方体。你该采用什么样的颜色切换规则?玩家还可以用键盘操作么?如何去做?
-当玩家完成了第四关,游戏却什么也没做就回到了第一关,这真是糟透了。你可以在这时候家电什么有趣的东西呢?
-大部分游戏显示一个数字的分数,并提供一个排行榜。你觉得什么样的分数比较适合“方块”游戏?你怎样来处理记分呢?

结论

由于“方块”游戏是我写的第一个电脑游戏,所以我决定作为我在这介绍的第一个游戏。在之后的Java娱乐和游戏专栏里,我将和你一同分享其他一些更有趣的游戏。

资源-下载本文的相关代码:   下载

-你可以利用再现开发工具DevSquare来编译和运行Java游戏和娱乐专栏里的applet:

-DevSquare: http://www.devsquare.com/index.html