(之五)完善用户界面 让界面更动起来 整个程序的界面总算是出来了,可惜不太漂亮,这种界面,别说别人,就连自己也不愿意多看几眼,因此,做一些适当的美化工作还是非常有必要的。 想要让界面变得漂亮,最好的办法就是大量使用帖图,可惜,图片太多不仅会影响到程序的执行效率,同时,由于美工不是我们的长项,因此,我们还是走走捷径算了。 首先,我们将各个用户控件设置好背景色,这是最简单的方法了,只要颜色搭配得当,也是最有效的办法了。 其次,为了使界面看上去不那么单薄,因此,我们可以想办法使界面更有立体感。好在 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); |