贪 吃 蛇

简介

简易贪吃蛇,使用 javax.swing 组件构建游戏界面,通过监听键盘按键实现游戏操纵。

功能设计

  • 按1 - 开始游戏
  • 按2 - 重新开始
  • 按3 - 暂停/继续
  • 按Esc-退出游戏
  • 统计吃到的苹果个数(得分)
  • 难度控制,得分超过阈值时难度增加(蛇身移动速度加快)

实现

定义 SnakeGame 类:

继承 JPanel 类, 重写其由 JComponent 类中继承的 paintComponent 方法,在此方法中进行图像的绘制。

实现 ActionListener 类, 实现其 actionPerformed 方法, 通过监听在 SnakeGame 类中的Timer 计时器步长内按键的输入完成对图像的操作。

java 复制代码
public class SnakeGame extends JPanel implements ActionListener {

	//(timer = new Timer(delay, this)).start();

	public void paintComponent(Graphics g) {
	    //在这里操作图像的绘制,蛇身和苹果等
	}
	
	public void actionPerformed(ActionEvent e) {
	    //在这里通过对定时器的监听完成对图像的操作
	}
	
}

自定义按键适配器, 将键盘输入转换为程序识别的方向值,同时记录键盘输入。

java 复制代码
/**
 * 定义方向
 */
public interface Direction {
        char LEFT = 'L';
        char RIGHT = 'R';
        char UP = 'U';
        char DOWN = 'D';
    }

/**
 * 按键适配器,用于监听输入按键
 */
public class MyKeyAdapter extends KeyAdapter {
    public void keyPressed(KeyEvent e) {
        int keyCode = e.getKeyCode();
        eventKeyCode = e.getKeyCode();
        if ((keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_A) && direction != Direction.RIGHT) {
            direction = Direction.LEFT;
        } else if ((keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_D) && direction != Direction.LEFT) {
            direction = Direction.RIGHT;
        } else if ((keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_W) && direction != Direction.DOWN) {
            direction = Direction.UP;
        } else if ((keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_S) && direction != Direction.UP) {
            direction = Direction.DOWN;
        }
    }
}

在 paintComponent 和 actionPerformed 方法中实现游戏逻辑。

注意:

  • 对可用像素点的处理:可用像素点个数 = 屏幕长 * 屏幕宽 / 单位大小(苹果大小)。
  • 对蛇身初始坐标的处理: 存储蛇身的初始坐标须在可用像素点中。
  • 对苹果坐标的处理:需判断新的苹果坐标是否在蛇身内。
  • 对游戏是否存活的处理:蛇头撞到蛇身或者蛇头撞到边缘都应视为结束。
  • 对接收到的按键值的处理:除方向按键外,其余按键值使用完之后需做清除。

完整实现代码如下

java 复制代码
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Arrays;
import java.util.Random;

public class SnakeGame extends JPanel implements ActionListener {

    public interface Direction {
        char LEFT = 'L';
        char RIGHT = 'R';
        char UP = 'U';
        char DOWN = 'D';
    }

    public interface RunStatus {
        char READY = '0'; // 就绪
        char RUN = '1'; // 运行
        char OVER = '2'; // 失败
        char PAUSE = '3'; // 暂停/继续
    }

    final Random random = new Random();
    volatile int eventKeyCode;

    //记时步长
    volatile int delay = 150;
    Timer timer = null;

    //屏幕大小
    static final int SCREEN_WIDTH = 600;
    static final int SCREEN_HEIGHT = 600;

    //可用像素点
    static final int UNIT_SIZE = 25;
    static final int GAME_UNITS = (SCREEN_WIDTH * SCREEN_HEIGHT) / UNIT_SIZE;

    //蛇身
    volatile int bodyParts;
    int snakeBodyX[];
    int snakeBodyY[];

    //目标
    int appleX;
    int appleY;

    //得分
    volatile int applesEaten;
    volatile char direction;
    volatile char runStatus;

    SnakeGame() {
        this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
        this.setFocusable(true);
        this.addKeyListener(new MyKeyAdapter());
        (timer = new Timer(delay, this)).start();
        init();
    }

    public void init() {
        bodyParts = 1;
        snakeBodyX = new int[GAME_UNITS];
        Arrays.fill(snakeBodyX, -1);
        snakeBodyX[0] = 0;
        snakeBodyY = new int[GAME_UNITS];
        Arrays.fill(snakeBodyY, -1);
        snakeBodyY[0] = 0;
        appleX = nextCoordinate(SCREEN_WIDTH, snakeBodyX);
        appleY = nextCoordinate(SCREEN_HEIGHT, snakeBodyY);
        applesEaten = 0;
        direction = Direction.RIGHT;
        runStatus = RunStatus.READY;
        timer.setDelay(delay);
    }

    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        String tip = "" + applesEaten;
        int y = SCREEN_HEIGHT / 2 - 120;
        switch (runStatus) {
            case RunStatus.READY:
                drawing(g);
                g.setColor(Color.BLUE);
                g.setFont(new Font(null, Font.ITALIC, 20));
                g.drawString("按1-开始游戏", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, (y = y + 40));
                g.drawString("按2-重新开始", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, (y = y + 40));
                g.drawString("按3-暂停/继续", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, (y = y + 40));
                g.drawString("按Esc-退出游戏", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, (y = y + 40));
                break;
            case RunStatus.PAUSE:
                drawing(g);
                g.setColor(Color.BLUE);
                g.setFont(new Font(null, Font.ITALIC, 20));
                y = y + 80;
                g.drawString("按3-继续", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, (y = y + 40));
                g.drawString("按Esc-退出游戏", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, (y = y + 40));
                break;
            case RunStatus.RUN:
                drawing(g);
                g.setColor(Color.blue);
                g.setFont(new Font(null, Font.BOLD, 20));
                g.drawString(tip, (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, g.getFont().getSize());
                break;
            case RunStatus.OVER:
                g.setColor(Color.RED);
                g.setFont(new Font("Ink Free", Font.BOLD, 40));
                g.drawString("Game Over", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, y - 120);
                g.setFont(new Font(null, Font.ITALIC, 20));
                g.drawString("得分:" + applesEaten, (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, y - 60);
                g.setColor(Color.BLUE);
                g.drawString("按2-重新开始", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, (y = y + 40));
                g.drawString("按Esc-退出游戏", (SCREEN_WIDTH - getFontMetrics(g.getFont()).stringWidth(tip)) / 2, (y = y + 40));
                break;
            default:
                break;
        }
    }

    /**
     * 绘制蛇身和苹果
     *
     * @param g
     */
    public void drawing(Graphics g) {
        //苹果颜色
        g.setColor(Color.PINK);
        //画苹果
        g.fillOval(appleX, appleY, UNIT_SIZE, UNIT_SIZE);
        //蛇身颜色
        g.setColor(Color.RED);
        //填充蛇身
        for (int i = 0; i < bodyParts; i++) {
            g.fillRect(snakeBodyX[i], snakeBodyY[i], UNIT_SIZE, UNIT_SIZE);
        }
    }

    /**
     * 执行的动作
     * 会定时执行此方法
     *
     * @param e
     */
    public void actionPerformed(ActionEvent e) {

        //前置事件
        doSomeThing();

        if (runStatus == RunStatus.RUN) {
            move();

            //撞到边缘
            if (snakeBodyX[0] < 0 || snakeBodyX[0] > SCREEN_WIDTH || snakeBodyY[0] < 0 || snakeBodyY[0] > SCREEN_HEIGHT) {
                runStatus = RunStatus.OVER;
            }
            //蛇身相撞
            for (int i = bodyParts; i > 0; i--) {
                if ((snakeBodyX[0] == snakeBodyX[i]) && (snakeBodyY[0] == snakeBodyY[i])) {
                    runStatus = RunStatus.OVER;
                }
            }

            //得分
            if ((snakeBodyX[0] == appleX) && (snakeBodyY[0] == appleY)) {
                applesEaten++;
                bodyParts++;

                //重新生成苹果
                appleX = nextCoordinate(SCREEN_WIDTH, snakeBodyX);
                appleY = nextCoordinate(SCREEN_HEIGHT, snakeBodyY);

                //预留难度设置的方法
                difficultySettings();
            }
        }

        //重新绘图, 执行 paintComponent 方法
        repaint();
    }

    /**
     * 蛇身移动
     */
    private void move() {
        for (int i = bodyParts; i > 0; i--) {
            snakeBodyX[i] = snakeBodyX[(i - 1)];
            snakeBodyY[i] = snakeBodyY[(i - 1)];
        }
        switch (direction) {
            case Direction.UP:
                snakeBodyY[0] -= UNIT_SIZE;
                break;
            case Direction.DOWN:
                snakeBodyY[0] += UNIT_SIZE;
                break;
            case Direction.LEFT:
                snakeBodyX[0] -= UNIT_SIZE;
                break;
            case Direction.RIGHT:
                snakeBodyX[0] += UNIT_SIZE;
                break;
            default:
                break;
        }
    }

    private void doSomeThing() {
        if (KeyEvent.VK_ESCAPE == eventKeyCode) {
            System.out.println("退出程序");
            System.exit(0);
            return;
        }

        if (KeyEvent.VK_1 == eventKeyCode) {
            System.out.println("开始游戏");
            runStatus = RunStatus.RUN;
        }

        if (KeyEvent.VK_2 == eventKeyCode) {
            if (runStatus != RunStatus.RUN) {
                System.out.println("重新开始");
                init();
            }
            runStatus = RunStatus.RUN;
        }
        if (KeyEvent.VK_3 == eventKeyCode) {
            if (runStatus == RunStatus.RUN) {
                System.out.println("暂停");
                runStatus = RunStatus.PAUSE;
                eventKeyCode = -1;
                return;
            }
            if (runStatus == RunStatus.PAUSE) {
                System.out.println("继续");
                runStatus = RunStatus.RUN;
                eventKeyCode = -1;
                return;
            }
        }
        eventKeyCode = -1;

    }

    /**
     * 难度设置, 默认得分超过16的倍数时速度提升1/4
     */
    private void difficultySettings() {
        //难度增加, 速度加快
        if (applesEaten % 16 == 0 && applesEaten != 0) {
            timer.setDelay(timer.getDelay() - timer.getDelay() / 4);
        }
    }

    /**
     * 下一个苹果坐标
     *
     * @param randomFactorint
     * @param arr
     * @return
     */
    synchronized private int nextCoordinate(int randomFactorint, int[] arr) {
        int coordinate = random.nextInt(randomFactorint / UNIT_SIZE) * UNIT_SIZE;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == -1) {
                break;
            }
            if (coordinate == arr[i]) {
                nextCoordinate(randomFactorint, arr);
            }
        }
        return coordinate;
    }

    /**
     * 按键适配器,用于监听输入按键
     */
    public class MyKeyAdapter extends KeyAdapter {
        public void keyPressed(KeyEvent e) {
            int keyCode = e.getKeyCode();
            eventKeyCode = e.getKeyCode();
            if ((keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_A) && direction != Direction.RIGHT) {
                direction = Direction.LEFT;
            } else if ((keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_D) && direction != Direction.LEFT) {
                direction = Direction.RIGHT;
            } else if ((keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_W) && direction != Direction.DOWN) {
                direction = Direction.UP;
            } else if ((keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_S) && direction != Direction.UP) {
                direction = Direction.DOWN;
            }
        }
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setTitle("贪吃蛇");
        SnakeGame snake = new SnakeGame();
        frame.add(snake);
        frame.setResizable(false);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

效果展示

游戏启动

游戏暂停

相关推荐
七星静香17 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员18 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU18 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie622 分钟前
在IDEA中使用Git
java·git
Elaine20239137 分钟前
06 网络编程基础
java·网络
G丶AEOM39 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201339 分钟前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀40 分钟前
LRU缓存算法
java·算法·缓存
镰刀出海43 分钟前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel