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