自学java小项目 贪吃蛇(三)

1. 写在前面

前面已经完成了食物的有关类设计,现在我们进入核心的蛇类设计

2. 蛇类设计

2.1 对obj类继承

继承上一篇文章写的Obj抽象类,回忆一下上一篇文章中抽象类包含了一个坐标,以及获取坐标的方法。在蛇类中这个坐标为蛇头坐标

2.2 设计

2.2.1 数据结构

贪吃蛇首先得存放身体的每一节的坐标,我们需要考虑其数据结构

2.2.1.1 蛇吃到食物后身体会身体会变长,也就是容器需要变大。

这个问题有两个方法:

  • 可变容器,可以自己根据存放的数据大小调整容器大小(例如ArrayList
  • 很大的不变容器,会造成空间浪费

那这样是不是可以直接选择可变容器呢?非也非也,继续考虑以下问题

2.2.1.2 大部分时间蛇都在移动,也就是蛇身的位置需要变化

蛇身的坐标位置怎么变化呢,仔细想一下,身体是随着蛇头移动的,所以每一节身体的坐标移动后就是前一个身体节的坐标,然后蛇头的坐标根据方向变化。

也就是说,对于存放蛇身位置得容器需要将每一个坐标往后移一个位置(舍弃掉蛇尾坐标)
考虑一下这个事情,对于可变容器,整个访问以及设置数据都是封装好的接口,调用速度会慢一些,而对于数组这类的容器访问会快速一些。

最后结合以上考虑还是选择使用了带封装的ArrayList,顺便可以学习how2j上泛型的内容
泛型链接

2.2.2 辅助数据结构

2.2.2.1 哈希表

贪吃蛇运行时需要判断蛇头是否碰撞到蛇身,通常有两种做法

  • 遍历蛇身,查看有无和蛇头重叠的坐标,时间代价是O(n)
  • 使用哈希表,每次移动将蛇尾从表中去除,判断蛇头有无在表内,没有则放入蛇头,反之说明蛇头撞到蛇身游戏结束。

使用哈希表可以使用空间代价换取时间代价,并且还可以学习到HashMap容器
how2j的hashmap教程

2.2.2.2 新的问题

蛇的坐标是两个整型,怎么整合成一个映射呢?

  • 可以用Pos类直接映射嘛?
  • 并不行,java对于自定义的类的实例都是引用类型参考资料,通常对比两个实例需要通过重写equals函数参考资料,或者可以使用工具包重载参考资料

那问题怎么解决呢?
一个整形是32位,利用高16位存储x坐标,低16位存储y坐标。那么坐标的范围在[0,2^16-1](只取其中的整数)这个范围以及足够储存界面内的坐标了。

2.2.3 类属性与类方法

  • 蛇包括了以下属性:

    • 蛇的长度
    • 蛇头位置(保护属性,继承而来
    • 蛇头的移动方向
    • 蛇的所有位置
    • 一节蛇身的大小(方便更换图片素材,是我后期添加的)
    • 游戏界面的大小(方便调节游戏素材)
    • 辅助容器HashMap:判断有无重合
    • 辅助整数hash_x,hash_y:用于hash函数
  • 方法

    • Snake()有参与无参构造函数,初始化
    • hash函数(私有方法,辅助判断有无重叠)
    • 获取蛇头的方法接口
    • 获取蛇头方向的方法接口
    • 获取蛇长度的方法接口
    • 获取蛇的所有坐标的方法接口
    • 初始化蛇接口(提供给失败后重启的接口
    • 设置蛇的移动方向的接口
    • 让蛇移动的接口
    • 让蛇吃食物变长的接口
    • 判断某个位置是否在蛇身上的接口

在这里插入图片描述

2.2.3 关键函数

2.2.3.1 Move() :让蛇位置移动

前面也描述了,蛇的移动是让后一节蛇身坐标变为前一个蛇身的位置,然后蛇头根据方向变化。
另外得判断移动后蛇头是否撞到蛇身,并且蛇跑到一个边界外面后要出现在另一个边界上

其中得注意的是在蛇头的移动中,蛇头的位置坐标我设置的是[0,console_y/block_y-2),是因为窗体边框有大小(这里可以学习狂胜的视频,他是在顶上做了一个区域放图片)

//蛇的移动
    //返回1表示正常移动
    //返回0表示蛇身重合
    public boolean Move(){
    
    
        //去掉蛇尾的在hash中的映射
        this.mp.remove(this.hash(this.snake_pos.get(length-1).x,this.snake_pos.get(length-1).y));

        //移动:即将后一个块的位置变成前一个块的位置
        for(int i=this.length-1;i>0;i--){
    
    
            Pos tmp=this.snake_pos.get(i-1);
            this.snake_pos.set(i,new Pos(tmp.x, tmp.y));
        }
        //设置蛇头坐标
        switch (this.dir){
    
    
            case UP://向上移动
                this.pos.setPos(pos.x,(pos.y-this.block_y+this.console_y-2*block_y)%(this.console_y-2*block_y));
                break;
            case DOWN://向下移动
                this.pos.setPos(pos.x,(pos.y+this.block_y+this.console_y-2*block_y)%(this.console_y-2*block_y));
                break;
            case LEFT://向左移动
                this.pos.setPos((pos.x-this.block_x+this.console_x-2*block_x)%(this.console_x-2*block_x),pos.y);
                break;
            case RIGHT://向右移动
                this.pos.setPos((pos.x+this.block_x+this.console_x-2*block_x)%(this.console_x-2*block_x),pos.y);
                break;
            default:
                System.out.println("in snake move enum error");
                break;
        }
        int headHash=hash(pos.x,pos.y);
        //判断身体是否包含了蛇头位置
        boolean flag=this.mp.containsKey(headHash);
        //将蛇头位置放入hash表
        this.mp.put(headHash, pos.x+pos.y);
        //设置蛇头位置
        this.snake_pos.set(0,this.pos);

        //判断蛇头是否撞到蛇身
        return !flag;
    }

2.2.3.2 Eat() :蛇吃到食物

蛇吃到食物后怎么处理呢?蛇吃到食物后会变成一节变成的位置可以设置成蛇尾。
也就是蛇吃到食物后让蛇向前走一步,然后把前一个蛇尾的位置变成新的蛇尾的位置。

    //蛇吃到食物的移动
    public boolean Eat(){
    
    //蛇吃到食物后
        //获取当前蛇尾位置
        Pos p= new Pos(snake_pos.get(length - 1).x, snake_pos.get(length - 1).y);
        //让蛇移动一步
        if(this.Move()==false){
    
    
            return false;
        }

        //设置新长度
        this.length+=1;

        //变长的部分在蛇尾
        snake_pos.add(length-1,new Pos(p.x, p.y));
        //将新的蛇尾位置放入hash表
        this.mp.put(hash(p.x,p.y), p.x+p.y);
        return true;
    }

2.3 所有代码

//包的声明
package object;
/**
 * @author: huluhulu~
 * @date: 2022-06-28 13:02
 * @description: 本文件为Snake.java 提供了一个蛇类,继承了Obj抽象类
 * @version: 1.0
 */
//导入包
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import position.Pos;

public class Snake extends Obj {
    
    
    /**********以下是类属性*********/
    //继承属性:protected Pos pos;

    private int length=0;   //蛇的长度
    private Dir dir;        //蛇头的方向

    //游戏界面的大小
    private int console_x, console_y;
    //一节蛇身的大小
    private int block_x,block_y;

    //记录蛇身的所有位置
    HashMap<Integer, Integer> mp;
    //用于做hash函数
    int hash_x,hash_y;

    //蛇身所有的坐标数组
    //考虑到一半时间是在查询坐标,一半时间需要修改坐标,使用ArrayList类型
    private ArrayList<Pos> snake_pos;
    //初始化块对蛇身坐标初始化
    {
    
    
        //初始化长度预设为50
        this.snake_pos=new ArrayList<Pos>();
        //public 构造函数
        //添加蛇头位置
        this.snake_pos.add(new Pos(0,0));
    };

    /**********以下是类方法*********/
    //无参构造函数
    public Snake(){
    
    
        //初始化位置:(0,0)
        this.pos=new Pos(0,0);

        this.dir=Dir.DOWN;  //初始化方向:向下
        this.length=1;      //初始化长度:1
        //一节蛇身的大小
        this.block_x=10;
        this.block_y=10;
        //游戏界面的大小
        this.console_x=800;
        this.console_y=800;

        //800=0b1100100000=>>10位
        //将x放入高位,y放入低位
        this.hash_x=10;
        this.hash_y=0;
        //将蛇头放入hash表
        this.mp=new HashMap<Integer,Integer>();
        this.mp.put(this.hash(0,0),0);
    }

    //带参构造函数
    //计划创建的界面大小
    public Snake(int console_x,int console_y,int body_x,int body_y){
    
    
        //游戏界面的大小
        this.console_x=console_x;
        this.console_y=console_y;
        //一节蛇身的大小
        this.block_x=body_x;
        this.block_y=body_y;

        //设置随机种子:默认是当前时间
        Random r=new Random();

        //位置初始化
        //利用random产生[0,console_x)的随机数
        //利用random产生[0,console_y)的随机数
        this.pos=new Pos(r.nextInt(console_x/block_x)*block_x,r.nextInt(console_y/block_y)*block_y);
        this.snake_pos.set(0,this.pos);

        //方向初始化
        //获取所有的枚举类的值
        Dir[] dirs=Dir.values();
        //获取[0,4)之间的整数值作为索引对dir进行初始化
        this.dir=dirs[r.nextInt(4)];

        //长度初始化
        this.length=1;

        //将x放入高位,y放入低位
        this.hash_x=0;
        int tmp=console_y;
        //计算y的最高位位置
        while(tmp>0){
    
    
            hash_x++;
            tmp=tmp>>1;
        }
        this.hash_y=0;
        if(tmp>16){
    
    //int范围放不下x、y
            System.out.println("hash failed");
            System.exit(1);
        }
        //将蛇头放入hash表
        this.mp=new HashMap<Integer,Integer>();
        this.mp.put(this.hash(pos.x,pos.y),0);
    }

    //对坐标进行hash取值
    //将x放入高位,y放入低位
    private int hash(int x,int y){
    
    
        //这里一定要注意优先级
        //!!!别问为什么
        return (x<<hash_x)+(y<<hash_y);
    }

    /**********以下是对外的接口*********/
    //复写抽象函数
    //获取坐标
    public Pos getPos() {
    
    
        return this.pos;
    }
    //获取蛇头方向
    public Dir getDir(){
    
    
        return  this.dir;
    }
    //获取蛇的长度
    public int getLength(){
    
    
        return  this.length;
    }
    //获取蛇的所有坐标
    public ArrayList<Pos> getSnakePos(){
    
    
        return this.snake_pos;
    }
    //初始化蛇
    public void init(){
    
    
        //设置随机种子:默认是当前时间
        Random r=new Random();

        //位置初始化
        //利用random产生[0,console_x)的随机数
        //利用random产生[0,console_y)的随机数
        this.pos.setPos(r.nextInt(console_x/block_x)*block_x,r.nextInt(console_y/block_y)*block_y);
        this.snake_pos.set(0,this.pos);

        //方向初始化
        //获取所有的枚举类的值
        Dir[] dirs=Dir.values();
        //获取[0,4)之间的整数值作为索引对dir进行初始化
        this.dir=dirs[r.nextInt(4)];

        //长度初始化
        this.length=1;

        //将蛇头放入hash表
        this.mp.clear();
        this.mp.put(this.hash(pos.x,pos.y), pos.x+pos.y);
    }
    //设置蛇移动的方向
    public void setDir(Dir d){
    
    
        this.dir=d;
    }

    //蛇的移动
    //返回1表示正常移动
    //返回0表示蛇身重合
    public boolean Move(){
    
    
        //去掉蛇尾的在hash中的映射
        this.mp.remove(this.hash(this.snake_pos.get(length-1).x,this.snake_pos.get(length-1).y));

        //移动:即将后一个块的位置变成前一个块的位置
        for(int i=this.length-1;i>0;i--){
    
    
            Pos tmp=this.snake_pos.get(i-1);
            this.snake_pos.set(i,new Pos(tmp.x, tmp.y));
        }
        //设置蛇头坐标
        switch (this.dir){
    
    
            case UP://向上移动
                this.pos.setPos(pos.x,(pos.y-this.block_y+this.console_y-2*block_y)%(this.console_y-2*block_y));
                break;
            case DOWN://向下移动
                this.pos.setPos(pos.x,(pos.y+this.block_y+this.console_y-2*block_y)%(this.console_y-2*block_y));
                break;
            case LEFT://向左移动
                this.pos.setPos((pos.x-this.block_x+this.console_x-2*block_x)%(this.console_x-2*block_x),pos.y);
                break;
            case RIGHT://向右移动
                this.pos.setPos((pos.x+this.block_x+this.console_x-2*block_x)%(this.console_x-2*block_x),pos.y);
                break;
            default:
                System.out.println("in snake move enum error");
                break;
        }
        int headHash=hash(pos.x,pos.y);
        //判断身体是否包含了蛇头位置
        boolean flag=this.mp.containsKey(headHash);
        //将蛇头位置放入hash表
        this.mp.put(headHash, pos.x+pos.y);
        //设置蛇头位置
        this.snake_pos.set(0,this.pos);

        //判断蛇头是否撞到蛇身
        return !flag;
    }

    //蛇吃到食物的移动
    public boolean Eat(){
    
    //蛇吃到食物后
        //获取当前蛇尾位置
        Pos p= new Pos(snake_pos.get(length - 1).x, snake_pos.get(length - 1).y);
        //让蛇移动一步
        if(this.Move()==false){
    
    
            return false;
        }

        //设置新长度
        this.length+=1;

        //变长的部分在蛇尾
        snake_pos.add(length-1,new Pos(p.x, p.y));
        //将新的蛇尾位置放入hash表
        this.mp.put(hash(p.x,p.y), p.x+p.y);
        return true;
    }
    //判断位置是否在蛇身上
    public boolean isInSnake(Pos tmp){
    
    
        return this.mp.containsKey(hash(tmp.x,tmp.y));
    }
}

3. 小结

现在贪吃蛇和食物类都已经有了,接下来要只需要一个图形界面就可以让显示出游戏画面了。

猜你喜欢

转载自blog.csdn.net/qq_51204877/article/details/125512836