用 JAVA 开发游戏连连看(之五)完善用户界面

来源:互联网 发布:餐饮库存软件 编辑:程序博客网 时间:2024/05/22 09:46
(之五)完善用户界面

让界面更动起来

整个程序的界面总算是出来了,可惜不太漂亮,这种界面,别说别人,就连自己也不愿意多看几眼,因此,做一些适当的美化工作还是非常有必要的。

想要让界面变得漂亮,最好的办法就是大量使用帖图,可惜,图片太多不仅会影响到程序的执行效率,同时,由于美工不是我们的长项,因此,我们还是走走捷径算了。

首先,我们将各个用户控件设置好背景色,这是最简单的方法了,只要颜色搭配得当,也是最有效的办法了。

其次,为了使界面看上去不那么单薄,因此,我们可以想办法使界面更有立体感。好在 JAVA 为我们提供了许多种 Border 控件,通过 Border 控件来组合其它控件的使用,将会使界面变得有立体感。

第三,使用图片。以上的方法,只会让控件变得漂亮,但控件仍然有控件的影子。而大多数人一看到控件,第一反应就会想起应用程序,而不是游戏。既然我们做的是游戏,那么,我们就可以自己做一些简单的图片来“掩蔽”控件的本来面目。好在这个游戏按钮不多,做几个也不太难。

经过以上的几步操作,界面变得漂亮多了,不是吗?

改变鼠标光标

很少看见过有人改变程序中光标的样子,是不是 JAVA 做不到?其实 JAVA 已经考虑到了这一点,只不过很少有人想去这么做这已。 createCustomCursor 就是为我们准备的,其具体用法是:

createCustomCursor(Image cursor, Point hotSpot, String name)

cursor 是我们要设置为光标的图片, hotSpot 是图片显示在实际光标位置的位移, name 就是光标的名字拉!

好了,现在我们找一张合适的图片来作为程序的光标吧,看看效果如何?

如果,你还不满意,或者,你要说:我们的光标不能动啊,人家 QQ 上的光标可是会动的呢。

这确实有点麻烦,因为 JAVA 提供的方法只能显示静态的光标,但是,通过一些简单的方法,我们还是可以实现的。

由于 JAVA 的光标只能是静态图片,因此,要显示动态的光标,我们只能是定时更改光标的图片,首先,我们准备好一系列图片,然后,我们需要使用 javax.swing.Timer(int, java.awt.event.ActionListener) 方法来设置一个定时器,当定时器的事件触发后,我们就改变光标显示的图片。在本程序中,由于考虑到效率问题,我们就没有使用动态光标了,不过,如果你有兴趣,可以试试的:)

将时间 / 分数的显示作为动画来显示

为了让程序更有活力,我们可以适当的将游戏中一些显示信息的地方做成小动画,比如说时间和分数。

在动画的处理过程中,我们要保证动画只是起到作为游戏的点缀,而不能影响到游戏的正常进行(比如说不能在动画进行的过程中中断游戏),同时,动画也不能太喧宾夺主,这样也会分散别人在游戏中的注意力的。

为了保证动画过程和游戏过程的平行运行,因此,我们非常有必要将动画分离成一个独立的控件,并且要保证动画有自己单独的线程来运行。好了,现在我们先来看看我们怎么把时间作为动画分离出来的吧。

//ClockAnimate.java

public class ClockAnimate

extends JPanel // 将时间的显示作为 Panel 控件

implements Runnable { // 使用线程保证动画的独立性

public ClockAnimate() {

this.setPreferredSize(new Dimension(156, 48)); // 设置好控件的大小

}

现在,我们就做一个的数字变化的效果,这种效果最简单的方式就是让数字每隔一段时间就变化一次。

public void start() {

startTime = System.currentTimeMillis(); // 当线程起动时,记录下当前的时间

thread = new Thread(this);

thread.start(); // 线程开始运行

}

public void run() { // 线程运行的主过程

Thread currentThread = Thread.currentThread();

while (thread == currentThread) {

long time = System.currentTimeMillis();

usedTime = time - startTime;

try {

repaint(); // 重画数字

thread.sleep( 100l); // 延时 100 毫秒,即 0.1 秒

}

catch (InterruptedException ex) {

}

}

}

public void paint(Graphics g) { // 重画时间

g.drawString("Time:" + usedTime, 16, 40);

}


怎么样,时间的显示是不是可以动了?为了使文字在使用大字体的情况下显示得更漂亮一些,我们可以适当的使用抗锯齿效果, JAVA 提供了现成的方法,很简单的,现在我们将 paint(Graphics g) 改动一下:

public void paint(Graphics g) {

Graphics2D g2 = (Graphics2D) g;

Dimension d = getSize();

g2.setBackground(new Color(111, 146, 212));

g2.clearRect(0, 0, d.width, d.height); // 使用背景色清除当前的显示区域

g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,

RenderingHints.VALUE_ANTIALIAS_ON); // 打开抗锯齿效果

g2.setColor(new Color(212, 255, 200));

g2.setFont(new Font("serif", Font.PLAIN, 28));

g2.drawString("Time:" + getTime(), 16, 40);

}


Graphics2D 是 JAVA 提供的增强型图形处理包,可以实现许多以前 Graphics 实现不了的功能。在由于系统记录的时间是以毫秒为单位记录的,因此,在上面我们需要写一个 getTime() 方法来将时间的显示格式化成类似于 123.4 这种形式。

时间的动画完成了,现在我们开始制作分数变化的动画。其实分数动画的基本设计方法与时间动画相同,但是有一点不同的是,时间动画在用户游戏的整个过程中是一直运行的,而分数的动画是要根据用户当前得分的情况进行变化的,也就是说,分数的动画是被用户干预的。

现在,我们将动画运行的主过程改动一下。为了简单起见,我们只考虑分数从低向高的变化,不考虑分数从高向低的变化。

public void run() {

Thread currentThread = Thread.currentThread();

while (thread == currentThread && lastScore < currentScore) {

try {

lastScore++;

repaint();

thread.sleep( 50l);

}

catch (InterruptedException ex) {

}

}

}

public void setScore(int l, int c) { // 根据用户得分前后的数值进行动画处理

this.lastScore = l;

this.currentScore = c;

start();

}


当每次用户的分数发生变化时,我们可以使用 setScore(int l, int c) 方法同步分数显示的动画效果。

实现消除图片时的动画效果

在完成了上面两个动画,现在我们来设计消除图片时的动画,动画的效果是当我们选中两个可消除的图片后,找到两个图片之间的连接直线,并且从第一个选中的图片向第二个选中的图片之间做点的拖尾效果的动画(如果不是很明白,看看程序运行的效果就知道了)。

这个动画比前两个动画处理起来要麻烦得多,因为,它牵扯到的部分比较多:首先,需要记录下两点之间的直线以及这些直线出现的先后次序,其次,还需要知道每条直线的方向,是横向还是纵向,第三,还需要在这些直线上依次进行动画效果的处理。这个动画效果不仅牵扯到了界面,还牵扯到了算法。现在,我们还是一起看看怎么实现吧。

首先,在算法中,当每次消除两个点的时候,我们就需要记录下这两个点之间的连线情况,是横着的还是竖着的,是 1 条直线还是 2 条或者 3 条。因此,对于 Map.java 中的 verticalMatch(Point a, Point b) 方法就必须改动一下

private boolean verticalMatch(Point a, Point b, boolean recorder) {

…………

if (!test && recorder) { // 如果当前不是测试并且要求记录路径

animate = new AnimateDelete(0, a, b);

}

return true;

}


recorder 说明了当前是否要求记录下路径,比如说在使用提示功能的时候,虽然我们是会调用该方法来进行试控两点是否可以消除,但实际上,这两个点并不是真的需要消除,所以,在这种情况下,就不应该记录下路径。

AnimateDelete 是我们新创建的一个类,其作用就是处理消除时的动画效果的,该类有几个构造函数,依次如下:

public class AnimateDelete

implements Runnable {

// 获得界面上的 JButton 控件

public AnimateDelete(JButton[] dots) {

this.dots = dots;

}

// 一条直线的情况

//direct 方向, 1 表示 a, b 在同一直线上, 0 表示 a, b 在同一竖线上

public AnimateDelete(int direct, Point a, Point b) {

……

}

// 两条直线的情况

//direct 方向, 1 表示 a, b 在同一直线上, b, c 在同一竖线上;

//0 表示 a, b 在同一竖线上, b, c 在同一直线上

public AnimateDelete(int direct, Point a, Point b, Point c) {

……

}

// 三条直线的情况

//direct 1 表示 a, b 为横线, b, c 为竖线 , c, d 为横线

//0 表示 a, b 为竖线, b, c 为横线, c, d 为竖线

public AnimateDelete(int direct, Point a, Point b, Point c, Point d) {

……

}


上面的 public AnimateDelete(JButton[] dots) 构造函数看起来似乎没用,实际上,这个是非常有用的,我后面会提到的。

好了,现在可以在 Map 算法中的 horizonMatch 、 verticalMatch 、 oneCorner 、 twoCorner 等方法中添加消除动画的构造函数了。

在 AnimateDelete 的几个构造函数中,还要记得将每种方式中涉及到的直线的路径记录下来,最后,将路径上的这些元素依次保存在一个一维数组中,同时,我们也需要记录下路径的长度,以便动画时的操作。现在我们来看看动画部分如何处理。

public void run() {

if (count < 2) { //count 是路径的长度,当 count<2 的时候,不进行动画

return;

}

Thread currentThread = Thread.currentThread();

boolean animate = true;

while (thread == currentThread && animate) {

// 先用图片来填充经过的路径

for (int i = 1; i < count - 1; i++) {

dots[array].setEnabled(true);

dots[array].setIcon(Kyodai.GuideIcon);

try {

thread.sleep( 20l);

}

catch (InterruptedException ex) {

}

}

// 然后恢复经过的路径

for (int i = 1; i < count - 1; i++) {

dots[array].setIcon(null);

dots[array].setEnabled(false);

try {

thread.sleep( 20l);

}

catch (InterruptedException ex) {

}

}

// 消除两点

dots[array[0]].setIcon(null);

dots[array[0]].setEnabled(false);

dots[array[count - 1]].setIcon(null);

dots[array[count - 1]].setEnabled(false);

animate = false;

}

stop(); // 停止动画

}


由于消除动画可以几个消除动画同时进行,因此,对于此效果,我们就要在每次使用该效果时实例化一个该对象了。

为程序添加声音

现在,我们来为我们的游戏添加声音。虽然 JAVA 自从出道之日起就能够处理声音,但是那只限于在 APPLET 中进行处理,即便是这样,声音的格式也只能是少见的 AU 格式。幸好, SUN 意识到了这个问题,在 JDK1.3 之后, JAVA 就提供了专门的声音处理包来满足声音处理的需求。 javax.sound.midi 和 javax.sound.sampled 就是分别是用来处理 MIDI 和波形文件的,虽然 JAVA 提供的这两个包还不支持如 MP3 、 RM 等这类格式的文件,但是对于我们的这个游戏来说,能处理 MIDI 和 WAV 文件也已经够用了。

MIDI 格式的文件其优点在于文件小,但缺点是只能保存乐曲而无法包含声音信息, WAV 格式虽然能包含声音信息,可惜文件太大。因此,我们选用 MIDI 来作为游戏的背景音乐,而 WAV 来作为音效。

我们先来看看如何处理 MIDI 格式的文件吧。

// 读取 midi 文件

public void loadMidi(String filename) throws IOException, InvalidMidiDataException {

URLClassLoader urlLoader = (URLClassLoader)this.getClass().getClassLoader();

URL url = urlLoader.findResource(filename);

sequence = MidiSystem.getSequence(url); //sequence 保存着 MIDI 的音序结构

}

// 播放 sequence

public void play() {

if (isPlaying) { // 如果已经在播放,返回

return;

}

try {

sequencer = MidiSystem.getSequencer();

sequencer.open();

sequencer.setSequence(sequence); // 加载 sequence

sequencer.addMetaEventListener(this); // 添加事件处理

}

catch (InvalidMidiDataException ex) {

}

catch (MidiUnavailableException e) {

}

// Start playing

thread = new Thread(this);

thread.start();

}

public void run() {

Thread currentThread = Thread.currentThread();

while (currentThread == thread && !isPlaying) { // 当 MIDI 没有播放的时候,播放 MIDI 音乐

sequencer.start();

isPlaying = true;

try {

thread.sleep( 1000l);

}

catch (InterruptedException ex) {

}

}

}


代码很短,但是已经能很好的完成我们需要的功能了,当然,如果你还嫌不满的话, JAVA 也提供了多种方法让你对 MIDI 格式的文件进行音调、频率的改变,由于这方面要牵扯到比较专业的知道,而我也不太了解,因此我就不说了:)

对 WAV 格式文件的操作和 MIDI 的操作基本上很类似,只不过使用的 API 包不同罢了,具体的我就不多说了,大家看看源代码就知道了。

让用户了解游戏规则

并非所有的人都玩过这个游戏,也并非所有的人都了解游戏的规则,因此,做一个帮助系统对刚接触的用户来说,还是非常有必要的。制作帮助系统非常简单,无非就是用一个对话框来显示游戏规则,显示的方式有多种,可以使用 JLabel 控件来显示,也可以使用 JTextArea 控件来显示,当然,在这里,为了使帮助系统更完美,使用 HTML 来制作帮助系统将来是最佳选择,由于在上面已经提到过如何使用 JEditorPane 控件显示 HTML 页面,因此,这个就留给大家自己完成吧。

并且使用自己的偏好来进行游戏

虽然我们为游戏提供了许多功能,可是并非所有的用户都能完全接受这些功能的,因此,我们需要提供一些设置使用户能够使用自己的偏好来进行游戏。

在此,我们提供了用户选择打开 / 关闭背景音乐、打开 / 关闭游戏音效、设置游戏的难度、设置消除动画的速度这 4 项功能,为了使用户能够自己设置,我们不仅需要使用上面提到过的配置文件来保存信息,还需要在程序中提供设置界面,在此,我们再添加一个 SetupDialog 类,这个类并不难实现,大家看看源程序就可以了。

原创粉丝点击