一步一步写自表达代码——消小球(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的实现。