You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
9.7 KiB

package com.black.play.entity;
import com.black.play.component.SnakeWindow;
import com.black.play.config.AppConfig;
import com.black.play.constant.AppConstant;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.geom.RoundRectangle2D;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
/**
* 蛇类 关键类
* 继承JPanel 用于定位所有Food节点对象
* 维护蛇身节点链表
*
* @author ylx
*/
public abstract class Snake extends JPanel {
// 蛇头坐标x
protected int headX;
// 蛇头坐标y
protected int headY;
// 蛇身长度
protected int length;
// 蛇身节点链表
protected LinkedList<Food> body;
// 下一食物
protected Food food;
// 蛇移动方向
protected Direction direction;
// 蛇移动定时器
protected Timer timer;
public int[][] offsets;
public static final int WIDTH = AppConfig.getInt("window.width");
public static final int HEIGHT = AppConfig.getInt("window.height");
public static final int INIT_LENGTH = AppConfig.getOrDefault("snake.init.length", 4);
public static final int MAX_DELAY = AppConfig.getInt("snake.move.max.delay");
public static final int MIN_DELAY = AppConfig.getInt("snake.move.min.delay");
public static final Random RANDOM = new Random();
// 匿名内部类加载配置
public static final Dimension NODE_SIZE = new Dimension(20, 20) {
{
int width = AppConfig.getInt("snake.node.width");
int height = AppConfig.getInt("snake.node.height");
if (width!=0 && height!=0) {
this.width = width;
this.height = height;
}
// 获取网格大小 该配置会覆盖节点大小
// 若不使用改配置 则空配置或配置0
int numX = AppConfig.getInt("snake.grid.numX");
int numY = AppConfig.getInt("snake.grid.numY");
if (numX!=0 && numY!=0) {
this.width = WIDTH / numX;
this.height = HEIGHT / numY;
}
}
};
/**
* 实例化时构造必要参数
*/
public Snake() {
// 初始化蛇身节点链表
this.body = new LinkedList<>();
// 初始化移动定时器
this.timer = new Timer(MAX_DELAY, this::move);
// 不适用JavaSwing布局管理器 采用(x,y)坐标定位布局
// 默认布局管理器是FlowLayout
// 坐标x→递增 y↓递增 窗口左上角为原点(0,0)
this.setLayout(null);
// 设置画板大小铺满窗口
this.setPreferredSize(new Dimension(WIDTH, HEIGHT));
this.setBackground(SnakeWindow.BACKGROUND);
}
/**
* 开始游戏时的初始化内容
*/
public void init() {
// 初始化蛇身长度 由配置决定
length = INIT_LENGTH;
if (length <= 0) {
throw new RuntimeException(AppConstant.INIT_ERROR);
}
// 初始化更新窗口内容
SnakeWindow instance = SnakeWindow.getInstance();
instance.updateLength(length);
instance.updateScore(0);
instance.updateSpeed((double) 1000 / MAX_DELAY);
// 清空蛇体列表
body.clear();
// 随机初始化蛇头坐标
Food head = randomFood();
headX = head.getX();
headY = head.getY();
// 设置头颜色并添加到蛇身第一个元素
head.setColor(Food.HEAD_COLOR);
body.add(head);
// 随机方向继续初始化蛇体
Direction[] directions = Direction.values();
init(head, directions, 1);
// 随机初始化下一食物
food = randomFood();
}
/**
* 随机初始化下一食物
* 直接对列表深拷贝过于费时
* 找到后对单独的进行拷贝
*
* @return 随机节点
*/
protected abstract Food randomFood();
/**
* 根据当前节点 随机方向初始化下一节点
* 并递归调用直到蛇长满足初始化设置
*
* @param food 当前节点
* @param directions 方向值
*/
protected abstract void init(Food food, Direction[] directions, int length);
/**
* 在给定集合中 根据当前节点和方向 看集合中是否有该方向上的邻居节点
*
* @param foods 节点列表 (传入除蛇身外全节点列表时可用于 生成平面连续的节点列表; 传入body时可以用于检测碰撞自身)
* @param food 当前节点
* @param direction 指定方向
* @return 该节点该方向存在节点返回该节点, 否则返回null
*/
public abstract Food getNextByDirection(List<Food> foods, Food food, Direction direction);
public abstract Direction getDirectionByNext();
/**
* 暂停事件恢复移动||首次开始按键开始移动
*/
public void start() {
timer.start();
}
/**
* 暂停点击回主菜单结束||死亡结束
*/
public void stop() {
timer.stop();
SnakeWindow window = SnakeWindow.getInstance();
window.stopClock();
window.showCard(AppConstant.MENU);
}
/**
* 空格或Esc暂停
*/
public void pause() {
timer.stop();
}
/**
* 键盘监听调用
*
* @param direction 按键输入方向
*/
public void changeDirection(Direction direction) {
if (body.isEmpty()) {
return;
}
if (body.size()==1) {
this.direction = direction;
move();
start();
return;
}
// 获取蛇头和蛇身第二节
Food head = body.getFirst();
Food second = body.get(1);
// 获取该方向下一节点
Food next = getNextByDirection(body, head, direction);
// 如果第二节点和该方向下一节点不同 则改变方向并移动一次
// 否则不做改变 即不允许180°调头走
if (!second.touchTo(next)) {
this.direction = direction;
move();
}
// 到此没有对方向赋值说明是起步就进行180°掉头
// 赋予反向启动方向
if (this.direction==null) {
this.direction = direction.reverse();
}
// 启动定时器
if (!timer.isRunning()) {
start();
}
SnakeWindow.getInstance().startClock();
}
/**
* 移动
*
* @param e 触发事件
*/
public synchronized void move(ActionEvent... e) {
if (direction==null || body.isEmpty()) {
return;
}
// 根据方向移动蛇头坐标
int ordinal = direction.ordinal();
int[] offset = offsets[ordinal];
headX += offset[0];
headY += offset[1];
// 根据判断是否碰壁或自身
if (touchSelfOrWall()) {
stop();
return;
}
// 判断是否碰到食物
if (touchFood()) {
addBody();
} else {
moveBody();
}
updateUI();
}
/**
* 碰撞墙体或自身检测
*
* @return 是否碰撞
*/
protected boolean touchSelfOrWall() {
// 获取蛇头和尾部
Food head = body.getFirst();
Food last = body.getLast();
// 根据头部和移动方向 获取该方向在蛇体上的相邻点
Food nextOnBody = getNextByDirection(body, head, direction);
// 如果该方向在蛇体上存在节点 并且 不是尾部节点 则说明吃到自身 判定死亡
if (nextOnBody!=null) {
if (nextOnBody.equals(last)) {
Direction lastDirection = getDirectionByNext();
int ordinal = lastDirection.ordinal();
int[] offset = offsets[ordinal];
Food expectLast = new Food(last.getX() + offset[0], last.getY() + offset[1]);
Food expectHead = new Food(headX, headY);
return expectHead.touchTo(expectLast);
}
return true;
}
// 否则判定蛇头坐标是否会超出边界
return headX < 0 || headX > WIDTH - NODE_SIZE.width || headY < 0 || headY > HEIGHT - NODE_SIZE.height;
}
/**
* 接触食物
*
* @return 是否接触到食物
*/
protected boolean touchFood() {
// 期望中移动后的蛇头与食物是否接触
Food expectHead = new Food(headX, headY);
return expectHead.touchTo(food);
}
protected abstract void addBody();
protected abstract void moveBody();
/**
* 重写绘制方法
*
* @param g the <code>Graphics</code> 画笔
*/
@Override
protected void paintComponent(Graphics g) {
// 父类设定边界大小 绘制背景等
super.paintComponent(g);
Graphics2D graphics2D = (Graphics2D) g;
// 设定边框颜色 绘制圆角边框
if (food!=null) {
graphics2D.setColor(Color.WHITE);
RoundRectangle2D.Float border = new RoundRectangle2D.Float(this.food.getX(), this.food.getY(), NODE_SIZE.width, NODE_SIZE.height, NODE_SIZE.width >> 2, NODE_SIZE.height >> 2);
graphics2D.draw(border);
// 设置食物颜色填充
graphics2D.setColor(this.food.getColor());
graphics2D.fill(border);
}
// 同理绘制身体
for (Food food : body) {
RoundRectangle2D.Float bodyBorder = new RoundRectangle2D.Float(food.getX(), food.getY(), NODE_SIZE.width, NODE_SIZE.height, NODE_SIZE.width >> 2, NODE_SIZE.height >> 2);
graphics2D.setColor(Color.WHITE);
graphics2D.draw(bodyBorder);
graphics2D.setColor(food.getColor());
graphics2D.fill(bodyBorder);
}
}
}