Java之贪吃蛇游戏的开发

时间:2021-08-29 15:25:56

额,端午三天假,一堆的作业,但手贱的我并没有兴趣去写什么高数作业,而是写了一个贪吃蛇游戏。界面版的,扩展性比较好,地图可以按自己的喜欢去重新做(有关扩展性后面会说),我只写了两个简单的地图。因为代码比较长(400行左右,不连扩展的地图),所以下面我只说思路做法,必要时我会以代码为示例,不在粘全部代码。

好了,先来看看,这个游戏的截图。(滑稽)
Java之贪吃蛇游戏的开发

这里可以自定义难度系数(其实就是蛇自己移动的速度),共分10级。这里后面我会说实现方法,这都可以改的。
Java之贪吃蛇游戏的开发

这里是初始的状态,食物是随机生成的,蛇位于地图的*。其中灰色代表的是石头,白色是空地,蓝色是食物,红色是蛇头,绿色是蛇身。

Java之贪吃蛇游戏的开发
Java之贪吃蛇游戏的开发
这里,你可以用键盘的方向键来控制蛇的移动,但遇到上面俩种情况(蛇撞墙或蛇咬到自己)就会死亡。

Java之贪吃蛇游戏的开发
这是一个扩展的地图的例子。

好了下面说实现的方法:
首先,我们知道java是面向对象编程方法,所以实现过程也肯定是面向对象的。其次,我们要明白“数据”与“界面”分离的思想,做界面不是难事,只要用的熟练就好,难的是核心的算法的实现。所以看到贪吃蛇这个游戏,我们首先想的是他的业务逻辑是什么,而不是这界面是咋做的,其实说真的,我的代码里,有关做图形界面的代码只有50行左右,而全部代码是400行左右,可见做界面之占1/8的任务量。

第一步:
我们先分析,在这个游戏里我们看到了那些对象或者说是变量。
1、地图 2、蛇 3、食物 (你可能还会说“石头”,但石头其实包含在地图里,因为地图就是由石头和空地组成的呀)(滑稽)

第二步:
下面我们一个一个的说:
1、地图如何实现?
首先,想用什么来储存地图的信息,呐,我们可以这样,地图由石头和空地组成,那么我们可以用字符 * 来表示石头,用空字符(就是一个空格键)来表示空地 ,这样我们就可以用char型的数组来存储地图。
然后就是,如何初始化地图,我们可以写一个intiMap()的方法来初始化地图(具体看代码,这里只粘部分代码,关键是思想)

//常量,表示地图的宽和高
protected static final int HEIGHT=30;
protected static final int WIDTH=30;
//char型数组来储存地图
protected char[][] map=new char[HEIGHT][WIDTH];

//初始化Map
public void intiMap() {

for(int i=0;i<HEIGHT;i++)
for(int j=0;j<WIDTH;j++)
{
if(i==0||(i==HEIGHT-1)){
map[i][j]='*';
}
else this.map[i][j]=' ';
}

2、蛇如何实现?
蛇分为蛇头和蛇身,我们可以用字符 表示蛇头,用字符 # 表示蛇身。呐,是不是我们就也用字符数组来储存蛇,NO,蛇是会动的,蛇移动时蛇头或蛇身的字符是不变的。用 和 # 只是为了表示蛇,但不能用来储存蛇的信息。我们可以这样,蛇是在地图上动的,如果地图有坐标,我们就可以用坐标来储存蛇的每一个状态。因为一条蛇是由多个节点组成的,即由多个坐标组成,所以我们可以用链表容器来存储Point类型的节点。
呐,我们接下来就是初始化蛇,这个好办,因为初始的时候蛇是静止的,在最*。写个方法就OK了。
难的来了,就是蛇的移动,我们如何让蛇在地图上移动?
其实也不难,蛇的移动我们可以看作链表添加节点和删除节点的操作,什莫意思呐,就是如果我们给蛇添加一个头,再删一个尾,不就相当于蛇走了吗。
因为 蛇有四个方向,所以可以定义四个常量表示四个方向(看代码,这里的代码是截取的,所以会用一些整体的方法,还是主要看思想,不用太在意代码)。

  //常量,表示蛇的方向
protected static final int UP_DIRECTION=1;
protected static final int DOWN_DIRECTION=-1;
protected static final int LEFT_DIRECTION=2;
protected static final int RIGHT_DIRECTION=-2;
//当前方向,默认为右方
private int currentdirection=RIGHT_DIRECT;
//容器,用来储存蛇的坐标信息
private LinkedList<Point> snake=new LinkedList<Point>();
//初始化snake
public void intiSnake(){
int x=WIDTH/2;
int y=HEIGHT/2;
snake.addFirst(new Point(x-1,y));
snake.addFirst(new Point(x, y));
snake.addFirst(new Point(x,y));
}
//蛇移动函数
public void move(){
//取蛇头
Point snakehead=snake.getFirst();
//依据方向,移动
switch(currentdirection){
//向上移动
case UP_DIRECTION :
//结束的时候不执行添加头结点
if(!GameOver){
snake.addFirst(new Point(snakehead.x,snakehead.y-1));
}

break;
//向下移动
case DOWN_DIRECTION :
//结束的时候不执行添加头结点
if(!GameOver){
snake.addFirst(new Point(snakehead.x,snakehead.y+1));
}

break;
//向左移动
case LEFT_DIRECTION :
if(!GameOver){
if(snakehead.x==0){
snake.addFirst(new Point(snakehead.x-1+WIDTH,snakehead.y));
}else{
snake.addFirst(new Point(snakehead.x-1,snakehead.y));
}
}

break;
//向右移动
case RIGHT_DIRECTION :
if(!GameOver){
snake.addFirst(new Point((snakehead.x+1)%WIDTH,snakehead.y));
}

break;
default : break;
}
if(eatFood()){
//先刷新,防止出现食物长到身上
repaint();
//重建食物
intiFood();
}else{
//游戏结束的时候不执行删除尾节点
if(!GameOver){
`
//删除蛇尾
snake.removeLast();
}

}

}

3、食物如何实现?
这个相比蛇就很容易了,首先食物虽然不会动,但他是随机在地图中生成的,所以我们用Point类,即用坐标来储存食物信息,并用 字符@来表示食物。
初始化食物,我们要明白一点,食物既不能长在墙上也不能长在蛇身上,所以在初始化食物的时候,要避开这些位置。并且,还有一点食物是随机的,这个好办,产生随机数就好了。(看代码)

    //食物的坐标用Point类来储存
private Point food=new Point();
//初始化食物
public void intiFood(){
while(true){
Random random=new Random();
int x=random.nextInt(WIDTH-1);
int y=random.nextInt(HEIGHT-1);
if(map[y][x]!='*'&&map[y][x]!='#'&&map[y][x]!='$')
{
food.x=x;
food.y=y;
break;

}

}
}

第三步:
我们在上面已经定义好了,地图、蛇、食物,三个事物,并一一初始化。下面是如何讲这三样事物之间起建立联系。这个很重要,就是如何把蛇的信息、把食物的信息反馈给地图,以实现互动。
我们可以通过两个函数,showSnake() 和 showFood() 来实现,如何做呐?
其实,不难,我们只要将蛇 或 食物 的位置即坐标 反馈给地图就好了呀,然后让地图去显示他们就好了。

//在地图上显示蛇
public boolean showSnake(){
{
//定位蛇头
int W=snake.getFirst().x;
int H=snake.getFirst().y;
map[H][W]='$';
//定位蛇身
for(int i=1;i<this.snake.size();i++)
{
W=snake.get(i).x;
H=snake.get(i).y;
map[H][W]='#';
}
return true;
}
}
//在地图上显示食物
public boolean showFood(){
{
map[food.y][food.x]='@';
return true;
}
}

第四步:
好了,现在你的蛇和食物,可以在地图上显示了,并且蛇也可以在地图上*的动了。但蛇还吃不了食物呀,还有蛇不会死呀。
如何让蛇吃到食物呐,我们这样,我们写个函数eatFood()返回值为boolean ,当蛇头的坐标和食物重合是返回真,否则假。然后,在第二步中的蛇的移动函数move()中判断eatFood(), 如果吃到了则就步删除尾结点了,不就好了吗。这样蛇吃了食物并且变长了。美滋滋(滑稽)
还有咋判断蛇死了? 我们还是写一个函数叫 Isover() ,在定义Game Over的布尔常量,当蛇头对应坐标下的地图坐标的值为 字符 * 或 # 时 ,就说明蛇撞墙了 或 咬到自己了 。然后在主函数中判断Game Over的真假,一旦为真,就结束。

    //蛇吃食物区 
public boolean eatFood(){
//取蛇头
Point snakehead=snake.getFirst();
if(snakehead.equals(food)){
return true;
}
return false;
}
//用来判断游戏是否结束
protected static boolean GameOver=false;
/* 结束游戏区 */
public void IsOver(){
//取蛇头
Point snakehead=snake.getFirst();
//撞墙死
if(map[snakehead.y][snakehead.x]=='*'){
GameOver=true;
}
//咬到自己死
if(map[snakehead.y][snakehead.xver]=='#'){
GameOver=true;
}
}

哈,有了以上四步,一个贪吃蛇的雏形就大概好了,具体的实现自己琢磨。下面讲一下如何画图,并把数据给图形。
添加事件监听器,要加一个keyListener,并重写keypressed方法,来实现用方向键来控制蛇的移动
这里要定义一个用来改变方向的函数,就是通过方向键来改变蛇的方向,这里我们可以用到之前的方向常量了,具体看代码

 frame1.addKeyListener(new KeyAdapter() {

@Override
public void keyPressed(KeyEvent e) {
super.keyPressed(e);
switch(e.getKeyCode()){
case KeyEvent.VK_UP:
IsAuto=false;
sg1.changeDirection(UP_DIRECTION);
break;
case KeyEvent.VK_DOWN:
IsAuto=false;
sg1.changeDirection(DOWN_DIRECTION);
break;
case KeyEvent.VK_LEFT:
IsAuto=false;
sg1.changeDirection(LEFT_DIRECTION);
break;
case KeyEvent.VK_RIGHT:
IsAuto=false;
sg1.changeDirection(RIGHT_DIRECTION);
break;
default: break;
}
sg1.move();
sg1.IsOver();
sg1.reFresh();
sg1.repaint();

if(SnakeGame_1.GameOver){
sg1.repaint();
/* // System.exit(0);
*/
}
IsAuto=true;
}

});
//如果IsAuto 为真,则自己走
if(IsAuto)sg1.runAuto();

利用数据画图,其实不难,比如地图,你把地图的宽(横向的方格数)和高(竖向的方格数)的参数传给画笔,让画笔画就好了。再比如石头,食物,蛇,当坐标位置的值为 *时 用灰色画笔画,为 $ 时用红笔,为 #时用绿笔,为 @时蓝笔,就好了。
下面只展示重写的JPanel类的paint(Graphics g)方法。

/* 图形刷新区 */
//重写Paint方法
@Override
public void paint(Graphics g) {
//画地图
for(int i=0;i<HEIGHT;i++)
for(int j=0;j<WIDTH;j++)
{
if(map[i][j]=='*'){
g.setColor(Color.GRAY);
}else{
g.setColor(Color.WHITE);
}
g.fill3DRect(j*CELL_W, i*CELL_H, CELL_W, CELL_H, true);
}

//画蛇
//定位蛇头
int W=snake.getFirst().x;
int H=snake.getFirst().y;
g.setColor(Color.RED);
g.fill3DRect(W*CELL_W,H*CELL_H , 20, 20,true);
//定位蛇身
for(int t=1;t<this.snake.size();t++)
{
W=snake.get(t).x;
H=snake.get(t).y;
g.setColor(Color.GREEN);
g.fill3DRect(W*CELL_W,H*CELL_H , 20, 20,true);

}

//画食物
map[food.y][food.x]='@';
g.setColor(Color.BLUE);
g.fill3DRect(food.x*CELL_W, food.y*CELL_H, 20, 20, true);

//画字
if(SnakeGame_1.GameOver){
g.setColor(Color.ORANGE);
g.setFont(new Font("宋体", Font.BOLD,30 ));
g.drawString("GAMW OVER !", CELL_W*(WIDTH/2), CELL_H*(HEIGHT/2));
}

最后提一下,常出现的难题:
1、数组越界的问题,因为键盘可以控制一直向某个方向,所以很容易越界。2、要实现蛇自己动,有点难度,我可想了好一会儿(滑稽)。

对了,有关扩展性是因为,你可以有继承,把你的贪吃蛇的类继承一下,然后重写 intiMap()方法就好呀,其他的不用重写。是不是扩展性很好呐(滑稽)

如果想看源码的童鞋,可以点击下面的连接:
http://blog.csdn.net/angellover2017/article/details/72861992