贪吃蛇(java实现)

在这里插入图片描述

这是MVC模式的完整Java项目,编译运行SnakeApp.java即可开始游戏。

可扩展功能:
  1. 积分功能:可以创建得分规则的类(模型类的一部分), 在GameController的run()方法中计算得分
  2. 变速功能:比如加速功能,减速功能,可以在GameController的keyPressed()方法中针对特定的按键设置每一次移动之间的时间间隔,将Thread.sleep(Settings.DEFAULT_MOVE_INTERVAL);替换为动态的时间间隔即可
  3. 更漂亮的游戏界面:修改GameView中的drawXXX方法,比如可以将食物渲染为一张图片,Graphics有drawImage方法

View

  • SnakeApp.java
/*
 * 属于View,用来根据相应的类展示出对应的游戏主界面,也是接收控制信息的第一线。
 */
public class SnakeApp {
    public void init() {
        //创建游戏窗体
        JFrame window = new JFrame("一只长不大的蛇");
        //初始化500X500的棋盘,用来维持各种游戏元素的状态,游戏的主要逻辑部分
        Grid grid = new Grid(50*Settings.DEFAULT_NODE_SIZE,50*Settings.DEFAULT_NODE_SIZE);
        //传入grid参数,新建界面元素对象
        GameView gameView = new GameView(grid);//绘制游戏元素的对象
        //初始化面板
        gameView.initCanvas();
        //根据棋盘信息建立控制器对象
        GameController gameController = new GameController(grid);
       
        //设置窗口大小
        window.setPreferredSize(new Dimension(526,548));
        
        //往窗口中添加元素,面板对象被加入到窗口时,自动调用其中的paintComponent方法。
        window.add(gameView.getCanvas(),BorderLayout.CENTER);
        
        //画出蛇和棋盘和食物
        GameView.draw();
        
        //注册窗口监听器
        window.addKeyListener((KeyListener)gameController);
        
        //启动线程
        new Thread(gameController).start();
        
        //窗口关闭的行为
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //设置窗口大小不可变化
        window.setResizable(false);
        //渲染和显示窗口
        window.pack();
        window.setVisible(true);
    }
	//可以忽略,以后每个类中都有这么一个测试模块
    public static void main(String[] args) {
        SnakeApp snakeApp = new SnakeApp();
        snakeApp.init();
    }
}
  • GameView.java
/*
 * 属于View,用于绘制地图、蛇、食物
*/

 /* Graphics 相当于一个画笔。对象封装了 Java 支持的基本呈现操作所需的状态信息。此状态信息包括以下属性:
	要在其上绘制的 Component 对象。
	呈现和剪贴坐标的转换原点。
	当前剪贴区。
	当前颜色。
	当前字体。
	当前逻辑像素操作函数(XOR 或 Paint)。
	当前 XOR 交替颜色
*/

/* java.awt.Component的repaint()方法
	作用:更新组件。
	如果此组件不是轻量级组件,则为了响应对 repaint 的调用,AWT 调用 update 方法。可以假定未清除背景。
	Component 的 update 方法调用此组件的 paint 方法来重绘此组件。为响应对 repaint 的调用而需要其他工作的子类通常重写此方法。重写此方法的 Component 子类应该调用 super.update(g),或者直接从其 update 方法中调用 paint(g)。
	图形上下文的原点,即它的(0,0)坐标点是此组件的左上角。图形上下文的剪贴区域是此组件的边界矩形。
 */
public class GameView {
	private final Grid grid;
	private static JPanel canvas;//画板,用于在这上面制作画面,然后返回。
	
	public GameView(Grid grid) {
		this.grid = grid;
	}
	//重新绘制游戏界面元素,不断重新调用paintComponent方法覆盖原本的面板。
	 public static void draw() {
	    	canvas.repaint();
	    }
	    //获取画板对象的接口
	 public JPanel getCanvas() {
	    	return canvas;
	    }
	//对画板进行初始化
	public void initCanvas() {
		canvas = new JPanel() {
		//指向一个方法被覆盖的新面板子类对象
		//paintComponent()绘制此容器中的每个组件,Swing会在合适的时机去调用这个方法,展示出合适的界面,这就是典型的回调(callback)的概念。
			public void paintComponent(Graphics graphics) {
				super.paintComponent(graphics); //这里必须调用一下父类 也就是 container的重绘方法,否则表现为之前的绘图不会覆盖
				drawGridBackground(graphics);//画出背景网格线
				drawSnake(graphics, grid.getSnake());//画蛇
		        drawFood(graphics, grid.getFood());//画食物
			}
		};
	}
	//画蛇
    public void drawSnake(Graphics graphics, Snake snake) {
    	for(Iterator<Node> i = snake.body.iterator();i.hasNext();) {
    		Node bodyNode = (Node)i.next();
    		drawSquare(graphics, bodyNode,Color.BLUE);
    	}
    }
    //画食物
    public void drawFood(Graphics graphics, Node food) {
    	drawCircle(graphics,food,Color.ORANGE);
    }
    
  //画格子背景,方便定位Snake运动轨迹,横竖各以10为单位的50个线。
    public void drawGridBackground(Graphics graphics) {
    	graphics.setColor(Color.GRAY);
    	canvas.setBackground(Color.BLACK);
    	for(int i=0 ; i < 50 ; i++) {
    		graphics.drawLine(0, i*Settings.DEFAULT_NODE_SIZE, this.grid.getWidth(), i*Settings.DEFAULT_NODE_SIZE);
    	}
    	for(int i=0 ; i <50 ; i++) {
    		graphics.drawLine(i*Settings.DEFAULT_NODE_SIZE, 0,  i*Settings.DEFAULT_NODE_SIZE , this.grid.getHeight());
    	}
    	graphics.setColor(Color.red);
    	graphics.fillRect(0, 0, this.grid.width, Settings.DEFAULT_NODE_SIZE);
    	graphics.fillRect(0, 0, Settings.DEFAULT_NODE_SIZE, this.grid.height);
    	graphics.fillRect(this.grid.width, 0, Settings.DEFAULT_NODE_SIZE,this.grid.height);
    	graphics.fillRect(0, this.grid.height, this.grid.width+10,Settings.DEFAULT_NODE_SIZE);
    	
    }
    /*
     * public abstract void drawLine(int x1,int y1,int x2,int y2)
		在此图形上下文的坐标系中,使用当前颜色在点 (x1, y1) 和 (x2, y2) 之间画一条线。
		参数:
				x1 - 第一个点的 x 坐标。
				y1 - 第一个点的 y 坐标。
				x2 - 第二个点的 x 坐标。
				y2 - 第二个点的 y 坐标。
     */
    //提供直接出现游戏结束的选项框的功能。
    public static void showGameOverMessage() {
    	JOptionPane.showMessageDialog(null,"游戏结束","短暂的蛇生到此结束", JOptionPane.INFORMATION_MESSAGE);
    }
    

//画图形的具体方法:
private void drawSquare(Graphics graphics, Node squareArea, Color color) {
    graphics.setColor(color);
    int size = Settings.DEFAULT_NODE_SIZE;
    graphics.fillRect(squareArea.getX(), squareArea.getY(), size - 1, size - 1);
}

private void drawCircle(Graphics graphics, Node squareArea, Color color) {
    graphics.setColor(color);
    int size = Settings.DEFAULT_NODE_SIZE;
    graphics.fillOval(squareArea.getX(), squareArea.getY(), size, size);
}

}

Controller

  • GameController

/*
 * 接收窗体SnakeApp传递过来的有意义的事件,然后传递给Grid,让Grid即时的更新状态。
 * 同时根据最新状态渲染出游戏界面让SnakeApp显示
 * 
 */
public class GameController implements KeyListener, Runnable{
	private Grid grid;
	private boolean running;
	
	public GameController(Grid grid){
		this.grid = grid;
		this.running = true;
	}

	@Override
	public void keyPressed(KeyEvent e) {
		int keyCode = e.getKeyCode();
		switch(keyCode) {
        case KeyEvent.VK_UP:	
        	grid.changeDirection(Direction.UP); 
        	break;
        case KeyEvent.VK_DOWN:	
        	grid.changeDirection(Direction.DOWN); 
        	break;
        case KeyEvent.VK_LEFT:	
        	grid.changeDirection(Direction.LEFT); 
        	break;
        case KeyEvent.VK_RIGHT:	
        	grid.changeDirection(Direction.RIGHT); 
        	break;
		}
		isOver(grid.nextRound());
		GameView.draw();
	}

	private void isOver(boolean flag) {
		if(!flag) {//如果下一步更新棋盘时,出现游戏结束返回值(如果flag为假)则
			this.running = false;
			GameView.showGameOverMessage();
			System.exit(0);
		}
	}
	
	@Override
	/*run()函数中的核心逻辑是典型的控制器(Controller)逻辑:
	修改模型(Model):调用Grid的方法使游戏进入下一步
	更新视图(View):调用GameView的方法刷新页面*/
	public void run() {
		while(running) {
			try {
				Thread.sleep(Settings.DEFAULT_MOVE_INTERVAL);
				isOver(grid.nextRound());
				GameView.draw();
			} catch (InterruptedException e) {
				break;
			}
			 // 进入游戏下一步
            // 如果结束,则退出游戏
            // 如果继续,则绘制新的游戏页面
		}
		running = false;
	}
	
	
	@Override
	public void keyTyped(KeyEvent e) {
	}
	@Override
	public void keyReleased(KeyEvent e) {
	}
}

Model

  • Grid
/*
 * 随机生成食物,维持贪吃蛇的状态,根据SnakeApp中的用户交互来控制游戏状态。
 */
public class Grid {
	private Snake snake;
	int width;
	int height;
	Node food;
	private Direction snakeDirection =Direction.LEFT;
	public Grid(int length, int high) {
		super();
		this.width = length;
		this.height = high;
		initSnake();
		food = creatFood();
	}
	
	//在棋盘上初始化一个蛇
	private void initSnake() {
		snake = new Snake();
		int x = width/2;
		int y = height/2;
		for(int i = 0;i<5;i++) {
			snake.addTail(new Node(x, y));
			x = x+Settings.DEFAULT_NODE_SIZE;
		}
	}
	//棋盘上随机制造食物的功能。
	//一直循环获取随机值,直到三个条件都不满足。
	private Node creatFood() {
		int x,y;
		do {
			x =(int)(Math.random()*100)+10;
			y =(int)(Math.random()*100)+10;
			System.out.println(x);
			System.out.println(y);
			System.out.println(this.width);
			System.out.println(this.height);
		}while(x>=this.width-10 || y>=this.height-10 || snake.hasNode(new Node(x,y)));
		food = new Node(x,y);
		return food;
	}
		
	//提供下一步更新棋盘的功能,移动后更新游戏和蛇的状态。
	public boolean nextRound() {
		Node trail = snake.move(snakeDirection);
		Node snakeHead = snake.getBody().removeFirst();//将头部暂时去掉,拿出来判断是否身体和头部有重合的点
		if(snakeHead.getX()<=width-10 && snakeHead.getX()>=10 
	    		&& snakeHead.getY()<=height-10 && snakeHead.getY()>=10
	    		&& !snake.hasNode(snakeHead)) {//判断吃到自己和撞到边界
			if(snakeHead.equals(food)) {
				//原本头部是食物的话,将move操作删除的尾部添加回来
				snake.addTail(trail);
				food = creatFood();
			}
			snake.getBody().addFirst(snakeHead);
			return true;//更新棋盘状态并返回游戏是否结束的标志
		}
		return false;
	}
	
	public Node getFood() {
		return food;
	}
	
	public Snake getSnake() {
		return snake;
	}
	
	public int getWidth() {
		return width;
	}
	
	public int getHeight() {
		return height;
	}
	
	//提供一个更改贪吃蛇前进方向的方法
	public void changeDirection(Direction newDirection){
	        snakeDirection = newDirection;
	}
}
  • Snake
/*
 * 蛇类,实现了自身数据结构,以及移动的功能
 */
public class Snake implements Cloneable{
	public LinkedList<Node> body = new LinkedList<>();
	public Node move(Direction direction) {
		//根据方向更新贪吃蛇的body
		//返回移动之前的尾部Node(为了吃到时候后增加尾部长度做准备)
		Node n;//临时存储新头部移动方向的结点
		switch (direction) {
	    case UP:
	    	n = new Node(this.getHead().getX(),this.getHead().getY()-Settings.DEFAULT_NODE_SIZE); 
	    	break;
	    case DOWN:
	    	n = new Node(this.getHead().getX(),this.getHead().getY()+Settings.DEFAULT_NODE_SIZE); 
	    	break;
    	case RIGHT: 
    		n = new Node(this.getHead().getX()+Settings.DEFAULT_NODE_SIZE,this.getHead().getY()); 
    		break;
    	default: 
    		n = new Node(this.getHead().getX()-Settings.DEFAULT_NODE_SIZE,this.getHead().getY());
    	
	}
		Node temp = this.body.getLast();
		this.body.addFirst(n);
		this.body.removeLast();
		return temp;
	}
	public Node getHead() {
		return body.getFirst();
	}
	public Node getTail() {
		return body.getLast();
	}
	public Node addTail(Node area) {
		this.body.addLast(area);
		return area;
	}
	public LinkedList<Node> getBody(){
		return body;
	}
	//判断参数结点是否在蛇身上
	public boolean hasNode(Node node) {
		Iterator<Node> it = body.iterator();
		Node n = new Node(0,0);
		while(it.hasNext()) {
			n = it.next();
			if(n.getX() == node.getX() && n.getY() == node.getY()) {
				return true;
			}
		}
		return false;
	}
}
  • Direction
/*
 * 用来控制蛇的移动方向
 */
public enum Direction {
	UP(0),
	DOWN(1),
	LEFT(2),
	RIGHT(3); //调用构造方法对方向枚举实例进行代码初始化
	//成员变量
	private final int directionCode;
	
	//成员方法
	public int directionCode() {
		return directionCode;
	}
	Direction(int directionCode){
		this.directionCode = directionCode;
	}
}
  • Node

public class Node {
	private  int x;
	private  int y;
	public Node(int x, int y) {
		this.x = ((int)(x/10))*10;
		this.y = ((int)(y/10))*10;
	}//使用这种方法可以使得节点坐标不会出现个位数
	
	public int getX() {
		return x;
	}
	
	public int getY() {
		return y;
	}
	@Override
	//判断两个Node是否相同
	public boolean equals(Object n) {
		Node temp;
		if(n instanceof Node) {
			temp = (Node)n;
			if(temp.getX()==this.getX() && temp.getY()==this.getY()) 
				return true;
		}
		return false;
	}
}
  • Settings
public class Settings {
	public static int DEFAULT_NODE_SIZE = 10;//每一个节点方块的单位
    public static int DEFAULT_MOVE_INTERVAL = 200;//蛇移动时间间隔
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值