欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

代码重构-以贪吃蛇为示例(三)-封装Snake

程序员文章站 2022-05-21 13:58:54
...

通过上一节的分离我们可以使程序的流程更清楚,但是这些功能还是冗杂在一个类中,添加和修改功能的时候就要不断对这个类进行改动,而此类中涉及内容过多,在更改一个功能的时候要考虑其他功能的实现,那么这样改起来肯定是相当麻烦的。所以我们要将不同的功能封装出来,比如分数记录器,蛇,地图等。

 

这一节我们要做的是将蛇分离出来作为单个的类(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)];
	}

}
 

 

下节预告:进一步封装类