实验内容
1. 实现贪吃蛇游戏基本功能,屏幕上随机出现一个“食物”,称为豆子,上下左右控制蛇的移动,吃到豆子以后“蛇”的身体加长一点,得分增加,
蛇碰到边界或蛇头与蛇身相撞,蛇死亡,游戏结束。为游戏设计初始欢迎界面,游戏界面,游戏结束界面。
2. 进行交互界面的设计,要有开始键、暂停键和停止退出的选项。对蛇吃到豆子进行分值计算,可以设置游戏速度等拓展元素。
实验思路
1. 有了上一次《简单计算器的实现》的实验基础,此次实验就方便了许多。
鄙人打算继续使用Java来实现程序描述,使用 Eclipse 开发环境编译调试,还是用 Java swing 框架来实现GUI。
2. 贪吃蛇游戏主要涉及线性表(List)的构造与使用。蛇身体的每一节都是一个节点(Node),可以用动态数组 ArrayList 存储坐标来实现。
本次实验我使用的是 Java 语言,JDK 中已有现成的 List 包,所以只需要在开头处引入,并在具体方法中调用即可。
涉及知识
数据结构(List, Node等),ArrayList 的调用与动态存储;
Java继承类与接口技术,Java swing 框架,Java swing 图形界面布局;
Java事件执行按钮,Java 虚拟键码 KeyEvent 等。
具体步骤
1. 学习了解程序需要调用的包:javax.swing,java.awt,java.util.list 等。
import javax.swing.*; // Java图形界面设计工具包
import java.awt.*; // GUI设计工具软件包
import java.awt.event.*; // 提供处理由 awt 组件所激发的各类事件的接口和类
import java.util.*; // Java集合框架以及各种实用工具类
import java.util.List; // 有序集合包接口
2. get() 和 set() 方法。
class SnakeAct {
private int x;
private int y;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
3.这个游戏我打算添加的拓展元素是速度,利用线程休眠时间控制蛇的移动速度。可以设置为吃完一个豆子蛇体移动速度加1。
int speed = 0,eat = 0;
eat++;
if(eat>=1){
Speed++;
} //可以将这段程序加到蛇体移动的方法的循环体里面
4.使用 Thread 类,一是将任务分成多个子任务并行计算,因为这样可以避免多缓冲的使用,减少内存消耗,运行速度更快;二是可以很方便地控制速度。
Thread nthread;
nthread = new Thread(this); //this访问Thread类
nthread.start(); //开始
/*......*/
nthread = null; //异常终止
Thread.sleep(300-30*Speed); //利用线程休眠时间控制蛇的移动速度
5.最关键的算法:当蛇吃到豆子时,让豆子坐标获取list最后一个元素的坐标,即蛇头坐标,每个元素使用前一个元素的坐标(相当于依次后移)。
SnakeAct tempAct = new SnakeAct(); //建立tempAct对象
for (int i = 0; i < list.size(); i++) {
if (i==1) {
list.get(i).setX(list.get(0).getX());
list.get(i).setY(list.get(0).getY()); //刚开始迭代不了,单独处理
}
else if(i>1){
tempAct=list.get(i-1);
list.set(i-1, list.get(i));
list.set(i, tempAct); //迭代法完成list的存储更新
}
}
6.keyEvent:键值与所调用方法的参数对应。
值得注意的是:布局时宽和高是以左上角为 “坐标原点” 画出来的,所以上下对应的键值得换一下。
public void keyPressed(KeyEvent e) {
if(start){
switch (e.getKeyCode()) { //获取键值
case KeyEvent.VK_UP:
move(0, -1); //up和down相反是因为坐标原点在左上角,横坐标向右,纵坐标向下
temp=1;
break;
case KeyEvent.VK_DOWN:
move(0, 1);
temp=2;
break;
case KeyEvent.VK_LEFT:
move(-1, 0);
temp=3;
break;
case KeyEvent.VK_RIGHT:
move(1, 0);
temp=4;
break;
default:
break;
}
}
}
7.关于图形界面的设计还在学习当中,整体布局如下图(虽然简陋但基本功能都是有的)。将在后面验收的代码中体现,实验准备里就不再赘述了。
8.最后分享一下作为一个丢三落四者的经历。
写迷糊了,蛇头坐标和身体其他节点坐标重合游戏结束,就这么个方法写了半天。
①刚开始忘了在move()方法里调用,我说为啥吃到自己咋没反应;
②调用完还是没用。一看发现条件语句我居然写出了 if(list.get(0).equals(get(i))) 这么个玩意。get()个锤子呢,,x和y两个变量怎么访问。
③改成getX()和getY()后还是不行。因为最后if(Dead)没看清,返回值搞反了,一开始游戏就结束。吐了呀,,差点就真的 Dead() 了。
每次测试做的挺麻烦的,主要是担心线程睡眠对位置蛇头和身体的重合的判断有影响,所以每次都吃到一定长度纵向去吃。长了速度变快还是很考验手速的。
***********************************
下附Dead()方法和调用部分。
public boolean Dead() {
for(int i = 1; i < list.size(); i++) {
if (list.get(0).getX() == list.get(i).getX()
&& list.get(0).getY() == list.get(i).getY()) {
return true;
}
}
return false;
}
public void move(int x,int y){
if(Dead()) {
nThread = null;
label.setText("您输了!您的分数是"+fenShu);
dialog.setVisible(true);
}
............................
}
9.完整源码。
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
class Node { //构造方法声明
private int x , y;
public Node() {}
public Node(int a , int b){
x = a;
y = b;
}
public Node(Node tmp) {
x = tmp.getX();
y = tmp.getY();
}
int getX() {
return x;
}
int getY() {
return y;
}
void setX(int a) {
x = a;
}
void setY(int b) {
y = b;
}
}
class SnakeOperator extends JPanel implements ActionListener,KeyListener,Runnable{
/**
* 功能函数实现游戏的操作:继承 JPanel 类,装配 ActionListener,KeyListener,Runnable 接口
*/
private static final long serialVersionUID = 1L; //定义程序序列化ID,将对象转换为字节
//定义变量
int fenShu=0,Speed=0;
boolean start=false;
int rx=0,ry=0;
int eat=0;
//添加按钮
JDialog dialog = new JDialog();
JLabel label = new JLabel("您输了!你的分数是"+fenShu);
JButton ok = new JButton("确认");
Random r = new Random();
JButton newGame,stopGame;
//定义动态数组list,引用snakeAct类中的构造方法
private List<Node> list = new ArrayList<Node>();
int temp=0;
Thread nThread; //定义线程nThread
//初步定义界面布局
public SnakeOperator() {
newGame = new JButton("开始");
stopGame = new JButton("结束");
//通过this实现接口,调用父类方法
this.addKeyListener(this); //添加键盘事件keyListener
this.setLayout(new FlowLayout(FlowLayout.LEFT)); //组件左对齐
this.add(newGame);
this.add(stopGame);
newGame.addActionListener(this); //为newGame添加事件接口
stopGame.addActionListener(this); //为stopGame添加事件接口
dialog.setLayout(new GridLayout(2, 1)); //定义网状布局
dialog.add(label);
dialog.add(ok);
dialog.setSize(200, 200); //大小
dialog.setLocation(200, 200); //位置
dialog.setVisible(false); //设置组件的可见性
ok.addActionListener(this);
}
//布局完善及动态实现
public void paintComponent(Graphics g){
super.paintComponent(g);
g.drawRect(10, 40, 400, 300); //画出矩形范围,原点在左上角
g.drawString("分数:"+fenShu, 150, 15);
g.drawString("速度:"+Speed, 150, 35);
g.setColor(new Color(255, 0, 0)); //设置豆子的颜色为红色
if(start){
g.fillRect(10+rx*10, 40+ry*10, 10, 10); //红色填充格子
for (int i = 0; i < list.size(); i++) {
//游戏开始后,蛇吃下豆子,豆子变为绿色添加到蛇身上
g.setColor(new Color(0, 255, 0));
g.fillRect(10+list.get(i).getX()*10, 40+list.get(i).getY()*10, 10, 10);
}
}
}
//事件描述及处理
public void actionPerformed(ActionEvent e) {
//重新开始游戏
if(e.getSource()==newGame){
newGame.setEnabled(false);
start = true;
rx=r.nextInt(40);
ry=r.nextInt(30);
Node tempAct = new Node();
tempAct.setX(20);
tempAct.setY(15);
list.add(tempAct);
this.requestFocus();
nThread = new Thread(this); //初始化线程
nThread.start();
repaint(); //调用paint(),刷新界面(重绘构件)
}
if(e.getSource()==stopGame){ //结束事件按钮停止游戏
System.exit(0);
}
//点击确认返回初始界面
if(e.getSource()==ok){
list.clear();
start=false;
newGame.setEnabled(true);
dialog.setVisible(false);
fenShu=0;
Speed=0;
repaint();
}
}
private void eat() {
//游戏开始在界面内生成随机位置
if (rx==list.get(0).getX()&&ry==list.get(0).getY()) {
rx = r.nextInt(40);
ry = r.nextInt(30);
Node tempAct = new Node();
//this指针指向list的尾元素的坐标
tempAct.setX(list.get(list.size()-1).getX());
tempAct.setY(list.get(list.size()-1).getY());
list.add(tempAct);
fenShu = 10*Speed+10; //加分数
eat++;
if(eat>=1){
Speed++;
}
}
}
public void otherMove(){
Node tempAct = new Node(); //为对象重新分配内存
/*关键算法,迭代法,tempAct获取list最后一个元素的坐标,也就是蛇头坐标,
* 每个元素使用前一个元素的坐标,第一个使用表头坐标
*/
for (int i = 0; i < list.size(); i++) {
if (i==1) {
list.get(i).setX(list.get(0).getX());
list.get(i).setY(list.get(0).getY());
}else if(i>1){
tempAct=list.get(i-1);
list.set(i-1, list.get(i));
list.set(i, tempAct);
}
}
}
public void move(int x,int y){
if (minYes(x, y)) {
otherMove(); //调用otherMove()方法
list.get(0).setX(list.get(0).getX()+x);
list.get(0).setY(list.get(0).getY()+y);
eat(); //调用eat()方法
repaint(); //实现list的更新
}else {
nThread = null;
label.setText("您输了!您的分数是"+fenShu);
dialog.setVisible(true); //显示窗口
}
}
public boolean minYes(int x,int y){
if (!maxYes(list.get(0).getX()+x,list.get(0).getY()+ y)) {
return false;
}
return true;
}
public boolean maxYes(int x,int y){
if (x<0||x>=40||y<0||y>=30) {
return false;
}
/*蛇头坐标和身体其他节点坐标重合游戏结束。
* 这地方真的是改死我了,专治丢三落四系列。
* 一开始忘了在move()方法里调用,我说为啥吃到自己咋没反应呢;
*调用完还是没用。一看发现条件语句if(list.get(0).equals(get(i))),get()个锤子呢,,x和y两个变量怎么访问;
* 改成getX()和getY()后还是不行,if(Dead)没看清,返回值搞反了,一开始游戏就结束。真是吐了呀,,差点猝!!!
*/
for (int i = 0; i < list.size(); i++) {
if (i>1&&list.get(0).getX()==list.get(i).getX()
&&list.get(0).getY()==list.get(i).getY()) {
return false;
}
}
return true;
}
//键值与方法的参数对应
public void keyPressed(KeyEvent e) {
if(start){
switch (e.getKeyCode()) { //获取键值
case KeyEvent.VK_UP:
move(0, -1); //up和down相反是因为坐标原点在左上角,横坐标向右,纵坐标向下
temp=1;
break;
case KeyEvent.VK_DOWN:
move(0, 1);
temp=2;
break;
case KeyEvent.VK_LEFT:
move(-1, 0);
temp=3;
break;
case KeyEvent.VK_RIGHT:
move(1, 0);
temp=4;
break;
default:
break;
}
}
}
public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {}
public void run() {
//move()方法与参数值对应
while (start) {
switch (temp) {
case 1:
move(0, -1);
break;
case 2:
move(0, 1);
break;
case 3:
move(-1, 0);
break;
case 4:
move(1, 0);
break;
default:
break;
}
repaint(); //调用paint(),刷新界面(重绘构件)
try {
//通过线程休眠时间控制蛇的移动速度
Thread.sleep(300-15*Speed);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class SnakeGame extends JFrame {
/**
* 继承JFrame类
*/
private static final long serialVersionUID = 1L; //定义程序序列化ID,将对象转换为字节
public SnakeGame() {
SnakeOperator win = new SnakeOperator(); //创建SnakeOperator的对象win
add(win); //将win添加到SnakeGame类中
setTitle("̰贪吃蛇");
setSize(435,390);
setLocation(200, 200);
setVisible(true); //显示布局
}
public static void main(String[] args) {
new SnakeGame(); //调用类运行程序
}
}