Java贪吃蛇

      首先声明文章为笔者原创,转载请声明出处;

      在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语句的繁杂;

最后,笔者想说的是代码可以复制粘贴,这差不多是每个初学者的必经之路,笔者也有类似经历,关键在于化我所用,掌握思想和技巧。

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
可以运行! (以下代码只是其中的一个类) package chy.snake.entities; import java.awt.Color; import java.awt.Graphics; import java.awt.Point; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; import chy.snake.listener.SnakeListener; import chy.snake.util.Global; public class Snake { public static final int up = 1; public static final int down = -1; public static final int left = -2; public static final int right = 2; private int oldDirection,newDirection; //newDirection:一次时间 间隔内输入的最后方向 private Point oldTail; private boolean life; //life 为 true或者false,初始为true, 用于118行 private LinkedList<Point> body = new LinkedList<Point> (); //需要经常访问蛇的第一个和最后一个节点,使用链表LinkedList存放蛇的身体节点,因为它有getFirst(),getLast(),removeLast(),方法 private Set<SnakeListener> listeners = new HashSet<SnakeListener>(); public Snake(){ init(); } public void init(){ //初始化 int x = Global.WIDTH/2; int y = Global.HEIGHT/2; for(int i=0;i<3;i++){ //初始长度3 body.addLast(new Point(x-i,y)); //是addLast } oldDirection = newDirection = right; //初始方向 右 life = true; } public void die(){ life = false; } public void move(){ System.out.println("Snake's move"); if (!(oldDirection + newDirection == 0)){ oldDirection = newDirection; } //1.去尾 oldTail = body.removeLast(); int x = body.getFirst().x; int y = body.getFirst().y; //蛇头的x,y坐标 switch(oldDirection){ case up: y--; break; case down: y++; break; case left: x--; break; case right: x++; break; } Point newHead = new Point(x,y); //2.加头 body.addFirst(newHead); } public void changeDirection(int direction){ /*无效方向:在蛇的这一次移动之后和下一次移动之前的 这个时间间隔内输入了多个方向,只有最后一个方向 是 有效方向,其余的都为无效方向*/ System.out.println("Snake's changeDirection"); newDirection = direction; //将一个时间间隔内按得最后方向,赋给 newDirection } public void eatFood(){ System.out.println("Snake's eatFood"); body.addLast(oldTail); //后面的节点不去掉 } public boolean isEatFood(){ System.out.println("Snake's isEatFood"); return false; } public boolean isEatBody(Snake snake){ //比较蛇是否吃到身体 System.out.println("snake's isEatBody"); for(int i= 1;i<body.size();i++){ //i 从蛇头结点的下一个节点开始,排除蛇头结点 if(body.get(i).equals(this.getHead())){ //如果i 的节点 和 头结点 相同 return true; } } return false; } public void drawMe(Graphics g){ System.out.println("Snake's drawMe"); g.setColor(Color.GREEN); //设置蛇的颜色 for(Point p : body){ g.fill3DRect(p.x * Global.CELL_SIZE, p.y * Global.CELL_SIZE, Global.CELL_SIZE, Global.CELL_SIZE, true); } } public Point getHead(){ //得到蛇头节点,判断吃食物 return body.getFirst(); } private class SnakeDriver implements Runnable{ //线程,不停的调用move方法 @Override public void run() { // TODO 自动生成的方法存根 while(life){ // 42和46行,life为true 或者false move(); for(SnakeListener l : listeners){ l.snakeMoved(Snake.this); //循环,依次调用SnakeMoved方法 } try { Thread.sleep(300); } catch (InterruptedException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } } } } public void start(){ new Thread(new SnakeDriver()).start(); //启动线程的方法 } public void addSnakeListener(SnakeListener l){ if(l != null){ this.listeners.add(l); } } }
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值