Java.GUI.Snake

简易的贪吃蛇游戏

基于所学的Java.AWTJava.Swing,实现简易的贪吃蛇游戏,以巩固所学知识。
实现效果:
贪吃蛇.gif

实现工具类

准备必要的图片素材:蛇头,蛇的身体,食物等素材图片,本demo采用的素材均来自于阿里巴巴矢量图标库
创建类用于加载数据,数据与实现分离,体会面向对象的思想。

LoadData类用于加载数据:

package xyz.lilmoon.snakegame;

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

public class LoadData {
    public ImageIcon up = this.load("statics/up.png");
    public ImageIcon down = this.load("statics/down.png");
    public ImageIcon right = this.load("statics/right.png");
    public ImageIcon left = this.load("statics/left.png");
    public ImageIcon food = this.load("statics/food.png");
    public ImageIcon body = this.load("statics/body.png");

    public ImageIcon load(String path) {
        URL url = this.getClass().getResource(path);
        assert url != null;
        return new ImageIcon(url);
    }
}

Location类用于作为坐标数据结构,保存位置的x、y坐标:

package xyz.lilmoon.snakegame;

public class Location {
    public int x;
    public int y;

    public Location(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Snake类包含贪吃蛇的相关信息(长度,头朝向,身体的坐标数组),提供初始化方法及改变朝向的方法:

package xyz.lilmoon.snakegame;

public class Snake {
    // 蛇的长度
    public int len = 3;
    // 蛇的朝向
    String direction;
    // 蛇的系列坐标
    public Location[] snake = new Location[800];

    public Snake() {
        init();
    }

    public void init() {
        direction = "R";
        snake[0] = new Location(5, 2);
        snake[1] = new Location(4, 2);
        snake[2] = new Location(3, 2);
    }

    public void setDirection(String direction) {
        this.direction = direction;
    }
}

创建绘图类

创建GamePanel类,继承JPanel类;实现KeyListener接口(用于监听键盘事件),ActionListener接口(用于监听计时器带来的事件)。

声明要用到的一系列属性

public static Boolean state;        // 游戏状态:暂停/开始,初始为暂停
public Boolean fail;                // 判输状态:初始为false

Snake snake;			    // 实例化蛇类,包含了蛇的长度及坐标

Timer timer = new Timer(100, this); // 创建计时器,让蛇随时间自动前进,触发间隔设为100ms
Location food;			    // 创建食物的坐标对象
LoadData data;			    // 实例化数据类,用于加载数据
Random random = new Random();	    // 实例化随机数类,生成随机数,实现食物的随机位置生成

创建构造器,用于初始加载个类数据

public GamePanel() {
    // 获得焦点
    this.setFocusable(true);
    // 添加键盘监听器
    this.addKeyListener(this);
    // 加载icon素材类
    data = new LoadData();
}

提供init()方法,用于初始化游戏状态

// 初始化游戏状态
public void init() {
    fail = false;
    state = false;
    // 创建蛇类
    snake = new Snake();
    // 随机生成食物位置
    food = new Location(random.nextInt(34), random.nextInt(24));

    // 启动计时器,计时器会不断触发事件监听器
    timer.start();
}

重写JComponent类的paintComponent()方法

该方法为绘画程序,随程序自动启动,可通过repaint()方法再次启动,游戏的各种绘制均由该方法完成:

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    // 绘制提示信息
    g.setColor(Color.BLACK);
    g.drawString("按下空格开始/暂停游戏,W/A/S/D控制上/下/左/右,死亡后按下空格可重新开始游戏。", 50, 50);

    // 绘制游戏区域
    g.setColor(Color.GRAY);
    g.fillRect(25, 75, 850, 600);

    // 绘制食物
    drawFood(food, g);

    // 蛇类
    // 根据朝向绘制蛇头
    switch (snake.direction) {
        case "R":
            data.right.paintIcon(this, g, snake.snake[0].x * 25 + 25, snake.snake[0].y * 25 + 75);
            break;
        case "L":
            data.left.paintIcon(this, g, snake.snake[0].x * 25 + 25, snake.snake[0].y * 25 + 75);
            break;
        case "U":
            data.up.paintIcon(this, g, snake.snake[0].x * 25 + 25, snake.snake[0].y * 25 + 75);
            break;
        case "D":
            data.down.paintIcon(this, g, snake.snake[0].x * 25 + 25, snake.snake[0].y * 25 + 75);
            break;
    }
    // 遍历蛇数组,绘制身体
    for (int i = 1; i < snake.len; i++) {
        data.body.paintIcon(this, g, snake.snake[i].x * 25 + 25, snake.snake[i].y * 25 + 75);
    }

    // 根据游戏状态,暂停/启动,绘制提示字符
    if (!state) {
        g.setColor(Color.WHITE);
        g.setFont(new Font(null, Font.BOLD, 40));
        g.drawString("Press SPACE to Start!", 240 ,400);
    }
    // 根据游戏输赢,绘制提示字符
    if (fail) {
        g.setColor(Color.RED);
        g.setFont(new Font(null, Font.BOLD, 500));
        g.drawString("寄", 200 ,560);
    }
}

提供工具方法简化重复代码

drawFood()方法用于绘制食物:

public void drawFood(Location food, Graphics g) {
    data.food.paintIcon(this, g, food.x * 25 + 25, food.y * 25 + 75);
}

eat()方法用于在蛇吃到食物时为其增加长度,添加块:

public static void eat(Snake snake) {
    int len = ++snake.len;
    // 在尾部的位置添加块
    snake.snake[len - 1] = new Location(snake.snake[len - 2].x, snake.snake[len - 2].y);
}

重写KeyPressed()方法,实现需要键盘监听效果

// 键盘监听器
@Override
public void keyPressed(KeyEvent e) {
    // 空格键会根据游戏状态实现游戏的暂停/开始或重新游戏
    if (e.getKeyCode() == KeyEvent.VK_SPACE) {
        if (fail) {
            init();
        }
        repaint();
        state = !state;
        repaint();
    }

    // 根据朝向设置键盘监听器,使蛇不能回头
    switch (snake.direction) {
        case "R":
            switch (e.getKeyCode()) {
                case KeyEvent.VK_S:
                    snake.setDirection("D");
                    break;
                case KeyEvent.VK_D:
                    snake.setDirection("R");
                    break;
                case KeyEvent.VK_W:
                    snake.setDirection("U");
                    break;
            }
            break;
        case "U":
            switch (e.getKeyCode()) {
                case KeyEvent.VK_D:
                    snake.setDirection("R");
                    break;
                case KeyEvent.VK_W:
                    snake.setDirection("U");
                    break;
                case KeyEvent.VK_A:
                    snake.setDirection("L");
                    break;
            }
            break;
        case "D":
            switch (e.getKeyCode()) {
                case KeyEvent.VK_S:
                    snake.setDirection("D");
                    break;
                case KeyEvent.VK_D:
                    snake.setDirection("R");
                    break;
                case KeyEvent.VK_A:
                    snake.setDirection("L");
                    break;
            }
            break;
        case "L":
            switch (e.getKeyCode()) {
                case KeyEvent.VK_S:
                    snake.setDirection("D");
                    break;
                case KeyEvent.VK_W:
                    snake.setDirection("U");
                    break;
                case KeyEvent.VK_A:
                    snake.setDirection("L");
                    break;
            }
            break;
    }
}

重写actionPerformed()方法,该方法会被计时器唤醒,用于实现蛇的随时间前进

@Override
public void actionPerformed(ActionEvent e) {
    // 每次timer触发都前进一格;若非暂停,且未失败,则继续移动
    if (state && !fail) {
        // 身体向前移动:从尾部开始,每一块依次获取前面的坐标,所以是从后往前依次前进,头部最后前进
        for (int i = snake.len - 1; i > 0; i--) {
            snake.snake[i].x = snake.snake[i - 1].x;
            snake.snake[i].y = snake.snake[i - 1].y;
        }

        // 蛇头向前移动:根据朝向改变蛇头的位置
        switch (snake.direction) {
            case "R":
                snake.snake[0].x = (snake.snake[0].x + 1) % 34;
                break;
            case "U":
                // +24 使得坐标不会为负
                snake.snake[0].y = (snake.snake[0].y - 1 + 24) % 24;
                break;
            case "D":
                snake.snake[0].y = (snake.snake[0].y + 1) % 24;
                break;
            case "L":
                // +34 使得坐标不会为负
                snake.snake[0].x = (snake.snake[0].x - 1 + 34) % 34;
                break;
        }

        // 吃到食物
        if (snake.snake[0].x == food.x && snake.snake[0].y == food.y ){
            // 调用eat方法,增加一格长度
            eat(snake);
            // 设置食物随机位置,重新绘制
            food.x = random.nextInt(34);
            food.y = random.nextInt(24);
        }

        // 失败判定
        for (int i = 1; i < snake.len; i++) {
            if (snake.snake[0].x == snake.snake[i].x && snake.snake[0].y == snake.snake[i].y) {
                fail = true;
                break;
            }
        }
        repaint();
    }
}

创建窗口类

创建Index类,继承JFrame类;使用写好的GamePanel类及方法,来实现游戏界面,并实例化窗口,打开界面:

package xyz.lilmoon.snakegame;

import javax.swing.*;

public class Index extends JFrame {
    public void init() {
        setBounds(300, 200, 900, 720);
        setResizable(false);
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        GamePanel gamePanel = new GamePanel();
        gamePanel.init();
        add(gamePanel);

        setVisible(true);
    }

    public static void main(String[] args) {
        new Index().init();
    }
}
tag(s):
show comments · back · home