先上效果图,子弹左边的滑稽就是我的鼠标控制的

其实只是用了“滑稽”的图片而已,把图片拿掉本质上就是用鼠标控制一个可以发射很多小球的小球,以及一堆随机产生的带有随机速度的小球而已,前面是讲解,代码放在了最后

随机产生的小球思路在java画图板之三中有过详细描述
https://blog.csdn.net/qq_37465638/article/details/81195210,
我们创建了四个类
窗体类Framball——建立画图板,开启线程的主方法
球类Ball——描述了球的属性,包括横纵坐标xy,半径R,运动方法run()
画球线程drawBall——将产生的球放在一个队列中,存到一定数量
动球线程moveBall——将队列中的球依次释放,每个球都执行Ball中的run,crash(),注意,此时要放在循环中,延迟时间0.1秒,这样球每run一次坐标就改变一次,花费0.1秒,看起来就像运动一样

其实稍微改一改就是球球大作战,飞机大战,大鱼吃小鱼这类游戏了。现在我们升级到“滑稽”版本,将画图板三升级到四我们需要以下四步操作


在Ball类中增加crash()方法,使用勾股定理判定距离,当接近时双方速度交换(有一个问题没有处理好,就是有时候新出生的两个球靠得很近,导致黏在一起抖动,因为速度一直在交换,以其中一个消失告终,用图片代替后又多了一个问题是图片的背景色和窗体背景色一致,看不出来是图片,但是有时候撞在一起时其中一张的图片背景色会掩盖另一张的表情)
添加blood属性,传入crash()中进行值的变化,同时添加一个方法画出血条,

public void crash(ArrayList<Ball> list) {
}
public void blood(Graphics g) {
}


增加子弹类Bullet,和球类相似,但是血量设为1,纵坐标速度设为0,大小恒定,同时设置自己的队列ListB,每次点击就新建一个Bullet

public class Bullet {
}

最重要的是Bullet是在点击后才产生,所以我们还需要


增加鼠标***ListenerBall,新建一个Ball的实例(作为我们控制的球),在mouseMoved事件中将鼠标的坐标传给这个球,并且加到包含球的队列List中,也就是说,他是List的第一个球。

public class ListernerBall implements MouseListener,MouseMotionListener{
}

同时,在mousePressed事件中新建Bullet的实例,注意,Ball的实例新建是放在属性中,因为我们只需要控制一个Ball,而Bullet的实例是放在mousePressed中,每点击一次,就产生一个bullet,并且加到队列ListB里面去

Bullet bullet=new Bullet(fb,g,listB);
		bullet.x=e.getX();
		bullet.y=e.getY();
		listB.add(bullet);

这个队列同样放在moveBall中执行,当然是新建一个for循环,因为这个队列和Ball队列的长度不一样


在moveBall中我们做了另外一个关键性的改动——引入次画布ig,还记得我们让小球动起来的本质是什么吗?在Ball类中的run()方法每执行一次就改变一次圆心坐标,但是这样其实原来的小球还在,就会使得图形变成一条射线,在画图板之三中我用的方法是在run()中一开始还没改坐标时画一个原坐标的背景色的小球来擦除,这样的方法在增加crash()方法后容易判断不准原坐标,导致擦除偏移留下像残月一样的痕迹

 g.setColor(fb.getContentPane().getBackground());//切换为背景色
        g.fillOval(x - R - speedX, y - R - speedY, R, R);//减掉R表示在坐标替换为圆心,减掉速度表示将上一个小球掩盖掉

而次画布ig比较奇特,所有的球都花在次画布上,画完之后,次画布消失,也就是所一切都消失不见了,无论上面有多少个球,包括子弹,然后再在极短的时间之内重画出来,然后各种球和子弹再画出他们变换坐标后的位置,这样可以永久性,无残留的擦除上一次所留下的一切痕迹,毕竟皮之不存毛将焉附?

所以他应该放在哪呢?当然是moveBall中了,因为他画的频率一定和画Ball和Bullet的频率一样,才能保证画面的正常,所以要一起放在run()中,而且要放在前面,先画出来,在run()的最后,别忘了次画布ig是我们新造的,所以要移花接木,将ig画在g上

Image img = fb.createImage(fb.getWidth(), fb.getHeight()); // 图片要多次创建!!!!!!
Graphics2D ig = (Graphics2D) img.getGraphics();
……
g.drawImage(img, 0, 0, fb);//等小球画完之后一次性清除一遍,如果在for循环中会闪烁

以下是代码(要想运行在你的编译器你还需要先建一个BallGame的包,以及有一张图片,在Ball类中引用)

窗体类,相对于画图板之三,需要增加一些代码:创建控制的小球,传入***

package BallGame;

import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.util.ArrayList;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class Framball extends JFrame{
	private static ArrayList <Ball> list = new ArrayList<Ball>();//新建的list一定要初始化
	static ArrayList<Bullet>listB=new ArrayList<Bullet>();
	public  Graphics g;
	
	
	public void showUI() {
		
		this.setTitle("滑稽大作战");
		this.setSize(1200, 700);
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setLocationRelativeTo(null);
		// 设置布局,流式布局
		this.setLayout(new FlowLayout());
		
		this.setVisible(true);
		g=this.getGraphics();
		
		//添加***
//		g.setColor(Color.black);
		Ball ball=new Ball(this,g);
		ball.set(600, 500, 40);
		list.add(ball);
		ListernerBall l=new ListernerBall(this,g,ball);
		l.setB(list,listB);
		this.addMouseListener(l);
		this.addMouseMotionListener(l);
	}
	
	public static void main(String[] args) {
		Framball fb=new Framball();
		fb.showUI();//可见才能得到画布
		
		drawBall db=new drawBall();
		db.setG(fb,fb.g,list);//传三个参数
		db.start();//启动线程,线程启动后,他会自动运行,并且只运行run方法,想要运行其他方法必须另外调用
	
		moveBall mb=new moveBall();
		mb.setL(fb, fb.g,list,listB);
		mb.start();//启动线程
		
	}

}

Ball类

package BallGame;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.util.ArrayList;
import java.util.Random;

import javax.swing.Icon;
import javax.swing.ImageIcon;

public class Ball {
	Random rand = new Random();
	int R = rand.nextInt(70) + 20;// 半径
	int x = rand.nextInt(1200);
	int y = rand.nextInt(600);
    int blood = rand.nextInt(5) + 3;
	int fblood = blood;//满血值
	int choose=rand.nextInt(4);
	//球的几种颜色的不停变换
	int []arr= {255,192,128,64,0};
	int i=rand.nextInt(arr.length);
	int j=rand.nextInt(arr.length);
	int k=rand.nextInt(arr.length);

	int speedX =  70-R-20, speedY = 70-R-20;// 小球运动速度
	
	private int r = 1200, d = 700;// 右限,下限
	public Graphics g;
	public Framball fb;

	public Ball(Framball fb,Graphics g) {//这个是必须要传的,放在构造方法中
		this.g = g;
		this.fb = fb;
		
	}
	public void set(int x,int y,int R) {//这个是可选传的
		this.x=x;
		this.y=y;
		this.R=R;
	}

	
	public void draw(Graphics g) {	
		Color C=new Color(arr[k],arr[j],arr[i]);
		g.setColor(C);
		
		ImageIcon image = null;
		//这里可以使用不同的图片,从而出现不同的滑稽表情,为了方便我全部用了一种
		if(choose==0) image = new ImageIcon("D:\\Pictures\\项目图片\\滑稽2.jpg");
		else if(choose==1)image = new ImageIcon("D:\\Pictures\\项目图片\\滑稽2.jpg");
		else if(choose==2)image = new ImageIcon("D:\\Pictures\\项目图片\\滑稽2.jpg");
		else if(choose==3)image = new ImageIcon("D:\\Pictures\\项目图片\\滑稽2.jpg");
		
		Image img=image.getImage();
		g.drawImage(img, x-R, y-R,2*R,2*R, null);
//		g.fillOval(x - R, y - R, 2*R, 2*R);//这里是宽和高,所以要写直径,否则就会在奇怪的地方相撞
		
		//设置字体,将血量值显示在图片内部
		Font mf=new Font(Font.DIALOG,Font.BOLD,15);
		g.setFont(mf);
		g.setColor(Color.black);
		String  Blood=String.valueOf(blood);//同步球的血量和血值
		g.drawString(Blood, x-6, y+5);
		
	}

	public void run() {// 表示小球的运动,可反弹,必须放在while循环中才能跑起来

		if (y >= d)// 当y接触到下限时
			speedY *= -1;// 速度反向
		else if (y <= 0)
			speedY *= -1;
		if (x >= r)
			speedX *= -1;
		else if (x <= 0)
			speedX *= -1;
		x += speedX;// 每一次run,就移动一次速度值
		y += speedY;
		// 只执行一次run方法,小球是不会动的

	}

	public void crash(ArrayList<Ball> list) {
		for (int i = 0; i < list.size(); i++) {
			Ball ball = list.get(i);
			if (this != ball) {
				int d = (x - ball.x) * (x - ball.x) + (y - ball.y) * (y - ball.y);// 横纵坐标平方和
				int dis = (R + ball.R) * (R + ball.R);// 半径之和的平方
				if (d <= dis) {
					int sx=speedX;
					int sy=speedY;
					speedX=ball.speedX;
					speedY=ball.speedY;
					ball.speedX=sx;
					ball.speedY=sy;
				}
			}
		}
	}

	public void blood(Graphics g) {
		g.setColor(Color.red);
		g.fillRect(x - R, y - 5 - R, blood * 10, 5);
		g.setColor(Color.gray);//失血时空去的血条
		g.fillRect(x - R + blood * 10, y - R - 5, (fblood - blood) * 10, 5);
	}
}

drawBall线程

package BallGame;

//import java.awt.Color;
import java.awt.Graphics;
import java.util.ArrayList;

public class drawBall extends Thread{
	ArrayList <Ball> list;//注意后面的写法,写错会报空指针异常
	Graphics g;
	Framball fb;
	int count=0;
	public void setG(Framball fb,Graphics g,ArrayList <Ball> list) {
		this.g=g;
		this.fb=fb;
		this.list=list;
	}
	
	public void run() {
		while(true) {
			if(list.size()==10) {
				break;//设置小球存储的个数
			}
			
			Ball b=new Ball(fb,g);//循环中不断新建一个球
			
			list.add(b);//把球放进队列	
		}
	}
}

moveBall线程

package BallGame;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;

import javax.swing.ImageIcon;

public class moveBall extends Thread  {
	int i = 0;
	private ArrayList<Ball> list;// =new ArrayList<Ball>();
	private ArrayList<Bullet> listB;
	private Framball fb;
	private Graphics g;
	private int x,y,r=20;
	Color c = new Color(238, 238, 238);//背景色
	int count=1;//控制球的数量

	public void setL( Framball fb, Graphics g,ArrayList<Ball> list,ArrayList<Bullet> listB) {
		this.list = list;
		this.listB=listB;
		this.fb = fb;
		this.g = g;
	}

	public void run() {

		while (true) {//创建次画布放在循环中,每次循环完一次队列中的小球之后就重新添加画布
			
			Image img = fb.createImage(fb.getWidth(), fb.getHeight()); // 图片要多次创建!!!!!!
			Graphics2D ig = (Graphics2D) img.getGraphics();
//			ImageIcon imgi=new ImageIcon("D:\\Pictures\\项目图片\\滑稽2.jpg");
//			ig.drawImage(imgi.getImage(), 0, 0,fb.getWidth(),fb.getHeight(),fb);//设置背景图片
			
			ig.setColor(Color.white);
			ig.fillRect(0, 0, fb.getWidth(), fb.getHeight());
			Ball mball=list.get(0);
			mball.draw(ig);
			mball.blood(ig);
			mball.crash(list);
			
			if(list.size()<7) {
				drawBall db=new drawBall();
				db.setG(fb,fb.g,list);//传参数
				db.start();//启动线程,线程启动后,他会自动运行,并且只运行run方法,想要运行其他方法必须另外调用
			}
			//执行队列中的小球
			for (i = 1; i < list.size(); i++) {
				// list.get(i).clear();
				Ball ball=list.get(i);
				ball.draw(ig);
				ball.blood(ig);
				ball.crash(list);
				ball.run();
				
			}
			//执行子弹
			for(i=0;i<listB.size();i++) {
				Bullet bullet=listB.get(i);
				bullet.draw(ig);
				bullet.crash(list);
				bullet.run();
			}
			
			g.drawImage(img, 0, 0, fb);//等小球画完之后一次性清除一遍,如果在for循环中会闪烁
			
			try {
				sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

Bullet类


package BallGame;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.util.ArrayList;
import java.util.Random;

import javax.swing.Icon;
import javax.swing.ImageIcon;

public class Bullet {
	ArrayList<Bullet>listB;
	int x,y;
	int R = 5;// 半径
	private int blood =  1;
	private int speedX =  10, speedY = 0;// 子弹运动速度
	Graphics g;
    Framball fb;

	public Bullet(Framball fb,Graphics g,ArrayList<Bullet>listB) {
		this.g = g;
		this.fb = fb;
		this.listB=listB;
		
	}
	public void set(int x,int y,int R) {
		this.x=x;
		this.y=y;
		this.R=R;
	}

	public void draw(Graphics g) {	
		ImageIcon image =new ImageIcon("D:\\Pictures\\项目图片\\滑稽图标.png");
		Image img=image.getImage();
		g.drawImage(img, x-R, y-R,2*R,2*R, null);
	}

	public void run() {// 子弹运动
		x += speedX;// 每一次run,就移动一次速度值
		y += speedY;
		// 只执行一次run方法,子弹是不会动的
	}

	public void crash(ArrayList<Ball> list) {
		for (int i = 1; i < list.size(); i++) {
			Ball ball = list.get(i);

				int d = (x - ball.x) * (x - ball.x) + (y - ball.y) * (y - ball.y);// 横纵坐标平方和
				int dis = (R + ball.R) * (R + ball.R);// 半径之和的平方
				if (d <= dis) {
					blood--;
					ball.blood--;
					if (ball.blood <= 0 && list != null)
						list.remove(i);// 不能和下行调换,否则会指针溢出,因为下面的可能为空
					if (blood <= 0)
						listB.remove(this);

				}
			
		}
	}

}

***ListenerBall

package BallGame;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;

public class ListernerBall implements MouseListener,MouseMotionListener{
	private int x,y,r=20;
	public Graphics g;
	Framball fb;
	Ball ball=new Ball(fb,g);
	
	ArrayList<Bullet>listB;
	ArrayList<Ball> list;
	public ListernerBall(Framball fb,Graphics g,Ball ball) {
		// TODO Auto-generated constructor stub
		this.g=g;
		this.fb=fb;
		this.ball=ball;
		
	}
	public void setB(ArrayList<Ball> list,ArrayList<Bullet>listB) {
		this.list=list;
		this.listB=listB;
	}
	public void mouseClicked(MouseEvent e) {	
		
	}

	public void mouseDragged(MouseEvent e) {
//     	x=ball.x;
//		y=ball.y;
	}
	@Override
	public void mouseMoved(MouseEvent e) {
		// TODO Auto-generated method stub
		ball.x=e.getX();
		ball.y=e.getY();
	}
	@Override
	public void mousePressed(MouseEvent e) {
		// TODO Auto-generated method stub
		Bullet bullet=new Bullet(fb,g,listB);
		bullet.x=e.getX();
		bullet.y=e.getY();
		listB.add(bullet);
	}
	@Override
	public void mouseReleased(MouseEvent e) {
		
	}
	@Override
	public void mouseEntered(MouseEvent e) {
		
	}
	@Override
	public void mouseExited(MouseEvent e) {
		
	}
	
}

看代码多累啊,再“滑稽”一下,玩了一个小时哈哈