1.1目的
利用多线程的思想,编写一个游戏。
1.2背景
1).游戏名称:弹球游戏。
2).开发环境:IDEA。
3).游戏规则:小球实现从不同角度,不同坐标落下,与挡板接触后反弹并加分,落在地面上则游戏结束失败。
由五个类构成:
1).三个线程类分别控制球的移动,球板的移动和画面的重绘。
2).UI类控制界面的初始化和绘制。
3).Main类用来创建线程和启动线程。
2.2游戏初始化界面设计
1).游戏界面显示:点击开始游戏。
2).点击以后,游戏状态改变,显示开始游戏界面。
2.3游戏开始界面设计
1).画出两个对象:球和球板。
2).显示得分。
2.4游戏结束界面设计
1).给出提示信息:游戏结束以及得分情况。
2).给出提示信息:按下空格可以重新开始。
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
public class Ui extends JFrame implements KeyListener ,MouseListener{
//判断球还活着
static boolean blIsOver;
//分数
static int intSore;
//定义游戏状态 0:游戏未开始/1:游戏开始/2:暂停/3:结束
static int state=0;
//双缓存解决界面闪烁
Image offScreenImage=null;
//构造函数
public Ui() {
initFrame();
initData();
addKeyListener(this);
addMouseListener(this);
setVisible(true);
}
public void initFrame(){
setTitle("弹球游戏");
setBackground(Color.WHITE);
setSize(900,600);
//setLocationRelativeTo(null);
setLocation(300,50);
setResizable(false);
setFocusable(true); //获取键盘焦点,将键盘聚集在游戏界面上
//setVisible(true);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}//初始化窗体
public void initData(){
//球的初始化坐标
ThreadBall.PositionX=(int)(Math.random()*100)+300;//300-400
ThreadBall.PositionY=(int)(Math.random()*100)+100;//100-200
//初始化角度
ThreadBall.intDu= (int)(Math.random()*3)+1;//1-4
//初始化球板
ThreadPaddles.PositionA=0;
ThreadPaddles.PositionB=450;
//初始化
intSore=0;
System.out.println("state="+state);
}//初始化数据
public void paint(Graphics gImage) {
if(offScreenImage==null){
offScreenImage=createImage(900,600);
}
//获取画笔对象
Graphics g=offScreenImage.getGraphics();//画笔对象
g.fillRect(0,0,900,600); //填充一个宽900,高600的区域
if(state==0){//游戏未开始
g.clearRect(0, 0, 900, 600);//0,0:是相对于容器的坐标
//g.drawImage(GameUtils.bgImg,0,0,null);
g.setFont(new Font("仿宋",Font.BOLD,40));
g.drawString("点击开始游戏",300,300);
repaint();
}
if(state==1){
g.clearRect(0, 0, 900, 600);//0,0:是相对于容器的坐标
//球板:fillRect用笔刷g(红色)填充一个矩形(从左上边界开始填充)
g.setColor(Color.red);
g.fillRect(ThreadPaddles.PositionA,ThreadPaddles.PositionB,
ThreadPaddles.RecWidth,ThreadPaddles.RecHeight);
//球:设置画笔颜色为绿色,fillOval用笔刷g(绿色)填充一个圆(从左上边界开始填充)
g.setColor(Color.green);
g.fillOval(ThreadBall.PositionX,ThreadBall.PositionY, ThreadBall.BallWidth,
ThreadBall.BallHeight);
g.setFont(new Font("宋体", ALLBITS, 50));
g.setColor(Color.BLUE);
g.drawString(new String("分数:" + String.valueOf(intSore)), 550, 150);
repaint();
}
if(state==3){//游戏结束
//if(blIsOver) {//绘制游戏结束的界面
g.clearRect(0, 0, 900, 600);//0,0:是相对于容器的坐标
g.setFont(new Font("宋体", ALLBITS, 50));
g.setColor(Color.RED);
g.drawString(new String("游戏结束!你的得分:" +
String.valueOf(intSore)) , 249, 250);
g.setColor(Color.BLUE);
g.drawString(new String("按下空格重新开始"), 250, 350);
repaint();
//}
}
//将绘制好的图片一次性呈现出来
gImage.drawImage(offScreenImage,0,0,null);
}//画笔
//监听
@Override//鼠标监听
public void mouseClicked (MouseEvent e){
if (e.getButton() == 1 && state == 0) {//按下鼠标或者状态为没开始
state = 1;//更改游戏状态并且重绘
repaint();
System.out.println("state=" +state);
}
}
@Override//键盘监听
public void keyPressed (KeyEvent e){
char b = e.getKeyChar();
int a = e.getKeyCode();
if (b == 'd' || b == 'D' || a == 39) {
ThreadPaddles.PositionA+=10;
} else if (b == 'a'||b == 'A' || a == 37) {
ThreadPaddles.PositionA-=10;
} else if(a==32&&state==3){
state=0;
initData();
repaint();
}
/*if(a==32){//暂停
switch (state){
case 1:
state=2;
System.out.println("s"+state);
break;
case 2:
state=1;
System.out.println("s"+state);
break;
default:
}
}*/
}
@Override
public void mousePressed (MouseEvent e){
}
@Override
public void mouseReleased (MouseEvent e){
}
@Override
public void mouseEntered (MouseEvent e){
}
@Override
public void mouseExited (MouseEvent e){
}
@Override
public void keyTyped (KeyEvent e){
}
@Override
public void keyReleased (KeyEvent e){
}
}
3.1.1 框架结构
1).属性:
(1)游戏状态=0: 0:游戏未开始/1:游戏开始/2:暂停/3:结束。
(2)分数=0。
(3)缓存画布:解决页面闪烁。
2).方法:
(1)无参构造。
(1)初始化窗体。
(2)初始化数据。
(3)重写鼠标监听和键盘监听。
(4)画笔。
3.1.2 构造函数
构造方法中调用了两个初始化方法和添加了两个监听。
3.1.3 窗体初始化
1).Init Frame用于初始化窗体。
2).标题:弹球游戏;
背景色:白色;
窗体大小:900*600;
窗体位置:(300,50);
窗体大小不可变;
键盘焦点汇聚在界面上;
设置退出方式:按下右上角×就可以结束进程。
3.1.4 数据初始化
1).initData用于初始化数据。
2).初始化球的坐标:用random函数获得一个随机数:x(300-400),y(100-200)。
3).初始化球的角度:用random函数获得一个随机数:intDu(1-4)。
4).初始化球板的坐标:(0,450)。
5).初始化分数:0。
3.1.5 Paint方法
1).state=0:定义了游戏未开始的界面。
2).在界面上画一个字符串“点击开始游戏”。
3).添加一个鼠标监听,当按下鼠标后,游戏状态=“1”。
1).state=1:定义了游戏开始的界面。
2).在界面上画一个绿色球和一个红色的球板。
3).在界面上画一个字符串“分数:”。
1).tate=3:定义了游戏结束的界面。
2).在界面上画两个字符串“游戏结束1 分数:”和提示信息“按下空格后重新开始”。
3).添加一个键盘监听,当按下空格,state=0,游戏重新开始。
3.1.6 监听
1).鼠标监听:当游戏状态state=0游戏未开始时,检测到鼠标按下,则将游戏状态state改为1,并且重绘。
2).键盘监听:
(1)监听键盘A/D/a/d/←/→按键,用来控制球板的坐标。
(2)当游戏状态state=3游戏结束时,监听空格键,检测到空格键按下时,将状态state改为0,初始化数据,并且重绘界面。
public class ThreadBall extends Thread{
//球的坐标及大小
static int BallWidth=25,BallHeight=25,PositionX,PositionY;
static int intDu;
static boolean blUpOrDown;//游戏状态开始?
public ThreadBall(){}
public void run(){
while(true){
if(Ui.state==1) {
if (PositionY >= 600) {
blUpOrDown=true;
Ui.state = 3;//游戏状态为结束
Ui.blIsOver = true;//游戏结束
System.out.println("state="+Ui.state);
} else if (PositionY < 600) {
Ui.state = 1;//游戏状态为开始
blUpOrDown = false;
}
//向上碰撞 上墙面的情况
if (PositionY <= 0) {
if (intDu == 3) { intDu = 1; }
else if (intDu == 4) { intDu = 2;}
}
//左侧墙面碰撞
if (PositionX <= 0) {
if (intDu == 2) { intDu = 1; }
else if (intDu == 4) { intDu = 3; }
}
//右侧墙面碰撞
if (PositionX >= 900) {
if (intDu == 1) { intDu = 2; }
else if (intDu == 3) { intDu = 2; }
}
}
if(!blUpOrDown){
switch (intDu){
//1为右下方行进,2为左下方,3 为右上方,4为左上方
case 1:
PositionY+=6;
PositionX+=6;
break;
case 2:
PositionY+=6;
PositionX-=6;
break;
case 3:
PositionY-=6;
PositionX+=6;
break;
case 4:
PositionY-=6;
PositionX-=6;
break;
}
}
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public int getBallWidth() {
return BallWidth;
}
public void setBallWidth(int ballWidth) {
BallWidth = ballWidth;
}
public int getBallHeight() {
return BallHeight;
}
public void setBallHeight(int ballHeight) {
BallHeight = ballHeight;
}
public int getPositionX() {
return PositionX;
}
public void setPositionX(int positionX) {
PositionX = positionX;
}
public int getPositionY() {
return PositionY;
}
public void setPositionY(int positionY) {
PositionY = positionY;
}
public int getIntDu() {
return intDu;
}
public void setIntDu(int intDu) {
this.intDu = intDu;
}
}
3.2.1 框架结构
1).属性:球的坐标,宽高和角度。
2).方法:
(1)重写Run方法。
(2)set和get方法。
3.2.2 run方法
1).首先判断游戏是否结束:
当状态为1时,并且小球坐标<600,继续,否则,将游戏状态改为3结束。
2).当小球存活时:判断与界面碰撞的情况,由入射角度将角度设置为相对应的初始角度。
3).用一个Switch语句控制小球运动的坐标。
4).sleep中的休眠时间控制小球移动速度,数值越小,小球移动速度越快。
import javax.swing.*;
public class ThreadPaddles extends Thread{
//球板
static int PositionA=0,PositionB=450,RecWidth=200,RecHeight=20,Width=900;
public ThreadPaddles(){}
public void run(){
while(true){
if(Ui.state==1&&ThreadBall.PositionX>PositionA&&ThreadBall.PositionX+25<=PositionA+RecWidth){
if(ThreadBall.PositionY>=PositionB&&ThreadBall.PositionY<=PositionB+RecHeight){
//ThreadBall.blUpOrDown=true;
Ui.intSore+=10;
System.out.println(Ui.intSore);
switch (ThreadBall.intDu){
case 1:
ThreadBall.intDu=3;
break;
case 2:
ThreadBall.intDu=4;
break;
}
JOptionPane.showMessageDialog(null,ThreadBall.PositionX,"2", JOptionPane.INFORMATION_MESSAGE);
}
}
try {
sleep(100);//参数改小时,分数会错误,因为执行的次数多。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public int getPositionA() {
return PositionA;
}
public void setPositionA(int positionA) {
PositionA = positionA;
}
public int getPositionB() {
return PositionB;
}
public void setPositionB(int positionB) {
PositionB = positionB;
}
public int getRecWidth() {
return RecWidth;
}
public int getRecHeight() {
return RecHeight;
}
}
3.3.1框架结构
1).属性:球板的坐标,宽高。
2).方法:
(1)重写Run方法。
(2)set和get方法。
3.3.2 run方法
1).当游戏状态为1开始状态,并且小球横坐标>球板的横坐标,并且小球的横坐标+宽度<球板的横坐标+板子宽度。并且小球的纵坐标<球板纵坐标,并且小球纵坐标>球板纵坐标+球板高度时,小球与球板碰撞。分数+10。
2).碰撞后重新设置角度。
3).sleep中的休眠时间控制球板移动速度,数值越小,控制球板移动速度越快。
public class ThreadControle extends Thread {
Ui ui=new Ui();
public void run(){
if (Ui.state==1) {//还活着,就重画
while (true) {
ui.repaint();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.4.1框架结构
1).实例化UI类
2).重写run方法。
3.4.2 run方法
1).当游戏状态为开始时,不断重绘界面。
public class Main {
public static void main(String[] args) {
//创建三个线程
ThreadControle threadControle = new ThreadControle();
ThreadBall threadBall = new ThreadBall();
ThreadPaddles threadPaddles = new ThreadPaddles();
//启动他们
threadPaddles.start();
threadControle.start();
threadBall.start();
try {
threadPaddles.join();//防止线程堵塞,相当于用了wait()
threadControle.join();
threadBall.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1).创建三个线程类对象
2).启动三个线程对象。
3).在try/catch中调用join,防止线程堵塞。
4.1游戏状态切换测试
4.1.1未开始-开始
state=0——state=1
4.2游戏运行bug改进
4.2.1 小球和球板碰撞bug
1.Bug描述:
休眠时间太长会造成:明明小球和球板已经已经碰上了,小球却穿过了球板并没有反弹。
休眠时间太短会造成:线程堵塞,分数出现错误。
2.解决方案:
1.选取合适的休眠值,并且将小球和球板重绘的休眠时间改为一样。
2.经过测试,选取100ms是比较合适的。
4.2.2 游戏界面闪烁
1.问题描述:
小球和球板实现视觉上的移动,原理就是在间隔很小的时间,将这些对象绘制在窗体上。由于是不断绘制的,会造成视觉上的闪烁。
2.解决方案:
定义一个缓冲画布,将这些对象一次性绘制在缓冲画布上,再一次性绘制出来,就能解决频闪问题。
1)建立了三个线程类,小球线程,球板线程,重绘线程,理解了多线程思想和怎么创建多线程。
(a)建立线程类有两种方法,一种是继承Thread,另一种是继承Implement接口,我采用的是第一种方法:继承Thread类。
(b)继承Thread后要重写run方法。我在run方法中写入了小球的坐标改变,球板的坐标改变,重绘。
(c)在Main函数中实例化三个线程类,线程实例化后的对象调用run方法启动线程。
(d)线程停止,采用用标志位的方法。
(e)线程休眠:运用在了控制球板和球的移动,重绘依次休眠一会,休眠时间越短,重绘速度越快,移动速度越快。
2)UI类实现了窗体的构建,利用构造函数初始化窗体,将初始化的语句封装成函数写入构造函数。 UI类继承了监听接口,所以要重写监听函数。继承了JFrame类,重写paint方法,获取画笔,将需要的图形对象画在缓冲区,再一次性呈现出来。
3)监听:实现监听接口,重写监听函数。
4)可以优化的地方:
(a)界面不够美观。
(b)游戏规则比较简单,可以添加一个时间线程,可以随着时间的增加,掉下来的小球数目增加,或者障碍物增加。
可以添加背景音乐线程,随着游戏的开