这是一篇学习分享博客,这篇博客将会介绍以下几项内容:

1、如何让一个程序同时做多件事?(多线程的创建、多线程的应用)
2、如何让小球在画面中真实地动起来?(赋予小球匀速直线、自由落体、上抛等向量运动)
3、多线程游戏仿真实例分享(飞机大战、接豆人、双线挑战三个游戏实例)

  • 涉及的知识点有:多线程的应用、双缓冲绘图、小球的向量运动、游戏的逻辑判断、键盘监听器的使用、二维数组的使用、添加音乐效果等

游戏效果:


怎么样?如果觉得还不错的话就请继续看下去吧!

热身

第一步:创建画布

  • 心急吃不了热豆腐,我们先从最简单的创建画布开始。
    首先我们创建一个窗体,然后设置一些参数,从窗体中取得画笔,尝试在画布中心画一个图形,以下是参考代码:
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;import javax.swing.JButton;
import javax.swing.JFrame;public class Frame {//声明画布对象public Graphics g;//主函数public static void main(String[] args) {//创建Frame类,然后运行showFrame函数Frame fr=new Frame();fr.showFrame();}//编写窗体显示的函数public void showFrame(){//创建窗体JFrame jf=new JFrame();jf.setTitle("小球演示");//设置窗体标题jf.setSize(900,900);//设置窗体大小jf.setDefaultCloseOperation(3);//设置点击窗体右上角的叉叉后做什么操作,这里的3代表点击叉叉后关闭程序jf.setLocationRelativeTo(null);//设置窗体居中显示FlowLayout flow=new FlowLayout();//设置窗体布局为流式布局jf.setLayout(flow);Mouse mou=new Mouse();//创建监听器对象JButton jbu=new JButton("START");//创建按钮,按下按钮后可以在画布中间画一个圆jbu.addActionListener(mou);//为按钮添加事件监听器jf.add(jbu);//设置窗体可见jf.setVisible(true);//从窗体获取画布g=jf.getGraphics();}//创建内部类监听器(也可以重新创建一个文件编写该类)class Mouse implements ActionListener{//重写按钮监听方法public void actionPerformed(ActionEvent e){//按下按钮后会执行这里的代码,下面这条代码指的是在画布中心画一个圆g.fillOval(300,300,300,300);}}
}
  • 我们可以试着运行一下,出现以下图片所示效果第一步就成功了。

第二步:让小球动起来

  • 用一段循环代码重复地画小球,每次循环让小球偏移一点距离
    我们在上述代码中的监听器类Mouse的按钮监听器方法actionPerformed(ActionEvent e)下加这样一段代码
//重复画100次小球,每次横纵坐标分别加1
for(int i=0;i<100;i++){g.fillOval(300+i,300+i,30,30);/*下面这段代码的意思是每执行一次循环,系统暂停30毫秒,否则画的太快我们就观察不到小球在动了*/try{Thread.sleep(30);}catch(Exception ef){}
}
  • 运行程序并点击START按键后,我们可以看到一个圆往右下角方向缓缓移动了一段距离,并且留下了痕迹。同时我们还可以发现,每次点击START键后,START键会保持被按下的状态,直至整个绘制小球的循环代码执行结束后才会弹起。这是因为我们现在写的程序只有一个线程在运行,所以只有当前任务执行完后按钮才能重新接收响应。想要解决这一点,可以利用下面将要讲到的多线程的原理。

那么,热身结束,下面让我们一起进入多线程的世界吧!

一、如何让一个程序同时做多件事情?

创建线程对象

  • 创建线程对象我们需要用到Thread类,该类是java.lang包下的一个类,所以调用时不需要导入包。下面我们先创建一个新的子类来继承Thread类,然后通过重写run()方法(将需要同时进行的任务写进run()方法内),来达到让程序同时做多件事情的目的。
import java.awt.Graphics;
import java.util.Random;public class ThreadClass extends Thread{public Graphics g;//用构造器传参的办法将画布传入ThreadClass类中public ThreadClass(Graphics g){this.g=g;}public void run(){//获取随机的x,y坐标作为小球的坐标Random ran=new Random();int x=ran.nextInt(900);int y=ran.nextInt(900);for(int i=0;i<100;i++){g.fillOval(x+i,y+i,30,30);try{Thread.sleep(30);}catch(Exception ef){}}}
}
  • 然后我们在主类的按钮事件监听器这边插入这样一段代码,即每按一次按钮则生成一个ThreadClass对象
public void actionPerformed(ActionEvent e){ThreadClass thc=new ThreadClass(g);thc.start();
}
  • 在这里我们生成ThreadClass对象并调用start()函数后,线程被创建并进入准备状态,每个线程对象都可以同时独立执行run()方法中的函数,当run()方法中的代码执行完毕时线程自动停止。

接下来我们试着运行一下吧!

加入清屏功能,让小球真正的动起来

  • 从上面的画图示范我们可以看出,小球在移动过程中是留下了轨迹的,那如果我只想看到小球的运动,不想看到小球的轨迹怎么办?
  • 很简单,我们只需要每次画新的小球之前先给整个画布画上一个大的背景色矩形,把原来的图案覆盖即可。

让我们试着把run()方法中的代码改为下面这样:

public void run(){//获取一个随机数对象Random ran=new Random();//生成一对随机的x,y坐标设为小球的坐标,范围都在0-399int x=ran.nextInt(400);int y=ran.nextInt(400);for(int i=0;i<100;i++){//画一个能够覆盖画面中一块区域的白色矩形来清屏(把原来的笔迹都覆盖掉)g.setColor(Color.white);g.fillRect(300,300,300,300);g.setColor(Color.black);g.fillOval(200+x+i,200+y+i,30,30);try{Thread.sleep(30);}catch(Exception ef){}}
}

让我们试着运行一下

  • 我们运行后发现,小球确实在中间的白色矩形区域内实现了不留轨迹的运动,但是小球闪烁的非常厉害。这其中的原因有两个,一个是多个线程对象同时运行会产生冲突,另一个是IO设备使用频率过高。后者我们在稍后的部分讲到用双缓冲绘图去解决,前者则通过下面的方法解决。
  • 我们只在主类中创建一次ThreadClass对象。然后再创建一个列表,每次按按钮时将一组坐标存到这个列表中,最后通过run()方法中依次读出这个列表中的每一项并画出。
    在主类中创建ThreadClass对象并运行(主类的showFrame方法中插入以下代码)

先创建坐标类

public class Location {public int x;public int y;public Location(int x,int y){this.x=x;this.y=y;}
}

然后在主类和ThreadClass类中创建列表

public ArrayList<Location> locs=new ArrayList<Location>();

然后在按钮监听器的方法下写入这段代码

public void actionPerformed(ActionEvent e){Random ran=new Random();int x=ran.nextInt(400);int y=ran.nextInt(400);Location loc=new Location(x,y);locs.add(loc);System.out.println(locs.size());
}

然后将画布g和列表locs传入创建的线程对象中,在主类的showFrame方法插入以下代码。

ThreadClass thc=new ThreadClass(g,locs);
thc.start();

重载Thread Class的run()方法

public void run(){while(true){g.setColor(Color.white);g.fillRect(300,300,300,300);for(int i=0;i<locs.size();i++){g.setColor(Color.black);//每次给小球坐标偏移一下int x=locs.get(i).x++;int y=locs.get(i).y++;g.fillOval(200+x,200+y,30,30);}try{Thread.sleep(30);}catch(Exception ef){}}
}

让我们再来试一下!

这下小球就没有闪烁的那么厉害了。

二、如何让小球在画面中真实地动起来?

  • 众所周知,要想描述物体的运动状态,需要知道物体的三个物理量——位置、速度和加速度。我们只需要找到方法描述这三个物理量,便可以很好的模拟真实小球的运动。

在这里我们可以创建一个Vector类来描述位置、速度和加速度这三个物理量

public class Vector {public int x;public int y;public Vector(int x,int y){this.x=x;this.y=y;}//向量的加和运算public void add(Vector vec){this.x+=vec.x;this.y+=vec.y;}
}

然后我们再创建一个Ball类来代表小球(move函数是本部分的关键)

public class Ball {public Vector location;//位置public Vector speed;//速度public Vector acce;//加速度//构造器传参,设定小球的基本参数public Ball(Vector location,Vector speed,Vector acce){this.location=location;this.speed=speed;this.acce=acce;}//小球移动,这是整个部分的关键!!!每画完一次小球就调用一次move函数,让小球依据速度和加速度来改变一次位置public void move(){this.speed.x+=acce.x;//每调用一次move函数小球的速度就和加速度做一次加法this.speed.y+=acce.y;this.location.x+=speed.x;//每调用一次move函数小球的位置坐标就和速度做一次加法this.location.y+=speed.y;}
}

有了这两个类,我们就可以表示任意二维的向量运动了

  • 比如说从原点出发,向右速度为5,向下加速度为10的平抛运动可以表示为
Vector location=new Vector(0,0);
Vector speed=new Vector(5,0);
Vector acce=new Vector(10,0);
  • 从原点出发,向右速度为5,向上速度为10,向下加速度为10的上抛运动可以表示为
Vector location=new Vector(0,0);
Vector speed=new Vector(5,10);
Vector acce=new Vector(10,0);