代码重构-以贪吃蛇为示例(三)-封装Snake
通过上一节的分离我们可以使程序的流程更清楚,但是这些功能还是冗杂在一个类中,添加和修改功能的时候就要不断对这个类进行改动,而此类中涉及内容过多,在更改一个功能的时候要考虑其他功能的实现,那么这样改起来肯定是相当麻烦的。所以我们要将不同的功能封装出来,比如分数记录器,蛇,地图等。
这一节我们要做的是将蛇分离出来作为单个的类(Snake),首先看原来的代码:
package snakes; import java.awt.Color; import java.awt.Graphics; import java.awt.Point; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Random; import javax.swing.JOptionPane; import javax.swing.JPanel; public class GamePanel extends JPanel implements KeyListener { private static final long serialVersionUID = -7269846451378790762L; private static final Random random = new Random(); /** * 分数 */ private int score = 0; /** * 每一个单元格的尺寸,像素 */ private final int sellSize = 20; /** * 地图横向包含的单元格数 */ private final int tableWidth = 30; /** * 地图纵向包含的单元格数 */ private final int tableHeight = 20; /** * 贪吃蛇的点链表 */ private final LinkedList<Point> snake = new LinkedList<Point>(); private final Direction[] da = { Direction.UP, Direction.DOWM, Direction.LEFT, Direction.RIGHT }; /** * 贪吃蛇行进方向 */ private Direction direction; /** * 虫子的位置 */ private Point target = new Point(random.nextInt(tableWidth), random.nextInt(tableHeight)); /** * 贪吃蛇初始长度 */ private final int initsnakeLenght = 3; private final Map<Integer, Direction> keyMap = new HashMap<Integer, Direction>(); /** * 移动速度 */ private volatile long speed = 1; private volatile long crrTime = System.currentTimeMillis(); public GamePanel () { initSnakeDirection(); initKeyMap(); initSnake(); initGameLoop(); } /** * 判断贪吃蛇是否撞墙或撞到自己 * * @return */ protected boolean checkSnack() { return !isAgainstWall() && !isAgainstSelf(); } /** * 绘制地图 * * @param g * 画布 */ private void drawMap(Graphics g) { g.setColor(new Color(0x555555)); for (int i = 0; i < tableWidth; i++) { for (int j = 0; j < tableHeight; ++j) { g.drawRect(i * sellSize, j * sellSize, sellSize, sellSize); } } } /** * 绘制蛇 * * @param g * 画布 */ private void drawSnake(Graphics g) { drawSnakeBody(g); drawSnakeHead(g); } /** * 绘制蛇身 * * @param g * 画布 */ private void drawSnakeBody(Graphics g) { g.setColor(new Color(0x3399cc)); for (Point p : snake) { g.fillRect(p.x * sellSize, p.y * sellSize, sellSize, sellSize); } } /** * 绘制蛇头 * * @param g * 画布 */ private void drawSnakeHead(Graphics g) { g.setColor(new Color(0x115599)); Point p = snake.peek(); g.fillRect(p.x * sellSize, p.y * sellSize, sellSize, sellSize); } /** * 绘制目标点(虫子) * * @param g * 画布 */ private void drawTarget(Graphics g) { g.setColor(new Color(0xdd7744)); g.fillRect(target.x * sellSize, target.y * sellSize, sellSize, sellSize); } /** * 随机生成方向 * * @return 方向 */ private Direction getRandomDirection() { return da[random.nextInt(4)]; } /** * 初始化游戏线程 */ private void initGameLoop() { /** * 游戏主循环线程 */ new Thread() { @Override public void run() { while (true) { if (System.currentTimeMillis() - crrTime > 500 / speed) { synchronized (GamePanel.class) { moveSnake(); if (!checkSnack()) { JOptionPane.showMessageDialog(null, "Game Over!"); return; } } repaint(); crrTime = System.currentTimeMillis(); } } }; }.start(); } /** * 初始化按键和方向的映射 */ private void initKeyMap() { keyMap.put(KeyEvent.VK_UP, Direction.UP); keyMap.put(KeyEvent.VK_DOWN, Direction.DOWM); keyMap.put(KeyEvent.VK_LEFT, Direction.LEFT); keyMap.put(KeyEvent.VK_RIGHT, Direction.RIGHT); } /** * 初始化蛇链表 */ private void initSnake() { Point p = new Point(random.nextInt(tableWidth - initsnakeLenght >> 1) + initsnakeLenght, random.nextInt(tableHeight - initsnakeLenght >> 1) + initsnakeLenght); snake.add(p); for (int i = 0; i < initsnakeLenght - 1; ++i) { p = direction.getPreviousPoint(p); snake.add(p); } } /** * 初始化蛇运行方向 */ private void initSnakeDirection() { direction = getRandomDirection(); } /** * 判断蛇头是否撞到自己的身体,是则返回true,否返回false * * @return */ private boolean isAgainstSelf() { Point p = snake.getFirst(); Iterator<Point> it = snake.iterator(); it.next(); while (it.hasNext()) { Point pBody = it.next(); if (p.equals(pBody)) { return true; } } return false; } /** * 判断蛇头是否撞到墙壁,是则返回true,否返回false * * @return */ private boolean isAgainstWall() { Point p = snake.getFirst(); int x = p.x, y = p.y; return x < 0 || x >= tableWidth || y < 0 || y >= tableHeight; } @Override public void keyPressed(KeyEvent e) {} @Override public void keyReleased(KeyEvent e) { Direction newd = keyMap.get(e.getKeyCode()); if (newd != null && direction.isAvailable(newd)) { direction = newd; synchronized (GamePanel.class) { moveSnake(); if (!checkSnack()) { JOptionPane.showMessageDialog(null, "Game Over!"); return; } } repaint(); crrTime = System.currentTimeMillis(); } } @Override public void keyTyped(KeyEvent e) {} /** * 移动贪吃蛇,包括吃虫 */ private void moveSnake() { snake.addFirst(direction.getNextPoint(snake.getFirst())); if (snake.getFirst().equals(target)) { target = new Point(random.nextInt(tableWidth), random.nextInt(tableHeight)); ++speed; ++score; } else { snake.removeLast(); } } /** * 绘制图形 */ @Override protected void paintComponent(Graphics g) { g.clearRect(0, 0, tableWidth * sellSize, tableHeight * sellSize); drawMap(g); drawSnake(g); drawTarget(g); } }
/* -------------------------------------分割线--------------------------------------------------------------------- */
要进行Snake类的封装首先要做的就是找到跟蛇有关的变量和方法(在Java中“方法”这个词比较常用,我也随大众吧):
贪吃蛇链表snake,行进方向direction,初始长度initsnakeLength(之前Length打错了,在此改正),碰撞检测checkSnake、isAgainstWall、isAgainstSelf,绘制蛇drawSnake、drawSnakeHead、drawSnakeBody,随机生成方向getRandomDirection,初始化蛇运行方向initSnakeDirection,移动蛇moveSnake。
然后将这些变量和方法移动到Snake类中:
在分离的过程中Sanke中需要用到sellSize,tableWidth,speed,score等变量,我们暂时先复制过来,并在构造的时候将这些变量先当做构造函数的参数传递给Snake(稍后有处理办法)。
这一阶段重构后的代码:
package snakes; import java.awt.Color; import java.awt.Graphics; import java.awt.Point; import java.util.Iterator; import java.util.LinkedList; import java.util.Random; public class Snake { /** * 分数 */ private int score; /** * 每一个单元格的尺寸,像素 */ private int sellSize; /** * 地图横向包含的单元格数 */ private int tableWidth; /** * 地图纵向包含的单元格数 */ private int tableHeight; private Point target; private volatile long speed; /** * 贪吃蛇的点链表 */ private static final Random random = new Random(); private final LinkedList<Point> snake = new LinkedList<Point>(); private final Direction[] da = { Direction.UP, Direction.DOWM, Direction.LEFT, Direction.RIGHT }; private Direction direction = da[random.nextInt(4)]; /** * 贪吃蛇初始长度 */ private final int initSnakeLenght = 3; public Snake (int score, long speed, int sellSize, int tableWidth, int tableHeight, Point target) { this.score = score; this.speed = speed; this.sellSize = sellSize; this.tableWidth = tableWidth; this.tableHeight = tableHeight; this.target = target; initSnakeDirection(); initSnake(); } /** * 判断贪吃蛇是否撞墙或撞到自己 * * @return */ public boolean checkSnack() { return !isAgainstWall() && !isAgainstSelf(); } /** * 绘制蛇 * * @param g * 画布 */ public void drawSnake(Graphics g) { drawSnakeBody(g); drawSnakeHead(g); } /** * 绘制蛇身 * * @param g * 画布 */ private void drawSnakeBody(Graphics g) { g.setColor(new Color(0x3399cc)); for (Point p : snake) { g.fillRect(p.x * sellSize, p.y * sellSize, sellSize, sellSize); } } /** * 绘制蛇头 * * @param g * 画布 */ private void drawSnakeHead(Graphics g) { g.setColor(new Color(0x115599)); Point p = snake.peek(); g.fillRect(p.x * sellSize, p.y * sellSize, sellSize, sellSize); } public Direction getDirection() { return direction; } /** * 随机生成方向 * * @return 方向 */ private Direction getRandomDirection() { return da[random.nextInt(4)]; } /** * 初始化蛇链表 */ private void initSnake() { Point p = new Point(random.nextInt(tableWidth - initSnakeLenght >> 1) + initSnakeLenght, random.nextInt(tableHeight - initSnakeLenght >> 1) + initSnakeLenght); snake.add(p); for (int i = 0; i < initSnakeLenght - 1; ++i) { p = getDirection().getPreviousPoint(p); snake.add(p); } } /** * 初始化蛇运行方向 */ private void initSnakeDirection() { setDirection(getRandomDirection()); } /** * 判断蛇头是否撞到自己的身体,是则返回true,否返回false * * @return */ private boolean isAgainstSelf() { Point p = snake.getFirst(); Iterator<Point> it = snake.iterator(); it.next(); while (it.hasNext()) { Point pBody = it.next(); if (p.equals(pBody)) { return true; } } return false; } /** * 判断蛇头是否撞到墙壁,是则返回true,否返回false * * @return */ private boolean isAgainstWall() { Point p = snake.getFirst(); int x = p.x, y = p.y; return x < 0 || x >= tableWidth || y < 0 || y >= tableHeight; } /** * 移动贪吃蛇,包括吃虫 */ public void moveSnake() { snake.addFirst(getDirection().getNextPoint(snake.getFirst())); if (snake.getFirst().equals(target)) { target = new Point(random.nextInt(tableWidth), random.nextInt(tableHeight)); ++speed; ++score; } else { snake.removeLast(); } } public void setDirection(Direction direction) { this.direction = direction; } }
package snakes;
import java.awt.Color; import java.awt.Graphics; import java.awt.Point; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.HashMap; import java.util.Map; import java.util.Random; import javax.swing.JOptionPane; import javax.swing.JPanel; public class GamePanel extends JPanel implements KeyListener { private static final long serialVersionUID = -7269846451378790762L; private static final Random random = new Random(); private Snake snake; /** * 分数 */ private int score = 0; /** * 每一个单元格的尺寸,像素 */ private final int sellSize = 20; /** * 地图横向包含的单元格数 */ private final int tableWidth = 30; /** * 地图纵向包含的单元格数 */ private final int tableHeight = 20; /** * 虫子的位置 */ private Point target = new Point(random.nextInt(tableWidth), random.nextInt(tableHeight)); private final Map<Integer, Direction> keyMap = new HashMap<Integer, Direction>(); /** * 移动速度 */ private volatile long speed = 1; private volatile long crrTime = System.currentTimeMillis(); public GamePanel () { snake = new Snake(score, speed, sellSize, tableWidth, tableHeight, target); initKeyMap(); initGameLoop(); } /** * 绘制地图 * * @param g * 画布 */ private void drawMap(Graphics g) { g.setColor(new Color(0x555555)); for (int i = 0; i < tableWidth; i++) { for (int j = 0; j < tableHeight; ++j) { g.drawRect(i * sellSize, j * sellSize, sellSize, sellSize); } } } /** * 绘制目标点(虫子) * * @param g * 画布 */ private void drawTarget(Graphics g) { g.setColor(new Color(0xdd7744)); g.fillRect(target.x * sellSize, target.y * sellSize, sellSize, sellSize); } /** * 初始化游戏线程 */ private void initGameLoop() { /** * 游戏主循环线程 */ new Thread() { @Override public void run() { while (true) { if (System.currentTimeMillis() - crrTime > 500 / speed) { synchronized (GamePanel.class) { snake.moveSnake(); if (!snake.checkSnack()) { JOptionPane.showMessageDialog(null, "Game Over!"); return; } } repaint(); crrTime = System.currentTimeMillis(); } } }; }.start(); } /** * 初始化按键和方向的映射 */ private void initKeyMap() { keyMap.put(KeyEvent.VK_UP, Direction.UP); keyMap.put(KeyEvent.VK_DOWN, Direction.DOWM); keyMap.put(KeyEvent.VK_LEFT, Direction.LEFT); keyMap.put(KeyEvent.VK_RIGHT, Direction.RIGHT); } @Override public void keyPressed(KeyEvent e) {} @Override public void keyReleased(KeyEvent e) { Direction newd = keyMap.get(e.getKeyCode()); if (newd != null && snake.getDirection().isAvailable(newd)) { snake.setDirection(newd); synchronized (GamePanel.class) { snake.moveSnake(); if (!snake.checkSnack()) { JOptionPane.showMessageDialog(null, "Game Over!"); return; } } repaint(); crrTime = System.currentTimeMillis(); } } @Override public void keyTyped(KeyEvent e) {} /** * 绘制图形 */ @Override protected void paintComponent(Graphics g) { g.clearRect(0, 0, tableWidth * sellSize, tableHeight * sellSize); drawMap(g); snake.drawSnake(g); drawTarget(g); } }
做到这里,如果要运行的话,会发现有个问题,就是当贪吃蛇吃到虫子以后,虫子不会消失,也不会产生新的虫子,如果记录score的话也不会更改。这是因为我们把这些变量复制到Snake,在吃虫的时候更改的是在snake中的变量,而显示的时候却是原来的变量。
有些人想到的解决办法是:在Snake类中公开这些变量,让GamePanel在绘制的时候获取这些变量,这样不就解决了吗?
这样虽然解决了问题,但是仔细想想,score是游戏的得分,tableWidth、tableHeight、sellSize是地图的尺寸,而且targe是虫子,这些变量其实并不属于Snake,所以这样不符合逻辑,不可行。
一个合理的方法是:score等属性还是放在GamePanel中(先不管GamePanel有多乱,现在要做的是把Snake弄清楚),在Snake中保存一个GamePanel的成员,在需要更改这些属性的时候调用GamePanel中的方法。
对于tableWidth、tableHeight、sellSize这类的属性我们可以通过在GamePanel添加getter和setter方法访问,score和speed需要添加increase方法,target比较麻烦,首先在蛇在移动过程中要判断是否吃到虫,如果吃到就要重新放置一条虫子,显然放置虫子的操作不应该归蛇管(让蛇放虫子,那直接放到嘴边岂不方便),可以在GamePanel中添加resetTarget方法。那么判断是否吃到虫应该放到哪里?这个就仁者见仁智者见智了,我个人倾向于放到Snake类里面,然后让GamePanel提供一个target的访问方法。
现在距离完成只有一步之遥,加油吧!
最后,我们可以进行一些小的改进,比如可以把初始化方向作为一个工具类,提供不同的初始化方案(随机、固定、读取配置文件等等),然后将命名调整一下,代码顺序调整一下等等。
下面展示重构后的Snake类(完整的源代码已经上传,接下来的几节都会有对应的源码,方便大家查看和运行):
package snakes; import java.awt.Color; import java.awt.Graphics; import java.awt.Point; import java.util.Iterator; import java.util.LinkedList; import java.util.Random; public class Snake { /** * 贪吃蛇初始长度 */ private final int initSnakeLenght = 3; /** * 贪吃蛇的点链表 */ private final LinkedList<Point> snakeList = new LinkedList<Point>(); private Direction direction; private GamePanel panel; private static final Random random = new Random(); private DirectionGenerator directionGenerator = new RandomDirectionGenerator(); public Snake (GamePanel panel) { this.panel = panel; initDirection(); initList(); } /** * 判断贪吃蛇是否可行,即没有撞墙或撞到自己 * * @return 可行则返回true,不可行(撞墙或撞到自己)则返回false */ public boolean checkAvailable() { return !isAgainstWall() && !isAgainstSelf(); } /** * 绘制蛇身 * * @param g * 画布 */ private void drawBody(Graphics g) { int sellSize = panel.getSellSize(); g.setColor(new Color(0x3399cc)); for (Point p : snakeList) { g.fillRect(p.x * sellSize, p.y * sellSize, sellSize, sellSize); } } /** * 绘制蛇头 * * @param g * 画布 */ private void drawHead(Graphics g) { g.setColor(new Color(0x115599)); Point p = snakeList.peek(); int sellSize = panel.getSellSize(); g.fillRect(p.x * sellSize, p.y * sellSize, sellSize, sellSize); } /** * 绘制蛇 * * @param g * 画布 */ public void draw(Graphics g) { drawBody(g); drawHead(g); } public Direction getDirection() { return direction; } public DirectionGenerator getDirectionGenerator() { return directionGenerator; } /** * 初始化蛇运行方向 */ private void initDirection() { setDirection(directionGenerator.generateDirection()); } /** * 初始化蛇链表 */ private void initList() { int tableWidth = panel.getTableWidth(), tableHeight = panel.getTableHeight(); Point p = new Point(random.nextInt(tableWidth - initSnakeLenght >> 1) + initSnakeLenght, random.nextInt(tableHeight - initSnakeLenght >> 1) + initSnakeLenght); snakeList.add(p); for (int i = 0; i < initSnakeLenght - 1; ++i) { p = getDirection().getPreviousPoint(p); snakeList.add(p); } } /** * 判断蛇头是否撞到自己的身体,是则返回true,否返回false * * @return */ private boolean isAgainstSelf() { Point p = snakeList.getFirst(); Iterator<Point> it = snakeList.iterator(); it.next(); while (it.hasNext()) { Point pBody = it.next(); if (p.equals(pBody)) { return true; } } return false; } /** * 判断蛇头是否撞到墙壁,是则返回true,否返回false * * @return */ private boolean isAgainstWall() { int tableWidth = panel.getTableWidth(), tableHeight = panel.getTableHeight(); Point p = snakeList.getFirst(); int x = p.x, y = p.y; return x < 0 || x >= tableWidth || y < 0 || y >= tableHeight; } /** * 移动贪吃蛇,包括吃虫 */ public void move() { snakeList.addFirst(getDirection().getNextPoint(snakeList.getFirst())); if (snakeList.getFirst().equals(panel.getTarget())) { panel.resetTarget(); panel.increaseScore(); panel.increaseSpeed(); } else { snakeList.removeLast(); } } /** * 设置蛇的运行方向 * * @param direction */ public void setDirection(Direction direction) { this.direction = direction; } public void setDirectionGenerator(DirectionGenerator directionGenerator) { this.directionGenerator = directionGenerator; } }
方向生成器DirectionGenerator和实现类:
package snakes; public interface DirectionGenerator { public Direction generateDirection(); }
package snakes; import java.util.Random; public class RandomDirectionGenerator implements DirectionGenerator { private static final Direction[] directionArray = { Direction.UP, Direction.DOWM, Direction.LEFT, Direction.RIGHT }; private static final Random random = new Random(); @Override public Direction generateDirection() { return directionArray[random.nextInt(4)]; } }
下节预告:进一步封装类
上一篇: 远程连接MySQL速度慢的解决
下一篇: python操作字典类型的常用方法