Java贪吃蛇小游戏

时间:2024-02-15 15:30:57

贪吃蛇

思路

  1. 首先构思游戏布局,计算合理的坐标系。

  2. 绘制静态数据(广告、初始小蛇、提示信息、棋盘)

    image-20200515134905430

  3. 添加键盘监听事件,改变游戏状态以及小蛇运动方向

  4. 添加定时器,让小蛇在一段时间内移动一定的距离

  5. 随机产生食物,并监听食物状态是否被吃

  6. 处理游戏结束事件

  7. 扩展相关游戏机制(积分、等级)

    • 定义数据
    • 绘制图像
    • 事件监听

注意事项

  • 导入文件资源时,通过类的相对路径获取

    • URL headURL = Data.class.getResource("header.png"); // 这是放在源代码同一个包下的文件

    • URL headURL = Data.class.getResource("/header.png"); // 这是放在项目根目录下的文件

  • 键盘监听时,需要自动获取焦点。

    • this.setFocusable(true); // 获取焦点
      this.addKeyListener(new MyKeyListener());
  • 在修改数据之后,需要repaint重绘图形

  • 小蛇运动时,需要注意边界问题

  • 食物随机产生的坐标也要限制在游戏区域内

  • 添加游戏的可玩性

具体实现

Data.java 存放所有图像数据

package snake;

import javax.swing.*;
import java.net.URL;

/**
 * 数据中心
 */
public class Data {

    // 相对路径
    // 绝对路径  / 当前项目-->"GUI编程目录"

    public static URL headURL = Data.class.getResource("header.png");
    public static URL upURL = Data.class.getResource("up.png");
    public static URL downURL = Data.class.getResource("down.png");
    public static URL leftURL = Data.class.getResource("left.png");
    public static URL rightURL = Data.class.getResource("right.png");
    public static URL bodyURL = Data.class.getResource("body.png");
    public static URL foodURL = Data.class.getResource("food.png");
    public static URL foodURL2 = Data.class.getResource("food2.png");

    public static ImageIcon header = new ImageIcon(headURL);
    public static ImageIcon up = new ImageIcon(upURL);
    public static ImageIcon down = new ImageIcon(downURL);
    public static ImageIcon left = new ImageIcon(leftURL);
    public static ImageIcon right = new ImageIcon(rightURL);
    public static ImageIcon body = new ImageIcon(bodyURL);
    public static ImageIcon food = new ImageIcon(foodURL);
    public static ImageIcon food2 = new ImageIcon(foodURL2);
}

StartGame.java 游戏启动类

package snake;

import javax.swing.*;
import java.awt.*;

public class StartGame {
    public static void main(String[] args) {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        int screenWidth = (int) screenSize.getWidth();
        int screenHeight = (int) screenSize.getHeight();
        System.out.println("屏幕宽度:" + screenWidth + ",屏幕高度:" + screenHeight);

        // 本游戏固定窗体大小(900,720)不可变
        int x = (screenWidth - 900) / 2;
        int y = (screenHeight - 720) / 2;
        System.out.println("相对坐标x:" + x + ",y:" + y);

        JFrame jFrame = new JFrame("贪吃蛇");
        jFrame.setBounds(x, y, 900, 720);
        jFrame.setResizable(false);
        jFrame.add(new GamePanel());
        jFrame.setVisible(true);
        jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    }
}

GamePanel.java 处理游戏逻辑

package snake;

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.Random;

public class GamePanel extends JPanel {
    // 蛇的数据结构
    int length;
    int[] snakeX = new int[600];
    int[] snakeY = new int[500];
    char direction;

    // 食物的坐标
    int foodx;
    int foody;
    Random random = new Random();

    // 计数器
    int count;
    int score;
    int worth;

    boolean isStart;
    boolean isFail;

    // 定时器,每隔100毫秒执行一次参数的事件。
    int interval = 100;
    Timer timer = new Timer(interval, new MyActionLinstener());


    public void init() {
        // 默认游戏未开始
        this.isStart = false;
        this.isFail = false;

        // 初始化速度
        interval = 200;
        timer.setDelay(interval);

        // 初始化小蛇数据
        this.length = 3;
        this.score = 0;
        this.snakeX[0] = 100;
        this.snakeY[0] = 100;
        this.snakeX[1] = 75;
        this.snakeY[1] = 100;
        this.snakeX[2] = 50;
        this.snakeY[2] = 100;
        this.direction = \'R\';

        // 随机产生事物的坐标
        this.foodx = 25 + 25 * random.nextInt(34);
        this.foody = 75 + 25 * random.nextInt(24);
        this.count = 1; // 统计食物数量

        // 获得焦点和键盘监听事件
        this.setFocusable(true);
        // 添加键盘监听事件
        this.addKeyListener(new MyKeyListener());
        // 开启定时器
        timer.start();
    }

    public GamePanel() {
        init();
    }

    // 自动绘制面板
    // paintComponent()是swing的一个方法,相当于图形版的main(),是会自执行的。
    @Override
    protected void paintComponent(Graphics g) {
        //清屏
        super.paintComponent(g);
        this.setBackground(Color.WHITE);
        //绘制静态面板
        // 顶部广告
        Data.header.paintIcon(this, g, 25, 11);

        // 游戏区域
        g.fillRect(25, 75, 850, 600);

        // 画积分
        g.setColor(Color.white);
        g.setFont(new Font("微软雅黑", Font.BOLD, 18));
        g.drawString("长度:" + length, 750, 30);
        g.drawString("分数:" + score, 750, 56);

        // 绘制食物
        if (count % 5 != 0) {
            Data.food.paintIcon(this, g, foodx, foody);
            worth = 1;
        } else {
            Data.food2.paintIcon(this, g, foodx, foody);
            worth = 3;
        }

        // 游戏状态 默认为暂停 需要提示信息
        if (!isStart) {
            g.setColor(Color.white);
            g.setFont(new Font("微软雅黑", Font.BOLD, 40));
            g.drawString("按下空格开始游戏", 300, 300);
        }

        // 画小蛇 初始化向右 长度为3
        // 选择头部的方向
        ImageIcon head = Data.right;
        switch (this.direction) {
            case \'U\':
                head = Data.up;
                break;
            case \'D\':
                head = Data.down;
                break;
            case \'L\':
                head = Data.left;
                break;
            case \'R\':
                head = Data.right;
                break;
            default:
                break;
        }

        head.paintIcon(this, g, snakeX[0], snakeY[0]);
        for (int i = 1; i < this.length; i++) {
            Data.body.paintIcon(this, g, snakeX[i], snakeY[i]);
        }

        // 结束状态
        if (isFail) {
            g.setColor(Color.red);
            g.setFont(new Font("微软雅黑", Font.BOLD, 100));
            g.drawString("Game Over", 150, 200);
            g.drawString("按空格重新开始", 100, 400);
        }


    }


    /**
     * 内部类监听键盘事件
     */
    class MyKeyListener extends KeyAdapter {
        // 只需要监听键盘按下
        @Override
        public void keyPressed(KeyEvent e) {
            int keyCode = e.getKeyCode();
            // System.out.println(keyCode);
            if (keyCode == KeyEvent.VK_SPACE) {
                if (isFail) {
                    // 重新开始
                    init();
                    isStart = true;
                } else {
                    isStart = !isStart;
                }

                repaint();
            }

            // 内层增加一个判断,180度转向 不生效
            switch (keyCode) {
                case KeyEvent.VK_UP:
                    if (direction != \'D\') {
                        direction = \'U\';
                    }
                    break;
                case KeyEvent.VK_DOWN:
                    if (direction != \'U\') {
                        direction = \'D\';
                    }
                    break;
                case KeyEvent.VK_LEFT:
                    if (direction != \'R\') {
                        direction = \'L\';
                    }
                    break;
                case KeyEvent.VK_RIGHT:

                    if (direction != \'L\') {
                        direction = \'R\';
                    }
                    break;
                default:
                    break;
            }


            // 调整速度  小键盘的 + -
            if (keyCode == 107) {
                interval = interval > 10 ? interval - 10 : 10;
                timer.setDelay(interval);
                System.out.println("加速" + interval);
            } else if (keyCode == 109) {
                interval += 20;
                timer.setDelay(interval);
                System.out.println("减速" + interval);
            }
        }
    }

    class MyActionLinstener implements ActionListener {
        // 事件监听类
        @Override
        public void actionPerformed(ActionEvent e) {
            // 通过事件刷新界面
            // 如果游戏开始且未失败,则刷新界面
            if (isStart && !isFail) {

                // 吃食物  蛇头与食物重合
                if (snakeX[0] == foodx && snakeY[0] == foody) {
                    length++; // 吃了边长
                    foodx = 25 + 25 * random.nextInt(34);
                    foody = 75 + 25 * random.nextInt(24);
                    count++; // 统计食物数量
                    score += worth;

                    // 这里可以设置积分到一定值,增加移动速度
                    if (score < 5) {
                        interval = 200;
                        timer.setDelay(interval);
                        System.out.println("一级速度");
                    } else if (score < 15) {
                        interval = 150;
                        timer.setDelay(interval);
                        System.out.println("二级速度");
                    } else if (score < 25) {
                        interval = 100;
                        timer.setDelay(interval);
                        System.out.println("三级速度");
                    } else if (score < 50) {
                        interval = 50;
                        timer.setDelay(interval);
                        System.out.println("四级速度");
                    }

                }


                for (int i = length - 1; i > 0; i--) {
                    // 从最后一节开始继承前一节的位置
                    snakeX[i] = snakeX[i - 1];
                    snakeY[i] = snakeY[i - 1];
                }
                // 头部找新的路,利用三元运算符判断边界重置
                switch (direction) {
                    case \'U\':
                        snakeY[0] = snakeY[0] <= 75 ? 650 : snakeY[0] - 25;
                        break;
                    case \'D\':
                        snakeY[0] = snakeY[0] >= 650 ? 75 : snakeY[0] + 25;
                        break;
                    case \'L\':
                        snakeX[0] = snakeX[0] <= 25 ? 850 : snakeX[0] - 25;
                        break;
                    case \'R\':
                        snakeX[0] = snakeX[0] % 850 + 25;
                        break;
                    default:
                        break;
                }

                // 失败判定
                for (int i = 1; i < length; i++) {
                    if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) {
                        isFail = true;
                    }
                }
                repaint();
            }
        }
    }

}

游戏效果

snake

该版本已知bug

小蛇不能反向运动,玩家操作过快导致的意外死亡。

比如小蛇向↑运动过程中,玩家按 ↓ 是无效的,但如果按 ← 的瞬间按 ↓

由于定时器的刷新速度没有跟上玩家的操作,游戏帧尚未刷新就已经有了下一次操作。

误判为小蛇回头撞了自己,游戏结束。

第二个问题是

空格本应该具有暂停/继续/重玩的功能。

游戏局数为偶数时,空格键暂停功能失效。

打包游戏

首先通过IDEA生成Jar包,再通过exe4j将jar打包为exe

image-20200516113408392

具体操作跳转至Java桌面应用程序打包。