第二十八讲——GUI之贪吃蛇

第二十八讲——GUI之贪吃蛇

Timer(定时器) 与 ActionListener

在此之前呢 ActionListener 一般用于组件如 JButton JComboBox(下拉框) 等监听应用,而此次是监听面板,用 Timer与 ActionListener 组合使用才能生效。

可以理解为多少时间执行一次监听

Timer JDK Api

在指定时间间隔触发一个或多个 ActionEvent。一个示例用法是动画对象,它将 Timer 用作绘制其帧的触发器。

设置计时器的过程包括创建一个 Timer 对象,在该对象上注册一个或多个动作侦听器,以及使用 start 方法启动该计时器。

构造 Timer 时要指定一个延迟参数和一个 ActionListener。延迟参数用于设置初始延迟和事件触发之间的延迟(以毫秒为单位)。启动了计时器后,它将在向已注册侦听器触发第一个 ActionEvent 之前等待初始延迟。第一个事件之后,每次超过事件间延迟时它都继续触发事件,直到被停止。

    // 构造方法
public Timer(int delay , ActionListener listener)
// 创建一个 Timer 并将初始延迟和事件间延迟初始化为 delay 毫秒。如果 delay 小于等于 0,则该计时器一启动就触发事件。如果 listener 不为 null,则它会在计时器上注册为动作侦听器。
参数:
delay - 初始延迟和动作事件间延迟的毫秒数
listener - 初始侦听器;可以为 null
// Timer timer = new Timer(100,this(ActionListener));
    

Application

package Test;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class Test03 {
    public static void main(String[] args) {
        new MyAction();

    }
}
class MyAction extends JFrame implements ActionListener, KeyListener {

     boolean start = false;

    Timer timer = new Timer(100,this);
    // 100 Ms 刷新一次 , 添加监听事件 this  省略了 timer.addActionListener(this);
    public MyAction(){
        this.setBounds(200,200,400,400);
        this.setVisible(true);

        this.addKeyListener(this);

        timer.start(); // 启动定时器
        
        // 初始化里 已经做了这一步  timer.addActionListener(this);
    }


    @Override
    public void keyPressed(KeyEvent keyEvent) {
        if (keyEvent.getKeyCode() == KeyEvent.VK_F1){
            start = !start;// 每按下 F1 会取反一次
        }
    }



    @Override
    public void actionPerformed(ActionEvent actionEvent) {

        if (start){
            System.out.println("TRUE");
            start= !start;// 这里的取反保证按下 F1 时,只执行一次打印 避免死循环
        }
        System.out.println("YES");// 计时器决定了 这个内容会执行 100Ms/次 一直打印...
    }


    @Override
    public void keyTyped(KeyEvent keyEvent) {

    }
    @Override
    public void keyReleased(KeyEvent keyEvent) {

    }
}

setFocusable(设置焦点)

焦点 ;

所谓焦点就是被选中的意思,或者说是“当前正在操作的组件”的意思。
如果一个组件被选中,或者正在被操作者,就是得到了焦点,而相反的,一个组件没有被选中或者失去操作,就是被转移了焦点,焦点已经到别的组件上去了。

最明显的两个例子:
一个按钮(Button)一旦被选中,就会有一个虚线框在按钮中,并且环绕着按钮的文字,一旦失去焦点,不被操作了,这个虚线框就消失了。
一个文本框(TextField)一旦被选中,就会有一个“|”在文本框里面闪动,提示可以输入信息,一旦失去或者转移焦点了,这个“|”就没有了,不闪动,表示这个文本框你没有在操作。


主要;

  • 在多级面板窗口应用中,使用KeyListener 时要注意设置焦点,没有焦点的面板,相对于没用选中则程序不会启用。
  • setVisble(); 一定要放在最后位置

Application

package Test;

import org.w3c.dom.ls.LSOutput;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class Test03 {
    public static void main(String[] args) {
        new MyAction();
    }
}
class MyAction extends JFrame  {

    public MyAction(){
        this.setBounds(200,200,400,400);
        this.add(new MyPanel());

        this.setVisible(true); // 一开始 按键盘没有反应 原来是背景的 visible 在搞鬼 一定要放在最后
    }


}
class  MyPanel extends JPanel implements ActionListener ,KeyListener {
     boolean start = false;
    Timer timer = new Timer(100,this);

    public MyPanel (){

        this.setBounds(0,0,400,400);
        this.setBackground(Color.CYAN);
        this.setFocusable(true); // 如果不 setFocusable 就不会取得焦点 相对于没有选中本窗口 自然也不会运行该程序

        timer.start();
        this.addKeyListener(this);

        this.setVisible(true);
    }

    @Override
    public void keyPressed(KeyEvent keyEvent) {
        if (keyEvent.getKeyCode() == KeyEvent.VK_F1){
            start = !start;

        } else {
            System.out.println("其他");
        }
    }
    @Override
    public void actionPerformed(ActionEvent actionEvent) {

        if (start){
            System.out.println("TRUE");
            start= !start; // 避免死循环
        }

    }
    @Override
    public void keyTyped(KeyEvent keyEvent) {

    }
    @Override
    public void keyReleased(KeyEvent keyEvent) {

    }

}

贪吃蛇

所需素材-原码;

贪吃蛇素材-源码

目录结构;

完成样式 ;

大概布局 ;


Application

package Snake;

import javax.swing.*;
public class StartGame {
    public static void main(String[] args) {

        JFrame jFrame = new JFrame("贪吃蛇");

        jFrame.setBounds(10,10,900,720);// 这个 Game 的窗口大小并不是随便设置的要通过计算
        jFrame.setResizable(false);// 窗口不可变
        jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 一般的游戏 都放在面板上
        jFrame.add(new GamePanel());


        jFrame.setVisible(true);// 设置为显示一般放在最后  避免组件不刷新
    }
}

GamePanel Class

package Snake;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;

public class GamePanel extends JPanel implements KeyListener , ActionListener {
    // 最暴力的方式 在类上直接实现键盘监听

    // 定义蛇的数据结构
    int length = 3; // 蛇的长度
    public static int[] snakeX = new int[600]; // 蛇的 x 坐标 25*25
    public static int[] snakeY = new int[500]; // 蛇的 Y 坐标 25*25
    String fx ;

    // 食物的坐标
    int foodX;
    int foodY;
    Random random = new Random(); // 随机数

    // 游戏当前的状态:   开始  停止
    boolean isStart = false; // 默认停止

    boolean isFail = false; // 游戏失败状态

    // 积分系统 初始值
    int score;

    // 定时器 以 ms 为单位 1000ms = 1s  用法; 第一步 new 出来 第二步 要用的时候 start一下
    Timer timer = new Timer (80,this);
    // 刷新的频率 单位 Millisecond 100毫秒执行一次  添加监听的类 因为这里暴力实现了监听类所以可以写 this

    // 构造器
    public  GamePanel(){
        // 初始化构造器 配合初始化方法 作用就是提供初始化的参数 意思就是实例化的时候用初始化方法中的参数 不调用的话并不会使用初始化参数
        init();
        // 获得焦点和键盘事件
        this.setFocusable(true);// 获得焦点事件
        this.addKeyListener(this);// 获得键盘监听事件    因为当前类就暴力继承了 KeyListener 所以直接写 this
        // 我重写后直接运行 发现行不通 原来是没把键盘监听添加到面板  低级错误

        timer.start(); // 游戏一开始定时器就启动
    }


    // 初始化方法
    public void init(){
        length = 3;
        snakeX[0] = 100; snakeY[0] = 100; // 脑袋的坐标
        snakeX[1] = 75;  snakeY[1] = 100; // 第一个身体的坐标
        snakeX[2] =50;   snakeY[2] = 100; // 第二个身体的坐标
        fx = "R";// 初始化方向   蛇头初始化向右

        // 初始化foot 并把实物分布在界面上 !
        // 这里也是需要计算的 foot 必须在页面上则需要满足
        // 25(边界) < X > 875(边界)
        // 75(边界) < Y > 675(边界)
        foodX = 25 +25*random.nextInt(34); // random.nextInt (int n ) 随机数 < n (875-25)/25 = 34
        foodY = 75 +25*random.nextInt(24); // (675-25)/25 = 24

    }


    // 绘制面板  我们游戏中的所有东西 都是用这个画笔来画
    @Override
    public void paintComponent(Graphics g) { // 画组件、
        super.paintComponent(g);// 清屏  如果不用会闪屏 用就不会
        // 1. 绘制静态面板
        this.setBackground(Color.cyan);
        Data.header.paintIcon(this,g,25,11);// 头部广告栏画上去 四个参数 画在哪里 用什么画 位置 x y
        g.fillRect(25,75,850,600); // 游戏区域 数据都是经过计算的 画笔默认黑色 所以不用setColor

        // 2. 画上初始蛇
        // 2.1 蛇头进行判断 方向
        if (fx.equals("R")){
            Data.right.paintIcon(this,g,snakeX[0],snakeY[0]); // 四个参数 画在哪里 用什么画 位置 x y
        } else if (fx.equals("L")){
            Data.left.paintIcon(this,g,snakeX[0],snakeY[0]);
        }else if (fx.equals("U")){
            Data.up.paintIcon(this,g,snakeX[0],snakeY[0]);
        }else {
            Data.down.paintIcon(this,g,snakeX[0],snakeY[0]);
        }
        // 2.2 身体 动态成长
        // 这个是静态的值  因为小蛇是动态的所以需要使用动态的值这里用 for
        for (int i = 1; i < length; i++) {
            Data.body.paintIcon(this,g,snakeX[i],snakeY[i]);// 这样能实现小蛇动态成长 取代下面两行代码
        }
//        Data.body.paintIcon(this,g,snakeX[1],snakeY[1]); // 第一个身体的坐标
//        Data.body.paintIcon(this,g,snakeX[2],snakeY[2]); // 第二个身体的坐标

        // 绘制完静态初始化小蛇后发现有间隔 间隔原因也很简单 就是在画图的时候提供空白部分 而蛇头是没有留白的 所以看起来蛇头与身体链接的部分像是没有空隔实际上只是空隔比较小而已

        // 画 foot
        Data.food.paintIcon(this,g,foodX,foodY);

        // 3. 游戏状态 设置 开始 停止
        if (isStart==false){
            g.setColor(Color.white);
            g.setFont(new Font("微软雅黑",Font.BOLD,40));// 设置字体
            g.drawString("按下空格开始游戏",300,300);// 字符串 , 位置
        }

        if (isFail){
            g.setColor(Color.pink);
            g.setFont(new Font("微软雅黑",Font.ITALIC,30));// 设置字体
            g.drawString("游戏失败按空格重新开始",300,300);// 字符串 , 位置
        }

        g.setColor(Color.white);
        g.setFont(new Font("微软雅黑",Font.BOLD,20));
        g.drawString("长度 "+length,770,32);
        g.drawString("积分 "+score,770,52);

    }

    // 键盘监听事件
    @Override
    public void keyPressed(KeyEvent keyEvent) {
        int keyCode = keyEvent.getKeyCode(); // 获得键盘按键的 code 值

        if (keyCode == KeyEvent.VK_SPACE){
            // 两种开局方式 一种 fail 一种 暂停开始
            if (isFail){
                // 重新开始
                isFail = false;
                init();
            } else {
                isStart = !isStart; //  暂停开始 取反
            }
            // 如果按下的是空格键 就开始 再按就停止 写死就没用了
            //isStart = !isStart;// 取反 和现在 isStart 的值相反
            // 因为值已经变化了 所以需要 repaint 重画一下 这里不太理解奥 这个repaint
            repaint(); // 重画
        }
        // 小蛇移动
        if (keyCode==KeyEvent.VK_UP){
            fx ="U";
        } else if(keyCode == KeyEvent.VK_DOWN){
            fx = "D";
        } else if (keyCode == KeyEvent.VK_LEFT){
            fx = "L";
        } else  if (keyCode == KeyEvent.VK_RIGHT){
            fx = "R";
        }

    }

    // 事件监听—— ——需要通过固定时间来刷新  比如 1s十次
    // 这里用 ActionListener 和 Timer 配合完成了 小蛇移动
    @Override
    public void actionPerformed(ActionEvent actionEvent) {
        if (isStart&& isFail==false){  // 如果游戏是开始状态,就动起来

            // 吃食物
            if (snakeX[0] == foodX && snakeY[0] == foodY){
                length++;// 长度 +1
                score = score +10;
                //再次生成随机 食物
                foodX = 25 +25*random.nextInt(34);
                foodY = 75 +25*random.nextInt(24);
                // 原本不懂为什么这里可以小蛇吃完实物就消失, 其实是食物的坐标改变了 配合 repaint 所以食物"消失"了
                // 原本以为要配合 visible 用
            }

            //右移 / 移动
            for (int i = length-1; i > 0 ; i--) { // 后一节移到前一节的位置 snakeX[1] = snakeX[0]
                snakeX[i] = snakeX[i-1]; // X向前往前一动节
                snakeY[i] = snakeY[i-1]; // Y向前往前一动节
                // 这里非常灵活 不只是实现死板的往右移动 它是灵活的跟着头移动 原理就是 每次移动 , 继承前一个坐标值
            }
//            // 头动起来
//                snakeX[0] = snakeX[0]+25;
            // 走向
            // 这里要关注蛇头的定位坐标 一直都在蛇头的左上角
            if (fx.equals("R")){

                snakeX[0] = snakeX[0]+25;
                if (snakeX[0]>850){ snakeX[0] = 25; } // 边界判断  防止小蛇“飞出屏幕”

            } else if (fx.equals("L")){

                snakeX[0] = snakeX[0]-25;
                if (snakeX[0]<25){ snakeX[0] = 850;} // 边界判断  防止小蛇“飞出屏幕”  850 = 850+25(空白处)-25(蛇头X )

            }else if (fx.equals("U")){

                snakeY[0] = snakeY[0]-25;
                if (snakeY[0]<75){ snakeY[0] = 675-25; } // 边界判断  防止小蛇“飞出屏幕”
                // 600 是游戏区域的宽 但是蛇头的坐标是根据面板的坐标来的 所以 600 + 75 是游戏区域的下限 但是由于蛇头的定位在左上角还得-25 才能露出整个蛇头 防止半个身子还在外面


            }else if (fx.equals("D")){

                snakeY[0] = snakeY[0]+25;
                if (snakeY[0] + 25 >675){ snakeY[0] = 75; } // 边界判断  防止小蛇“飞出屏幕”
                //这里 边界值需要计算 主要考虑小蛇的 头坐标 以及循环的意义

            }

            // 失败判定 撞到自己就算失败

            for (int i = 1; i < length; i++) {
                if (snakeX[0]==snakeX[i]&&snakeY[0]==snakeY[i]){
                    // 如果蛇头与身体相撞判断 fail
                    // 我一开始还想遍历出全部的点坐标来做判断 逻辑没搞对
                    // 一开始 就弹出游戏失败 原来是我把 for循环初始值设置为 0 了 0 是蛇头 蛇头的坐标自然等于蛇头所以判断失败
                    isFail = true;
                }

            }



//            // 边界判断 防止小蛇“飞出屏幕”
//            if(snakeX[0]>850){ // 如果小蛇跑出边界 则回到游戏区域的 X 边界
//                snakeX[0] = 25;
//            }

            repaint(); // 重画页面
        }
        timer.start(); // 定时器开启
    }



    // 后面两个一个是打字 一个是弹起 我只要用压下就行 但是必须重写全部
    @Override
    public void keyTyped(KeyEvent keyEvent) {

    }

    @Override
    public void keyReleased(KeyEvent keyEvent) {
    }


}

Data Class

package Snake;

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

// 数据中心
public class Data {

    // 相对路径  up.png  相对于当前类所在包(包即是文件夹)下的路径
    // 绝对路径  加斜杆 /Application/Statics/up.png 相对于当前的项目的路径 | 我猜测就是当前类所在盘符的根目录开始查找  还是不太理解 用相对路径会容易些
    public static URL headerURL = Data.class.getResource("Statics/header.png");
    public static URL upURL = Data.class.getResource("Statics/up.png");
    public static URL downURL = Data.class.getResource("Statics/down.png");
    public static URL leftURL = Data.class.getResource("Statics/left.png");
    public static URL rightURL = Data.class.getResource("Statics/right.png");
    public static URL foodURL = Data.class.getResource("Statics/food.png");
    public static URL bodyURL = Data.class.getResource("Statics/body.png");

    public static ImageIcon header = new ImageIcon(headerURL);
    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);



}

1. 定义数据
boolean isFail = false; // 游戏失败状态
2. 画上去
        if (isFail){
            g.setColor(Color.cyan);
            g.setFont(new Font("微软雅黑",Font.ITALIC,30));// 设置字体
            g.drawString("按下空格开始游戏",300,300);// 字符串 , 位置
        }
3. 监听事件
     键盘监听
                 // 两种开局方式 一种 fail 一种 暂停开始
                 if (isFail){
                     // 重新开始
                     isFail = false;
                     init();
                 } else {
                     isStart = !isStart; //  暂停开始 取反
                 }
     事件监听
                 for (int i = 0; i < length; i++) {
                     if (snakeX[0]==snakeX[i]&&snakeY[0]==snakeY[i]){
                         // 如果蛇头与身体相撞判断 fail
                         // 我一开始还想遍历出全部的点坐标来做判断 逻辑没搞对
                         isFail = true;
                     }

                 }
4.

为什么 长度 +1 能加一节上去


新增单词

0 snake
1 space 空格 丝倍丝
2 setFocusable 是否获取鼠标的焦点 否克斯伯
3 delay 延迟 迪累
4 fail 不及格 非L
5 BOLD 粗体 伯的
6 ITALIC 斜体 唉他类可
posted @ 2021-09-19 22:27  项晓忠  阅读(92)  评论(0编辑  收藏  举报