注:本内容会随时更新,从基础到复杂,直到处理完善。
在前面讲述了进程,线程,并发,并行的概念,以及线程如何启动,现在开始制作一个线程小游戏。
目前完成的功能有:
面板创建
一个线程控制多个小球
小球的运动
小球不会运动到界面以外
小球之间的相互碰撞
左键控制添加小球,右键控制小球暂停
不会出现闪屏
监听键盘,A,D键控制左右移动;空格键控制游戏开始
添加背景图片并保证循环显示
创建一个ThreadUI类,用于创建窗体添加面板以及运行程序。因为是在窗体添加的面板上体现动态,所以要在面板上获取画笔,再添加鼠标点击监听器和键盘监听器,在使用键盘监听器时,需要获取焦点requestFocus()才可以使用。
import javax.swing.*;
import java.awt.*;
/**
* 线程游戏
*/
public class ThreadUI {
//使用静态变量(类变量)
static JPanel jPanel = new JPanel();
public void initUI()
{
JFrame jf = new JFrame();
jf.setSize(900,900);
jf.setTitle("线程游戏");
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setLocationRelativeTo( null);
//面板对象
jPanel.setBackground(Color.WHITE);
jf.add(jPanel,BorderLayout.CENTER);
jf.setVisible(true);
//获取面板上的画笔对象
Graphics g = jPanel.getGraphics();
//监听器
ThreadListener listener = new ThreadListener(g);
jPanel.addMouseListener(listener);
//添加键盘监听器
jPanel.addKeyListener(listener);
//让事件源得到焦点,获取焦点
jPanel.requestFocus();
}
public static void main(String[] args) {
ThreadUI ui = new ThreadUI();
ui.initUI();
}
}
创建一个监听器类ThreadListener类,用于监听事件。extends MouseAdapter比implements MouseListener更优化的点在于,extends MouseAdapter不需要重写MouseListener中的所有代码,可以简化程序。将画笔引用传递到监听器中,在点击类中定义坐标位置x,y。然后创建线程对象tb,通过tb.start()启动线程,(每个线程只能启动一次)。判断线程对象为空时才能创建线程对象,线程对象最后通过鼠标点击监听器的左键来控制小球的更新创建,右键控制小球暂停运动。在键盘监听器中键盘按压方法里面获取按键,使用case语句判断键盘的内容,A键左移,D键右移,空格键控制线程的开始,在键盘释放方法里面设置速度为0,这样就可以确保按压时的一动一定。
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
/**
* 鼠标监听器-时间处理类
*/
public class ThreadListener extends MouseAdapter implements KeyListener, ActionListener {
//画笔 引用传递
public Graphics g;
//动作数组
public ArrayList<Ball> balls = new ArrayList<>();
//线程对象
public ThreadBall tb;
//玩家对象
public MRect mr;
public int speedX = 5;
//构造方法初始化属性
public ThreadListener(Graphics g)
{
this.g = g;
}
@Override
public void mouseClicked(MouseEvent e)
{
//获取坐标
int x = e.getX();
int y = e.getY();
//判断鼠标左键,右键问题
if (e.getButton() ==1)
{
//创建Ball对象
Ball ball = new Ball(x, y, balls);
balls.add(ball);
} else if (e.getButton() == 3) {//右键
for (int i = 0; i < balls.size(); i++)
{
Ball ball = balls.get(i);
ball.start = !ball.start;
}
}
}
@Override
public void actionPerformed(ActionEvent e) {
ThreadUI.jPanel.requestFocus();
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
switch (key)
{
case KeyEvent.VK_A:
mr.spaceX = -speedX;
break;
case KeyEvent.VK_D:
mr.spaceX = speedX;
break;
case KeyEvent.VK_SPACE://开始游戏
//线程对象为空
//在启动线程之前,创建玩家对象
if (tb == null)
{
//创建玩家对象
mr = new MRect(400,700);
//创建线程对象
tb = new ThreadBall(g, balls,mr);
//启动线程,每个线程只能启动一次
new Thread(tb).start();
}
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
switch (key) {
case KeyEvent.VK_A:
mr.spaceX = 0;
break;
case KeyEvent.VK_D:
mr.spaceX = 0;
break;
}
}
}
创建一个ThreadBall类,这是一个自定义线程类,首先将画笔,坐标引入,再调用run()方法,通过sleep()方法控制小球运动的速度,设置黑球运动。run()方法是由主线程来执行,不能直接调用run()方法,必须先调用start()方法,因为run方法不会开辟新的栈空间。
出现闪屏的原因是每次按顺序添加小球之后,白色添加一次,黑色添加一次,就会出现一闪一闪的现象,解决办法是在run方法里创建一个缓冲图片,在缓冲图片上面画画就要在上面添加画笔buffG,通过画笔绘制整个游戏背景,绘制完之后再将其添加到面板中显示界面。
在绘制游戏背景时,也需要将图片插入缓冲图片中去,在线程run方法中的死循环中添加两张图片,将两张图片拼接起来,这样可以实现当上面一张图片恰好满屏时,将下面的图片y轴设置为0,可以循环利用两张图片,避免图片的“不够用”。
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
public class ThreadBall implements Runnable {
public Graphics g;
public ArrayList<Ball> balls;
public MRect mr;
public int x,y,w,h;
//加载背景图片
public Image image;
//初始化属性
public ThreadBall(Graphics g, ArrayList<Ball> balls, MRect mr)
{
this.g = g;
this.balls = balls;
this.mr = mr;
image = new ImageIcon("C:\\Users\\bkf\\IdeaProjects\\SummerTest\\.idea\\img_1.png").getImage();
}
//run方法是线程启动后自动执行的方法
//该方法执行完,当前线程的所有资源被回收(不能再次启动)
public void run()
{
System.out.println(Thread.currentThread().getName()+"线程启动");
//创建缓冲图片,避免出现闪屏
BufferedImage bufferedImage = new BufferedImage(ThreadUI.jPanel.getWidth(),
ThreadUI.jPanel.getHeight(),BufferedImage.TYPE_INT_RGB);
//获取缓冲图片的画笔
Graphics buffG = bufferedImage.getGraphics();
w = bufferedImage.getWidth();
h = bufferedImage.getHeight();
//循环绘制
while(true)
{
//延时
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//绘制整个游戏背景
buffG.drawImage(image,x,y-h,w,h,null);
buffG.drawImage(image,x,y++,w,h,null);
if (y - h >= 0){
y = 0;
}
mr.drawRect(buffG);
//取出所有的小球对象
for (int i = 0; i < balls.size(); i++)
{
Ball ball = balls.get(i);
ball.drawBall(buffG);
ball.collision();
}
//把缓冲图片显示在界面
g.drawImage(bufferedImage,0,0,null);
}
}
}
创建一个Ball类,如果要实现一个线程管理所有小球,那么就要定义一个小球类,将小球的所有属性定义出来,在构造方法中定义小球基础属性,这样就可以在创建类时定义出小球,x,y表示小球坐标,speedX,speedY表示小球分别在x轴,y轴的速度,size表示小球的大小。
在小球中创建一个collision类用于解决小球碰撞问题。首先需要获取两个小球对象,通过计算这两个小球的距离与刚好碰撞时的距离相比较(也就是小球的直径)来判断小球是否会发生碰撞,如果碰撞之后则交换他们的速度。
在定义的属性中有一个布尔类型的start,它用来控制小球暂停,还需要每次点击的时候将start取反,内容在ThreadListener中:ball.start = !ball.start;。
import java.awt.*;
import java.util.ArrayList;
public class Ball {
//属性
public int x, y;
public int speedX, speedY;
public int size;
public ArrayList<Ball> balls;
public boolean start = true;
public Ball(int x, int y, ArrayList<Ball> balls)
{
this.x = x;
this.y = y;
this.balls = balls;
this.size = 50;
this.speedX = 3;
this.speedY = 3;
}
//画小球
public void drawBall(Graphics g)
{
g.setColor(Color.BLACK );
g.fillOval(x,y,size,size);
//控制移动
if ( start)
{
x += speedX;
y += speedY;
}
//判断是否需要反弹
if (x > ThreadUI.jPanel.getWidth() - size || x < 0)
{
speedX = -speedX;
}
if (y > ThreadUI.jPanel.getHeight() - size || y < 0)
{
speedY = -speedY;
}
}
//小球碰撞
public void collision()
{
//遍历所有小球对象
for (int i = 0;i<balls.size();i++)
{
Ball ball = balls.get(i);
int w =this.x - ball.x;
int h = this.y - ball.y;
double len = Math.sqrt(w * w + h * h);
if (len < size)
{
int tx = this.speedX;
this.speedX = ball.speedX;
ball.speedX = tx;
int ty = this.speedY;
this.speedY = ball.speedY;
ball.speedY = ty;
}
}
}
}
创建一个MRect类:和创建小球方法一样,通过画笔绘制,本类先初步绘制好玩家移动体验界面。
import java.awt.*;
public class MRect {
public int x,y,size;
public int spaceX,spaceY;
public MRect(int x, int y)
{
this.x = x;
this.y = y;
size = 100;
}
public void drawRect(Graphics g)
{
g.setColor(Color.RED);
g.fillRect(x, y, size, size);
move();
}
public void move()
{
x += spaceX;
y += spaceY;
}
}
运行界面如下:运行效果是可以运动的。