一步一步写自表达代码——消小球(2)

本章,我们来讨论如何为游戏加上动作。

先从整体上考虑,很多Java例子程序喜欢直接在代码中加入:

button.addActionListener(new ActionListener() { public void performAction(Event e) {}});

这样的代码。这是不好的,因为这不符合单一职责原则,ActionListener应该独立出去,单独成类。这样,当发生问题的时候也容易寻找。有两种形式,一种是本文采用的单独成类的形式,另外一种是在对应的对象中声明内部类。例如:

class MyButton.ActionListener {}。

整体上来讲,我们会加入:

StartActionListener, ClearActionListener, ExitActionListener和BallActionListener几个类,然后绑定到对象上,下一步对每个动作进行细化。在这期间,我们发现,显示小球的JLabel无法加入ActionListener,于是修改成为JButton——这种情况在编码中很常见,就是无法在详细设计时将设计完美化,只有在编码时才能够发现问题。也是为什么我们主张不需要进行详细设计的原因,具体参照软件开发过程中的浪费——详细设计

 

关于如何绑定和加入的过程我们省略了。

第一个是Start。当点击Start的时候开始游戏。并且Start按钮变为Restart,表示重新开始当前局。

start之后的动作依次是:

生成随机的小球       Game.getInstance().shuffle();

布局到屏幕上          Game.getInstance().gamePad.main.render();

清空当前分数          Game.getInstance().score.current = 0;

                           Game.getInstance().score.selected = 0;

刷新分数显示          Game.getInstance().gamePad.helper.render();

更改Start按钮的文字为Restart  Game.getInstance().gamePad.control.start.setText("Restart");


据上述代码会产生如下的编译问题:

     1.Game中没有包含gamePad对象。

     2.很多变量都不是public的无法访问。

     3.render方法在MainFrame和HelperFrame中没有声明。

     4.shuffle()方法没有声明。

的确如此。自表达代码主张先用可以自表达的方式写出需要的操作,然后再去实现这些操作,而不是先实现一些操作,然后调用。

另外,这些代码可以抽取Game.getInstance()成为一个局部变量。

但是这里,我们发现,Listener试图同时更新Model层和View层,并且加大了代码的访问权限。这并不是一种好的方法。虽然按照这种方法继续下去也可以实现整个代码,但是这中书写方式会把原来拆开的MVC结构又给混合到一起。所以,为了保持分离性,我们不采用上述方式,而是引入消息机制,当数据更新的时候发送消息更新UI。提起这种消息机制,也要提一下不要滥用消息机制,不管什么东西都采用消息传递,因为消息机制虽然可以解耦合代码,但是也降低了可读性和可理解性。
所以,在这里,我们先顶一下,消息机制仅仅用于UI更新。

分布讲解,

startActionListener的代码如下:

 1 package org.stephen.bubblebreaker.listener;
 2 
 3 import java.awt.event.ActionEvent;
 4 import java.awt.event.ActionListener;
 5 
 6 import org.stephen.bubblebreaker.control.EventDispatcher;
 7 import org.stephen.bubblebreaker.model.Event;
 8 import org.stephen.bubblebreaker.model.Game;
 9 
10 public class StartActionListener implements ActionListener {
11 
12     @Override
13     public void actionPerformed(ActionEvent e) {
14         Game game = Game.getInstance();
15         game.shuffle();
16         game.score.clearCurrentScore();
17         game.state = Game.State.PLAYING;
18 
19         EventDispatcher.send(Event.UPDATE_BALLS);
20         EventDispatcher.send(Event.UPDATE_SCORES);
21         EventDispatcher.send(Event.UPDATE_BUTTONS);
22     }
23 }

其中,game.shuffle()代表生成随机小球。

其代码如下:

 1 public void shuffle() {
 2         for (int y = 0; y < 12; y++) {
 3             for (int x = 0; x < 12; x++) {
 4                 grid.balls[y][x] = new Ball();
 5                 grid.balls[y][x].color = Ball.Color.values()[(int) (Math
 6                         .random() * Ball.Color.values().length)];
 7                 grid.balls[y][x].x = x;
 8                 grid.balls[y][x].y = y;
 9                 grid.balls[y][x].state = Ball.State.NORMAL;
10             }
11         }
12     }

但其中涉及到Ball.Color的变更。因为Ball的Color选值范围有限,所以,改为自定义的内部枚举Color

该枚举定义为:

1 public enum Color {
2         RED, GREEN, BLUE, YELLOW, PURPLE;
3         public ImageIcon getImageIcon() {
4             return new ImageIcon("image/" + name().toLowerCase() + ".png");
5         }
6     }

可见,限制了选值范围为红绿蓝黄紫5种颜色,并且每种颜色都配上了一张图片,当重新排列的时候,可以通过名称获取相关的图片并显示。

然后是Score的clearCurrentScore();

1 public void clearCurrentScore() {
2         selected = 0;
3         current = 0;
4     }

再然后是,Game.State,这是一个用来通知按钮变化的中间变量,这个变量也将在游戏结束时起作用。

1 public enum State {
2         STAND_BY, PLAYING, GAME_OVER;
3     }

之后是Event和EventDispatcher

1 package org.stephen.bubblebreaker.model;
2 
3 public class Event {
4     public static final int UPDATE_BALLS = 0x1000;
5     public static final int UPDATE_SCORES = 0x1001;
6     public static final int UPDATE_BUTTONS = 0x1002;
7 }

 

 1 package org.stephen.bubblebreaker.control;
 2 
 3 import org.stephen.bubblebreaker.model.Event;
 4 import org.stephen.bubblebreaker.model.Game;
 5 import org.stephen.bubblebreaker.view.GamePad;
 6 
 7 public class EventDispatcher {
 8 
 9     public static void send(int event) {
10         GamePad gamePad = Game.getInstance().gamePad;
11         switch (event) {
12         case Event.UPDATE_BALLS:
13             gamePad.main.render();
14             break;
15         case Event.UPDATE_SCORES:
16             gamePad.helper.render();
17             break;
18         case Event.UPDATE_BUTTONS:
19             gamePad.control.render();
20             break;
21         }
22     }
23 }

然后开始运行,调试,发现还是有不少Bug的,首先,开始时显示的按钮的边界都不需要,删掉相关代码。

然后,Game初始化时应该初始化其中的变量。

1     private Game() {
2         grid = new Grid();
3         score = new Score();
4         state = State.STAND_BY;
5     }

对于Game.gamePad应该在GamePad初始化时进行赋值。

1     public GamePad() {
2         Game.getInstance().gamePad = this;
3     }

Grid中的变量声明也应该初始化。

1 package org.stephen.bubblebreaker.model;
2 
3 public class Grid {
4 
5     public Ball[][] balls = new Ball[12][12];
6 }

然后是各个render的实现

MainFrame.render()

1 public void render() {
2         Ball[][] balls = Game.getInstance().grid.balls;
3         for (int y = 0; y < 12; y++) {
4             for (int x = 0; x < 12; x++) {
5                 this.balls[y][x].setIcon(balls[y][x].color.getImageIcon());
6                 this.balls[y][x].invalidate();
7             }
8         }
9     }

HelperFrame.render()

1     public void render() {
2         Score score = Game.getInstance().score;
3         current.setText(String.valueOf(score.current));
4         selected.setText(String.valueOf(score.selected));
5         highest.setText(String.valueOf(score.highest));
6         average.setText(String.valueOf(score.average));
7         playCount.setText(String.valueOf(score.playCount));
8     }

同时,我们发现旧代码中的selected没有正确赋值,一并修改。

ControlFrame.render()

1     public void render() {
2         Game.State state = Game.getInstance().state;
3         if (state == Game.State.PLAYING) {
4             start.setText("Restart");
5         } else {
6             start.setText("Start");
7         }
8     }

然后调试,修正错误。

点击Start后的界面如下:

下一章,我们将会讲解BallActionListener的实现。

 

 

 

 

posted @ 2012-11-27 14:42  史蒂芬.王  阅读(227)  评论(0编辑  收藏  举报