一、问题提出
利用Javafx界面设计一个弹球游戏,包括重玩和下一关的按键,并且显示游戏时间和得分。
二、解决思路
创建一个多线程的小球类,随机生成小球的位置和速度,并实现多个小球下落和碰撞效果;创建一个游戏窗体,显示各个按键和标签,并设置按键驱动,实现不同关卡的切换。设计的游戏规则为:计时10秒后判断界面中剩余小球即得分,按键下一关、重玩、挡板变短都会重新开始游戏,重新计时,按键键盘‘A’可以增加小球数量,最后显示出各次得分,以及最佳得分。
三、代码实现
1.球类定义
初始化
class BallThread extends Thread{
BallPane bp; //线程没有窗体类不能调用this
int n=0;
Random r=new Random();
double x;
double y;
double rad;
double vy;
double vx;
double a; //重力加速度
double d; //触碰边界摩擦力
double d2; //地面摩擦力
Circle c=new Circle(0,0,rad);;
boolean dragging=false;
public void init(){ //初始化
r=new Random();
x=300+r.nextDouble()*600-300; //0-600
y=250+r.nextDouble()*400-200; //50-450
c.setCenterX(x);
c.setCenterY(y);
rad=30+r.nextDouble()*30-25;//5-35
c.setRadius(rad);
vy=1.5+r.nextDouble()*4-1;//0.5-4.5
vx=1.5+r.nextDouble()*4-1;//0.5-4.5
a=0.08+r.nextDouble()*.06-.02;//0.06-0.12
d=.8;
d2=.99;
dragging=false;
c.setFill(Color.rgb(r.nextInt(255),r.nextInt(255),r.nextInt(255),r.nextDouble()));
}
public BallThread(BallPane bp){
this.bp=bp;
}
小球交叠调整
public void adjust(){
for(int i=0;i<10;i++)
for(BallThread b:bp.bt){
if(this==b||b==null)
continue;
double dist=distCen(b);
while (dist<=rad+b.rad){
x=300+r.nextDouble()*600-300;
y=250+r.nextDouble()*400-200;
c.setCenterX(x);
c.setCenterY(y);
rad=30+r.nextDouble()*30-25;
c.setRadius(rad);
dist=distCen(b);
}
}
}
小球初始距离
public double distCen(BallThread b){ //初始距离
double x=c.getCenterX();
double y=c.getCenterY();
double x2=b.c.getCenterX();
double y2=b.c.getCenterY();
return Math.sqrt((x-x2)*(x-x2)+(y-y2)*(y-y2));
}
小球运动后的距离
public double distCenNext(BallThread b){ //运动后的距离
double x=c.getCenterX()+vx;
double y=c.getCenterY()+vy;
double x2=b.c.getCenterX()+b.vx;
double y2=b.c.getCenterY()+b.vy;
return Math.sqrt((x-x2)*(x-x2)+(y-y2)*(y-y2));
}
小球运动函数
public void run() {
init(); //初始化
adjust();
c.setStroke(Color.BLACK);
c.setStrokeWidth(2);
bp.getChildren().add(c);
c.setOnMousePressed(e->{
vx=0;
vy=0;
dragging=true;
});
c.setOnMouseDragged(e->{ //鼠标拖动位置
c.setCenterX(e.getX());
c.setCenterY(e.getY());
});
c.setOnMouseReleased(e->{ //松开鼠标
dragging=false;
});
Timeline t=new Timeline(new KeyFrame(Duration.millis(20),e->{
if(dragging)
return;
vy+=a;
c.setCenterX(c.getCenterX()+vx);
if(c.getCenterX()+vx-rad>0 &&c.getCenterX()+vx+rad<600) //左右运动范围
c.setCenterX(c.getCenterX()+vx);
else if(c.getCenterX()+vx+rad>=600){ //右边界
c.setCenterX(600 - rad);
vx*=-d; //反弹
}
else{ //左边界
c.setCenterX(rad);
vx*=-d;
}
//碰撞中交叠处理
for(BallThread b:bp.bt){
if(this==b || b==null) //同一个小球
continue;
if(distCen(b)<=rad+b.rad) {
double s=rad+b.rad-distCen(b);
double x=c.getCenterX();
double y=c.getCenterY();
double x2=b.c.getCenterX();
double y2=b.c.getCenterY();
double k=(y-y2)/(x-x2);
double theta=Math.atan(k);
c.setCenterX(x+s*Math.cos(theta));
c.setCenterY(y+s*Math.sin(theta));
}
}
//小球碰撞
for(BallThread b:bp.bt){
if(this==b || b==null) //同一个小球
continue;
if(distCenNext(b)<=rad+b.rad) {
vx *= -d2;
vy *= -d;
}
}
//挡板向下移
if(c.getCenterY()+rad>bp.pad.getY()-bp.pad.getHeight()/2 //小球下边缘在挡板上
&& c.getCenterY()-rad<=bp.pad.getY()+bp.pad.getHeight()/2 //小球上边缘在挡板下
&& c.getCenterX()>=bp.pad.getX()
&& c.getCenterX()<=bp.pad.getX()+bp.pad.getWidth()){
c.setCenterY(
bp.pad.getY() - bp.pad.getHeight()/2 - c.getRadius());
vx*=d2;
vy*=-d;
}
if(c.getCenterY()+rad<bp.pad.getY()*1.1&& //碰到挡板反弹
c.getCenterY()+vy+rad>=bp.pad.getY()&&
c.getCenterX()>=bp.pad.getX()&&
c.getCenterX()<=bp.pad.getX()+bp.pad.getWidth()){
c.setCenterY(bp.pad.getY() - c.getRadius());
vx*=d2;
vy*=-d;
}
else
c.setCenterY(c.getCenterY()+vy);
//先碰撞再下落,否则会出现小球进入另一个小球内继续下落
}));
t.setCycleCount(Timeline.INDEFINITE);
t.play();
}
}
2.主界面定义
class BallPane extends Pane{
BallThread[] bt;
Rectangle pad;
MyTask myTask = new MyTask();
public int judge(){ //判断有几个球在界面内
int n=0;
for(int i=0;i< bt.length;i++)
if(bt[i].c.getCenterX()+bt[i].c.getRadius()<=600
&&bt[i].c.getCenterX()-bt[i].c.getRadius()>=0
&&bt[i].c.getCenterY()+bt[i].c.getRadius()<=600
&&bt[i].c.getCenterY()-bt[i].c.getRadius()>=0)
n++;
return n;
}
public void newcre() //重新初始化
{
for(int i=0;i<bt.length;i++) {
bt[i].init();
bt[i].adjust();
}
myTask.restart();
myTask.setStartNumber(-1);
}
设置界面初始布局,包括标签,挡板,小球等
public BallPane() throws InterruptedException {
Label count=new Label();
Label label=new Label("time:");
label.setFont(new Font("Times New Roman",20));
count.setFont(new Font("Times New Roman",20));
label.setLayoutX(450);
label.setLayoutY(20);
count.setLayoutX(500);
count.setLayoutY(20);
TextField tf=new TextField("score:0");
tf.setFont(new Font("Times New Roman",20));
tf.setLayoutX(100);
tf.setLayoutY(20);
tf.setPrefWidth(100);
TextField tf2=new TextField("bset score:0");
tf2.setFont(new Font("Times New Roman",20));
tf2.setLayoutX(200);
tf2.setLayoutY(20);
tf2.setPrefWidth(150);
pad=new Rectangle(400,500,250,10); //挡板
pad.setFill(Color.INDIANRED);
pad.setStroke(Color.BLACK);
pad.setStrokeWidth(2);
this.getChildren().addAll(pad,count,label);
this.setOnMouseMoved(e->{
pad.setX(e.getX()-pad.getWidth()/2);
});
bt=new BallThread[10];
for(int i=0;i<bt.length;i++)
{
bt[i]=new BallThread(this);
bt[i].run();
}
myTask.restart();
count.textProperty().bind(myTask.messageProperty());
关卡设置
System.out.print("第一关:");
Timer timer=new Timer();
TimerTask task=new TimerTask(){
public void run(){
if(Integer.parseInt(tf2.getText(11,tf2.getLength()))<judge())
tf2.setText("\tbest score:"+judge());
tf.setText("score:"+judge());
System.out.println(judge());
timer.cancel();
}
};
timer.schedule(task,10000); //第一关时间
按键盘A,可增加小球
this.setOnKeyPressed(e->{
if(e.getCode()== KeyCode.A) //按键A 增加小球
{
System.out.print("增加了10个小球:");
for(int i=0;i<bt.length;i++) {
bt[i]=new BallThread(this);
bt[i].run();
}}});
功能按钮设置
Button b=new Button("reset"); //重新开始
b.setFont(new Font(30));
b.setLayoutY(520);
b.setOnAction(e->{
tf.setText("score:0");
System.out.print("本关重新开始:");
newcre();
Timer timer1=new Timer();//第二关时间
TimerTask task1=new TimerTask(){
public void run(){
if(Integer.parseInt(tf2.getText(11,tf2.getLength()))<judge())
tf2.setText("\tbest score:"+judge());
tf.setText("score:"+judge());
System.out.println(judge());
timer1.cancel();
}
};
timer1.schedule(task1,10000);
});
Button b2=new Button("next"); //下一关 可垂直移动
b2.setFont(new Font(30));
b2.setLayoutY(520);
b2.setLayoutX(450);
b2.setOnAction(e->{
tf.setText("score:0");
System.out.print("第二关:");
newcre();
Timer timer2=new Timer();
TimerTask task2=new TimerTask(){
public void run(){
if(Integer.parseInt(tf2.getText(11,tf2.getLength()))<judge())
tf2.setText("\tbest score:"+judge());
tf.setText("score:"+judge());
System.out.println(judge());
timer2.cancel();
}
};
this.setOnMouseMoved(e2->{
pad.setX(e2.getX()-pad.getWidth()/2);
pad.setY(e2.getY()-pad.getHeight()/2);
});
timer2.schedule(task2,10000);
});
Button b3=new Button("short");
b3.setFont(new Font(30));
b3.setLayoutY(520);
b3.setLayoutX(200);
b3.setOnAction(e->{
tf.setText("score:0");
System.out.print("挡板变短后:");
pad.setWidth(pad.getWidth()-50);
newcre();
Timer timer3=new Timer();
TimerTask task3=new TimerTask(){
public void run(){
if(Integer.parseInt(tf2.getText(11,tf2.getLength()))<judge())
tf2.setText("\tbest score:"+judge());
tf.setText("score:"+judge());
System.out.println(judge());
timer3.cancel();
}
};
timer3.schedule(task3,10000);
});
this.getChildren().addAll(b,b2,b3,tf,tf2);
}
}
3.任务类定义
class MyTask extends Service<Void> {
private int startNumber = -1;
public void setStartNumber(int startNumber) {
this.startNumber = startNumber;
}
@Override
protected Task<Void> createTask() {
return new Task<Void>() {
@Override
protected Void call() throws Exception {
if (startNumber == -1)startNumber = 0;
for (int i = startNumber; i <=10000 ; i++) {
updateMessage(Integer.toString(i));
Thread.sleep(1000);
}
return null;
}
};
}
}
4.主函数定义
public class BallFall extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
BallPane();
Scene s=new Scene(p,600,600);
primaryStage.setScene(s);
primaryStage.show();
}
}
四、结果分析
初始界面:
第一关10秒后:
第一关重试:reset
第二关:next
挡板变短后:short
控制台打印出每次得分:
未到10秒显示当前得分为0:
超过10秒仍可以继续游戏,但不会再计分:
五、实验总结
为实现多个小球共同运动,需要将它们设置为多线程任务,互不干扰,实验中设置重力加速度和摩擦力模拟小球运动,每隔一定时间更新小球坐标即可实现运动的动画效果,若不同小球间发生碰撞则令它们速度反向,若出现小球重叠的现象需要重新设置它们的坐标。小球碰到挡板会发生反弹,但会出现挡板移动过快时从上往下穿过小球并接住,需要重新修改小球坐标。
对于窗体界面,设计三个按钮以及一个键盘事件实现不同的事件驱动,难点在于计时器的实现,我设计了一个mytask类来实现,每过一秒更新显示。另外,当时间到达10秒时打印出当前界面中小球,我利用的是TimerTask来实现延迟事件,但是存在一定问题,进行下一个TimerTask时必须先取消上一个,即必须完成了第一关才能点击下一个按键,点击按键后会重置计时器以及新建TimerTask。
对于游戏规则,可以计时(记录达到指定小球数的时间),也可以计分(记录在指定时间内达到的小球数),我选择的是计分,但是我只能将每次得分打印出来,不能显示在图形界面中,我试过将得分设置为标签文字,但是报错“Not on FX application thread;”,于是我改用textfild可以实现,显示当前得分以及最佳得分,还将每一次的得分都打印出来,程序结束时也能得知本次游戏过程。另外,为了避免代码冗余,可以将一些重复的代码改为方法,需要时调用即可。