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 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 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 Graphics 画笔 */ @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); } } }