方块游戏”简介
“方块”游戏使用一个3x3的网格,其中每一个单元格要么显示一种颜色,要么什么都没有(表示为黑色)。游戏开始时一些单元格随机填充颜色,其他的都用默认黑色。只要你在30秒内清除所有单元格的颜色(全部变为黑色,没有其他颜色存在),游戏机上分器 http://www.hfded.com 你就获胜了。
你要么移动鼠标点击一个单元格,要么直接按小键盘的相应数字键,都可以清除那个单元格里的颜色。类似的,假如你所点击的单元格本身是黑色,那么那个单元格就会被填充一种其他颜色。也就是说会有这样的循环:黑色变彩色,彩色变黑色。假如仅仅这样游戏就太轻易了,因此我设计的方块游戏是,你对单元格的点击/按键会影响他自己和他的四周单元格,如图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”按钮会恢复有效,这样你就可以开始另一个游戏了。游戏机上分器 http://www.hfded.com
下边是源代码:
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及以后的版本中已经得到纠正。
游戏机上分器 http://www.hfded.com
音效
到目前为止,“方块”游戏并没有想象中的那么有趣。不过我们可以通过增加音效来让游戏更有趣。我们至少可以有三种音效:当玩家切换单元格(及其四周)颜色时的音效,当玩家获胜时的音效,以及当玩家失败时的音效。
我为这些情形预备了一套适当的音效,分别是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);