`
Chris_bing
  • 浏览: 6396 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
社区版块
存档分类
最新评论

代码重构-以贪吃蛇为示例(三)-封装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)];
	}

}
 

 

下节预告:进一步封装类

 

2
1
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics