摘要
现在是java娱乐和游戏专栏介绍一个游戏的时间了。这一部分由jeff friesen展示他的一个叫做“方块”的java游戏。
备注:java娱乐和游戏专栏里展示的applets都可以用devsquare这个在线开发工具编译和运行。请在使用之前阅读相应的用户文档(文档可以在资源区里找到)
在90年代初,我在microsoft的dos下写了第一个游戏,方块。过了这么多年没再碰过它,不过现在我决定在这个专栏里重新翻看一下这个游戏。
在java娱乐和游戏专栏的这一部分,我将向你介绍“方块”,并且用swing来写这个游戏。另外我还将用另外三个swing applets来增加音效、视觉特效、以及更多的游戏关卡,以此增强游戏的可玩性。
版权声明:任何获得matrix授权的网站,转载时请务必保留以下作者信息和链接
作者:jeff friesen;jerric(作者的blog:http://blog.matrix.org.cn/page/jerric)
原文:http://www.javaworld.com/javaworld/jw-03-2006/jw-0327-funandgames_p.html
matrix:http://www.matrix.org.cn/resource/article/44/44456_java+game.html
关键字:java;game
“方块游戏”简介
“方块”游戏使用一个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 ();
}
}
// 加载玩家切换单元格颜色、获胜、以及失败时播放的声音剪辑。
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);
// 如果玩家获胜,则通知游戏结束监听器。
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 ();
}
// 绘制游戏板,以及有颜色的单元格。
repaint ();
// 播放颜色切换的声音。如果你用早期的java 1.5.0或后期的java 1.4.x,那有一个bug会阻止很短
// 的声音文件播放出来,因此你可能听不到声音(或者只听到的一声)。你可以在以下链接了解到更多信息:
// http://bugs.sun.com/bugdatabase/view_bug.do;:yfig?bug_id=6251460
actoggle.play ();
// 如果玩家获胜,通知游戏结束监听器,并且动态显示祝贺信息。
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);
}
// 通过从左到右滚动一条消息而在玻璃板上显示动画。
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 ();
}
// 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;
}
}
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 ();
Java Asp PHP .Net XML C/C++ CGI VB Jsp J2ee J2se J2me EJB Servlet Tomcat Resin Struts Weblogic Eclipse ANT GUI JMS Web servise IDEA Webphere Hibernate Spring Jboss Applet Swing Socket Javamail Perl Ajax P2P 安全 模式 框架 测试 开源 游戏
Windows XP Windows 2000 Windows 2003 Windows Me Windows 9.x Linux UNIX 注册表 操作系统 服务器 应用服务器