贪 吃 蛇

简介

简易贪吃蛇,使用 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);
    }
}

效果展示

游戏启动

游戏暂停

相关推荐
GoodStudyAndDayDayUp6 分钟前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶36 分钟前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗43 分钟前
常用类晨考day15
java
骇客野人1 小时前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言
yuanbenshidiaos2 小时前
c++---------数据类型
java·jvm·c++
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
Lojarro2 小时前
【Spring】Spring框架之-AOP
java·mysql·spring
莫名其妙小饼干2 小时前
网上球鞋竞拍系统|Java|SSM|VUE| 前后端分离
java·开发语言·maven·mssql
isolusion2 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp3 小时前
Spring-AOP
java·后端·spring·spring-aop