基于Java的超级玛丽游戏的设计与实现【源码+文档+部署讲解】

news2025/1/7 13:24:47

目    录

1、绪论

1.1背景以及现状

1.2 Java语言的特点 

1.3  系统运行环境及开发软件: 

1.4   可行性的分析 

1.4.1 技术可行性

1.4.2  经济可行性 

1.4.3  操作可行性 

2、 需求分析

2.1 用户需求分析

2.2功能需求分析

2.3界面设计需求分析

3、 系统概要设计

3.1系统模块设计

3.1.1窗体类

3.1.2初始化类

3.1.3背景类

3.1.4马里奥类

3.1.5障碍物类

3.1.6敌人类

3.2系统流程设计

4、 系统详细设计

4.1 设计目标

4.2 系统模块设计

4.2.1窗体类

4.2.2初始化类

4.2.3背景类

4.2.4马里奥类

4.2.5障碍物类

4.2.6敌人类

5、系统的实现

5.1游戏开发所需要的图片

5.1.1马里奥的所有图片

5.1.2游戏中障碍物的图片

5.1.3游戏中怪物的图片

5.1.4游戏中的背景图片

5.1.5游戏开始时的图片

5.2游戏设计的界面

5.2.1 游戏逻辑展示

5.2.1 游戏逻辑展示

6、系统测试

6.1 测试的意义

6.2 测试过程

6.3 测试结果

7、总结与展望

7.1  总结 

7.2  设计中的不足之处 

7.3  展望 

致 谢

外文原文

外文翻译

  1. 系统概要设计

3.1系统模块设计

首先在对于系统的需求进行了分析,因为设计者的最初是要做一款游戏,所以窗体类必不可少。接下来继续分析,游戏中还需要背景类、障碍物类、敌人类、马里奥类这及格类。其次为了游戏的流畅以及游戏中图片调用的方便,专门为此再设计一个初始化类。

3.1.1窗体类

该类主要用于存放游戏的场景以及其他各类,并且实现KeyListener接口,用于从键盘的按键中读取信息。该类中的一些属性主要包括了用于存放所有场景的list集合 allBG,马里奥类 mario,当前的场景 nowBG以及其他一些游戏中需要的标记等。而且在该类中,运用双缓存的技术使得游戏的流畅度更高,解决了游戏中出现的闪屏问题。

Myframe

- allBG:List

- mario:Mario

- nowBG:BackGround

......

+ main():void

+ paint():void

+ keyPressed():void

+ kerReleased():void

......

3.1.2初始化类

用于存放游戏所需要的所有静态文件,在游戏开始的时候将所有文件导入,提高游戏的运行速度。并且在该类中将所有需要用到的图片进行分类,分为障碍物类,马里奥类,敌人类以及背景图片。当游戏运行时可以直接调用这些集合中的图片进行遍历,在调用的时候更加方便,而且可以使马里奥或者敌人在移动的时候产生动态效果。

StaticValue

+ allMarioImage:List

+ startImage:BufferedImage

......

+ init():void

......

3.1.3背景类

该类表示马里奥及障碍物和敌人所处的场景,并且将障碍物和敌人绘制到场景中。在该类中包括用于存放敌人和障碍物的list集合,以及当敌人或者障碍物被消灭后用于存放已经消失的敌人和障碍物的集合,这样做是为了在马里奥死亡时重置场景所用的。其次在该类中还使用了控制敌人移动的方法,是为了在程序之初控制敌人静止,然后在玩家点击空格以后在使得敌人开始移动。

BackGround

- bgImage:BufferedImage

- isOver:boolean

- isDown:boolean

- allEnemy:List

- removeEnemy:List

......

+ enemyStartMove():void

+ reset():void

......

3.1.4马里奥类

用来控制马里奥的行动,并且在该类中加入碰撞检测,判断马里奥是否与障碍物或者敌人发生碰撞。该类中的属性主要定义了马里奥所在的场景,马里奥的移动和跳跃的速度,以及马里奥在移动过程中需要显示的图片。另外该类中还定义了玩家的生命值和所获得的分数。并且在run()方法中还定义了当马里奥到达最后一关的旗子时,玩家将失去对马里奥的控制,剩下的由程序控制走到城堡,完整全部游戏。

Mario

- x:int

- y:int

- xmove:int

- ymove;int

- life:int

- isDead:boolean

......

+ leftMove():void

+ leftStop():void

+ jump():void

+ down():void

+ dead():void

......

3.1.5障碍物类

绘制场景中所需要的障碍物,例如地面、砖块、水管等等。该类中的属性包括了障碍物的坐标,障碍物所需要显示的图片等。并且在该类中也定义了障碍物类的重置方法,当马里奥死亡时,场景类会调用该方法。

Obstruction

- x:int

- y:int

- type:int

- starttype:int

- showImage:BufferedImage

......

+ reset():void

+ setImage():void

......

3.1.6敌人类

该类中主要设置了两种敌人,一种是蘑菇怪,可以被马里奥踩死,另一种是食人花,不能被踩死。该类中的属性包括了敌人的坐标,敌人的初始坐标,需要显示的图片,以及敌人的移动方向和移动范围等。敌人的初始坐标主要是为了当敌人执行重置方法后将敌人的位置还原。

Enemy

- x:int

- y:int

- startx:int

- starty:int

- showImage:BufferedImage

- upMax:int

- downMax:int

......

+ reset():void

+ dead():void

......

3.2系统流程设计

                                  

系统详细设计

4.1 设计目标

本软件是针对超级玛丽小游戏的JAVA程序,进入游戏后首先按空格键开始,利用方向键来控制的马里奥的移动,同时检测马里奥与场景中的障碍物和敌人的碰撞,并判断马里奥的可移动性和马里奥的生命值。当马里奥通过最后一个场景后游戏结束。

4.2 系统模块设计

本系统共包括6各类:

4.2.1窗体类

     该类主要用于存放游戏的场景以及其他各类,并且实现KeyListener接口,用于从键盘的按键中读取信息。该类中的一些属性主要包括了用于存放所有场景的list集合 allBG,马里奥类 mario,当前的场景 nowBG以及其他一些游戏中需要的标记等。而且在该类中,运用双缓存的技术使得游戏的流畅度更高,解决了游戏中出现的闪屏问题。

     将该类的名字定义为MyFrame,并且要在该类中实现KeyListener接口和Runnable接口。然后首先要在该类中定义一个List集合,集合的泛型为背景类BackGround,集合的名字定义为allBG,用于存放所有的背景。接着定义一个Mario类属性,名字为mario,这个就是游戏运行时候的所需要的mario。接下来还要在类中定义一个BackGround属性,nowBG,默认值应当为空,会在构造方法中赋予该属性初值,这个属性主要是用来存放当前游戏运行时马里奥所处的游戏场景。另外该类中还应该有一个Thread类属性t,这个属性主要是为了在游戏运行的时候控制游戏的线程。然后就可以在类中定义main()方法,将该类实现就可以了。值得一提的是该类的构造方法相对来说是比较复杂的。

    在该类的构造方法中,应当首先绘制窗体类的标题,以及窗体类的大小,并且要对窗体类在初始化的时候的位置,也就是在屏幕中显示的位置,最好是显示的时候居中,这样的话在游戏运行时会比较美观一些。其次还要对窗体的一个是否可拉升属性进行一下设置,这个设置的主要目的是因为游戏的界面都是开发者经过深思熟虑考虑出来的比较美观的界面,玩家随意改变游戏的窗口大小可能会对游戏的体验造成影响,所以在这里应该设置游戏的窗体默认不可以被拉伸。

public MyFrame(){

this.setTitle("玛丽奥");

this.setSize(900, 600);

           //这里是为了获得电脑屏幕的整体大小,以便于下面确定窗体的位置

int width = Toolkit.getDefaultToolkit().getScreenSize().width;

int height = Toolkit.getDefaultToolkit().getScreenSize().height;

this.setLocation((width-900)/2, (height-600)/2);

       //设置窗体默认不可以被拉伸

this.setResizable(false);

//初始化图片

StaticValue.init();

    当这些都设置好以后,接下来就应当在构造方法中绘制了,当然最先应当将游戏的场景绘制到窗体类中,然后在窗体类中还应当绘制马里奥类,这是游戏中必不可少的。当然在绘制场景类的时候因为不知一个场景,所以可以使用循环,将所有的场景全部绘制。然后在将所需要的所有监视设置好以后就可以开启该类的线程了。

//使用循环创建全部场景

for(int i=1;i<=7;i++){

this.allBG.add(new BackGround(i, i==7?true:false));

}

//将第一个场景设置为当前场景

this.nowBG = this.allBG.get(0);

//初始化玛丽奥

this.mario = new Mario(0, 480);

//将玛丽奥放入场景中

this.mario.setBg(nowBG);

this.repaint();

this.addKeyListener(this);

this.t = new Thread(this);

t.start();

      //使窗口在关闭的时候,程序也同时停止。

this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

this.setVisible(true);

}

    在这些最基本的东西设置完以后,还需要一个方法来解决游戏中经常会出现的闪屏问题。这个方法就是双缓存方法,现在类中定义一个BufferedImage的图片,然后从该图片中获取到图片的Graphics  g2,然后利用画笔g2将所要绘制的东西绘制到这个空的图片中,然后在利用窗体类中的paint方法中的画笔g将这个已经绘制好的图片绘制到窗体类中,这样利用空白图片作为程序运行中的中转,就可以很好的解决游戏运行过程中出现的闪屏问题。

public void paint(Graphics g) {

//先定义一个图片,然后利用双缓存解决闪屏问题

BufferedImage image = new BufferedImage(900, 600, BufferedImage.TYPE_3BYTE_BGR);

Graphics g2 = image.getGraphics();

      //利用上面图片中得到的画笔g2,将所需绘制到图片中

if(this.isStart){

//绘制背景

g2.drawImage(this.nowBG.getBgImage(), 0, 0, this);

//绘制生命

g2.drawString("生命:    "+this.mario.getLife(), 800, 50);

//绘制怪物敌人

Iterator<Enemy> iterEnemy = this.nowBG.getAllEnemy().iterator();

while(iterEnemy.hasNext()){

Enemy e = iterEnemy.next();

g2.drawImage(e.getShowImage(), e.getX(), e.getY(), this);

}

//把缓存图片绘制进去

g.drawImage(image, 0, 0, this);

}

    当然游戏的宗旨是让玩家和电脑之间的互动,那么就又涉及到一个问题,就是玩家对游戏中的马里奥的控制。我们前面已经说过了该类中必须要实现KeyListener接口,这个接口的作用就是使该类中实现一些方法,以便于达到玩家在游戏进行时可以对游戏中的马里奥进行控制。我们这里拟定对于马里奥的控制可以使用我们常见的四个方向键,即我们说的上下左右。并且通过控制台打印,可以知道上对应的是38,右对应的是39,左对应的是37。并且游戏的设定是开始后游戏不会直接运行,而是要使用空格键以后游戏才会真正开始,所以还要加入当按空格键的时候游戏正式开始,空格键对应的是32。

public void keyPressed(KeyEvent e) {

if(this.isStart){

//玛丽奥的移动控制

if(e.getKeyCode()==39){

this.mario.rightMove();

}

if(e.getKeyCode()==37){

this.mario.leftMove();

}

//跳跃控制

if(e.getKeyCode()==38){

this.mario.jump();

}

}else if(e.getKeyCode()==32){

this.isStart = true;

}

}

    对于按键,那么相对应的就是当抬起建的时候。因为你向右移动的时候,如果这时候突然停止,那么很可能马里奥会保持一个运动的状态停下来,那么就必须在玛丽奥停止的时候给他一个指令,让他的移动图片变为静止。相对于运动的时候是类似的,这里不做累述。

public void keyReleased(KeyEvent e) {

if(this.isStart){

//控制玛丽奥的停止

if(e.getKeyCode()==39){

this.mario.rightStop();;

}

if(e.getKeyCode()==37){

this.mario.leftStop();;

}

}

    当这一切都做好以后,那么最后就应该在类中重写一下run方法了,在这个方法中应当提一下游戏的通关和死亡后的状态。即游戏通关,或者马里奥死亡时应当弹出一个窗口,说明游戏通关或者马里奥死亡,并且点击了这个窗口以后,游戏应当结束,而且整个游戏也应当关闭。

if(this.mario.isDead()){

JOptionPane.showMessageDialog(this, "游戏结束");

System.exit(0);

}

if(this.mario.isClear()){

JOptionPane.showMessageDialog(this, "恭喜游戏通关!");

System.exit(0);

}

4.2.2初始化类

用于存放游戏所需要的所有静态文件,在游戏开始的时候将所有文件导入,提高游戏的运行速度。并且在该类中将所有需要用到的图片进行分类,分为障碍物类,马里奥类,敌人类以及背景图片。当游戏运行时可以直接调用这些集合中的图片进行遍历,在调用的时候更加方便,而且可以使马里奥或者敌人在移动的时候产生动态效果。

首先在类中应当定义一个静态的List,泛型为BufferedImage,属性名字为allMarioImage,这个属性的作用在于存放所有的马里奥图片,里面包括了马里奥的移动图片,站立图片以及马里奥跳跃的图片。这样在程序运行的时候就可以从该类中的这个属性里面将所需要的马里奥图片直接调用出来,并且还可以在马里奥移动时不断遍历里面的图片,这样就可以使马里奥产生移动的动态效果。接下来要在该类中定义开始图片,结束图片以及背景图片,默认的初始值都为null。注意这些所有的属性都是静态的,包括下面要提到的所有的属性,这样做的目的是为了在程序运行时先加载这些图片。然后应当定义存放食人花的List集合allFlowerImage,这个集合将食人花的不同形态,张嘴、闭嘴图片存放进去,这样在运行的时候进行遍历就可以打到动态效果。同理存放蘑菇怪的集合allTrangleImage,以及存放所有障碍物的集合allObstructionImage。

public class StaticValue {

public static List<BufferedImage> allMarioImage = new ArrayList<BufferedImage>();

public static BufferedImage startImage = null;

public static BufferedImage endImage = null;

public static BufferedImage bgImage = null;

public static List<BufferedImage> allFlowerImage = new ArrayList<BufferedImage>();

public static List<BufferedImage> allTriangleImage = new ArrayList<BufferedImage>();

public static List<BufferedImage> allObstructionImage = new ArrayList<BufferedImage>();

定义完这些属性之后,剩下的就是初始化了,在该类中定义一个init()方法,这个方法在执行的时候会将所需的所有图片放入到之前定义好的各个集合中。因为图片存放的路径都是一样的,所以为了减少代码量会定义一个公共路径ImagePath。然后就可以利用循环,将存放的图片全部导入进去。

   //介绍代码量,定义公共路径

public static String ImagePath = System.getProperty("user.dir")+"/bin/";

//定义方法init(),将图片初始化

public static void init(){

//利用循环将玛丽奥图片初始化

for(int i=1;i<=10;i++){

try {

allMarioImage.add(ImageIO.read(new File(ImagePath+i+".png")));

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

//导入背景图片

try {

startImage = ImageIO.read(new File(ImagePath+"start.jpg"));

bgImage = ImageIO.read(new File(ImagePath+"firststage.jpg"));

endImage = ImageIO.read(new File(ImagePath+"firststageend.jpg"));

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

//导入玛丽奥死亡图片

try {

mariDeadImage = ImageIO.read(new File(ImagePath+"over.png"));

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

4.2.3背景类

    该类表示马里奥及障碍物和敌人所处的场景,并且将障碍物和敌人绘制到场景中。在该类中包括用于存放敌人和障碍物的list集合,以及当敌人或者障碍物被消灭后用于存放已经消失的敌人和障碍物的集合,这样做是为了在马里奥死亡时重置场景所用的。其次在该类中还使用了控制敌人移动的方法,是为了在程序之初控制敌人静止,然后在玩家点击空格以后在使得敌人开始移动。并且在第六个关卡处设置了一个隐形通关要点,只有当马里奥顶到这个隐形砖块时才会出现,马里奥就可以借助这个砖块通过关卡。

    首先背景类中肯定要有一个标记来表示现在是第几个场景,因为不同的背景中所绘制的场景,障碍物等也不同,所以该类中要有一个int类型的场景顺序sort。并且在游戏的设定中,如果玩家玩到最有一关的时候马里奥会失去玩家的控制,自己走向城堡。那么这里就要这几一个标记,是否为最后的场景,类型为boolean类型。如果马里奥失去所有生命值,或者游戏通关的话,那么游戏就会结束,这里还应当加一个boolean的标记isOver判断游戏是否结束。

public class BackGround {

//当前场景图片

private BufferedImage bgImage = null;

//场景顺序

private int sort;

//是否为最后的场景

private boolean flag;

//游戏结束标记

private boolean isOver = false;

    在最后一个关卡中,马里奥到达旗杆的位置后就会失去控制,同时旗子将会开始下降,只有等旗子下降完毕后,马里奥才能开始移动,所以这里还要定义一个旗子是否下降完毕的boolean类型的属性isDown,用于判断马里奥什么时候移动。

//定义降旗结束

private boolean isDown = false;

    当马里奥失去生命的时候,但是并没有失去所有的生命,那么这个时候应当重置这个场景,将所有消灭掉的障碍物和敌人全部还原。因此除了在该类中除了要定义存放敌人和障碍物的List集合以外,还应当有存放被消灭的敌人或者障碍物的List,当敌人或者障碍物被消灭的时候先放入到这个List中,这样在充值的时候就可以直接将这个集合中的数据在还原到原先的集合里面。

//用集合保存敌人

private List<Enemy> allEnemy = new ArrayList<Enemy>();

//用集合保存障碍物

private List<Obstruction> allObstruction = new ArrayList<Obstruction>();

//被消灭的敌人

private List<Enemy> removeEnemy = new ArrayList<Enemy>();

//被消灭的障碍物

private List<Obstruction> removeObstruction = new ArrayList<Obstruction>();

    在游戏的设定中,应当是游戏开始的时候,所有的敌人其实是静止的,而且玩家也不能控制马里奥,必须要等到玩家按空格键开始以后游戏才会进行,那么这里就应当在定义一个方法,即当玩家空格键的时候会调用这个方法,同时游戏中的敌人开始移动,游戏正式开始。这个方法也就是相当于控制敌人开始移动的方法,所以命名为enemyStartMove()方法。

//敌人开始移动

public void enemyStartMove(){

       //遍历当前场景中的敌人,使之开始移动

for(int i=0;i<this.allEnemy.size();i++){

this.allEnemy.get(i).startMove();

}

}

    接下来就应当定义背景类的构造方法了,通过获取场景的顺序,即场景的sort,来判断是哪一个场景,同时将场景绘制好。

//构造方法

public BackGround(int sort,boolean flag){

//第一个场景

if(sort==1){

for(int i=0;i<15;i++){

this.allObstruction.add(new Obstruction(i*60, 540, 9,this));

}

//绘制砖块和问号

this.allObstruction.add(new Obstruction(120, 360, 4,this));

this.allObstruction.add(new Obstruction(300, 360, 0,this));

......

}

    前面提到,如果马里奥死亡,但是却没有失去所有的生命值,那么游戏应当重置,当前场景中的所有敌人和障碍物,也包括马里奥都应当回到初始位置。为了达到这个效果,那么我们的场景类中就必须要定义一个reset()方法,来调用障碍物和场景还有马里奥的各自的重置方法,来使当前的场景还原。并且在这之前我们还要将消灭掉的敌人和障碍物从消灭掉的存放的List中提出来,放回到原来的List中。然后遍历障碍物和敌人的List,使用循环调用他们的重置方法。

//重置方法,重置障碍物和敌人

public void reset(){

//将移除的障碍物和敌人还原

this.allEnemy.addAll(this.removeEnemy);

this.allObstruction.addAll(this.removeObstruction);

//调用障碍物和敌人的重置方法

for(int i=0;i<this.allEnemy.size();i++){

this.allEnemy.get(i).reset();

}

for(int i=0;i<this.allObstruction.size();i++){

this.allObstruction.get(i).reset();

}

}

4.2.4马里奥类

用来控制马里奥的行动,并且在该类中加入碰撞检测,判断马里奥是否与障碍物或者敌人发生碰撞。该类中的属性主要定义了马里奥所在的场景,马里奥的移动和跳跃的速度,以及马里奥在移动过程中需要显示的图片。另外该类中还定义了玩家的生命值和所获得的分数。并且在run()方法中还定义了当马里奥到达最后一关的旗子时,玩家将失去对马里奥的控制,剩下的由程序控制走到城堡,完整全部游戏。

在游戏中,玛丽奥要在玩家的控制下完成移动、跳跃等动作,那么这些动作首先肯定要涉及到坐标,那么我们在该类中首先要定义两个属性,这两个属性即为马里奥的坐标x和y。并且该类还要实现Runnable接口,在run()方法中写马里奥的移动规则。

public class Mario implements Runnable{

//坐标

private int x;

private int y;

//定义玛丽奥所在场景

private BackGround bg;

//加入线程

private Thread t = null;

    为了玩家在游戏过程中的良好体验,那么对于马里奥的移动速度和跳跃速度就必须要定义好。所以该类里面还应当定义马里奥的移动速度和跳跃速度,其本质就是马里奥在移动过程中坐标加减的值。当然初始值为零,必须等到马里奥构造的时候,再将这些属性赋予相对应的值。在本类中还要定义游戏的分数以及马里奥的生命数,这些都是必不可少的。

//移动速度

private int xmove = 0;

//跳跃速度

private int ymove = 0;

//状态

private String status;

//显示图片

private BufferedImage showImage;

//生命和分数

private int score;

private int life;

    在马里奥这个类中,还要定义马里奥的移动和跳跃方法,以便玩家在按下方向键后调用这些方法,来达到控制马里奥的移动。下面是马里奥向左移动的方法,其他方法同理。

public void leftMove(){

//移动速度

xmove = -5;

//改变状态

//如果当前已经是跳跃,应该保持原有状态,不能再改变

if(this.status.indexOf("jumping") != -1){

this.status = "left-jumping";

}else{

this.status = "left-moving";

}

}

 ......

    在定义马里奥的跳跃方法的时候,不单单定义一个方法就行,而且还要判断马里奥的状态。如果马里奥是在地面或者是在障碍物的上方,那么马里奥可以进行跳跃,如果马里奥处于空中,那么马里奥就不可以继续跳跃。

public void jump(){

//判断马里奥是否可以进行跳跃

if(this.status.indexOf("jumping") == -1){

if(this.status.indexOf("left") != -1){

this.status = "left-jumping";

}else{

this.status = "right-jumping";

}

ymove = -5;

upTime = 36;

}

}

    接下来就要写马里奥中的run()方法了,这个方法中的内容相对来说比较麻烦,因为要在这个方法中对马里奥和障碍物或者敌人之间进行逻辑判断,即所谓的碰撞检测。首先在这个类中对马里奥是否处于最后一个场景进行判断,如果马里奥处于最后一个场景,并且坐标大于520,那么说明马里奥已经撞到的旗杆,这个时候马里奥将不会由玩家控制。并且同时调用旗子的移动方法,使旗子进行下落,当旗子下落完毕后给马里奥一个标记,马里奥开始移动到城堡。当马里奥的坐标大于780,即马里奥到达城堡的门口的时候,这个时候游戏结束。

public void run() {

while(true){

//判断是否与障碍物碰撞

//定义标记

if(this.bg.isFlag() && this.x >= 520){

this.bg.setOver(true);

if(this.bg.isDown()){

//降旗后玛丽奥开始移

this.status = "right-moving";

if(this.x < 580){

//向右

this.x += 5;

}

if(this.x >= 780){

//游戏结束

this.setClear(true);

}

    然后对当前马里奥所处的场景中的所有障碍物进行遍历,获取到所有障碍物的坐标,通过障碍物的坐标和马里奥的坐标的之间的关系的判断,来决定马里奥是否与障碍物发生了碰撞,并且通过判断的结果来对马里奥和障碍物的状态进行相应的变化。

for(int i=0;i<this.bg.getAllObstruction().size();i++){

Obstruction ob = this.bg.getAllObstruction().get(i);

//不能向右移动

if(ob.getX()==this.x+60&&(ob.getY()+50>this.y&&ob.getY()-50<this.y)){

if(ob.getType() != 3){

canRight = false;

}

}

......

    当马里奥撞到障碍物的时候,那么就要根据障碍物的类型进行接下来的判断,如果是砖块或者是问号的话,那么障碍物消失,马里奥被弹回,即马里奥的状态由上升状态变为下落状态,并且将消失掉的障碍物放入相对应的消失的List集合当中。如果障碍物的类型为其他,比如说是石头的话,那么障碍物不变,马里奥直接被弹回。

//判断玛丽奥跳跃时是否撞到障碍物

if(ob.getY()==this.y-60&&(ob.getX()+50>this.x && ob.getX()-50<this.x)){

//如果是砖块

if(ob.getType()==0){

//移除砖块

this.bg.getAllObstruction().remove(ob);

//保存到移除的障碍物中

this.bg.getRemoveObstruction().add(ob);

}

    为了游戏的可玩性,将会在游戏中加入一个隐藏的陷阱,或者是隐藏的通关点。这个隐藏的障碍物在游戏进行的时候不会显示出来,当然马里奥从他的左右两边过去的时候也不会触发这个隐藏的障碍物,必须是从下方撞到这个障碍物时才会显示出来。同时马里奥由上升状态变为下落状态。而且他和砖块障碍物相同,被顶到后会变为石头,改变类型。

//如果是问号||隐藏的砖块

if((ob.getType()==4 || ob.getType()==3) && upTime > 0){

ob.setType(2);

ob.setImage();

}

      //马里奥开始下落

upTime = 0;

在游戏中敌人大致可以分为两类。一类是蘑菇怪,这种敌人是可以被杀死的,当马里奥从蘑菇怪的正上方踩到蘑菇怪时,那么蘑菇怪就会被消灭,同时马里奥向上跳起一小段距离。而消失掉的蘑菇怪就会被放到消失掉的敌人的List集合中,等到重置的时候在调用出来。但是如果马里奥从蘑菇怪的左右两边碰到蘑菇怪的话就会失去一条生命,并且重置游戏。第二类是食人花,这种敌人不会被马里奥消灭掉,不论马里奥从哪个方向去碰撞食人花,食人花都不会消失,而且如果马里奥碰到了食人花,自身还会失去一条生命,并且游戏重置,当然前提是马里奥没有失去所有的生命值,否则的话游戏就结束。

首先马里奥对于所有的敌人,如果从左右两边碰撞到敌人,那么马里奥死亡,失去一条生命,游戏重置。

//对敌人的判断

for(int i=0;i<this.bg.getAllEnemy().size();i++){

Enemy e = this.bg.getAllEnemy().get(i);

      //对于所有的敌人都适用

if((e.getX()+50>this.x && e.getX()-50<this.x) && (e.getY()+60>this.y && e.getY()-60<this.y)){

//玛丽奥死亡

this.dead();

}

      //这里开始区分敌人的类别,对于不同的敌人做出不同的反应

if(e.getY()==this.y+60 && (e.getX()+60>this.x && e.getX()-60<this.x)){

if(e.getType() == 1){

e.dead();

this.upTime = 10;

this.ymove = -5;

}else if(e.getType() == 2){

this.dead();

}

}

4.2.5障碍物类

绘制场景中所需要的障碍物,例如地面、砖块、水管等等。该类中的属性包括了障碍物的坐标,障碍物所需要显示的图片等。并且在该类中也定义了障碍物类的重置方法,当马里奥死亡时,场景类会调用该方法。

游戏中的场景是由背景中的障碍物绘制而成的,不同的障碍物所在的位置肯定也不相同,那么对于障碍物而言,就必须要有坐标属性来使绘制的时候将不同的障碍物绘制到不同的位置,所以必须要有两个int属性x和y来表示障碍物的坐标。同时该类也必须要实现Runnable接口,实现这个接口的作用主要是为了在最有一个场景中控制旗子的运动,当然同时还要为该类加入线程。

public class Obstruction implements Runnable{

//坐标

private int x;

private int y;

//控制旗子

private Thread t = new Thread(this);

    前面说过,当马里奥顶到问好或者是隐藏的砖块时,那么这个障碍物的类型就会改变,变为石头。那么在障碍物这个类里面就必须要定义一个属性stype,这个属性用于表示当前障碍物的类型,以便于变化形态的时候调用。这个类型的值就可以用初始化类中的相对应的List集合里面的下标表示。既然有改变,就要有恢复,所以还要定义一个不变的type,命名为starttype,这个属性是为了当游戏重置的时候,障碍物可以通过调用这个属性恢复到最初始的状态。而且不同的状态对应不同的显示图片,所以还要有showImage属性。

//类型

private int type;

//初始类型

private int starttype;

//显示图片

private BufferedImage showImage = null;

//取得场景

private BackGround bg;

    在该类中还要写入reset()方法,这个方法是为了当马里奥死的时候调用重置方法,对已经被消灭掉的障碍物进行重置。因为有的障碍物被顶掉以后会给变类型和图片,所有还要定义一个setImage()方法,用来改变障碍物的显示图片。

//重置方法

public void reset(){

this.type = starttype;

this.setImage();

}

//根据状态改变显示图片

public void setImage(){

showImage = StaticValue.allObstructionImage.get(type);

}

    最后该类中的run方法主要是为了控制最后一个场景中的旗子的移动,并且在旗子移动完毕后要设置一个标记,并且将该标记表示给马里奥类,这样马里奥就可以开始自主移动了。

if(this.bg.isOver()){

if(this.y < 420){

this.y += 5;

}else{

                  //设计标记为true,即表示马里奥可以开始移动了

this.bg.setDown(true);

}

4.2.6敌人类

    该类中主要设置了两种敌人,一种是蘑菇怪,可以被马里奥踩死,另一种是食人花,不能被踩死。该类中的属性包括了敌人的坐标,敌人的初始坐标,需要显示的图片,以及敌人的移动方向和移动范围等。敌人的初始坐标主要是为了当敌人执行重置方法后将敌人的位置还原。

在该类中首先要实现Runnable接口,因为在游戏中的敌人是可以移动的,所以一定要通过重写run()方法来达到敌人可以移动的效果。当然还要在该类中定义一个Thread属性,用于控制线程。然后说说到移动,必然少不了坐标问题,那么在该类中就要定义两个int属性x和y,用于控制敌人的位置以及敌人的移动。

public class Enemy implements Runnable{

//坐标

private int x;

private int y;

   //加入线程

private Thread t = null;

    当马里奥失去一条生命值的时候,游戏会被重置,敌人回回到初始的位置,所以还要定义另外两个int属性startx和starty,用来当游戏进行重置的时候,可以根据这个初始坐标回复敌人的位置。

//初始坐标

private int startx;

private int starty;

    对于不同的敌人,所显示的图片肯定是不同的,所以要定义一个现实的图片属性showImage,并且在马里奥中,马里奥要通过判断敌人的类型,来决定是马里奥死亡,还是敌人死亡,对于不同的敌人有不同的反应,所以还要在该类中定义一个type属性,用来表示敌人的类型。

//怪物类型

private int type;

//显示图片

private BufferedImage showImage;

    对于敌人里面的食人花而言,他是在水管中直上直下的,所以他的上下移动应当有一个界限,不论是向上移动还是向下移动,都不能超过这个界限,否则的话食人花就会从水管中飞出来或者是移动到MyFrame外面了。

//移动范围

private int upMax = 0;

private int downMax = 0;

    在这个类中应当有两个构造方法,对于不同的敌人,所需要的属性都是不同的。并且在两个类中都有一个共同的代码,那就是要在开启线程后应当先将线程挂起。这是为了配合游戏在开始的时候敌人不移动,必须要等到玩家按空格键的时候才会开始,所以先将线程挂起来,当点击空格键以后在将线程开启。

//蘑菇怪的构造方法

public Enemy(int x,int y,boolean isLeft,int type,BackGround bg){

... ...

this.t = new Thread(this);

t.start();

t.suspend();

}

    接下来是写敌人类中的run()方法了,该方法主要是为了控制蘑菇怪以及食人花敌人的移动的。因为不同的敌人在不同的场景中有不同的移动方法,所以对于敌人的移动而且,首先要判断敌人的类型和敌人所处的场景。

public void run() {

while(true){

//判断怪物类型

if(type==1){

if(this.isLeftOrUp){

this.x -= 5;

}else{

this.x += 5;

}

       ... ...

    在游戏中,当马里奥死亡的时候会对整个场景中的障碍物进行重置,当然敌人也不例外。当马里奥死亡的时候,不仅要将所有被消灭的敌人全部显示出来,即从消灭的List中还原到原来的敌人List中,并且敌人的状态和坐标也要进行重置。要将敌人的坐标还原到最开始的坐标,而且把图片进行还原。并且在重置方法中也要对敌人的类型进行判断,使得敌人的类型和他的显示图片相对应。

public void reset(){

//还原坐标

this.x = this.startx;

this.y = this.starty;

//还原图片

if(this.type == 1){

this.showImage = StaticValue.allTriangleImage.get(0);

}else if(this.type == 2){

this.showImage = StaticValue.allFlowerImage.get(0);

}

}

    最后在该类中定义一个死亡方法,主要是针对蘑菇怪被消灭的时候所调用的方法。在这个方法中要定义蘑菇怪死亡的时候的显示图片,也就是蘑菇怪被踩扁的图片。并且要将这个敌人从相对应的场景的敌人集合中除去,放入别消灭的敌人的List集合。

public void dead(){

//死亡图片

this.showImage = StaticValue.allTriangleImage.get(2);

//从原来的List集合中删除,让入被消灭的List集合中

this.bg.getAllEnemy().remove(this);

this.bg.getRemoveEnemy().add(this);

}

}

5、系统的实现

5.1游戏开发所需要的图片

5.1.1马里奥的所有图片

这组图片中包含了马里奥的移动,跳跃以及死亡的图片:

5.1.2游戏中障碍物的图片

这组图片中包含了游戏中的各种障碍物,以及最后通过关卡的旗帜图片还有设置陷阱的隐形图片:

  1. 地面及普通障碍物图片

5.1.3游戏中怪物的图片

这组图片中包含了游戏中所有的敌人图片,以及敌人被消灭时的图片:

5.1.4游戏中的背景图片

    这组图片中有一张游戏中的背景图片(图5.1)和一张马里奥通关时的最后一关的背景图片(图5.2):

5.1.5游戏开始时的图片

    在游戏的最开始会显示该图片(图5.3),然后玩家按空格键开始游戏,之后游戏才正式开始运行。

              

图5.3

5.2游戏设计的界面

5.2.1 游戏逻辑展示

    这一组图片中包括了一些系统中的逻辑图片,如马里奥的控制移动示例图片(图5.4),玩家通过方向键控制马里奥的移动、跳跃等功能;马里奥与障碍物进行碰撞之后的效果图片(图5.5),这张图片中显示了马里奥再与障碍物碰撞后,问号会消失变成石头,而且砖块会被撞碎;玩家控制游戏开始的图片(图5.6),游戏打开后并不会立即运行,必须等到玩家按空格键启动游戏后游戏才会正式开始;当马里奥失去所有的生命以后,游戏结束(图5.7);如果马里奥顺利通过所有关卡,那么游戏同样结束(图5.8)。

      

5.2.1 游戏逻辑展示

    这一组图片中主要对游戏的关卡进行展示,其中包括第一关(图5.9),马里奥顺利通过第一管来到第二关(图5.10),第三关的场景(图5.11),第四关的大悬崖场景(图5.12),第五关的场景借鉴了魂斗罗(图5.13),第六关的高墙(图5.14),在这一个关卡中为了提升游戏的可玩性,加了一个隐藏的过关要点,只有找到这个要点才能通过(图5.15),第七关也是最后一关的场景(图5.16)。

      

6、系统测试

6.1 测试的意义

系统测试是为了发现错误而执行程序的过程,成功的测试是发现了至今尚未发现的错误的测试。 测试的目的就是希望能以最少的人力和时间发现潜在的各种错误和缺陷。应根据开发各阶段的需求、设计等文档或程序的内部结构精心设计测试用例,并利用这些实例来运行程序,以便发现错误。系统测试是保证系统质量和可靠性的关键步骤,是对系统开发过程中的系统分析系统设计和实施的最后复查。根据测试的概念和目的,在进行信息系统测试时应遵循以基本原则。

6.2 测试过程

(1)拟定测试计划。在制定测试计划时,要充分考虑整个项目的开发时间和开发进童以及一些人为因素和客观条件等,使得测试计划是可行的。测试计划的内容主要有测试的内容、进度安排、测试所需的环境和条件、测试培训安排等。

(2)编制测试大纲。测试大纲是测试的依据。它明确详尽地规定了在测试中针对系统的每一项功能或特性所必须完成的基本测试项目和测试完成的标准。

(3)根据测试大纲设计和生成测试用例。在设计测试用例的时候,可综合利用前面介绍的测试用例和设计技术,产生测试设计说明文档,其内容主要有被测项目、输人数据、测试过程、预期输出结果等。    

(4)实施测试。测试的实施阶段是由一系列的测试周期组成的。在每个测试周期中,测试人员和开发人员将依据预先编制好的测试大纲和准备好的测试用例,对被测软件或设备进行完整的测试。   

(5)生成测试报告。测试完成后,要形成相应的测试报告,主要对测试进行概要说明,列出测试的结论,指出缺陷和错误,另外,给出一些建议,如可采用的修改方法,各项修改预计的工作量及修改的负责人员。  

6.3 测试结果

程序运行正常,没有发现什么太大的错误。

7、总结与展望

7.1  总结 

    本次设计已是大学最后一次对专业知识的综合实践活动,同时也是我所做的工作量最大的一次作业,因此从一开始我对本次毕业设计就给予了高度重视。从选题、收集资料、学习相关技术到实际编程,我都一丝不苟的对待了。当然其间我也走了不少弯路,有时甚至需要推倒重来,但同时我也多次体会过克服困难后的成就感。 

通过这次毕业设计以及撰写本毕业论文,我学会了一些编程技巧,而且对调试的错误有进一步的认识,有时候就一个小小的语法错误就会导致程序调试不通过。所以每个字符,每句程序都要认真对待。使用不同的编程环境,其效率完全不一样,所以我选择了Eclipse,它自动找错/纠错功能、Debug调试和代码自动生成等一些重要的功能大大提高了我的设计效率。 

7.2  设计中的不足之处 

    本系统实现了超级玛丽游戏所应有的基本功能,我对这样的软件开发还只是一个开始,了解的不多,时间和能力有限,还有一部分功能未能实现,如吃到蘑菇会变大,或者吃到花朵可以发子弹,还有就是一些其他的怪物类。因此做的不是很好,游戏的场景设计和布局还比较简单,有些模块和功能的设计不是那么的完善,没有突出特色出来,这也可能是我这个程序的不足之处。 

7.3  展望 

    本系统基本实现了超级玛丽游戏所应有的基本功能,在大学中最后一次专攻式的学习了Java语言,使我对Java语言有了更深层次的理解,通过该游戏设计,提高了我的编程能力,也让我养成了良好的编程习惯。这个次的毕业设计当然也使自己深深的认识到了java这门语言的博大精深,希望将来在工作中能够不断学习,不断进步,逐步的通过自己的积累去慢慢的学习,慢慢的融汇这门语言,争取早日成为能独当一面的java技术开发人才。

致 谢

    经过一两个月的忙碌和工作,本次毕业设计业已完成了,作为一个本科生的毕业设计,由于经验的匮乏和业务逻辑的不熟悉,难免有许多考虑不周全和不完善的地方,但是在指导老师.任课老师和同学的帮助下很多困难都得以解决,所以在此本人要特别感谢他们对我的帮助。 

首先我要感谢我的学校指导老师,感谢他们在整个毕业设计过程中的指导,为我提示游戏设计的逻辑思路;为我提供参考书籍;为我提供了技术方面资料,而且在遇到问题的时候,总是鼓励我去解决;尤其在论文格式的修改方面,让我明白了要写出一个标准的论文,它的格式的重要性,哪怕就算是一个标点符号都要符合其标准和格式要求。在设计的整个过程中从最初的毕业设计题目的选定,以及中期检查,以及定稿的过程中都给予了我细心的指导。 

其次还要特别感谢大学四年来所有的老师,为我们打下计算机专业知识的基础。以前总是觉得学的课程没有什么用处,但是当真正用计算机来解决实际问题的时候,才知道每门课程的重要性,甚至觉得所学习的那些课程还远远不够,所以以后还应该不断的学习。也可以这么说要不是你们在大学四年中严格要求我们,现在要完成整个毕业设计那是根本不可能的。 

再次,感谢我们班级的几位同学,在我遇到一些难以解决的问题时,给与我支持,鼓励和帮助,在论文撰写过程中,认真仔细的帮我修改,包括一些难以发觉的语法,符号错误,使我受益匪浅。 

最后感谢我的院系和我的母校——太原理工大学软件学院这四年来对我的精心培养。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2271726.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于数组的一些应用--------数组作函数的返回值(斐波那契数列数列的实现)

数组在作为函数的返回值&#xff0c;一个很经典的例子就是获取斐波那契数列的前N项 代码思路&#xff1a; 设计思路 输入&#xff1a; 输入一个整数 n&#xff0c;表示要生成斐波那契数列的长度。 输出&#xff1a; 输出一个长度为 n 的整数数组&#xff0c;其中每个元素为斐…

灰度图的Stride和RGB的Stride有什么区别呢?

灰度图&#xff08;Grayscale&#xff09;和RGB图像的步长&#xff08;Stride&#xff09;计算确实有所不同&#xff0c;主要是因为它们每个像素占用的字节数不同。以下是两者的区别及对齐要求&#xff1a; 灰度图&#xff08;Grayscale&#xff09; 每个像素占用的字节数&…

使用WebSocket 获取实时数据

回车发送数据&#xff0c;模拟服务器发送数据 效果图&#xff1a; 源码&#xff1a; <template><div><h1>WebSocket 实时数据</h1><input type"text" v-model"ipt" keyup.enter"sendMessage(ipt)"><div v-if…

Python:交互式物质三态知识讲解小工具

学着物理写着Python 以下是一个使用Python的Tkinter库实现的简单示例程序&#xff0c;通过图形界面展示并讲解固态、液态、气态的一些特点&#xff0c;代码中有详细的注释来帮助你理解各部分功能&#xff1a; 完整代码 import tkinter as tk from tkinter import ttk import …

基于64QAM的载波同步和定时同步性能仿真,包括Costas环和gardner环

目录 1.算法仿真效果 2.算法涉及理论知识概要 3.MATLAB核心程序 4.完整算法代码文件获得 1.算法仿真效果 matlab2022a仿真结果如下&#xff08;完整代码运行后无水印&#xff09;&#xff1a; 仿真操作步骤可参考程序配套的操作视频。 2.算法涉及理论知识概要 载波同步是…

Arduino 小白的 DIY 空气质量检测仪(5)- OLED显示模块、按钮模块

最终章 这一章把剩下的OLED显示模块、按钮模块分享一下&#xff0c;当前这个离线无存储的版本&#xff0c;基本告一段落。 如果后续能进化成&#x1f236;存储、联网版本&#xff0c;就再开一个小系列分享一下。 逐个分析 display.h #include <Arduino.h> #include &l…

基于机器视觉和Dijkstra算法的平面建筑群地图路线规划matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 1.程序功能描述 基于机器视觉和Dijkstra算法的平面建筑群地图路线规划matlab仿真&#xff0c;输入一张平面建筑群的地图&#xff0c;然后通过机器视觉识别地图里面的障碍物&#xff0c;然后通…

计算机的错误计算(二百零一)

摘要 用两个大模型计算 &#xff0c;结果保留 10位有效数字。实验表明&#xff0c;两个大模型的输出均只有1位正确数字&#xff1b;并它们几乎相同&#xff1a;仅最后1位数字不同。 例1. 计算 , 结果保留 10位有效数字。 下面是与一个数学解题器的对话。 以上为与一个数学解…

【Motion Builder】配置c++插件开发环境

目录 准备环境构建官方案例另行构建经验分享附录 准备环境 安装Motion Builder 2024并破解安装Qt 5.15.2 截止至2024年12月19日&#xff0c;Qt的在线安装器的默认页面是没有5.15.2版本的。你需要&#xff1a;在“选择组件”界面&#xff0c;选择“Archive”&#xff0c;点击“…

大学生入学审核系统的设计与实现(源码+数据库+文档)

亲测完美运行带论文&#xff1a;文末获取源码 文章目录 项目简介&#xff08;论文摘要&#xff09;运行视频包含的文件列表&#xff08;含论文&#xff09;后台运行截图 项目简介&#xff08;论文摘要&#xff09; 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理…

使用高云小蜜蜂GW1N-2实现MIPI到LVDS(DVP)转换案例分享

作者&#xff1a;Hello&#xff0c;Panda 大家晚上好&#xff0c;熊猫君又来了。 今天要分享的是一个简单的MIPI到LVDS&#xff08;DVP&#xff09;接口转换的案例。目的就是要把低成本FPGA的应用潜力充分利用起来。 一、应用背景 这个案例的应用背景是&#xff1a;现在还在…

单片机-独立按键矩阵按键实验

1、按键介绍 按键管脚两端距离长的表示默认是导通状态&#xff0c;距离短的默认是断开状态&#xff0c; 如果按键按下&#xff0c;初始导通状态变为断开&#xff0c;初始断开状态变为导通 我们开发板是采用软件消抖&#xff0c;一般来说一个简单的按键消抖就是先读取按键的状…

供应链系统设计-供应链中台系统设计(七)- 商品中心设计篇

概述 上篇文章我们大致讲了一些商品中心相关的概念&#xff0c;例如&#xff1a;SPU、SKU、Item等等&#xff0c;在这里我们来简单的回顾一下&#xff1a; 商品概念的分层与定义&#xff1a; SPU&#xff08;Standard Product Unit&#xff09;&#xff1a;代表产品系列或产品…

RAG(Retrieval-Augmented Generation,检索增强生成)流程

目录 一、知识文档的准备二、OCR转换三、分词处理四、创建向量数据库五、初始化语言聊天模型1.prompt2.检索链3.对话 完整代码 知识文档的准备&#xff1a;首先需要准备知识文档&#xff0c;这些文档可以是多种格式&#xff0c;如Word、TXT、PDF等。使用文档加载器或多模态模型…

mysql自定义安装

1、下载安装包 我是在windows上安装&#xff0c;所以选择“Mysql Installer for Windows” 2、安装mysql 双击“mysql-installer-community-8.0.40.0.msi”&#xff0c;开始启动安装 这里选择安装项&#xff0c;这里只选择了两项。workbench是图形化管理工具&#xff0c;比较吃…

Innodisk iSMART V6使用说明_SSD还能用多久?已经读写了多少次数?……

Innodisk iSMART是一款SSD健康数据读取软件。它能轻松获取大部分SSD内部寄存器中的健康数据&#xff0c;并以简洁的图形界面展示给用户。在程序界面的顶部&#xff0c;是页面标签&#xff0c;点击页面标签就能切换到相应的页面。页面标签的下面是磁盘选择栏。点击磁盘编号&…

JAVA:利用 Redis 实现每周热评的技术指南

1、简述 在现代应用中&#xff0c;尤其是社交媒体和内容平台&#xff0c;展示热门评论是常见的功能。我们可以通过 Redis 的高性能和丰富的数据结构&#xff0c;轻松实现每周热评功能。本文将详细介绍如何利用 Redis 实现每周热评&#xff0c;并列出完整的实现代码。 2、需求分…

LSP介绍并实现语言服务

首发于Enaium的个人博客 LSP (Language Server Protocol) 介绍 前段时间我为Jimmer DTO实现了一个 LSP 的语言服务&#xff0c;这是我第一次实现 LSP&#xff0c;所以在这里我分享一下我实现LSP的经验。 首先来看一下效果&#xff0c;图片太多&#xff0c;我就放一部分&#…

【微软,模型规模】模型参数规模泄露:理解大型语言模型的参数量级

模型参数规模泄露&#xff1a;理解大型语言模型的参数量级 关键词&#xff1a; #大型语言模型 Large Language Model #参数规模 Parameter Scale #GPT-4o #GPT-4o-mini #Claude 3.5 Sonnet 具体实例与推演 近日&#xff0c;微软在一篇医学相关论文中意外泄露了OpenAI及Claud…

一文大白话讲清楚TCP连接的三次握手和断开连接的四次挥手的原理

文章目录 一文大白话讲清楚TCP连接的三次握手和断开连接的四次挥手的原理1.TCP建立连接需要3次握手1.1 先讲个你兄弟的故事1.2 TCP 3次握手1.2 TCP 3次握手8件事1.3 TCP握手能不能是两次 2. TCP 断开连接要4次挥手2.1 还回到你兄弟的故事上2.2 TCP 4次挥手2.2 TCP4次挥手4件事2…