对于对Java不是很熟悉的同学(比如我)就得画大把时间去了解一下这类游戏设计需要用到得库、类以及方法。比如util包、linkedList类、awt常用类、swing常用类、多线程操作、随机函数生成等等, 光是查询这些很多不了解得东西就会花费很多时间,不用担心,我在学习过程中已经把主要得资料都以超链接得方式整理好附在了博客内。
谈及编写还力不从心,主要是借鉴网上各位大佬得程序设计思路,然后加以修改,不断查询资料,最后理解并掌握了贪吃蛇设计得主要流程和各种操作实现得方法。
1、语言:JAVA
2、开发环境:IntelliJ IDEA Community Edition 2020
实现贪吃蛇游戏基本功能,屏幕上随机出现一个“食物”,称为豆子,上下左右控制“蛇”的移动,吃到“豆子”以后“蛇”的身体加长一点,得分增加,“蛇”碰到边界或,蛇头与蛇身相撞,蛇死亡,游戏结束。为游戏设计初始欢迎界面,游戏界面,游戏结束界面。
//实验准备部分是我在学习过程中遇到的一些麻烦或者是太不理解的部分把这些问题的我觉得能解决问题并且还不错的链接附在文章中。
util包链接
java.util是包含集合框架、遗留的 collection 类、事件模型、日期和时间设施、国际化和各种实用工具类(字符串标记生成器、随机数生成器和位数组、日期Date类、堆栈Stack类、向量Vector类等)。集合类、时间处理模式、日期时间工具等各类常用工具包。
util包的主要类的详解链接
;
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。 注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
List list = Collections.synchronizedList(new LinkedList(...));
;
Java提供了对观察者模式的支持接口和实现类。其中接口 java.util.Observer 用来指定观察者,观察者必须实现 void update(Observable o, Object arg) 方法。而 java.util.Observable 用来指定观察物(被观察者、可被观察的),并且提供了一系列的方法。读者可以很轻易的使用这个接口和实现类来实现观察者模式。
观察者相关学习链接
;
Java实用工具类库中的类java.util.Random提供了产生各种类型随机数的方法。它可以产生int、long、float、double以及Goussian等类型的随机数。这也是它与java.lang.Math中的方法Random()最大的不同之处,后者只产生double型的随机数。
java.util.Observable 用来指定观察物(被观察者、可被观察的),并且提供了一系列的方法。读者可以很轻易的使用这个接口和实现类来实现观察者模式。
;
1、在对一个不断变化的动画来说,就会容易出现线程同步问题.
new Thread(new Runnable(){})
2、Thread 是类,且实现了Runnable接口。
通过Thread类建立线程对象
Java中的Runnable
;
Random类,提供功能 , 名字 nextInt() 产生一个随机数, 结果是int类型
出现随机数的范围, 在功能nextInt(写一个整数), 整数: 随机出来的范围
随机数的范围在 0 - 指定的整数之间的随机数 nextInt(100) [0,99]
;
Java按键事件KeyEvent对应的字符表示链接
此处举出一些常用的按键常量:
按键常量:
VK_UP 上箭头
VK_DOWN 下箭头
VK_LEFT 左箭头
VK_RIGHT 右箭头
VK_ENTER 回车键
KeyReleased(e: KeyEvent) 在源组件上释放一个键后被调用;
KeyTyped(e: KeyEvent) 在源组件上按下一个键然后释放该键后被调用;
按键事件可以利用键盘来控制和执行一些动作,或者从键盘上获取输入,只要按下,释放一个键或者在一个组件上敲击,就会触发按键事件。KeyEvent对象描述事件的特性(按下,放开,或者敲击一个键)和对应的值。java提供KeyListener接口处理按键事件。
;
Java中的键盘监听事件KeyListener
;
Java消息提示框JOptionPane的使用方法:
Java消息提示框JOptionPane的使用方法
JAVA(6)import javax.swing.JOptionPane
setChanged()方法用于将此Observable对象状态设置为已更改。
observable_Java Observable setChanged()方法与示例
当对象已更改时, notifyObservers()方法用于通知列表中的所有观察者。
observable_Java Observable notifyObservers()方法与示例
;
java awt 简单示例 BorderLayout
链接里的代码执行过就是这种样式,大家只要知道是这么添加就好。
如果测试改段代码记得添加语句:
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
Frame f = new Frame("My Frame");
Button b1 = new Button("BN");
f.setLayout(new BorderLayout());
f.add(b1, BorderLayout.NORTH);
1、在学习中也有一些小问题比如下面突然想到Java中的类的构造函数中习惯用的this指针,之前一这是习惯性的使用,但是这次就去特地查了一下;以下是参考链接:
Java中构造函数的this指针的详解
2、Java中try catch语句:
try语句块中的代码可能会引发多种类型的异常,当引发异常时,会桉顺序查看每个catch语句,并执行与之异常类型相匹配的第一个catch语句,后面的catch语句被忽略。
Java中try catch语句
3、控件得设置相关参考链接:
java中setSize(),setLocation()和setBounds()的关系
setSize(int width, int height):其实就是定义控件的大小,有两个参数,分别对应宽度和高度;
setLocation(int x, int y):将组件移到新位置,用x 和 y 参数来指定新位置的左上角
setBounds(int x, int y, int width, int height):四个参数,既定义组件的位置,也定义控件的大小; 其实它就是上面两个函数的功能的组合
;
(当然实际情况十边做边确定我们需要用到那些库);
如下,我们需要用到的库,在实验准备部分我基本上都有标注相关参考链接;
import java.util.Arrays; //提供数组的相关操作;
import java.util.LinkedList; //提供堆栈、队列等功能的的list;
import java.util.Observable; //用以创建观察者对象;
import java.util.Random; //提供随机数;
import javax.swing.JOptionPane; //向用户界面弹出要求弹框;
;
变量得相关解释都在代码框打了出来;
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 int sum = 0; //用以计算分数
public boolean coordinate[][]; //用这个来当做界面的坐标;
public LinkedList node = new LinkedList(); //创建线性表的对象
public int direction = 2; //设置头部方向direction的初始默认值为向上;
boolean running = false;
public int maxX,maxY; //界面大小设置;
Node food; //线性表单元food
public int sleeptime=200; //设置移动的间隔时间(刷新时间);
;
设计思想得体现:我们需要对图上得每一个坐标点附属上一个标识符,来标志是都满足蛇得通行要求;第一步我们”刷版“把整个面板得坐标点得标志位都标记为0,即可通过。
public void reset() {
direction = this.up; //设置默认的头部移动方向;
sleeptime = 200;
coordinate = new boolean[maxX][];
for (int i = 0; i < maxX; i++) {
coordinate[i] = new boolean[maxY];
Arrays.fill(coordinate[i], false); //设置边界坐标标志为0,不可达;
}
;
就是把蛇身包括得坐标点得标志位标识为1,即不可通行:
设置蛇得初始长度,初始头部得移动方向;
设置食物所处坐标标志位为1;
//构造蛇身初始状态;
int initlenght=10; //设置蛇的初始长度为10
node.clear();
for (int j = 0; j < initlenght; j++) {
int x = maxX/2+j;
int y = maxY/2; //安排初始情况下蛇的每一个节点的坐标位置
node.addLast(new Node(x,y)); //将蛇的身体坐标存入线性链表
coordinate[x][y] = true; //对于蛇身位置坐标标记为1,不可走;
}
food = createFood(); //初始化时随机生成位置坐标
coordinate[food.x][food.y] = true; //有食物的坐标位置标记为1;
}
;
设计思路主要是链表得头部更迭问题,
//蛇的核心移动部分;
public boolean move(){
Node n = (Node)node.getFirst(); //取出代表蛇身链表首部的位置坐标;
int x = n.x;
int y = n.y;
switch(direction){
//对于键盘获取的方向的不同对蛇的首部坐标进行修改;
case up:
y--;
break;
case down:
y++;
break;
case left:
x--;
break;
case right:
x++;
break;
default:
break;
}
if ((x >= 0&&x<maxX)&&(y >= 0&&y<maxY)) {
//判断该位置是否有食物;
if (coordinate[x][y]) {
if (x == food.x && y == food.y) {
//判断如果蛇头坐标等于食物坐标;
node.addFirst(food); //把食物坐标增添到蛇身链表的首部;
if (sleeptime > 40) {
//在时间间隔最小限度内不断缩短时间间隔(就是随着吃到食物不断增大难度);
sleeptime -= 20;
sum += 10;
}
food = createFood(); //迟到食物后,再随机生成一个食物;
coordinate[food.x][food.y] = true; //标记食物的坐标标识符为1;
return true; //结束该次移动;
} else {
return false; //如果下一步的坐标标识符为1,但是又不是食物则时吃到自己了,结束;
}
}
else {
//如果下一个位置坐标标识符号不是1;
node.addFirst(new Node(x,y)); //将下一个坐标添加为新的蛇头首部;
coordinate[x][y] = true; //由于该点被纳入蛇身,则将该点的标识符置1;
n = (Node)node.getLast(); //获取当前蛇的尾部坐标,注意不要和下一句颠倒位置;
node.removeLast(); //移除蛇这个链表的尾部的坐标;
coordinate[n.x][n.y] = false; //将之前的蛇的尾部坐标标识符置0;
return true; //返回移动正确;
}
}
return false; //如果撞到边界返回false;
}
;
调用Random类,使用nextInt(n)产生0–n的随机整数;采用do while语句,先生成再判断是否产生在边界上或者是否产生在蛇的身上,如果都不是则成功生成一个食物坐标,并把它的位置标识符置1;代码如下:
public Node createFood() {
//食物的创建函数;
int x = 0,y = 0;
do {
Random r = new Random(); //随机对象r;
x = r.nextInt(maxX); //生成【0,maxX)的随机数;
y = r.nextInt(maxY);
} while (coordinate[x][y]); //当生成的随机坐标已经有标识符为1了就再次执行循环生成一次;
return new Node(x, y); //把生成正确的坐标存入新的链表单元中;
}
;
包含游戏结束和游戏获得分数的累计;
JOptionPane.showMessageDialog(null, "游戏结束,你获得的分数为:"+sum+"分。"); //JOptionPane(只有一个确认按钮的消息提示框)
运行结果如下:
当然如果想要拓展实验就可以加上:这样就可以增加更多的互动性,让游戏的可玩性更高一点,
JOptionPane.showConfirmDialog(null, "你想再玩一次吗?","是否继续",JOptionPane.YES_NO_OPTION);
可以让玩家选择是否继续游戏,根据选择就可以执行重玩游戏等等操作了;
执行结果如下:
1、在控制类的设计中,首先要调用awt包中的事件类和事件的监视类;
package GS;
import java.awt.event.KeyEvent; //按键事件对象来从键盘上获取输入的信息;
import java.awt.event.KeyListener; //java提供KeyListener接口处理按键事件
;
通过查询按键对应的关键标识符使用switch case 语句对按键进行操作和按键反馈进行绑定。
if (model.running) {
switch (key) {
case KeyEvent.VK_UP: //此处要查询按键对应的字符表示;
model.changeDirection(Model.up); //将按键获取的对应方向作为参数代入changeDirection进行蛇的头部更迭;
break;
public static final int canvasWidth=800,canvasHeight=500;
public static final int nodeWidth=10,nodeHeight=10;
public View(Model model,Control control){
super(" CZF 的 贪吃蛇小游戏 ");
this.control=control;
this.model=model;
this.setLocation(400, 300); //设置界面大小;
this.setResizable(false); //设置此窗体不可由用户调整大小;
this.setDefaultCloseOperation(EXIT_ON_CLOSE); //设置点×就可关闭窗口;
;
但是好像运行后直接进入游戏了,并不能有一个enter键入的过程;不太清楚是这么一回事。
canvas=new Canvas();
canvas.setSize(canvasWidth+1, canvasHeight+1); //设置控件大小
canvas.addKeyListener(control); //监听按键;
this.add(canvas,BorderLayout.NORTH);
JPanel panel=new JPanel();
this.add(panel,BorderLayout.SOUTH);
JLabel label=new JLabel("Enter for restart");
panel.add(label);
this.pack();
this.addKeyListener(control);
this.setVisible(true);
}
;
背景:白色不太容易看见边界,但也可以自己设置一个有颜色的边界框;
蛇和食物的颜色随便就好了。
public void repaint() {
Graphics g=canvas.getGraphics();
// 背景设计
g.setColor(Color.black); // 设置颜色
g.fillRect(0, 0, canvasWidth, canvasHeight);
// 蛇的设计
g.setColor(Color.red);
LinkedList node=model.node;
Iterator it=node.iterator();
while(it.hasNext()){
Node n=(Node)it.next();
drawNode(g,n);
}
// 食物设计
g.setColor(Color.blue);
Node n=model.food;
drawNode(g,n);
}
;
private void drawNode(Graphics g, Node n) {
g.fillOval(n.x*nodeWidth, n.y*nodeHeight, nodeWidth, nodeHeight);
}
public void update(Observable o, Object arg) {
repaint();
}
这就是对上面写好的三个类的调用了;
关键在于一个启动线程的问题上,具体的解释在实验准备部分也有对Thread()的解释,我就不在此赘述了。
package GS;
public class GreedySnake {
public static void main(String[] args) {
Model model=new Model(80, 50); //创建一个80*50像素作为擦书的对象model;创建线程;
Control control=new Control(model);
View view=new View(model,control);
model.addObserver(view); //展示界面
(new Thread(model)).start(); //启动线程
}
}
贪吃蛇小游戏对于初学者来说还是比较不友好的,难点如下:
1、主函数类部分:
package GS;
public class GreedySnake {
public static void main(String[] args) {
Model model=new Model(80, 50); //创建一个80*50像素作为擦书的对象model;创建线程;
Control control=new Control(model);
View view=new View(model,control);
model.addObserver(view); //展示界面
(new Thread(model)).start(); //启动线程
}
}
2、模型类部分代码;
package GS;
import java.util.Arrays; //提供数组的相关操作;
import java.util.LinkedList; //提供堆栈、队列等功能的的list;
import java.util.Observable; //用以创建观察者对象;
import java.util.Random; //提供随机数;
import javax.swing.JOptionPane; //向用户界面弹出要求弹框;
public class Model extends Observable implements Runnable{
//创建观察者对象model,装配runnable接口;
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 int sum = 0; //用以计算分数
public boolean coordinate[][]; //用这个来当做界面的坐标;
public LinkedList node = new LinkedList(); //创建线性表的对象
public int direction = 2; //设置头部方向direction的初始默认值为向上;
boolean running = false;
public int maxX,maxY; //界面大小设置;
Node food; //线性表单元food
public int sleeptime=200; //设置移动的间隔时间(刷新时间);
public Model(int maxX,int maxY){
//构造函数;
this.maxX=maxX;
this.maxY=maxY;
reset();
}
//当获取一次按键操作后,执行一次响应操作
public void reset() {
direction = this.up; //设置默认的头部移动方向;
sleeptime = 200;
coordinate = new boolean[maxX][];
for (int i = 0; i < maxX; i++) {
coordinate[i] = new boolean[maxY];
Arrays.fill(coordinate[i], false); //设置边界坐标标志为0,不可达;
}
//构造蛇身初始状态;
int initlenght=10; //设置蛇的初始长度为10
node.clear();
for (int j = 0; j < initlenght; j++) {
int x = maxX/2+j;
int y = maxY/2; //安排初始情况下蛇的每一个节点的坐标位置
node.addLast(new Node(x,y)); //将蛇的身体坐标存入线性链表
coordinate[x][y] = true; //对于蛇身位置坐标标记为1,不可走;
}
food = createFood(); //初始化时随机生成位置坐标
coordinate[food.x][food.y] = true; //有食物的坐标位置标记为1;
}
//蛇的核心移动部分;
public boolean move(){
Node n = (Node)node.getFirst(); //取出代表蛇身链表首部的位置坐标;
int x = n.x;
int y = n.y;
switch(direction){
//对于键盘获取的方向的不同对蛇的首部坐标进行修改;
case up:
y--;
break;
case down:
y++;
break;
case left:
x--;
break;
case right:
x++;
break;
default:
break;
}
if ((x >= 0&&x<maxX)&&(y >= 0&&y<maxY)) {
//判断该位置是否有食物;
if (coordinate[x][y]) {
if (x == food.x && y == food.y) {
//判断如果蛇头坐标等于食物坐标;
node.addFirst(food); //把食物坐标增添到蛇身链表的首部;
if (sleeptime > 40) {
//在时间间隔最小限度内不断缩短时间间隔(就是随着吃到食物不断增大难度);
sleeptime -= 20;
sum += 10;
}
food = createFood(); //迟到食物后,再随机生成一个食物;
coordinate[food.x][food.y] = true; //标记食物的坐标标识符为1;
return true; //结束该次移动;
} else {
return false; //如果下一步的坐标标识符为1,但是又不是食物则时吃到自己了,结束;
}
}
else {
//如果下一个位置坐标标识符号不是1;
node.addFirst(new Node(x,y)); //将下一个坐标添加为新的蛇头首部;
coordinate[x][y] = true; //由于该点被纳入蛇身,则将该点的标识符置1;
n = (Node)node.getLast(); //获取当前蛇的尾部坐标,注意不要和下一句颠倒位置;
node.removeLast(); //移除蛇这个链表的尾部的坐标;
coordinate[n.x][n.y] = false; //将之前的蛇的尾部坐标标识符置0;
return true; //返回移动正确;
}
}
return false; //如果撞到边界返回false;
}
//新的头部更迭;
public void changeDirection(int newdir){
//从键盘获取新的头部;
if (direction != newdir) {
direction = newdir;
}
}
public Node createFood() {
//食物的创建函数;
int x = 0,y = 0;
do {
Random r = new Random(); //随机对象r;
x = r.nextInt(maxX); //生成【0,maxX)的随机数;
y = r.nextInt(maxY);
} while (coordinate[x][y]); //当生成的随机坐标已经有标识符为1了就再次执行循环生成一次;
return new Node(x, y); //把生成正确的坐标存入新的链表单元中;
}
//
public void run() {
running = true; //
while(running){
try {
Thread.sleep(sleeptime); //try catch 对改语句保护;
} catch (Exception e) {
//会出现的错误;
break;
}
if(move()) {
setChanged(); //标志当前对象已经更改;
notifyObservers(); //通知观察者当前对象move已经更改状态;
}
else {
JOptionPane.showMessageDialog(null, "游戏结束,你获得的分数为:"+sum+"分。"); //JOptionPane(只有一个确认按钮的消息提示框)
break;
}
}
}
}
class Node{
//创建类Node用于存储食物作为链表单元;
public int x,y;
public Node(int x,int y){
this.x = x;
this.y = y;
}
}
3、控制类部分:
package GS;
import java.awt.event.KeyEvent; //按键事件对象来从键盘上获取输入的信息;
import java.awt.event.KeyListener; //java提供KeyListener接口处理按键事件
public class Control implements KeyListener{
Model model; //创建Model类的对象model
public Control(Model model){
this.model=model; //his就会指向对象的地址
}
//按下一个键得到的反馈情况以及需要获取的参数以及对应操作;
public void keyPressed(KeyEvent e) {
//创建事件对象,监视按键情况;
int key=e.getKeyCode();
if (model.running) {
switch (key) {
case KeyEvent.VK_UP: //此处要查询按键对应的字符表示;
model.changeDirection(Model.up); //将按键获取的对应方向作为参数代入changeDirection进行蛇的头部更迭;
break;
case KeyEvent.VK_DOWN:
model.changeDirection(Model.down);
break;
case KeyEvent.VK_LEFT:
model.changeDirection(Model.left);
break;
case KeyEvent.VK_RIGHT:
model.changeDirection(Model.right);
break;
default: `在这里插入代码片` //输入其他内容,不识别
break;
}
}
if (key==KeyEvent.VK_ENTER) {
//按下回车键;
model.reset();
}
}
public void keyReleased(KeyEvent e) {
//在源组件上释放一个键后被调用
}
public void keyTyped(KeyEvent e) {
//在源组件上按下一个键然后释放该键后被调用
}
}
4、视图展示类部分:
package GS;
import java.awt.BorderLayout; //默认布局管理器
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Observable;
import java.util.Observer;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class View extends JFrame implements Observer{
Control control;
Model model;
Canvas canvas;
public static final int canvasWidth=800,canvasHeight=500;
public static final int nodeWidth=10,nodeHeight=10;
public View(Model model,Control control){
super(" CZF 的 贪吃蛇小游戏 ");
this.control=control;
this.model=model;
this.setLocation(400, 300); //设置界面大小;
this.setResizable(false); //设置此窗体不可由用户调整大小;
this.setDefaultCloseOperation(EXIT_ON_CLOSE); //设置点×就可关闭窗口;
canvas=new Canvas();
canvas.setSize(canvasWidth+1, canvasHeight+1); //设置控件大小
canvas.addKeyListener(control); //监听按键;
this.add(canvas,BorderLayout.NORTH);
JPanel panel=new JPanel();
this.add(panel,BorderLayout.SOUTH);
JLabel label=new JLabel("Enter for restart");
panel.add(label);
this.pack();
this.addKeyListener(control);
this.setVisible(true);
}
public void repaint() {
Graphics g=canvas.getGraphics();
// 背景设计
g.setColor(Color.black); // 设置颜色
g.fillRect(0, 0, canvasWidth, canvasHeight);
// 蛇的设计
g.setColor(Color.red);
LinkedList node=model.node;
Iterator it=node.iterator();
while(it.hasNext()){
Node n=(Node)it.next();
drawNode(g,n);
}
// 食物设计
g.setColor(Color.blue);
Node n=model.food;
drawNode(g,n);
}
private void drawNode(Graphics g, Node n) {
g.fillOval(n.x*nodeWidth, n.y*nodeHeight, nodeWidth, nodeHeight);
}
public void update(Observable o, Object arg) {
repaint();
}
}