Compare commits
No commits in common. 'master' and 'main' have entirely different histories.
@ -0,0 +1,301 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package com.black.play.listener;
|
||||
|
||||
import com.black.play.component.SnakeWindow;
|
||||
import com.black.play.constant.AppConstant;
|
||||
import com.black.play.entity.Direction;
|
||||
import com.black.play.entity.Snake;
|
||||
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
/**
|
||||
* 主界面全局监听 snake
|
||||
*
|
||||
* @author ylx
|
||||
*/
|
||||
public class SnakeListener extends KeyAdapter {
|
||||
// 与SnakeWindow持有同一Snake引用
|
||||
private final Snake snake;
|
||||
|
||||
public SnakeListener(Snake snake) {
|
||||
this.snake = snake;
|
||||
}
|
||||
|
||||
/**
|
||||
* 键盘按键监听
|
||||
*
|
||||
* @param e 按键事件
|
||||
*/
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
int keyCode = e.getKeyCode();
|
||||
switch (keyCode) {
|
||||
case KeyEvent.VK_UP:
|
||||
case KeyEvent.VK_W:
|
||||
handleDirection(Direction.UP);
|
||||
break;
|
||||
case KeyEvent.VK_DOWN:
|
||||
case KeyEvent.VK_S:
|
||||
handleDirection(Direction.DOWN);
|
||||
break;
|
||||
case KeyEvent.VK_LEFT:
|
||||
case KeyEvent.VK_A:
|
||||
handleDirection(Direction.LEFT);
|
||||
break;
|
||||
case KeyEvent.VK_RIGHT:
|
||||
case KeyEvent.VK_D:
|
||||
handleDirection(Direction.RIGHT);
|
||||
break;
|
||||
case KeyEvent.VK_ESCAPE:
|
||||
handleEscape();
|
||||
break;
|
||||
case KeyEvent.VK_SPACE:
|
||||
handleSpace();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDirection(Direction direction) {
|
||||
SnakeWindow snakeWindow = SnakeWindow.getInstance();
|
||||
if (!AppConstant.GAME.equals(snakeWindow.getActiveCard())) {
|
||||
return;
|
||||
}
|
||||
snake.changeDirection(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 空格
|
||||
* 游戏→暂停
|
||||
* 暂停→游戏
|
||||
* 菜单→开始
|
||||
*/
|
||||
private void handleSpace() {
|
||||
SnakeWindow snakeWindow = SnakeWindow.getInstance();
|
||||
String activeCard = snakeWindow.getActiveCard();
|
||||
switch (activeCard) {
|
||||
case AppConstant.GAME:
|
||||
snakeWindow.pauseGame();
|
||||
break;
|
||||
case AppConstant.PAUSE:
|
||||
snakeWindow.continueGame();
|
||||
break;
|
||||
case AppConstant.MENU:
|
||||
snakeWindow.startGame();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Esc
|
||||
* 菜单→退出
|
||||
* 暂停→退出
|
||||
* 游戏→暂停
|
||||
*/
|
||||
private void handleEscape() {
|
||||
SnakeWindow snakeWindow = SnakeWindow.getInstance();
|
||||
String activeCard = snakeWindow.getActiveCard();
|
||||
switch (activeCard) {
|
||||
case AppConstant.GAME:
|
||||
snakeWindow.pauseGame();
|
||||
break;
|
||||
case AppConstant.PAUSE:
|
||||
case AppConstant.MENU:
|
||||
System.exit(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue