目录
一.任务分配
姓名 | 学号 | 任务分配及实现 |
包佳莉(组长) | 202221336069 | 球(Ball)我的球(MyBall),其他球(OtherBall),方向接口(Direction),小组博客编写 |
张子翔(组员) | 202221336080 | 多线程实现球类的移动和倒计时(Frame2),选择难度和代码的整合(SelectFrame) |
胡凌瑞(组员) | 202221336090 | 背景音乐的添加(Music),登陆界面(GameLogin),重开/退出(Again) |
二、系统简介
一个大球吃小球的小游戏,模拟真实的碰撞和运动效果,使得我的球能够吞噬其他球类而变大,本游戏有初级,中级,高级,终极四个难度关卡,并同时有倒计时的限制来完成吃到一定大小的要求,失败时提供重来一次或者退出游戏的选择。
三、前期调查
别人是怎么做的:登录 - Gitee.com
四、具体实现
项目功能架构图:
主要功能流程图:
包名类名:
使用文件:
代码示例 :
Again-是否重来一次:
public class Again extends JFrame implements ActionListener {
/**
* 重新开始以及结束游戏,当游戏失败后弹出该界面
* 定义两个按钮,一个用于重新开始游戏,一个用于结束游戏
*/
JButton bt1 = new JButton("重新开始");
JButton bt2 = new JButton("结束游戏");
Music audioPlayWave = new Music("E:\\ideal\\BallGamebjl\\src\\BalleatBall\\失败音乐.wav");
/**
* 构造函数,用于初始化界面
*/
public Again() {
audioPlayWave.start();
@SuppressWarnings("unused")
int musicOpenLab = 1;
// 设置窗口关闭时的操作
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 设置窗口为可见
this.setVisible(true);
// 设置窗口的大小
this.setSize(200, 200);
// 设置按钮的动作命令,这些命令将在动作事件中使用
bt1.setActionCommand("bt1");
bt2.setActionCommand("bt2");
// 为按钮添加动作监听器
bt1.addActionListener(this);
bt2.addActionListener(this);
// 创建一个JPanel对象,并将其设置为窗口的内容面板
JPanel panel = new JPanel();
this.setContentPane(panel);
// 将按钮添加到面板
panel.add(bt1);
panel.add(bt2);
// 设置面板的布局为网格布局,1行2列
panel.setLayout(new GridLayout(1, 2));
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("bt1")) {
// 关闭当前窗口
this.dispose();
// 如果点击的是"重新开始"按钮,那么创建一个新的选择难度的窗口
SelectFrame frame = new SelectFrame();
// 确保选择难度的窗口是可见的
frame.setVisible(true);
}
if (e.getActionCommand().equals("bt2")) {
// 如果点击的是"结束游戏"按钮,那么结束程序
System.exit(0);
}
}
Ball-实现球类的移动和吞噬:
public void move(int m) {
// 根据随机生成的方向值来移动球
switch (this.direction) {
//随机方向
case 1 -> x -= m;
case 2 -> x += m;
case 3 -> y -= m;
case 4 -> y += m;
case 5 -> {
x -= m;
y -= m;
}
case 6 -> {
x -= m;
y += m;
}
case 7 -> {
x += m;
y -= m;
}
case 8 -> {
x += m;
y += m;
}
default -> {
}
}
}
public void eat(Ball b1, int i) {
// eat方法,用于处理球吃掉其他球的逻辑
int x1 = b1.getX();
int y1 = b1.getY();
int d1 = b1.getD();
// 如果被吃球的直径小于当前球的直径
if (d1 < this.d) {
// 计算两球中心点的距离
// 小球被吃掉
int d = (int) Math.sqrt((x1 + d1 / 2 - this.getX() - this.getD() / 2) * (x1 + d1 / 2 - this.getX() - this.getD() / 2) + (y1 + d1 / 2 - this.getY() - this.getD() / 2) * (y1 + d1 / 2 - this.getY() - this.getD() / 2));
// 如果距离小于两球半径之和,表示可以吃掉
if (d < (d1 + this.getD()) / 2) {
// 增加当前球的直径,并将被吃球移出界面
this.setD(this.d + d1 / i);
b1.setX(-100);
b1.setY(-100);
b1.setD(0);
b1 = null;
}
// b1 = null;
} else if (d1 > this.getD()) {
// 如果被吃球的直径大于当前球的直径
int d = (int) Math.sqrt((x1 + d1 / 2 - this.getX() - this.getD() / 2) * (x1 + d1 / 2 - this.getX() - this.getD() / 2) + (y1 + d1 / 2 - this.getY() - this.getD() / 2) * (y1 + d1 / 2 - this.getY() - this.getD() / 2));
// 如果距离小于两球半径之和,表示当前球被吃掉
if (d < (d1 + this.getD()) / 2) {
// 小球应该被吃掉
// 将当前球移出界面
this.setX(-100);
this.setY(-100);
this.setD(0);
}
}
}
public void draw(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
// 设置剪辑区域为圆形
g2d.setClip(new Ellipse2D.Float(this.x - this.d / 2, this.y - this.d / 2, this.d, this.d));
// 在剪辑区域内绘制图片
g2d.drawImage(this.image, this.x - this.d / 2, this.y - this.d / 2, this.d, this.d, null);
g2d.dispose();
}
Direction-方向接口
public enum Direction {
// 枚举类型:四个方向
LEFT,RIGHT,UP,DOWN,
}
Frame2-多线程实现球类的移动和倒计时
public Frame2(int i, int k, int m, int j) {
audioPlayWave.start();
@SuppressWarnings("unused")
int musicOpenLab = 1;
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setFocusable(true);
setSize(new Dimension(1000, 700));
setLocationRelativeTo(null);
setResizable(false);
setTitle("大球吃小球");
getContentPane().setBackground(Color.black);
this.setVisible(true);
// Initialize the timer label
timerLabel = new JLabel("Time left: 01:00", SwingConstants.CENTER);
timerLabel.setForeground(Color.WHITE);
timerLabel.setFont(new Font("Arial", Font.BOLD, 20));
getContentPane().add(timerLabel, BorderLayout.NORTH);
// Initialize the timer
timer = new Timer(1000, e -> {
timeLeft--;
int minutes = timeLeft / 60;
int seconds = timeLeft % 60;
timerLabel.setText(String.format("Time left: %02d:%02d", minutes, seconds));
if (timeLeft <= 0) {
timer.stop();
isAlive = false;
JOptionPane.showMessageDialog(Frame2.this, "时间到,游戏失败!", "游戏结束", JOptionPane.ERROR_MESSAGE);
// showContinueGameDialog(i, k, m, j);
}
});
timer.start();
gamePanel = new GamePanel();
add(gamePanel, BorderLayout.CENTER);
this.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
// 根据按键改变玩家控制的球的移动方向
Direction dir = mb.getDirection();
switch (e.getKeyCode()) {
case KeyEvent.VK_A:
dir = Direction.LEFT;
System.out.println("左键");
break;
case KeyEvent.VK_W:
dir = Direction.UP;
System.out.println("上键");
break;
case KeyEvent.VK_S:
dir = Direction.DOWN;
System.out.println("下键");
break;
case KeyEvent.VK_D:
dir = Direction.RIGHT;
System.out.println("右键");
break;
case KeyEvent.VK_LEFT:
dir = Direction.LEFT;
System.out.println("左键");
break;
case KeyEvent.VK_UP:
dir = Direction.UP;
System.out.println("上键");
break;
case KeyEvent.VK_DOWN:
dir = Direction.DOWN;
System.out.println("下键");
break;
case KeyEvent.VK_RIGHT:
dir = Direction.RIGHT;
System.out.println("右键");
break;
case KeyEvent.VK_SPACE:
isAlive = !isAlive;
requestFocus();
break;
default:
break;
}
if (dir != mb.getDirection()) {
mb.changeDir(dir);
}
}
});
// 启动一个新的线程,用于控制游戏的主要逻辑, 匿名内部类和线程池
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (isAlive) {
// 判断是否吃球,球的移动,以及游戏结束后的处理
ob.isEat(mb, i);
ob.isAlive();
gamePanel.repaint();
if (mb.getDiameter() != 0 && mb.getX() > 0 && mb.getX() < 1000 && mb.getY() > 0 && mb.getY() < 1000) {
//判断小球球在游戏界面内
ob.move(m);
mb.move(k);
repaint();
//请求重新绘制组件
} else {
// 停止计时器
timer.stop();
isAlive = false;
audioPlayWave.stop();
// showContinueGameDialog(i, k, m, j);
int choice = JOptionPane.showConfirmDialog(Frame2.this, "游戏失败,是否重新开始游戏?", "游戏结束", JOptionPane.YES_NO_OPTION);
if (choice == JOptionPane.YES_OPTION) {
// 创建新的 Frame2 对象来重新开始游戏
Frame2 newGame = new Frame2(i, k, m, j);
Frame2.this.dispose();
} else {
// 关闭当前窗口
Frame2.this.dispose();
// 创建一个新的选择难度的窗口
SelectFrame frame = new SelectFrame();
frame.setLocationRelativeTo(null); // 将窗口居中显示(可选)
frame.setVisible(true);
// Again again = new Again();
// break;
}
}
try {
Thread.sleep(j);//线程刷新,
//控制刷新率在游戏中非常重要,因为它影响到游戏的响应速度和流畅度。
// 如果刷新率设置得过高,可能会导致屏幕闪烁或游戏运行不稳定;如果设置得太低,则可能会影响游戏的响应性和视觉效果。
// 因此,根据游戏的需求和性能要求,合理设置线程的刷新率是必要的。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
setVisible(true);
}
private class GamePanel extends JPanel {
private Image bgImage;
public GamePanel() {
bgImage = Toolkit.getDefaultToolkit().getImage("E:\\ideal\\BallGamebjl\\src\\BalleatBall\\微信图片_20240111185433.jpg");
setDoubleBuffered(true);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.drawImage(bgImage, 0, 0, getWidth(), getHeight(), this);
mb.draw(g2);
ob.draw(g2);
}
}
GameLogin-用户登录界面
public void actionPerformed(ActionEvent e) {
try {
String username = userText.getText();
String password = new String(passwordText.getPassword());
String code = codeText.getText();
if (username.isEmpty() || password.isEmpty() || code.isEmpty()) {
JOptionPane.showMessageDialog(panel, "请填写所有字段。");
} else if (username.equals("bjl") && password.equals("123456") && code.equals("0420") || username.equals("zzx") && password.equals("123456") && code.equals("0918") || username.equals("hlr") && password.equals("123456") && code.equals("1030")) {
JOptionPane.showMessageDialog(panel, "登录成功!");
frame1.setVisible(false); // 隐藏登录窗口
selectFrame.setVisible(true); // 显示选择难度的窗口
} else {
JOptionPane.showMessageDialog(panel, "登录失败,请检查输入是否正确。");
}
} catch (Exception e1) {
JOptionPane.showMessageDialog(panel, "登录失败,请检查输入是否正确。");
}
}
});
}
Music-背景音乐的播放
public class Music extends Thread {
private String fileName;
private final int EXTERNAL_BUFFER_SIZE = 524288;
public Music(String wavFile) {
this.fileName = wavFile;
}
@SuppressWarnings("unused")
@Override
public void run() {
File soundFile = new File(fileName);
// 播放音乐的文件名
boolean flag = soundFile.exists();
System.out.println(soundFile.getAbsolutePath());
if (!flag) {
System.err.println("Wave file not found:" + fileName);
return;
}
while (true) {
// 设置循环播放
AudioInputStream audioInputStream = null;
// 创建音频输入流对象
try {
audioInputStream = AudioSystem.getAudioInputStream(soundFile);
// 创建音频对象
} catch (UnsupportedAudioFileException e1) {
e1.printStackTrace();
return;
} catch (IOException e1) {
e1.printStackTrace();
return;
}
AudioFormat format = audioInputStream.getFormat();
// 音频格式
SourceDataLine auline = null;
// 源数据线
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
try {
auline = (SourceDataLine) AudioSystem.getLine(info);
auline.open(format);
} catch (LineUnavailableException e) {
e.printStackTrace();
return;
} catch (Exception e) {
e.printStackTrace();
return;
}
if (auline.isControlSupported(FloatControl.Type.PAN)) {
FloatControl pan = (FloatControl) auline.getControl(FloatControl.Type.PAN);
}
auline.start();
int nBytesRead = 0;
byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
try {
while (nBytesRead != -1) {
nBytesRead = audioInputStream.read(abData, 0, abData.length);
if (nBytesRead >= 0) {
auline.write(abData, 0, nBytesRead);
}
}
} catch (IOException e) {
e.printStackTrace();
return;
} finally {
auline.drain();
// auline.close();
}
}
Myball-玩家球类的操作
(和Ball类相似但有内容上的重构)
* changeDir方法,用于改变球的移动方向
*/
public void changeDir(Direction dir) {
this.direction = dir;
}
/**
* move方法,用于根据当前的移动方向移动球
*/
public void move(int m) {
if (this.direction != null) {
switch (direction) {
case UP:
y -= m;
break;
case DOWN:
y += m;
break;
case LEFT:
x -= m;
break;
case RIGHT:
x += m;
break;
default:
break;
}
}
}
/**
* eat方法,用于处理球吃掉其他球的逻辑
*/
public void eat(Ball ball, int i) {
int x1 = ball.getX();
//获取小球的 x 坐标
int y1 = ball.getY();
// 获取小球的 y 坐标
int d1 = ball.getD();
// 获取小球的直径
if (this.diameter < 600) {
// 如果大球的直径小于600
if (d1 < this.diameter) {
// 如果小球的直径小于大球的直径
// 小球被吃掉
// 计算小球和大球之间的距离
int d = (int) Math.sqrt((x1 + d1 / 2 - this.getX() - this.getDiameter() / 2) * (x1 + d1 / 2 - this.getX() - this.getDiameter() / 2) + (y1 + d1 / 2 - this.getY() - this.getDiameter() / 2) * (y1 + d1 / 2 - this.getY() - this.getDiameter() / 2));
if (d < (d1 + this.getDiameter()) / 2) {
// 如果小球在大球的半径范围内 该小球应该被吃掉
this.setDiameter(this.diameter + d1 / i);
// 大球直径+=小球的直径除以i的值
ball.setX(-100);
//小球 x 坐标设置为 -100,使其移出屏幕或不再可见
ball.setY(-100);
// 小球 y 坐标设置为 -100,使其移出屏幕或不再可见
ball.setD(0);
// 小球直径设置为0,使其不再可见或无效
ball = null;
// 将小球对象设置为null,释放内存
}
} else if (d1 > this.getDiameter()) {
// 如果小球的直径大于大球的直径
// 计算小球和大球之间的距离
int d = (int) Math.sqrt((x1 + d1 / 2 - this.getX() - this.getDiameter() / 2) * (x1 + d1 / 2 - this.getX() - this.getDiameter() / 2) + (y1 + d1 / 2 - this.getY() - this.getDiameter() / 2) * (y1 + d1 / 2 - this.getY() - this.getDiameter() / 2));
if (d < (d1 + this.getDiameter()) / 2) {
// 如果小球在大球的半径范围内
// 小球应该被吃掉
this.setX(-100);
// 大球 x 坐标设置为 -100,使其移出屏幕或不再可见
this.setY(-100);
// 大球 y 坐标设置为 -100,使其移出屏幕或不再可见
this.setDiameter(0);
// 大球直径设置为0,使其不再可见或无效
}
}
} else {
// 如果大球的直径大于或等于600
JOptionPane.showMessageDialog(new Frame2(), "恭喜您,球球够大了", "提示", JOptionPane.INFORMATION_MESSAGE);
System.exit(0);
}
}
OtherBall-其他球的操作
public void isAlive() {
// 遍历所有的球,如果球出界,则删除这个球,并创建一个新的球()
for (int i = 0; i < balls.size(); i++) {
Ball ball = balls.get(i);
if (ball.getX() < -200 || ball.getX() > 1000 || ball.getY() < -200 || ball.getY() > 1000) {
ball = null;
}
}
int count2 = (int) (Math.random() * 100 + 1);
//判断是否需要创建球
if (count2 > 90) {
newBall();
}
}
public void newBall() {
Random rand = new Random();
// 生成新球的直径
int size = rand.nextInt(70) + 15;
// 生成新球的位置
int x3 = rand.nextInt(1001);
int y3 = rand.nextInt(1001);
// 选择颜色
int num = rand.nextInt(13);
int num1 = rand.nextInt(6);
Color color = chooseColor(num);
String[] imagePaths =
{
"E:\\ideal\\BallGamebjl\\src\\BalleatBall\\微信图片_20240111192818.jpg",
"E:\\ideal\\BallGamebjl\\src\\BalleatBall\\微信图片_20240111215933.jpg",
"E:\\ideal\\BallGamebjl\\src\\BalleatBall\\微信图片_20240111215941.jpg",
"E:\\ideal\\BallGamebjl\\src\\BalleatBall\\微信图片_20240111215946.jpg",
"E:\\ideal\\BallGamebjl\\src\\BalleatBall\\微信图片_20240111215951.jpg",
"E:\\ideal\\BallGamebjl\\src\\BalleatBall\\微信图片_20240111215959.jpg",
};
String imagePath = imagePaths[num1];
// 创建球并添加到列表
Ball ball = new Ball(x3, y3, size, color, imagePath);
balls.add(ball);
}
/**
* }
* 移动所有的球
*/
public void move(int m) {
for (Ball ball : balls) {
ball.move(m);
}
}
/**
* 判断球是否被吃
*/
public void isEat(MyBall mb, int i) {
for (int j = 0; j < balls.size() - 1; j++) {
Ball ball = balls.get(j);
mb.eat(ball, i);
ball.eat(balls.get(j + 1), i);
}
}
/**
* 在给定的Graphics对象上绘制所有的球
* 遍历balls数组,获取每一个Ball对象,并调用它的draw方法绘制到Graphics对象g上
*/
public void draw(Graphics g) {
for (int i = 0; i < balls.size(); i++) {
Ball ball = balls.get(i);
ball.draw(g);
}
}
public Color chooseColor(int num) {
Color color = switch (num) {
case 0 -> Color.BLACK;
case 1 -> Color.BLUE;
case 2 -> Color.RED;
case 3 -> Color.GREEN;
case 4 -> Color.YELLOW;
case 5 -> Color.ORANGE;
case 6 -> Color.GRAY;
case 7 -> Color.PINK;
case 8 -> Color.CYAN;
case 9 -> Color.DARK_GRAY;
case 10 -> Color.MAGENTA;
case 11 -> Color.WHITE;
case 12 -> Color.LIGHT_GRAY;
default -> throw new IllegalStateException("Unexpected value: " + num);
};
return color;
}
SelectFrame-选择难度
public SelectFrame() {
audioPlayWave.start();
JPanel panel = new JPanel();
this.setContentPane(panel);
this.setTitle("选择难度");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(400, 400);
this.setVisible(false);
// 初始时隐藏选择难度的窗口
bt1.setText("初级");
bt2.setText("中级");
bt3.setText("高级");
bt4.setText("终极");
bt1.setActionCommand("bt1");
bt2.setActionCommand("bt2");
bt3.setActionCommand("bt3");
bt4.setActionCommand("bt4");
bt1.addActionListener(this);
bt2.addActionListener(this);
bt3.addActionListener(this);
bt4.addActionListener(this);
panel.add(bt1);
panel.add(bt2);
panel.add(bt3);
panel.add(bt4);
panel.setLayout(new GridLayout(4, 1));
}
/**
* 球的移动
*/
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("bt1")) {
dispose();
audioPlayWave.stop();
this.setVisible(false);
Frame2 frame2 = new Frame2(2, 2, 1, 10);
} else if (e.getActionCommand().equals("bt2")) {
dispose();
audioPlayWave.stop();
this.setVisible(false);
Frame2 frame2 = new Frame2(4, 2, 1, 10);
} else if (e.getActionCommand().equals("bt3")) {
dispose();
audioPlayWave.stop();
this.setVisible(false);
Frame2 frame2 = new Frame2(6, 2, 1, 10);
} else if (e.getActionCommand().equals("bt4")) {
dispose();
audioPlayWave.stop();
this.setVisible(false);
Frame2 frame2 = new Frame2(6, 2, 1, 8);
}
}
运行结果图:
1.用户登录
设置了小组三人对应的用户名,密码和验证码(三个账号都能登录成功)
当输入错误时会提醒失败,见p4(包括三个登录内容没输全会提醒:请填写所有字段,这里往截出来了)
2.选择难度界面(初级,中级,高级,终极四个难度选择)
3.游戏界面
4.游戏成功时的界面
5.游戏失败时的界面 (选择是否重开)
a.如果重开则在现在选的难度再一次开始
b.如果不重开则重新选择难度(关闭选择页面意味着游戏结束)
项目代码扫描结果及改正
(下面的图是修改后,有部分是修改不来,还有是感觉没什么大碍)
项目总结
不足:游戏难度层次改变的内容不大,仅是玩家球的移动速度和生成球的大小改变,有一些小问题在球类的吞噬上
展望:想要实现可选择单人或双人的游戏模式,实现球球的动态生成范围的变化,实施玩家排行榜,添加障碍物,会导致球球缩小。