本次我们主要实现了三个功能:
1,小球碰壁反弹、小球相互碰撞反弹。
2,使用缓冲绘图解决闪屏问题。
3,增加暂停按钮,实现界面动、停控制。
一,创建一个类ShowUI实现窗体界面
package com.yzd0126.BallsRebound;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
public class ShowUI extends JFrame{
//展示界面
public void UI() {
this.setSize(800,600);//设置界面大小
this.setTitle("球球碰撞");
this.setLocationRelativeTo(null);//界面位于屏幕中央
this.setLayout(new FlowLayout());//使用流式布局
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//退出时关闭
this.setVisible(true);//界面设置可见
}
//主函数 程序入口
public static void main(String[] args) {
ShowUI showui = new ShowUI();
showui.UI();
}
}
二,创建小球类Ball
1,在创建完Ball类之后,在界面类ShowUI中创建一个全局用来保存所有小球的动态数组balls。
//全局用来存储小球对象的数组
List<Ball> balls = new ArrayList<>();
之所以将ArrayList向上转型为List,是因为这样写体现了面向接口编程的思想。List是一个接口,ArrayList是实现List接口的一个具体实现类,能够降低程序的耦合度,当我们发现代码使用的集合不正确应该使用的是LinkedList时,只需要修改以下一行代码就可:
List<Ball> balls = new LinkedList<>();
因为之后所有的操作都是针对List这个接口定义的方法,而与实现类的独特方法无关,如果我们一开始直接使用ArrayList类型而不向上转型,那么之后如果涉及到ArrayList类独特的方法时需要修改的地方比较多,不便于程序的维护。
2,在Ball类中定义每个小球需要的属性以及方法
关于运动小球的属性有:小球外接圆左上角坐标lx、ly,小球速度vx、vy,小球直径R、小球颜色color,小球球心rx、ry,画笔参数g。
关于运动小球的方法有:
2.1构造方法传参,随机生成小球位置、速度、颜色。
//构造函数 传参
public Ball(Graphics g,List balls) {
this.g=g;
this.balls=balls;
Random random = new Random();
//随机产生小球坐标 速度 颜色
lx=random.nextInt(600);
ly=random.nextInt(200)+50;
vx=(float) (random.nextInt(4)+2);
vy=(float) (random.nextInt(3)+2);
color=new Color(100+random.nextInt(90),100+random.nextInt(90),100+random.nextInt(90));
}
2.2判断小球是否碰到边界。
//判断小球是否碰到边界 碰撞了返回true 没碰撞返回false
public Boolean isknockbound() {
//小球球心坐标
rx=lx+R/2;
ry=ly+R/2;
//是否碰到边界 因为缓冲画布不能遮挡鼠标 故下方距离应该往上提arguemwnt
Boolean b1=((rx)<=(R/2))||((rx)>=(SCREENX-R/2))||((ry)<=(R/2))||((ry)>=(SCREENY-R/2-arguement));
if(b1) {
return true;
}
return false;
}
2.3判断小球之间是否相互碰撞。
小球之间的相互碰撞和碰壁略有不同,碰壁对于每个边界只有一种情况,但是对于球之间的碰撞有可能该球和左边的球碰也有可能是和右边的球碰,因此碰完速度改变的方向也不同,在该方法中直接改变小球速度。
//判断小球之间是否碰撞 碰撞了返回true 没碰撞返回false
public Boolean isknockballs() {
for (int i = 0; i < balls.size(); i++) //判断小球间是否发生碰撞
{
Ball ball = (Ball) balls.get(i);
//排除取到两个相同的球
if (this.equals(ball))
continue;
//发生了碰撞
if ((ball.lx-lx) * (ball.lx-lx) + (ball.ly-ly) * (ball.ly -ly) <= R*R)
{
//获取于自己发生碰撞的小球之间的夹角(反正切) 角度只能再-pi/2~pi/2 因此还需判断两球x坐标关系
double degree = Math.atan((ly - ball.ly) / (lx - ball.lx));
if (lx > ball.lx) //如果自己的x坐标大于发生碰撞的小球的x坐标,由数学知识可知自己应该往正向运动
{
vx = (float) Math.cos(degree)+2;
vy = (float) Math.sin(degree)+2;
}
else //如果自己的x坐标小于发生碰撞的小球的x坐标,由数学知识可知应该朝负向运动
{
vx = (float) -Math.cos(degree)-2;
vy = (float) -Math.sin(degree)-2;
}
return true;
}
}
return false;
}
2.4小球移动方法。
小球的基本移动方法是:位置=位置+速度。
//移动方法
public void move() {
lx=lx+vx;//位置=位置+速度
ly=ly+vy;
}
但小球发生了碰撞之后移动方法要发生变化,要对小球的速度进行操作。当与边界发生碰撞时速度直接取反,当小球之间发生碰撞时直接在isknockballs方法中直接对速度进行改变。
//如果小球碰到边界 则反弹 速度取反
if(isknockbound()) {
vx=-vx;
vy=-vy;
}//如果小球之间相互碰撞 按情况改变速度 在isknockballs()方法中改变
else if(isknockballs()) {
}
2.5画小球方法。
将画笔的颜色指定为生成该小球对象时的颜色,在指定位置画出小球。
//画方法
public void draw(Graphics g) {
g.setColor(color);
g.fillOval((int)lx, (int)ly, R, R);
}
三,在线程中重写run()方法,使用缓存画图
1,使用构造方法,将小球参数传入,使的全局使用的参数一致。
Graphics g;
Color color;
List balls;
//构造方法 传参
public MyThread(Graphics g,Color color,List balls) {
this.g=g;
this.color=color;
this.balls=balls;
}
2,重写run()方法,将画的操作全部放在缓存中,每隔10ms在缓存中将所有的小球取出移动、画完之后,再在界面中展示出来,这样可以解决闪屏问题。
//重写run方法
public void run() {
//创建缓存
BufferedImage buffer = new BufferedImage(800,600,BufferedImage.TYPE_INT_ARGB);
//获取缓存上的画布
Graphics bgriphics = buffer.getGraphics();
//使线程不终止
while(true) {
//清屏
bgriphics.setColor(Color.BLACK);
bgriphics.fillRect(0, 0 , 800,600);
bgriphics.setColor(color);
for(int i=0;i<balls.size();i++) {
Ball ball=(Ball) balls.get(i);
ball.move();
ball.draw(bgriphics);
}
//显示缓存 在界面上 将所有缓存上用bufferGraphics画完的图形只用一次用之前界面上的画笔g展现处理啊
g.drawImage(buffer, 0, 65,null);//0,65为图形左上角坐标 65为了不遮挡鼠标
//每过10ms利用缓存将数组中全部的小球移动+画出+清屏
try{
Thread.sleep(10);
}catch(Exception ef) {};
}
}
四,创建监听,实现暂停功能
1,在线程类Mythread中定义一个Boolean类型的变量isgo,根据isgo的状态判断是否执行清屏、画等操作,而不是让线程终止,因为线程一旦终止便无法再开启。同时加入IsGo()方法,每调用一次改变一次isgo状态,这样就能够实现暂停和开始的切换。
Boolean isgo=false;//参数 判断是否然后小球动起来
//按钮调用此方法 每按下一次 改变一次isgo状态
public void IsGo() {
isgo=!isgo;
}
//重写run方法
public void run() {
//创建缓存
BufferedImage buffer = new BufferedImage(800,600,BufferedImage.TYPE_INT_ARGB);
//获取缓存上的画布
Graphics bgriphics = buffer.getGraphics();
//使线程不终止
while(true) {
//根据每过10ms 根据isgo状态判断是否让小球动起来
if(isgo) {
//清屏
bgriphics.setColor(Color.BLACK);
bgriphics.fillRect(0, 0 , 800,600);
bgriphics.setColor(color);
for(int i=0;i<balls.size();i++) {
Ball ball=(Ball) balls.get(i);
ball.move();
ball.draw(bgriphics);
}
//显示缓存 在界面上 将所有缓存上用bufferGraphics画完的图形只用一次用之前界面上的画笔g展现处理啊
g.drawImage(buffer, 0, 65,null);//0,65为图形左上角坐标 65为了不遮挡鼠标
}
//每过10ms利用缓存将数组中全部的小球移动+画出+清屏
try{
Thread.sleep(10);
}catch(Exception ef) {};
}
}
2,创建监听类MyListener,对鼠标动作进行监听
2.1,界面添加按钮绑定监听
//创建按钮 添加到界面上
JButton jbutton=new JButton("start");
this.add(jbutton);
//创建监听器对象
MyListener mylistener=new MyListener(jbutton);
//按钮绑定监听
jbutton.addActionListener(mylistener);
2.2,在ShowUI类中创建线程对象thread,将线程对象传入MyListener类中,鼠标每点击一次,线程对象thread都调用一次MyThread中的IsGo()方法,改变isgo状态,同时通过count计数取余改变按钮上的字。
//创建线程
MyThread mythread=new MyThread(g,color,balls);
//创建监听器对象
MyListener mylistener=new MyListener(jbutton,mythread);
package com.yzd0126.BallsRebound;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
public class MyListener implements ActionListener{
private int count=0;//取余 用来切换按钮上的字
JButton jbutton;
MyThread mythread;
public MyListener(JButton jbutton,MyThread mythread) {
this.jbutton=jbutton;
this.mythread=mythread;
}
public void actionPerformed(ActionEvent e) {
//每按下一次按钮 就要调用线程的IsGo方法 切换isgo状态 判断是否执行画操作
mythread.IsGo();
//count计数 取余切换按钮上的字符
count++;
if(count%2==1) {
jbutton.setText("stop");
}else
{
jbutton.setText("start");
}
}
}
五,启动线程,实现功能
在ShowUI类中一次性添加20个小球,然后启动线程运行程序。
//一次添加20个小球进去
for(int i=0;i<20;i++) {
Ball ball=new Ball(g,balls);
balls.add(ball);
}
//启动线程
mythread.start();