要实现小球
的运动
,可以从以下几点切入:
小球都有那些具体特征?
- 涉及动能定理就需要考虑质量了,除此之外常规的几个变量也不能忘:方向、球的尺寸,所在位置以及当前速度
谁能初始小球的状态?
- 小球的状态无非两种:(随机)默认值、人工手动输入
谁能控制小球的运动?
- 我们控制小球是需要给予一定的指令的,就算是鼠标点击也是简单的指令之一,除此之外如果想要拥有稍微复杂一点的指令可以使用按钮来实现
根据分析,我们大概能构造出大概的类,无非是一个专门描述小球状态的,一个控制所有命令的,一个构建出窗口和选项的。我们还可以在细化这三个类的功能:
球类
因为窗口也是二维的
构造方法仅需要包含:
- 质量 - 沿x、y轴的分速度 - 二维坐标表示的位置 - 颜色 - 大小
画个小球
- 颜色
- 尺寸
移动小球
- 遇到边界直接反弹
- 移动距离利用公式:距离=当前距离+速度×时间(这边直接简化成1)
- 小球之间的完全弹性碰撞,根据公式:
$$\frac{{{\rm{(}}{{\rm{m}}_1} - {m_2}){v_{10}} + 2{m_2}{v_{20}}}}{{{{\rm{m}}_1} + {m_2}}}$$
$$\frac{{{\rm{(}}{{\rm{m}}_2} - {m_1}){v_{20}} + 2{m_1}{v_{10}}}}{{{{\rm{m}}_1} + {m_2}}}$$
代码如下:
import java.awt.Color;
import java.awt.Graphics;
import java.util.ArrayList;
import javax.swing.JFrame;
public class Ball{
/**
* declare variables of ball
*/
private int xPos, yPos, size, xSpeed, ySpeed,mass;
private Color color;
/**
* Constructor for objects of class Ball
*/
public Ball(int xPos, int yPos, int size, int xSpeed, int ySpeed, Color color,int mass) {
super();
this.xPos = xPos;
this.yPos = yPos;
this.size = size;
this.xSpeed = xSpeed;
this.ySpeed = ySpeed;
this.color = color;
this.mass=mass;
}
/**
* use method of Graphics to create a colorful ball
* @param g A ball
*/
public void drawBall(Graphics g) {
g.setColor(color);
g.fillOval(xPos, yPos, size, size);
}
/**
* This method tries to figure out
* @param g A ball
* if ball is going to hit the boundary,change its direction
*/
public void moveBall(Graphics g,BallFrame bf) {
if (xPos+size>bf.getWidth() || xPos <0)
{
xSpeed = -xSpeed;
}
if (yPos<0 ||(yPos+size >bf.getHeight()-160))
{
ySpeed = -ySpeed;
}
xPos += xSpeed;
yPos += ySpeed;
}
public void collision(Graphics g, ArrayList<Ball> balls) {
for (int i = 0; i < balls.size(); i++) {
Ball ball = (Ball) balls.get(i);
if (ball != this) {
double d1 = Math.abs(this.xPos - ball.xPos);
double d2 = Math.abs(this.yPos - ball.yPos);
double d3 = Math.sqrt(Math.pow(d1,2) + Math.pow(d2,2));
if (d3 <= (this.size / 2 + ball.size / 2)) {
/**
* Using perfectly elastic collision
*/
this.xSpeed=((this.mass-ball.mass)*this.xSpeed+2*ball.mass*ball.xSpeed)/(this.mass+ball.mass);
this.ySpeed=((this.mass-ball.mass)*this.ySpeed+2*ball.mass*ball.ySpeed)/(this.mass+ball.mass);
ball.xSpeed=((ball.mass-this.mass)*ball.xSpeed+2*this.mass*this.xSpeed)/(this.mass+ball.mass);
ball.ySpeed=((ball.mass-this.mass)*ball.ySpeed+2*this.mass*this.ySpeed)/(this.mass+ball.mass);
}
}
}
}
}
2.事件监听器(鼠标、按钮和文本框)
- 构造方法中需要同时引入涉及文本框、按钮和小球所在的类
- 如果点击鼠标,生成一个除了大小给定其他随机的彩色小球
- 如果点“Play” 文本框的信息被读取,生成指定的小球
- 如果点“Stop” 小球停止运动但不消失
- 如果点“Reset” 文本框恢复默认值,用户可以选择重新输入
- 如果点“Continue” 小球继续刚刚的运动
- 如果点“Clear” 小球停止运动且线程立即中断
代码如下:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.JFrame;
public class Listener extends MouseAdapter implements ActionListener,Runnable {
private BallFrame bf;
private Graphics g;
private Random rand = new Random();
private volatile boolean clearFlag = false, pauseFlag = false;
private ArrayList<Ball> ball = new ArrayList<Ball>();
Thread playing;
public Listener(BallFrame bf, ArrayList ball) {
this.bf = bf;
this.ball = ball;
}
public void mousePressed(MouseEvent e) {
int x = e.getX();
int y = e.getY();
Ball myball = new Ball(x, y, 30, (1+rand.nextInt(9)*(Math.random()>0.5?1:-1)),
(1+rand.nextInt(9)*(Math.random()>0.5?1:-1)),
new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)),rand.nextInt(9)+1);
ball.add(myball);
}
@Override
public void run() {
while (!clearFlag) {
if(!pauseFlag)
{
bf.repaint();
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void actionPerformed(ActionEvent event) {
String command = event.getActionCommand();
if (command.equals("Play")) {
startPlaying();
}
if (command.equals("Stop")) {
stopPlaying();
}
if (command.equals("Reset")) {
setAllFields();
}
if (command.equals("Continue")) {
continuePlaying();
}
if (command.equals("Clear")) {
clearPlaying();
}
}
void setAllFields() {
bf.massText.setText("0");
bf.xSpeedText.setText("0");
bf.xPositionText.setText("0");
bf.sizeText.setText("0");
bf.ySpeedText.setText("0");
bf.yPositionText.setText("0");
bf.reset.setEnabled(false);
bf.play.setEnabled(true);
bf.Continue.setEnabled(false);
bf.clear.setEnabled(true);
}
void startPlaying() {
playing = new Thread(this);
playing.start();
clearFlag = false;
bf.play.setEnabled(false);
bf.Continue.setEnabled(false);
bf.stop.setEnabled(true);
bf.reset.setEnabled(true);
bf.clear.setEnabled(true);
String xP=bf.xPositionText.getText();
int x=Integer.parseInt(xP);
String yP=bf.yPositionText.getText();
int y=Integer.parseInt(yP);
String Size=bf.sizeText.getText();
int size=Integer.parseInt(Size);
String Xspeed=bf.xSpeedText.getText();
int xspeed=Integer.parseInt(Xspeed);
String Yspeed=bf.ySpeedText.getText();
int yspeed=Integer.parseInt(Yspeed);
String Mass=bf.massText.getText();
int mass=Integer.parseInt(Mass);
Ball myball = new Ball(x, y, size, xspeed,yspeed,
new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)),mass);
ball.add(myball);
}
void stopPlaying() {
bf.stop.setEnabled(false);
bf.play.setEnabled(true);
bf.reset.setEnabled(true);
bf.Continue.setEnabled(true);
bf.clear.setEnabled(true);
pauseFlag=true;
}
void continuePlaying()
{
bf.stop.setEnabled(true);
bf.play.setEnabled(true);
bf.reset.setEnabled(true);
bf.Continue.setEnabled(false);
bf.clear.setEnabled(true);
pauseFlag=false;
}
void clearPlaying()
{
bf.clear.setEnabled(false);
bf.stop.setEnabled(false);
bf.play.setEnabled(true);
bf.reset.setEnabled(true);
bf.Continue.setEnabled(false);
playing = null;
clearFlag = true;
ball.clear();
bf.repaint();
}
}
- GUI框架
- 需要文本框实现输入,两行,每行3个变量
- 需要5个按钮,分别代表"Play"、"Stop"、"Reset"、"Continue"、"Clear"
- 需要一个画布,可以显示出球类的动画
代码如下
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.UIManager;
public class BallFrame extends JFrame {
private ArrayList<Ball> ball = new ArrayList<Ball>();
private Image img;
private Graphics2D graph;
// set up row 1
JPanel row1 = new JPanel();
JLabel mass = new JLabel("mass:", JLabel.RIGHT);
JTextField massText = new JTextField("0");
JLabel xSpeed = new JLabel("xSpeed:", JLabel.RIGHT);
JTextField xSpeedText = new JTextField("0");
JLabel xPosition = new JLabel("xPosition:", JLabel.RIGHT);
JTextField xPositionText = new JTextField("0");
JLabel size = new JLabel("size:", JLabel.RIGHT);
JTextField sizeText = new JTextField("0");
JLabel ySpeed = new JLabel("ySpeed:", JLabel.RIGHT);
JTextField ySpeedText = new JTextField("0");
JLabel yPosition = new JLabel("yPosition:", JLabel.RIGHT);
JTextField yPositionText = new JTextField("0");
// set up row 2
JPanel row2 = new JPanel();
JButton stop = new JButton("Stop");
JButton Continue = new JButton("Continue");
JButton clear = new JButton("Clear");
JButton play = new JButton("Play");
JButton reset = new JButton("Reset");
public BallFrame()
{
super("BallGame!");
setSize(600, 600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
/**
* set to BorderLayout
* This borderLayout do nothing important except makes our rows at the top.
* if user perfer bottom, all we need is to change "NORTH" into "South".
*/
setLayout(new BorderLayout());
add(row1, BorderLayout.NORTH);
add(row2, BorderLayout.NORTH);
//set to GridLayout whole framework
GridLayout layout = new GridLayout(5,2,10,10);
setLayout(layout);
//specify how many text fields should be filled in one line.
GridLayout layout2 = new GridLayout(2, 3, 10, 10);
row1.setLayout(layout2);
// the following process is to add them into window
row1.add(mass);
massText=new JTextField();
row1.add(massText);
row1.add(xSpeed);
xSpeedText=new JTextField();
row1.add(xSpeedText);
row1.add(xPosition);
xPositionText=new JTextField();
row1.add(xPositionText);
row1.add(size);
sizeText=new JTextField();
row1.add(sizeText);
row1.add(ySpeed);
ySpeedText=new JTextField();
row1.add(ySpeedText);
row1.add(yPosition);
yPositionText=new JTextField();
row1.add(yPositionText);
add(row1);
//make button in the center
FlowLayout layout3 = new FlowLayout(FlowLayout.CENTER,
10, 10);
row2.setLayout(layout3);
row2.add(play);
row2.add(stop);
row2.add(reset);
row2.add(Continue);
row2.add(clear);
add(row2);
setResizable(false);
setVisible(true);
}
//Main function, nothing else need to explain...
public static void main(String[] args) {
BallFrame.setLookAndFeel();
BallFrame bf = new BallFrame();
bf.UI();
}
public void UI() {
Listener lis = new Listener(this, ball);
// Add listeners
this.addMouseListener(lis);
clear.addActionListener(lis);
Continue.addActionListener(lis);
stop.addActionListener(lis);
play.addActionListener(lis);
reset.addActionListener(lis);
Thread current = new Thread(lis);
current.start();
}
/**
* This method is to ensure across operation system could have a ability to show window
*/
private static void setLookAndFeel() {
try {
UIManager.setLookAndFeel(
"com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"
);
} catch (Exception exc) {
// ignore error
}
}
public void paint(Graphics g) {
img = this.createImage(this.getWidth(), this.getHeight());
graph = (Graphics2D)img.getGraphics();;
graph.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for (int i = 0; i < ball.size(); i++) {
Ball myball = (Ball) ball.get(i);
myball.moveBall(graph,this);
myball.collision(graph,ball);
myball.moveBall(graph,this);
myball.drawBall(graph);
}
//If the canvas's height over 160, it will hide our buttons.
g.drawImage(img, 0, 160, this);
}
}
输出窗口的画面截图
总结
最近初学GUI,学校讲课用的是小球运动的例子,大一暑假拜读认识的学姐的一篇文章:我说这是一篇很严肃的技术文章你信吗,出于搞清楚GUI原理和好玩的目的,我就开始了我的改进之路。总体来说是符合我的预期的,但还是有两个问题:
- 忽视了错误输入可能造成异常的结果,暂时没学到在GUI如果输入错误怎么弹出界面,不过应该挺简单的,学到了再补。
- 动能定理的实践出了问题,因为鼠标点击的小球生成是随机的,无法得知每个小球的情况,动能定理公式给定了,可能是算法不够精致。
- Github源码