首先声明文章为笔者原创,转载请声明出处;
在java学习的初级阶段写一写小项目是十分必要的,笔者在此提供两个java实现的贪吃蛇,用到的知识不多,基本就是swing图形界面和多线程的基本知识。这两个是通过不同的方式实现的贪吃蛇,第一种方式是笔者原创的,基于面向对象的设计原则,所以分了5个类,可能有些冗余;第二种方法是笔者在课堂中学习老师的,运用了一些相对高级一些的处理方式,只有一个类,相对整洁干练。初学者的话建议学习第一种,可以学习下面对对象的编程思路和基本思想,基本功扎实一些的建议学习第二种,一些地方的处理方式更具巧妙性。
接下来就不多说废话了,直接上代码:
// 程序入口类
public class AppMain
{
public static void main(String[] args)
{
// new出窗口类的实例对象
GameFrame gf = new GameFrame();
// 使得窗口可见,默认是不可见的
gf.setVisible(true);
}
}
程序入口类的作用只有两个,就是通过main方法启动程序和设置窗体可见;
import javax.swing.JFrame;
// 游戏窗体类
public class GameFrame extends JFrame
{
public GameFrame()
{
/**
* 下面代码的作用分别是:
* 设置标题;
* 设置窗体大小;
* 设置窗体居中显示;
* 设置窗体关闭时退出程序;
* 实例化面板类;
* 将面板类添加至窗体;
* */
super("贪吃蛇");
this.setSize(500,520);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
GamePanel gp=new GamePanel();
this.add(gp);
}
}
窗体类的作用是设置窗体一些基本属性和作为程序的“中流砥柱”面板类的顶层容器;
import java.util.Random;
// 食物类
public class GameFood
{
// 将蛇类作为自己的成员变量,这样做的目的是需要引用蛇类的方法和变量
private GameSnake snake;
// 设置食物的基本属性,分别是食物的大小,食物的两个坐标
private int size;
private int foodX;
private int foodY;
// 引入Random类,作用是需要随机生成食物的坐标
private Random r;
public GameFood()
{
/**
* 在构造方法中对基本属性进行初始化
* 以下代码的作用分别是:
* new出蛇类的实例对象;
* new出Random类的实例对象;
* 为size赋值;
* 为两个食物的坐标赋值;
* 调用Collision方法确保生成的食物坐标与蛇的身体不会发生碰撞;
*/
snake=new GameSnake();
r=new Random();
size=20;
foodX=r.nextInt(24)*20;
foodY=r.nextInt(24)*20;
Collision(snake.snakeX,snake.snakeY);
}
// 如果食物生成时碰撞到蛇,则递归调用重新生成,确保不会在食物的生成是与蛇发生碰撞
public void Collision(int[] arr1,int[] arr2)
{
// 这个方法需要传入两个数组作为参数,分别是蛇身体的X和Y坐标
for (int i = 0; i < arr1.length; i++)
{
// 如果发生碰撞则执行if块内的语句,会再次生成食物的坐标,并递归调用
if(getFoodX()==arr1[i]&&getFoodY()==arr2[i])
{
foodX=r.nextInt(24)*20;
foodY=r.nextInt(24)*20;
Collision(arr1,arr2);
}
}
}
/**
* 下面的一些方法是设置和返回食物类的属性,分别是:
* 设置食物的两个坐标;
* 返回食物的两个坐标;
* 返回食物的大小;
* */
public void setFoodX(int foodX)
{
this.foodX = foodX;
}
public void setFoodY(int foodY)
{
this.foodY = foodY;
}
public int getFoodX()
{
return foodX;
}
public int getFoodY()
{
return foodY;
}
public int getSize()
{
return size;
}
}
食物类是一个实体类,这个很容易想得到,因为它是一个实实在在的实体,不像程序入口类、窗体类等那么抽象,在面对对象编程中,一般会对每一个实体定义对应的实体类,实体类的作用通常都是包含这个实体属性和行为(即方法),此处的食物类也是这个作用;
import javax.swing.JOptionPane;
// 蛇类
public class GameSnake
{
/**
* 定义蛇类的基本属性,以下代码的作用分别是:
* 定义保存蛇身体X和Y坐标的两个数组;
* 定义蛇身体每一节的大小;
* 定义蛇的长度;
* 定义蛇头的方向(在程序中通过蛇头的方向来移动);
* */
public int[] snakeX;
public int[] snakeY;
private int size;
public int len;
private int snakeFX;
public GameSnake()
{
/**
* 在构造方法中初始化基本属性,以下代码的作用分别是:
* 为两个数组初始化(100是任意定义的,它代表蛇的最大长度,100就差不多了)
* 为两个数组的前三个变量赋值(代表蛇的初始身体所在位置,我这里初始长度是3)
* 为蛇身体的节点大小赋值;
* 为蛇头的方向赋值(39是键盘右键对应的Code值,也就是说设置的方向为右);
* 为蛇的长度赋值(代表初始长度,这个也是任意设置的,如果修改的话也需要修改上面数组的初始值);
* */
snakeX=new int[100];
snakeY=new int[100];
snakeX[0]=100;
snakeY[0]=100;
snakeX[1]=80;
snakeY[1]=100;
snakeX[2]=60;
snakeY[2]=100;
size=20;
snakeFX=39;
len=3;
}
// 以下三个方法分别是:返回蛇头的方向、设置蛇头的方向、返回蛇蛇头节点大小
public int getSnakeFX()
{
return snakeFX;
}
public void setSnakeFX(int snakeFX)
{
this.snakeFX = snakeFX;
}
public int getSize()
{
return size;
}
// 蛇的死亡方法,返回布尔值
public boolean isDie()
{
// for循环遍历,注意是从1开始,而不是0,具体原因请往下看
for (int i = 1; i < snakeX.length; i++)
{
// 判断蛇头是否碰撞到身体
if(snakeX[0]==snakeX[i]&&snakeY[0]==snakeY[i])
{
// 弹出消息框
JOptionPane.showMessageDialog(null, "你吃到自己了!!!");
return true;
}
// 判断蛇头是否出界
if(snakeX[0]<0||snakeX[0]>460||snakeY[0]<0||snakeY[0]>460)
{
// 弹出消息框
JOptionPane.showMessageDialog(null, "你碰到墙壁了!!!");
return true;
}
}
return false;
}
}
蛇类也是一个实体类,也包含了一些基本属性,以及定义了蛇的死亡方法;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;
import javax.swing.JPanel;
// 游戏面板类
public class GamePanel extends JPanel
{
// 为了使用食物类和蛇类的变量和方法,将它们定义为成员变量
private GameFood gfood;
private GameSnake snake;
public GamePanel()
{
// new出两个类的实例对象
gfood = new GameFood();
snake = new GameSnake();
// 开启线程
new Move().start();
// 设置焦点,这行代码是很必要的,因为默认的焦点在窗体,需要设置到这个面板中来
this.setFocusable(true);
// 通过匿名内部类添加键盘监听事件
this.addKeyListener(new KeyAdapter()
{
@Override
public void keyPressed(KeyEvent e)
{
// 根据玩家按压键盘所对应的Code值做出相应的处理
switch (e.getKeyCode())
{
// 37-40分别对应右、上、左、下,前面if语句的作用是不支持蛇头的180度转向
case 37:
if (snake.getSnakeFX() != 39)
snake.setSnakeFX(37);
break;
case 38:
if (snake.getSnakeFX() != 40)
snake.setSnakeFX(38);
break;
case 39:
if (snake.getSnakeFX() != 37)
snake.setSnakeFX(39);
break;
case 40:
if (snake.getSnakeFX() != 38)
snake.setSnakeFX(40);
break;
default:
break;
}
}
});
}
// 重写面板类的画图方法
@Override
public void paint(Graphics g)
{
// 设置背景色为黄色
g.setColor(Color.yellow);
// 画背景
g.fillRect(0, 0, 500, 500);
// 设置食物为红色
g.setColor(Color.red);
// 画食物
g.fillRect(gfood.getFoodX(), gfood.getFoodY(), gfood.getSize(), gfood.getSize());
// 设置蛇头为蓝色
g.setColor(Color.blue);
// 画蛇头
g.fillRect(snake.snakeX[0], snake.snakeY[0], snake.getSize(), snake.getSize());
// 设置蛇身为绿色,如果你绝对原谅色不好看则另设
g.setColor(Color.green);
// 通过for循环画蛇身
for (int i = 1; i < snake.len; i++)
{
g.fillRect(snake.snakeX[i], snake.snakeY[i], snake.getSize(), snake.getSize());
}
}
// 定义内部类实现多线程
class Move extends Thread
{
@Override
public void run()
{
while (true)
{
try
{
// 设置线程休眠时间,400是任意的,越小就会执行的越快,相当于增加难度
Thread.sleep(400);
} catch (InterruptedException e)
{
e.printStackTrace();
}
// 移动蛇身,一定要放在移动蛇头的前面,不然会导致蛇头坐标的丢失,读者可以自行尝试
for (int i = snake.len; i > 0; i--)
{
// for循环的遍历需要从大到小,不然会导致坐标值的覆盖变为一点,这是我推测的,没试过,读者可以自行尝试
snake.snakeX[i] = snake.snakeX[i - 1];
snake.snakeY[i] = snake.snakeY[i - 1];
}
// 根据方向移动蛇头
switch (snake.getSnakeFX())
{
case 37:
snake.snakeX[0] -= 20;
break;
case 38:
snake.snakeY[0] -= 20;
break;
case 39:
snake.snakeX[0] += 20;
break;
case 40:
snake.snakeY[0] += 20;
break;
default:
break;
}
// 吃到食物
if (snake.snakeX[0] == gfood.getFoodX() && snake.snakeY[0] == gfood.getFoodY())
{
// 长度增加
snake.len++;
// 重新生成食物
Random r = new Random();
gfood.setFoodX(r.nextInt(24) * 20);
gfood.setFoodY(r.nextInt(24) * 20);
// 确保新生成的食物不会碰撞到蛇
gfood.Collision(snake.snakeX, snake.snakeY);
}
// 判断是否死亡,如果死亡则结束while循环,相当于结束了这个线程
if (snake.isDie())
break;
// 重绘方法,也是很必要的方法,如果没有的话看不到动的效果
repaint();
}
}
}
}
面板类的作用最大,发挥了功能整合的作用,swing绘图和多线程的相应实现都在此类;
至此,第一个程序结束,读者如果发现有什么问题的话欢迎讨论,最后笔者得声明一个bug,那就是如果手速快的话是可以实现180度转向的,这是由于程序的执行间隔导致的,实属正常。
接下来就是第二个程序:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.LinkedList;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
@SuppressWarnings("serial")
public class Snake extends JFrame
{
// 食物
private Point point;
// 蛇
private LinkedList<Point> list;
// 方向
private int key = 37;
public Snake()
{
super("贪吃蛇");
point = new Point();
list = new LinkedList<Point>();
init();
Set();
addKeyListener(new KeyAdapter()
{
@Override
public void keyPressed(KeyEvent e)
{
System.out.println(e.getKeyCode());
if (e.getKeyCode() >= 37 && e.getKeyCode() <= 40)
{
if (Math.abs((key - e.getKeyCode())) != 2)
{
key = e.getKeyCode();
}
}
}
});
}
private void init()
{
this.setSize(500, 500);
this.setLocationRelativeTo(null);
this.setResizable(false);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
@Override
public void paint(Graphics g)
{
// 图片对象
Image img = createImage(500, 500);
// 画笔对象
Graphics g2 = img.getGraphics();
g2.setColor(Color.white);
g2.fillRect(0, 0, 500, 500);
g2.translate(50, 50);
g2.setColor(Color.red);
g2.drawRect(0, 0, 400, 400);
g2.setColor(Color.green);
// 获取list中的每一个节点,遍历集合
for (Point point : list)
{
g2.fillRect(point.x, point.y, 10, 10);
}
g2.setColor(Color.RED);
g2.fillRect(point.x, point.y, 10, 10);
g.drawImage(img, 0, 0, 500, 500, this);
}
// 初始化
private void Set()
{
point.setLocation(100, 100);
list.add(new Point(300, 300));
list.add(new Point(310, 300));
list.add(new Point(320, 300));
//list.add(new Point(330, 300));
//list.add(new Point(340, 300));
//list.add(new Point(350, 300));
new Thread(new Move()).start();
}
// 内部类实现线程
class Move implements Runnable
{
@Override
public void run()
{
while (true)
{
try
{
Thread.sleep(200);
} catch (InterruptedException e)
{
e.printStackTrace();
}
// 获取第一个点
Point p = list.getFirst().getLocation();
switch (key)
{
case 37:
p.x = p.x - 10;
break;
case 38:
p.y = p.y - 10;
break;
case 39:
p.x = p.x + 10;
break;
case 40:
p.y = p.y + 10;
break;
}
// 添加头结点
list.addFirst(p);
// 死亡
if (p.x < 0 || p.x > 390 || p.y < 0 || p.y > 390)
{
JOptionPane.showMessageDialog(null, "碰到墙壁!!!");
return;
}
// 吃食物
if (p.equals(point))
{
int x = (int) (Math.random() * 40) * 10;
int y = (int) (Math.random() * 40) * 10;
point.setLocation(x, y);
} else
{
// 删除尾节点
list.removeLast();
}
for (int i = 1; i < list.size(); i++)
{
if(p.x==list.get(i).x&&p.y==list.get(i).y)
{
JOptionPane.showMessageDialog(null, "吃到自己了!!!");
return;
}
}
repaint();
}
}
}
public static void main(String[] args)
{
new Snake();
}
}
第二个程序并非笔者原创,所以代码注释不多,敬请见谅,相对于笔者的程序具有以下优势:
1.使用链表存储蛇的身体数据,以及通过添加头结点和删除尾节点实现蛇的移动;
2.通过判断键盘的Code值与蛇头方向差的绝对值不为2来禁止180度转向,避免了switch语句的繁杂;