目录
线程游戏V1——多线程
目标效果
一 Ball类
二 BallThread类
三 UIListenner类
四 GameUI类
运行结果
问题分析
线程游戏V2——三线程
目标效果
新增MoveThread线程实现类
修改监听器类UIListenner代码
运行结果
问题分析
问题2代码实现
V2版本如何实现不同的小球随机方向移动的
最终代码
线程游戏V1——多线程
目标效果
多线程实现,每点击一次鼠标就启动一个线程,单个线程实现重复绘制小球,绘制的小球所有属性相同。
一 Ball类
属性:
* 坐标xy,尺寸size,移动速度speedx,speedy,球的颜色
方法:
* 构造方法Ball:构造所有属性
* 封装方法:所有属性的get和set方法
* 绘制方法:draw把球画出来
* 移动方法:move小球的移动
二 BallThread类
线程实现类,实现Runnable接口,传入球对象和画笔,重写run方法,写个死循环,实现一直绘制随机大小位置颜色尺寸的圆
三 UIListenner类
继承MouseAdapter,将监听器和线程结合,实现每次鼠标按下时刻创建线程对象,启动线程,在鼠标按下的位置画圆
四 GameUI类
继承JFrame,写一个界面,传入两个监听器实现类对象
运行结果
问题分析
看UIListenner类中的以下代码
原本的目的是:每次按下鼠标都启动一个线程,绘制无数个小球,这无数个小球应该有相同的属性(尺寸,速度,颜色)。
但是看运行结果,同一个线程画出的小球颜色是随机的,出现了线程不安全问题。
主要原因:画笔g是从窗体上获取的,绘制图像的所有画笔都是同一个对象g,而鼠标每次点击,都会随机创建一个color设置给g,造成了画笔颜色更改。
如果只按一次鼠标,就不会出现线程安全问题,单一线程绘制出来的所有小球都是相同的。
点击两次鼠标的效果:
解决方式:去掉判断,每次按下鼠标都获取一次画笔
这样单个线程画出的小球都有相同的属性
但是这样频繁的获取画笔会造成资源的浪费。可以用下面的解决方法,实现V2。
线程游戏V2——三线程
鼠标监听器相当于一个线程,点击一次在列表中新增一个小球对象
目标效果
三个线程,一个线程用于绘制,一个线程用于移动,监听器用于往列表中添加球对象
新增MoveThread线程实现类
修改BallThread类代码
删除绘制小球的代码。
修改监听器类UIListenner代码
修改后就不是每次点击鼠标都启动一个线程了。只在第一次按下鼠标时获取画笔,创建两个线程。
运行结果
问题分析
问题1:ArrayList不支持并发,BallThread类的run方法中对balls列表进行遍历绘制,MoveThread类的run方法也在对balls列表进行遍历移动,UIListenner类中鼠标每点击一次就给列表新增一个球。三个线程都对同一个列表操作,一边遍历一遍新增,迅速点击鼠标就会出现问题。
问题1解决方法:把两个线程实现类的run方法中的增强for循环改为普通for循环既可
问题2:点击鼠标次数很多很多时,发现小球的绘制移动速度下降
问题2解决方法:限制每个线程绘制小球的上限,比如200个。
问题2代码实现
创建多个队列,控制每个队列只add200个小球对象,每个队列用一个BallThread线程绘制,一个MoveThread线程移动。
V2版本如何实现不同的小球随机方向移动的
如果只点击一次鼠标 队列中只有一个球,BallThread线程会不断的绘制这个球,MoveThread线程会不断的移动球,由于遍历的都是同一个队列,视觉上就是一个球在不断的移动
如果点击两次鼠标,队列中有两个球,ball01和ball02,有不同的颜色尺寸位置和速度
对于ball01来说,每次移动的距离都是创建ball01时随机生成的speedx01和speedy01,是不变的,绘制的颜色尺寸也是创建ball01时随机生成的,也不会变。
对于ball02来说,每次移动的距离都是创建ball01时随机生成的speedx02和speedy02,也是不变的。绘制的颜色尺寸也是创建ball02时随机生成的,也不会变。
但是speedx01和speedx02不相等;speedy01和speedy02也不相等
视觉上的效果就是ball01和ball02以不同的速度移动
对于balls列表中的某个特定的ball对象来说,它的所有属性都是固定的,两个线程在处理这个特定的ball对象时,就可以实现这个ball的无限次被绘制和无限次被移动,且每次画出的小球是一样的,移动的距离也是一样的。
最终代码
package project3.lry0428V22;import java.awt.*;public class Ball {//private防止外部调用,写为私有的private int x, y, size, speedx, speedy;//speedx和speedy的正负可以代表方向private Color color;/*** 构造方法 构造所有的属性* @param x* @param y* @param size* @param speedx* @param speedy* @param color*/public Ball(int x, int y, int size, int speedx, int speedy, Color color){this.x = x;this.y = y;this.size = size;this.speedx = speedx;this.speedy = speedy;this.color = color;}/*** 绘制方法;把球画出来* @return*/public void draw(Graphics g){g.setColor (color);g.fillOval (x, y, size, size);//球的坐标是外切矩形左上角,size是矩形的宽高}/*** 球的移动** @return*/public void move(){// 要注意移动边界,坐标超过边界,速度反向// 球的坐标是外切矩形左上角,size是矩形的宽高,所以要-sizeif(x < 0 || x > 800 - size){speedx = -speedx;}if(y < 0 || y > 800 - size){speedy = -speedy;}x += speedx;//x移动speedxy += speedy;//y移动speedy}//以下为封装方法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;}public int getSize() {return size;}public void setSize(int size) {this.size = size;}public int getSpeedx() {return speedx;}public void setSpeedx(int speedx) {this.speedx = speedx;}public int getSpeedy() {return speedy;}public void setSpeedy(int speedy) {this.speedy = speedy;}public Color getColor() {return color;}public void setColor(Color color) {this.color = color;}}
package project3.lry0428V22;import java.awt.*;
import java.util.ArrayList;/*** 在V21的基础上修改BallThread的run方法中的增强for循环为普通for循环**/public class BallThread implements Runnable {//绘制球需要球对象以及画笔private ArrayList<Ball> balls;//队列存入小球private Graphics g;//构造方法,用于把球对象和画笔传进来public BallThread(ArrayList<Ball> balls, Graphics g){this.balls = balls;this.g = g;}@Overridepublic void run(){while(true){//死循环try {// 线程运行很快,休眠让人眼可以看见花球的过程,大概30ms就可以Thread.sleep (30);} catch (InterruptedException e) {e.printStackTrace ();}for (int i = 0; i < balls.size(); i++) {Ball ball = balls.get(i);ball.draw(g);}}}
}
package project3.lry0428V22;import javax.swing.*;/*** V22版本实现每个线程只能处理200个小球*/
public class GameUI extends JFrame {private UIListenner uiListenner = new UIListenner();//创建监听器对象//构造方法,进行界面初始化public GameUI() {initUI();uiListenner.setGui(this);//把窗体传给监听器}//界面初始化void initUI() {this.setTitle("线程游戏");this.setSize(800,800);this.setLocationRelativeTo(null);this.setDefaultCloseOperation(3);this.setVisible(true);this.addMouseListener(uiListenner);//给窗体添加鼠标监听器,传入监听器对象uiListenner,实现拖动监听this.addMouseMotionListener(uiListenner);//给窗体添加鼠标运动监听器,传入监听器对象uiListenner,实现按下监听}public static void main(String[] args) {new GameUI();}}
package project3.lry0428V22;import java.util.ArrayList;
/*** 在V21的基础上修改MoveThread的run方法中的for循环为普通for循环**/
public class MoveThread implements Runnable{private ArrayList<Ball> balls;//存多个球,队列//构造方法public MoveThread(ArrayList<Ball> balls){this.balls=balls;}@Overridepublic void run(){//遍历队列while(true){try {Thread.sleep (30);//休眠30ms,肉眼可见} catch (InterruptedException e) {e.printStackTrace ();}for(int i = 0; i < balls.size (); i++){Ball ball = balls.get (i);ball.move();}}}
}
package project3.lry0428V22;import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Random;public class UIListenner extends MouseAdapter {//继承MouseAdapter不用重写所有的方法private GameUI gui;//直接传gui,免得还要传画笔这种小弟。为了方便传参数,不用.来调用,写个set方法。放在最后Graphics g;Random ran = new Random();//ArrayList不支持并发ArrayList<Ball> balls1 = new ArrayList<>();ArrayList<Ball> balls2 = new ArrayList<>();ArrayList<Ball> balls3 = new ArrayList<>();//每次鼠标鼠标按下就创建一个线程@Overridepublic void mousePressed(MouseEvent e) {//鼠标按下就获取画笔if (g == null) {//加这个判断是因为,不要每次按下鼠标都要获取画笔,因为都是同一个窗体上获取的画笔,每次都拿是浪费资源g = gui.getGraphics();//UI类中窗体已经传过来了BallThread ballThread1 = new BallThread(balls1,g);BallThread ballThread2 = new BallThread(balls2,g);BallThread ballThread3 = new BallThread(balls3,g);MoveThread moveThread1 = new MoveThread(balls1);MoveThread moveThread2 = new MoveThread(balls2);MoveThread moveThread3 = new MoveThread(balls3);new Thread(ballThread1).start();//启动绘制线程new Thread(ballThread2).start();//启动绘制线程new Thread(ballThread3).start();//启动绘制线程new Thread(moveThread1).start();//启动移动线程new Thread(moveThread2).start();//启动移动线程new Thread(moveThread3).start();//启动移动线程}//准备球的数据:坐标xy,尺寸size,speedx,speedy,颜色int x = e.getX();int y = e.getY();int size = ran.nextInt(50) + 25;int speedx = 50;/*ran.nextInt(5) + 1;*/int speedy = 50;/*ran.nextInt(5) + 1;*/Color color = new Color(ran.nextInt(Integer.MAX_VALUE));//颜色取值是整数的最大值Ball ball = new Ball(x,y,size,speedx,speedy,color);//创建球对象,把上面的参数传进来if (balls1.size() < 200) {balls1.add(ball);//将每次创建的球加到列表中}else if (balls2.size() < 200) {balls2.add(ball);}else if (balls3.size() < 200) {balls3.add(ball);}// BallThread ballThread = new BallThread(ball,g);//每次鼠标按下就创建一个线程,给线程传入球和画笔
// new Thread(ballThread).start();//启动线程}//鼠标拖动@Overridepublic void mouseDragged(MouseEvent e) {}public void setGui(GameUI gui) {//为了方便传参数,不用.来调用,写个set方法this.gui = gui;}}