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 新的问题
蛇的坐标是两个整型,怎么整合成一个映射呢?
那问题怎么解决呢?
一个整形是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. 小结
现在贪吃蛇和食物类都已经有了,接下来要只需要一个图形界面就可以让显示出游戏画面了。