首先感谢大家对上篇博客的支持与鼓励,我会再接再厉,记录自己的学习Java的进程。

        今天详解我在写贪吃蛇的时候遇到的问题

先上代码:

Main类

import javax.swing.*;

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

GameStart类

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

public class GameStart extends JFrame {
    public GameStart(){
        this.setTitle("Java贪吃蛇");
        int width = Toolkit.getDefaultToolkit().getScreenSize().width;
        int height = Toolkit.getDefaultToolkit().getScreenSize().height;
        this.setBounds((width-800)/2,(height-600)/2,800,600);
        this.setVisible(true);//设置窗体可视化
        this.setResizable(false);//设置窗体不可调整大小
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//关闭设置
        this.add(new GamePanel());
    }
}

GamePanel类

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 snakeX[]=new int[500];
    int snakeY[]=new int[500];
    int foodX;
    int foodY;
    int length;
    boolean isStart;
    boolean isDie;
    Timer timer;
    String direction;
    int score;
    Random random=new Random();
    public GamePanel(){
        intiPanel();
        this.setFocusable(true);
        this.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                super.keyPressed(e);
                if(e.getKeyCode()==KeyEvent.VK_SPACE){
                    if(!isDie){
                        isStart=!isStart;
                        direction=" ";
                    }else{
                        intiPanel();
                        isDie=false;
                        isStart=true;
                        repaint();
                    }
                }
                if(e.getKeyCode()==KeyEvent.VK_UP&&direction!="S"){
                    direction="W";
                }
                if(e.getKeyCode()==KeyEvent.VK_DOWN&&direction!="W"){
                    direction="S";
                }
                if(e.getKeyCode()==KeyEvent.VK_LEFT&&direction!="D"){
                    direction="A";
                }
                if(e.getKeyCode()==KeyEvent.VK_RIGHT&&direction!="A"){
                    direction="D";
                }
            }
        });
        timer=new Timer(100, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if(isStart&&!isDie){

                    for(int i=length-1;i>0;i--){
                        snakeX[i]=snakeX[i-1];
                        snakeY[i]=snakeY[i-1];
                    }
                    if("W".equals(direction)){
                        snakeY[0]-=20;
                    }
                    if("S".equals(direction)){
                        snakeY[0]+=20;
                    }
                    if("A".equals(direction)){
                        snakeX[0]-=20;
                    }
                    if("D".equals(direction)){
                        snakeX[0]+=20;
                    }
                    if(" ".equals(direction)){
                        snakeX[0]+=20;
                    }
                    if (snakeX[0] > 760) {
                        snakeX[0] = 20;
                    }
                    if (snakeX[0] < 0) {
                        snakeX[0] = 760;
                    }
                    if (snakeY[0] < 60) {
                        snakeY[0] = 500;
                    }
                    if (snakeY[0] > 500) {
                        snakeY[0] = 60;
                    }
                    if(snakeX[0]==foodX&&snakeY[0]==foodY){
                        length++;
                        score++;
                        foodX=(int)random.nextInt(39)*20;
                        foodY=(int)random.nextInt(22)*20+80;
                    }
                    for(int i=1;i<length;i++){
                        if(snakeX[0]==snakeX[i]&&snakeY[0]==snakeY[i]){
                            isDie=true;
                        }
                    }
                }
                repaint();
            }
        });
        timer.start();
    }
    public void intiPanel(){
        snakeX[0]=60;
        snakeY[0]=100;
        snakeX[1]=40;
        snakeY[1]=100;
        snakeX[2]=20;
        snakeY[2]=100;
        length=3;
        isStart=false;
        isDie=false;
        foodX=300;
        foodY=300;
        score=0;
        direction="D";
    }
    public void paintComponent(Graphics g){
        g.setColor(Color.WHITE);
        g.fillRect(0,0,800,600);
        g.setColor(Color.YELLOW);
        g.fillRect(0,80,800,520);
        //g.setColor(Color.MAGENTA);
        //g.fillRect(snakeX[0],snakeY[0],20,20);
        for(int i=0;i<length;i++){
            if(i==0){
                g.setColor(Color.MAGENTA);
                g.fillRect(snakeX[0],snakeY[0],20,20);
            }else {
                g.setColor(Color.BLUE);
                g.fillRect(snakeX[i], snakeY[i], 20, 20);
            }
        }
        g.setColor(Color.CYAN);
        g.fillRect(foodX,foodY,20,20);

        g.setColor(Color.RED);
        g.setFont(new Font("微软雅黑",10,20));
        g.drawString("得分"+score,300,25);

        if(!isStart){
            g.setColor(Color.RED);
            g.setFont(new Font("微软雅黑",10,50));
            g.drawString("点击空格开始游戏",200,300);
        }
        if(isDie){
            g.setColor(Color.RED);
            g.setFont(new Font("微软雅黑",10,20));
            g.drawString("游戏结束,得分为:"+score,300,65);
        }
    }

}

        在写贪吃蛇的时候,我接触到了两个新东西:

1.定时器Timer类。

 2.paint()绘图方法。第一次出现在java.awt.Component类中,当我们的类继承JFrame(容器)或者JPanel(轻量级容器)时,我们可以通过重写paint()方法,去进行绘图。

关于paint()方法可以参考文章:

https://blog.csdn.net/gydjsz/article/details/88924447

但是网上讲解的都比较简单,想系统学习还是要参考书籍《Java从入门到精通》。

        通过paint()方法,我们解决了绘图问题,可以通过paint()方法,实现小蛇,以及小蛇移动区域、计分器的绘制。

        首先,我们创建一个游戏界面。

public class GameStart extends JFrame {
    public GameStart(){
        this.setTitle("Java贪吃蛇");
        int width = Toolkit.getDefaultToolkit().getScreenSize().width;
        int height = Toolkit.getDefaultToolkit().getScreenSize().height;
        this.setBounds((width-800)/2,(height-600)/2,800,600);
        this.setVisible(true);//设置窗体可视化
        this.setResizable(false);//设置窗体不可调整大小
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//关闭设置
        this.add(new GamePanel());
    }
}

int width = Toolkit.getDefaultToolkit().getScreenSize().width;
int height = Toolkit.getDefaultToolkit().getScreenSize().height;

 这两行代码是为了获取系统屏幕分辨率,从而来调整窗口位置居中。

然后,我们用画笔填充两个矩形,分别表示游戏区域和小蛇运动的区域,以及计分器

 g.setColor(Color.WHITE);
        g.fillRect(0,0,800,600);//游戏区域
        g.setColor(Color.YELLOW);
        g.fillRect(0,80,800,520);//小蛇运动区域

       

g.setColor(Color.RED);
        g.setFont(new Font("微软雅黑",10,20));
        g.drawString("得分"+score,300,25);

         这样,我们的游戏基本界面就设置完毕。

然后,进行对小蛇移动、游戏机制的实现.

        那么,我们接下来的问题就是,如何表示小蛇的位置?如何实现小蛇的不间断移动?小蛇的转向?以及死亡判定,得分判定?

        关于小蛇的位置,我们可以通过坐标来进行表示,并通过坐标的改变来改变小蛇的位置。

    int snakeX[]=new int[500];
    int snakeY[]=new int[500];
    int length;

        数组空间设置为500,这样小蛇的最大长度为500。

        同时在游戏开始之前,我们要进行游戏的初始化操作。

        

public void intiPanel(){
        snakeX[0]=60;
        snakeY[0]=100;
        snakeX[1]=40;
        snakeY[1]=100;
        snakeX[2]=20;
        snakeY[2]=100;
        length=3;
        isStart=false;
        isDie=false;
        foodX=300;
        foodY=300;
        score=0;
        direction="D";
    }

 游戏开始之前,小蛇有头部和两节身子,长度为3,以及初始位置.游戏开始状态为false,死亡状态为false,分数为0,初始方向为向右.

        接下来,我们可以通过键盘监听,改变小蛇的移动方向,以及暂停、开始游戏。

       

this.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                super.keyPressed(e);
                if(e.getKeyCode()==KeyEvent.VK_SPACE){
                    if(!isDie){
                        isStart=!isStart;
                        direction=" ";
                    }else{
                        intiPanel();
                        isDie=false;
                        isStart=true;
                        repaint();
                    }
                }
                if(e.getKeyCode()==KeyEvent.VK_UP&&direction!="S"){
                    direction="W";
                }
                if(e.getKeyCode()==KeyEvent.VK_DOWN&&direction!="W"){
                    direction="S";
                }
                if(e.getKeyCode()==KeyEvent.VK_LEFT&&direction!="D"){
                    direction="A";
                }
                if(e.getKeyCode()==KeyEvent.VK_RIGHT&&direction!="A"){
                    direction="D";
                }
            }
        });

        我们GamePanel类里面添加一个键盘监听器,这里要注意的是,当一个类继承JPanel的时候,这个类就是一个面板,可以直接用this调用.

        并通过匿名内部类的方式来实现KeyAdapter的接口,并重写Keypressed()方法,并通过多级if的形式,进行键盘操作判断.

        当不是死亡状态的时候,按下空格会改变游戏状态为暂停或者开始,并赋值方向为" "(空格),至于为什么要让方向等于空格,后面会讲.

         如果是死亡状态,那么调用初始化方法和游戏状态,并进行重绘.

        W,S,A,D分别表示上下左右,对方向进行赋值.

这样,我们的键盘监听结束.

        然后,我们迎来了第一个难点.        如何让小蛇向前移动?

我们是否要不断地改变小蛇头部以及每节身体地坐标去实现它地移动?但是我们不可能对小蛇所有身体进行方向判断并改变,这样显得繁杂很多.

        于是,有了一个方案.我们是否可以只改变小蛇的头部的方向,让小蛇的头部去引导身体去移动?

        这个想法很巧妙,于是就有了.

 timer=new Timer(100, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if(isStart&&!isDie){

                    for(int i=length-1;i>0;i--){
                        snakeX[i]=snakeX[i-1];
                        snakeY[i]=snakeY[i-1];
                    }
                    if("W".equals(direction)){
                        snakeY[0]-=20;
                    }
                    if("S".equals(direction)){
                        snakeY[0]+=20;
                    }
                    if("A".equals(direction)){
                        snakeX[0]-=20;
                    }
                    if("D".equals(direction)){
                        snakeX[0]+=20;
                    }
                    if(" ".equals(direction)){
                        snakeX[0]+=20;
                    }
                    if (snakeX[0] > 760) {
                        snakeX[0] = 20;
                    }
                    if (snakeX[0] < 0) {
                        snakeX[0] = 760;
                    }
                    if (snakeY[0] < 60) {
                        snakeY[0] = 500;
                    }
                    if (snakeY[0] > 500) {
                        snakeY[0] = 60;
                    }
                    if(snakeX[0]==foodX&&snakeY[0]==foodY){
                        length++;
                        score++;
                        foodX=(int)random.nextInt(39)*20;
                        foodY=(int)random.nextInt(22)*20+80;
                    }
                    for(int i=1;i<length;i++){
                        if(snakeX[0]==snakeX[i]&&snakeY[0]==snakeY[i]){
                            isDie=true;
                        }
                    }
                }
                repaint();
            }
        });
        timer.start();

以游戏状态为开始状态,并且小蛇存活为前提.

        刚开始,我在想如何让蛇头一直向前跑,我直接在定时器里面的事件监听器里面,不通过if条件语句,写下这样一行代码:

                snakeX[0]+=20;

        这样,不就实现了小蛇一直在往前走吗?

        结果,出现来搞笑的一幕,小蛇一旦转弯就会斜着走,当时我死活想不通,那天晚上,被窝里都是小蛇在斜着乱飞,哭死了.

        后来,我发现了这个问题,我设定一直往右走, 所以一旦转弯,就会斜着跑.

        我进行了改良,但由于时间问题,效果还并不是很满意.很多代码想法都是自己敲的,所以经常不尽如人意.

我是这样解决这个问题的:

        只有当我们开始游戏的时候,我们会按下空格,这个时候方向direction就被赋值为空格.用if语言判断direction是否为空格,如果是空格说明游戏开始,小蛇可以开始向右移动了,如果direction不是空格,即进行了转向,并按照转向后的方向跑,那么我们的自动向右跑的条件就不会符合,也就不会自动向右跑了.

 解决了蛇头在游戏刚开始向前跑的问题,那就开始实现如何让身体跟着头部一起跑?

for(int i=length-1;i>0;i--){
    snakeX[i]=snakeX[i-1];
    snakeY[i]=snakeY[i-1];
 }

        通过for循环,让蛇的后一节身体去继承上一节身体的坐标.

接下来,是我们的第二个难点.如何让小蛇不间断的移动,并且时刻监测到我们是否进行了转向、吃到食物、是否撞到自己死亡?

        我们可以通过定时器和事件监听器的组合使用,实现对定时器内线程任务的不断检测,进而是实现小蛇的不间断移动以及各种判定.

        我们要明确监听器和定时器它们各自的作用.

        监听器的作用是监听代码块是否发生改变,并进行获取监听信息.

        定时器的作用是每隔自定义的时间间隔便通过线程执行监听器内的任务.

        我们缺一不可.  我们意念合一(不好意思中二一下qwq).

第三个问题,如何随机设置食物位置?

其实用Random类来获取随机数就好啦

        不过需要一点点的计算:

       

if(snakeX[0]==foodX&&snakeY[0]==foodY){
                        length++;
                        score++;
                        foodX=(int)random.nextInt(39)*20;
                        foodY=(int)random.nextInt(22)*20+80;
                    }

如果吃到食物,也就是蛇头坐标与食物坐标重合.就生成下一个食物,因为我们的小蛇像素是20*20的,所以食物的位置一定要保证小蛇是可以吃到的哦!

最后,我们已经没有难点啦,进行收尾工作!

游戏机制:如果小蛇抛出运动区域,就会从区域的另一边出来,2D镜像?

if (snakeX[0] > 760) {
                        snakeX[0] = 20;
                    }
                    if (snakeX[0] < 0) {
                        snakeX[0] = 760;
                    }
                    if (snakeY[0] < 60) {
                        snakeY[0] = 500;
                    }
                    if (snakeY[0] > 500) {
                        snakeY[0] = 60;
                    }

其实还是改变坐标的位置啦.

然后是对小蛇和食物进行绘制.

for(int i=0;i<length;i++){
    if(i==0){
        g.setColor(Color.MAGENTA);
        g.fillRect(snakeX[0],snakeY[0],20,20);
    }else {
           g.setColor(Color.BLUE);
           g.fillRect(snakeX[i], snakeY[i], 20, 20);
      }
 }
 g.setColor(Color.CYAN);
 g.fillRect(foodX,foodY,20,20);

通过for循环进行绘制,记得把小蛇的头部换个颜色,看起来更棒!

能看到这里,就已经很耐心了,小白的第二篇博客,如果有理解不到位的地方,请大佬立即指出,以免误人子弟.最后感谢阅读(鞠躬).

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐