软件实习项目二:贪吃蛇的游戏开发


本项目配套代码链接:
https://blog.csdn.net/qq_50944418/article/details/111772735

项目要求

  1. 实现贪吃蛇游戏的基本功能,屏幕上随机出现一个“食物”,称为豆子,上下左右控制“蛇”的移动,吃到“豆子”以后“蛇”的身体边长一点,得分增加,“蛇”碰到边界或蛇头与蛇身相撞,蛇死亡,游戏结束。
  2. 为游戏设计初始欢迎界面,游戏界面,游戏结束界面。
  3. 进行交互界面的设计,要有开始键、暂停键和停止退出的选项。对蛇吃豆子进行分值计算,可以设置游戏速度,游戏音乐等拓展元素。

项目框架

在这里插入图片描述

项目设计流程图

在这里插入图片描述

项目实现步骤

第一步 页面设计

SnakeFrame.java
(1)设置游戏边框、位置、颜色等要素。
(2)主要设置了六个标签:游戏标签下的游戏菜单、帮助标签下的帮助菜单、速度标签下的速度菜单、状态标签、速度标签、分数标签。
(3)添加各种监听已经实现键盘控制蛇身移动功能。
(4)添加两个线程,一个负责贪吃蛇的运行,另一个负责计算它的状态分数等。

package gyt;

import java.awt.Color;
import java.awt.Component;

import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;

public class SnakeFrame extends JFrame
{
	private JLabel statusLabel;
	private JLabel speedLabel;
	private JLabel scoreLabel;
	private JPanel snakePanel;
	private Snake snake;
	private JMenuBar bar;		
	private JMenu gameMenu;		
	private JMenu helpMenu;
	private JMenu speedMenu;
	private JMenuItem newItem;
	private JMenuItem pauseItem;
	private JMenuItem beginItem;
	private JMenuItem aboutItem;		
	private JMenuItem slowItem;
	private JMenuItem midItem;
	private JMenuItem fastItem;
	
	public SnakeFrame()
	{
		init();//初始化
		ActionListener actionListener = event ->
		{
			if(event.getSource() == pauseItem)
				snake.isRun = false;
			else if(event.getSource() == beginItem)
				snake.isRun = true;
			else if(event.getSource() == newItem)
				newGame();
			else if(event.getSource() == slowItem)
			{
				snake.speed = Snake.SLOW;
				speedLabel.setText("slow");
			}
			else if(event.getSource() == midItem)
			{
				snake.speed = Snake.MID;
				speedLabel.setText("mid");
			}
			else if(event.getSource() == fastItem)
			{
				snake.speed = Snake.FAST;
				speedLabel.setText("fast");
			}
		};
		
		pauseItem.addActionListener(actionListener);//各种监听
		beginItem.addActionListener(actionListener);
		newItem.addActionListener(actionListener);
		slowItem.addActionListener(actionListener);
		midItem.addActionListener(actionListener);
		fastItem.addActionListener(actionListener);
		aboutItem.addActionListener(actionListener);
		
		addKeyListener(new KeyAdapter()
			{
				public void keyPressed(KeyEvent event)
				{
					switch(event.getKeyCode())
					{
					case KeyEvent.VK_DOWN:
						snake.changeDerection(Snake.DOWN);
						break;
					case KeyEvent.VK_UP:
						snake.changeDerection(Snake.UP);
						break;
					case KeyEvent.VK_LEFT:
						snake.changeDerection(Snake.LEFT);
						break;
					case KeyEvent.VK_RIGHT:
						snake.changeDerection(Snake.RIGHT);
						break;
					case KeyEvent.VK_SPACE:
						if(snake.isRun == true)
						{
							snake.isRun = false;
							snake.status = Snake.PAUSED;
							break;
						}
						else
						{
							snake.isRun = true;
							snake.status = Snake.RUNNING;
							break;
						}
					}
				}
			});
		
	}
	private void init()//初始化
	{
		snake = new Snake();
		setSize(380,460);//面板大小
		setLayout(null);//将容器的布局设为绝对布局	
		this.setResizable(false);//用户不可以自由改变该窗体的大小
		
		bar = new JMenuBar();//菜单栏
		gameMenu = new JMenu("Game");		
		gameMenu.add((newItem = new JMenuItem("New Game")));
		gameMenu.add((pauseItem = new JMenuItem("Pause")));
		gameMenu.add((beginItem = new JMenuItem("continue")));
		
		helpMenu = new JMenu("help");		
		helpMenu.add((aboutItem = new JMenuItem("about")));
		
		speedMenu = new JMenu("speed");
		speedMenu.add(( slowItem = new JMenuItem("slow")));
		speedMenu.add(( midItem  = new JMenuItem("mid")));
		speedMenu.add(( fastItem = new JMenuItem("fast")));
		
		bar.add(gameMenu);
		bar.add(helpMenu);
		bar.add(speedMenu);
		setJMenuBar(bar);
		
		statusLabel = new JLabel();//状态栏
		speedLabel = new JLabel();//速度栏
		scoreLabel = new JLabel();//分数栏
		snakePanel = new JPanel();//贪吃蛇运行的区域
		
		snakePanel.setBounds(0, 0, 300, 400);// 从左上角(0,0)开始 ,大小 300 * 400	
		snakePanel.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
		add(snakePanel);
		//设置位置和大小
		statusLabel.setBounds(310,25,60,20);//四个参数依次是该组件在JFrame中的x坐标、y坐标、组件宽度、组件高度
		add(statusLabel);
		speedLabel.setBounds(310,75,60,20);
		add(speedLabel);
		scoreLabel.setBounds(310,125,60,20);
		add(scoreLabel);
		
		JLabel temp = new JLabel("状态");
		temp.setBounds(310, 5, 60, 20);
		add(temp);
		temp = new JLabel("速度");
		temp.setBounds(310,55,60,20);
		add(temp);
		temp = new JLabel("分数");
		temp.setBounds(310, 105, 60, 20);
		add(temp);			
	}
	private void newGame()
	{
		this.remove(snakePanel);//移除原来的状态、分数等
		this.remove(statusLabel);
		this.remove(scoreLabel);
		speedLabel.setText("slow");
		statusLabel = new JLabel();//新建状态、分数等
		scoreLabel = new JLabel();
		snakePanel = new SnakePanel(snake);
		snakePanel.setBounds(0,0,300,400);
		snakePanel.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));  
		//两个线程,一个负责贪吃蛇的运行,另一个负责计算它的状态分数等
		Runnable r1 = new SnakeRunnable(snake,snakePanel);
		Runnable r2 = new StatusRunnable(snake,statusLabel,scoreLabel);
		Thread t1 = new Thread(r1);
		Thread t2 = new Thread(r2);
		t1.start();
		t2.start();
		add(snakePanel);
		statusLabel.setBounds(310,25,60,20);
		add(statusLabel);
		scoreLabel.setBounds(310,125,60,20);
		add(scoreLabel);
	}
}

结果图:
在这里插入图片描述
SnakePanel.java
确定蛇身和食物的颜色:蛇身设置成统一的黑色,食物颜色共有五种可能,随机产生不同颜色的食物。

package gyt;

import javax.swing.JPanel;
import java.util.*;

import java.awt.Graphics;
import java.awt.Color;

public class SnakePanel extends JPanel
{
	Snake snake;//声明实例
	Color[] c = new Color[]{Color.GREEN, Color.BLUE,Color.WHITE,Color.orange,Color.pink};//存出多种颜色共随机选择
	int number=(int)((Math.random()*100)/20);//随机选择一个颜色,用在生成食物的时候
	Node temp = null;//暂时的结点
	public SnakePanel(Snake snake)
	{
		this.snake = snake;
	}
	
	public void paintComponent(Graphics g)
	{
		super.paintComponent(g);
		//设置蛇身体颜色
		for(Node node : snake.body)
		{
			g.setColor(Color.BLACK);
			g.fillRect(node.x, node.y, Node.W,Node.H);//填色
		}
		//设置食物颜色
		Node food = snake.food;
		
		//g.setColor(Color.RED);
		
		g.setColor(c[number]);//首先设置一种颜色
		if(temp!=food){//如果食物发生变化,就会改变颜色
		    number = (int)((Math.random()*100)/20);//另外再生成一种颜色
	      g.setColor(c[number]);
	       temp=food;//设置temp为当前食物,方便继续判断
	    }
		g.fillRect(food.x,food.y,Node.W, Node.H);
	}
}

结果图:
在这里插入图片描述
在这里插入图片描述

第二步 对蛇身和点的设置

Snake.java
1.对蛇身的初始化设置,游戏开始时蛇的位置是固定的。
2.设置游戏速度,以及用键盘反映方向。
3.设置对蛇的各种状态的处理
(1)通过后方身体减一,前方身体加一来实现蛇的正常运动
(2)通过数组中的方向位置来实现转弯
(3)方向向右并且碰到右边界等类似情况视为碰壁
(4)方向向右,且右边一个就是食物(蛇头横坐标加点宽度等于食物横坐标),那么就吃到了豆子
(5)遍历身体每一格,如果头和身体重合就是咬到自己

package gyt;

import java.util.ArrayList;
//贪吃蛇的类,包括属性和方法
class Snake
{
	boolean isRun;  //是否在运行
	ArrayList<Node> body;  //身体的列表,里面元素是每一节身体
	Node food;
	int direction; //方向
	int score;  //得分
	int status;//̬状态
	int speed; //速度
	public static final int SLOW = 500;//速度慢/中/快
	public static final int MID = 300;
	public static final int FAST = 100;
	public static final int RUNNING = 1;//状态,运行中/暂停/游戏结束
	public static final int PAUSED = 2;
	public static final int GAMEOVER = 3;
	public static final int LEFT = 1;//贪吃蛇的方向
	public static final int UP = 2;
	public static final int RIGHT = 3;
	public static final int DOWN = 4;
	
	public Snake()//初始化贪吃蛇
	{
		speed = Snake.SLOW;
		score = 0;
		isRun = false;
		status = Snake.PAUSED;
		direction = Snake.RIGHT;
		body = new ArrayList<Node>();
		body.add(new Node(60,20));//最开始蛇的长度只有三格
		body.add(new Node(40,20));
		body.add(new Node(20,20));
		makeFood();//生成食物
	}
	
	//判断吃没吃到食物
	private boolean isEaten()
	{
		Node head = body.get(0);//获取头部位置
		if (direction == Snake.RIGHT && (head.x + Node.W) == food.x//方向向右,且右边一个就是食物(蛇头横坐标加点宽度等于食物横坐标),那么就吃到了,返回true;下面内容意思一样
				&& head.y == food.y)
			return true;
		else if(direction == Snake.LEFT && (head.x - Node.W) == food.x 
				&& head.y == food.y)
			return true;
		else if(direction == Snake.UP && (head.y - Node.H) == food.y 
				&& head.x == food.x)
			return true;
		else if(direction == Snake.DOWN && (head.y + Node.H) == food.y 
				&& head.x == food.x)
			return true;
		else
			return false;			
	}
	
	//判断是否发生碰撞,包括碰到墙壁和自己身体
	public boolean isCollsion() 
	{
		Node head = body.get(0);
		
		//碰到墙返回true
		if(direction == Snake.RIGHT && head.x == 280)//方向向右并且碰到右边界;下面内容意思一样
			return true;
		else if(direction == Snake.UP && head.y == 0)
			return true;
		else if(direction == Snake.LEFT && head.x == 0)
			return true;
		else if(direction == Snake.DOWN && head.y == 380)
			return true;
		
		//碰到身体返回true
		Node temp = null;
		int i;
		for(i =3; i < body.size(); i++)//遍历身体每一格,如果头和身体重合就是碰到;从3开始因为蛇身至少要为四节才有可能咬尾
		{
			temp = body.get(i);
			if (temp.x == head.x && temp.y == head.y)
				return true;
		}
		if(i < body.size())
			return true;
		return false;
	}
	
	//随机产生一个食物
	public void makeFood()
	{
		boolean isInBody = true;//true表示被吃了
		int ax = 0, ay = 0;
		int X = 0, Y = 0;
		while(isInBody)//当食物被蛇吃了,就立刻产生食物
		{
			isInBody = false;
			ax = (int)(Math.random() * 15);//math.random是随机数
			ay = (int)(Math.random() * 20);
			X = ax*Node.W;
			Y = ax*Node.H;
			for(Node temp :body)
			{
				if(X== temp.x && Y == temp.y)
					isInBody = true;
			}
		}
		food = new Node( X, Y); 
	}
	
	//改变方向
	public void changeDerection(int newDer)
	{
		if(direction % 2 !=  newDer % 2) //不能改变为相反的方向,其他都可以
		{
			direction = newDer;
		}
	}
	
	public void move()//移动蛇
	{
		if(isEaten())    
		{
			body.add(0,food);//吃了的话身体加一格,加分,随机产生食物
			score += 10;
			makeFood();
		}
		else if(isCollsion())
		{
			isRun = false;
			status = Snake.GAMEOVER;//碰撞的话游戏结束
		}
		else if(isRun)//正在运行的话,要时刻判断是否改变了方向
		{
			Node head = body.get(0);
			int X = head.x;
			int Y = head.y;
			switch(direction)
			{
			case RIGHT:
				X += Node.W;
				break;
			case LEFT:
				X -= Node.W;
				break;
			case UP:
				Y -= Node.H;
				break;
			case DOWN:
				Y += Node.H;
				break;
			}
			body.add(0,new Node(X,Y));//用前面多一节,后面少一节的方式来表示移动
			body.remove(body.size()-1);   
		}
	}
}

Node.java
设置平面内20*20为一个小点。

package gyt;

class Node
{
	public static final int W = 20;
	public static final int H = 20; //设置每个小点的长和宽
	int x;
	int y;
	
	public Node(int x, int y)
	{
		this.x = x;
		this.y = y;
	}
}

第三步 创建两个线程统一更新标签中的分数和状态。

SnakeRunnable.java
用于统一更新分数的线程。

package gyt;

import javax.swing.JComponent;

class SnakeRunnable implements Runnable
{
	private Snake snake;
	private JComponent component;
	int i = 0;
	//一个线程,负责蛇的移动
	//使用两个线程,两个线程都可以同时运行,这样就能比较实时的计算
	public SnakeRunnable(Snake snake, JComponent component)
	{
		this.snake = snake;
		this.component = component;
	}
	public void run()
	{
		while(true)
		{
			try {
				snake.move();//蛇移动的计算部分
				component.repaint();//蛇移动的显示部分,视觉上在移动
				Thread.sleep(snake.speed);
			}catch(Exception e) {}//如果运行代码出现异常,什么也不做
		}
	}
}

StatuesRunnable.java
用于同一更新状态的线程。

package gyt;

import javax.swing.JLabel;

class StatusRunnable implements Runnable
{
	private JLabel statusLabel;
	private JLabel scoreLabel;
	private Snake snake;
	
	//另一个线程,负责计算蛇的状态、分数
	public StatusRunnable(Snake snake, JLabel statusLabel, JLabel scoreLabel)
	{
		this.statusLabel = statusLabel;
		this.scoreLabel = scoreLabel;
		this.snake = snake;
	}
	
	public void run()
	{
		String sta = "";
		String spe = "";
		
		while(true)
		{
			switch(snake.status)
			{
			case Snake.RUNNING:
				sta = "running";
				break;
			case Snake.PAUSED:
				sta = "Paused";
				break;
			case Snake.GAMEOVER:
				sta = "GameOver";
				break;
			}
			statusLabel.setText(sta);//显示状态
			scoreLabel.setText("" + snake.score);//显示分数
			try {
				Thread.sleep(100);
			}catch(Exception e) {}//如果运行代码出现异常,什么也不做		
		}
	}
}

大致要点就是这些,具体详解可以看代码中的注释部分,注释部分写的很仔细
暂时还没有实现的功能有:初始欢迎界面、游戏界面、游戏结束界面以及背景音乐等一些能让用户体验感更加的功能。有时间将补充。

项目中关键方法分析

方法 1:判断是否吃到食物的方法

方法功能:判断是否迟到食物,返回逻辑值,供后续调用,来进行生成新食物等操作。

方法基本思想:获取头部位置,如果蛇向右运动,且右边一个就是食物(蛇头横坐标加点宽度等于食物横坐标),那么就吃到了,返回true。其他方向以此类推。

private boolean isEaten()
	{
		Node head = body.get(0); 
		if (direction == Snake.RIGHT && (head.x + Node.W) == food.x						&& head.y == food.y)
			return true;
		else if(direction == Snake.LEFT && (head.x - Node.W) == food.x 
				&& head.y == food.y)
			return true;
		else if(direction == Snake.UP && (head.y - Node.H) == food.y 
				&& head.x == food.x)
			return true;
		else if(direction == Snake.DOWN && (head.y + Node.H) == food.y 
				&& head.x == food.x)
			return true;
		else
			return false;			
	}

方法 2:判断是否碰到墙的方法

方法功能:判断是否碰到墙,返回逻辑值,实现撞墙则蛇死亡以及游戏结束效果。

方法基本思想:获取头部位置,如果蛇运动方向向右并且碰到右边界,则判断撞到墙,返回true。其他方向以此类推。

if(direction == Snake.RIGHT && head.x == 280)
			return true;
		else if(direction == Snake.UP && head.y == 0)
			return true;
		else if(direction == Snake.LEFT && head.x == 0)
			return true;
		else if(direction == Snake.DOWN && head.y == 380)
			return true;

方法 3:判断是否撞到身体的方法

方法功能:判断是否碰到身体,返回逻辑值,实现碰到身体则蛇死亡以及游戏结束效果。

方法基本思想:获取头部位置,然后遍历身体每一节,如果头和身体某节重合就是碰到身体,返回true。

Node temp = null;
		int i;
		for(i =3; i < body.size(); i++)		{
			temp = body.get(i);
			if (temp.x == head.x && temp.y == head.y)
				return true;
		}
		if(i < body.size())
			return true;
		return false;
	}

方法 4:实现蛇运动的方法

方法功能:实现直行或转弯是蛇身的前后变换,以达到蛇身移动的效果。

方法基本思想:用坐标的改变来实现蛇的转弯,用蛇头延伸一节,蛇尾缩减一节的方式来达到蛇移动的效果。

else if(isRun)
		{
			Node head = body.get(0);
			int X = head.x;
			int Y = head.y;
			switch(direction)
			{
			case RIGHT:
				X += Node.W;
				break;
			case LEFT:
				X -= Node.W;
				break;
			case UP:
				Y -= Node.H;
				break;
			case DOWN:
				Y += Node.H;
				break;
			}
			body.add(0,new Node(X,Y)); 
			body.remove(body.size()-1);   
		}

项目总结分析

  1. 通过标签实现了“新游戏”、“暂停”和“继续”的功能,并且可以设置游戏速度,有“慢”、“中”和“快”三种难度。
  2. 能够实现随机出现不同颜色的食物,一共有五种可能,分别是绿色、蓝色、白色、橙色和粉色。
  3. 能够实现吃下食物蛇身边长,且分数增加,直到撞墙或者撞到蛇自己的身体游戏结束。

心得体会

个人认为,对于这个项目,我大致还是满意的,明显的缺陷可能是没有进一步的对贪吃蛇游戏界面进行进一步的美化,例如让蛇更加生动,给游戏增加开始界面和背景音乐等,仅仅是简单的完成了贪吃蛇的基本操作。主要原因是可能对于现在的我来说这部分加工还有些许困难,后续我也逐渐通过学习来完善这次的项目。

本项目配套代码链接:
https://blog.csdn.net/qq_50944418/article/details/111772735

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值