如何在五天里从无到有地利用Java Swing写一个跳一跳小游戏
前言
实际上这是我的java课程结课时的大作业。本来不是什么特别难的事情,但是由于我自己和队友都不是时间管理大师,硬是把这个大作业拖到了考前最后一周。当别人快速写完开始复习的时候我还在纠缠这个要命小游戏的需求,每天熬夜修仙改代码。当然最痛苦的事情就是,别人轻轻松松在网上找到了其他课题的相关代码,关于这个跳一跳小游戏的代码是一点没有,,我只能无中生有硬造一个出来,其中辛酸可想而知。所以完事了我的第一个愿望就是开源,一定要开源,不能再让其他人受这个苦了,也当是造福学弟学妹们了。(PS:我不推荐直接搬走我的代码,我更推荐的是由我的代码给个参考然后避免写一些重复的工作,然后在我没有完成的一些功能的基础上更近一步)
项目地址
(PS:虽然造福学弟学妹,但是我确实还是有点私心的,希望各位借鉴了代码的给仓库点个星星,毕竟我也想得到点正反馈嘛)
github仓库
gitee仓库
作业要求
大作业课题1
(1)课题代号: 1
(2)课题名称:跳一跳游戏程序
(3)课题要求:
①基本要求:设计单机版的跳一跳游戏程序。基本要求中只需要设计单人游戏模式。采用鼠标键控制自己在游戏中的小人,完成起跳(按住鼠标左键不放可以向下压缩脚下的弹簧跳板,释放鼠标左键则进行起跳,按住鼠标左键的时间越长,弹簧跳板被压得越短,小人跳得越远)。
设计一个平面上的矩形游戏窗口,左边为起点,右边为终点。游戏开始时,小人位于起点跳板,右侧有第一个程序自动产生的中间跳板。同时程序开始倒计时。控制小人起跳,如果正确落在中间跳板上,则其右侧下一个中间跳板自动出现,直到小人跳到终点游戏结束。中间跳板个数,长度和高度为程序自动产生,但跳板之间的最长距离需要保证小人能够在最大弹跳时可以到达。当小人跳到右边终点时,游戏获胜结束。屏幕显示小人从起点到终点花费的时间。如果在到达右边终点前倒计时时间到,则游戏失败。如果小人在途中没有跳到跳板上,则游戏失败。
②提高要求:设计多回合游戏功能,每个回合的游戏难度不同;设计在游戏中可以用键盘控制小人起跳(按键按下不放压缩跳板,按键释放进行起跳,按下时间决定跳出距离);如实现了键盘控制起跳功能,在此基础上设计双人对战模式,即两个游戏者,各自用一个按键控制自己的小人起跳。游戏窗口划分为上下两部分,两个游戏者在各自的窗口中游戏,游戏中出现的跳板个数,大小,高度对两个游戏者均相同。在倒计时结束前最先抵达窗口右边终点者获胜;给游戏设计良好的动画效果;给游戏设计良好的声音效果。
(4)备注:课题要求(3)中给出的基本要求和提高要求将作为评分依据,根据大作业评分标准进行评分,在基本要求和提高要求以外实现的其它功能不作为评分依据。本大作业课题不需要设计立体版本程序。
经过整理的要求清单
功能指标分析(含实现情况)
- [√] 主页面(含单人模式和多人模式的入口)
- [√] 设计一个平面上的矩形游戏窗口,左边为起点,右边为终点。
- [√] 踏板生成
- [√] 随机生成大小长度
- [√] 固定区域计算(根据跳跃函数)
- [√] 最右边一个踏板固定生成
- [√] 踏板样式(随机颜色)
- [√] 跳跃函数
- [√] 更丝滑的跳跃
- [√] 碰撞计算
- [√] 游戏失败界面
- [√] 重新开始/下一关卡
- [√] 计时
- [√] 多回合游戏(简单、一般、困难)
- [√] 最高分统计
- [√] 鼠标+键盘
- [√] 双人对战
- [√] 动画
流程图
实现代码
面向对象逻辑分块
跳一跳小游戏主要页面为主页和游戏页面。其中游戏页面因模式难度不同而具有一定的差异性。主页面承担了对游戏模式的选择,而游戏界面则是游戏主体。在代码编写过程中,由main.class负责调用生成主页面的函数,然后在主页面中通过事件监听来决定系统执行什么类型的level类构造函数实现游戏界面。游戏界面主要靠level.class类实现,在游戏界面中的跳跃棋子由jumpChess.class实现,而在游戏界面中的平台由Plat.class实现,游戏界面中的倒计时事件由timeCounter.class实现。
主函数main.class
public class Main {
private static double[] minTime = new double[]{30.0D, 25.0D, 20.0D};
private static double[] limitTime = new double[]{30.0D, 25.0D, 20.0D};
public Main() {
}
public static double getMinTime(int level) {
return minTime[level - 1];
}
public static double getLimitTime(int level) {
return limitTime[level - 1];
}
public static void setMinTime(int level, double time) {
minTime[level - 1] = time;
}
public static void getToHome() {
final JFrame homePage = new JFrame();
homePage.setDefaultCloseOperation(3);
homePage.setVisible(true);
homePage.setResizable(false);
homePage.setBounds(350, 300, 400, 400);
JPanel panel = new JPanel();
panel.setBackground(new Color(250, 250, 250));
panel.setLayout((LayoutManager)null);
panel.setBounds(0, 0, 400, 400);
homePage.add(panel);
JLabel Welcome = new JLabel();
Welcome.setBounds(70, 70, 300, 40);
Welcome.setFont(new Font("Times New Roman", 1, 20));
Welcome.setForeground(Color.black);
Welcome.setVisible(true);
panel.add(Welcome);
Welcome.setText("Welcome To Play JumpJump!!!");
Welcome.setForeground(Color.black);
JButton play1P = new JButton("Play1P");
JButton play2P = new JButton("Play2P");
play1P.setBounds(130, 140, 140, 30);
play2P.setBounds(130, 200, 140, 30);
panel.add(play1P);
panel.add(play2P);
play1P.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
homePage.setVisible(false);
(new Thread(new Runnable() {
public void run() {
new Level(1);
}
})).start();
}
});
play2P.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
homePage.setVisible(false);
(new Thread(new Runnable() {
public void run() {
new Level(1, 2);
}
})).start();
}
});
}
游戏界面level.class
游戏界面主要分为两类,双人模式以及单人模式。单人模式由level(int i)构造函数实现。双人模式 由level(int i ,int j)重载构造函数实现。游戏的过程集成在该类构造函数中。level.class继承自JFrame类,通过实例化该类并调用构造函数,即可开始一局游戏。由于该类函数的高度集成性,level类中使用了众多其他类构造函数以辅助完成。
public class Level extends JFrame {
public static Instant start;
public static Instant end;
public static double pressTime;
public static double start1p;
public static double end1p;
public static double pressTime1p;
public static double start2p;
public static double end2p;
public static double pressTime2p;
public static int End2p = 0;
public static int keyEnd = 0;
private int level;
private int pass;
private int pass1p;
private int pass2p;
public Level(int level1) {
this.setLevel(level1);
this.setDefaultCloseOperation(3);
this.setVisible(true);
this.setResizable(false);
JPanel panel = new JPanel();
panel.setBackground(new Color(250, 250, 250));
panel.setLayout((LayoutManager)null);
BufferedImage img1 = null;
try {
img1 = ImageIO.read(new File("src/images/chess.png"));
} catch (IOException var17) {
System.out.println("not find img!!!");
}
ImageIcon chess = new ImageIcon(img1);
JumpChess jlChess = new JumpChess(chess);
this.setBounds(350, 300, 1000, 400);
panel.setBounds(0, 0, 1000, 400);
panel.add(jlChess);
Plat plat1 = new Plat();
panel.add(plat1);
plat1.Random();
Plat plat2 = new Plat((int)((double)plat1.getX() + plat1.xDistance), (int)((double)plat1.getY() - plat1.yDistance), plat1.getWidth(), plat1.getHeight());
panel.add(plat2);
this.add(panel);
this.AddMousePressHandle();
this.AddKeyPressHandle();
Plat thisOne = new Plat();
plat2.Asign(thisOne);
panel.add(thisOne);
panel.repaint();
this.repaint();
this.setVisible(true);
TimeCounter gameTime = new TimeCounter(this);
panel.add(gameTime);
panel.repaint();
double startTime = gameTime.recordTime();
while(true) {
double TimeNow = gameTime.recordTime();
gameTime.setGameTime(TimeNow - startTime);
if (gameTime.getGameTime() == Main.getLimitTime(this.level)) {
this.pass = 0;
break;
}
gameTime.timeChange(this);
panel.repaint();
if (this.getPressTime() > 0.0D) {
jlChess.jump(this.getPressTime(), thisOne);
if (jlChess.getState() == 2) {
this.pass = 0;
break;
}
this.clearPressTime();
thisOne.Random();
if (thisOne.getX() + thisOne.getWidth() + 70 > 1000) {
this.pass = 1;
break;
}
Plat nextOne = new Plat((int)((double)thisOne.getX() + thisOne.xDistance), (int)((double)thisOne.getY() - thisOne.yDistance), thisOne.getWidth(), thisOne.getHeight());
nextOne.Asign(thisOne);
panel.add(nextOne);
panel.repaint();
}
}
JLabel result = new JLabel();
result.setBounds(450, 0, 200, 40);
result.setFont(new Font("Times New Roman", 1, 20));
result.setForeground(Color.black);
result.setVisible(true);
panel.add(result);
JButton playAgain = new JButton("Play Again");
JButton playNextMode = new JButton("Play Next Model");
JButton BackHome = new JButton("Back to Home");
playAgain.setBounds(800, 60, 140, 30);
playNextMode.setBounds(800, 100, 140, 30);
BackHome.setBounds(800, 140, 140, 30);
panel.add(playAgain);
panel.add(playNextMode);
panel.add(BackHome);
if (this.pass == 0) {
playNextMode.setVisible(false);
result.setText("You Lose!");
result.setForeground(Color.black);
panel.repaint();
} else if (this.pass == 1) {
if (this.level == 3) {
playNextMode.setVisible(false);
} else {
playNextMode.setVisible(true);
}
result.setText("You Win!");
result.setForeground(Color.black);
panel.repaint();
if (gameTime.getGameTime() <= Main.getMinTime(this.level)) {
Main.setMinTime(this.level, gameTime.getGameTime());
JLabel congratulation = new JLabel();
congratulation.setBounds(275, 50, 500, 40);
congratulation.setFont(new Font("Times New Roman", 1, 20));
congratulation.setForeground(Color.black);
congratulation.setVisible(true);
congratulation.setText("Congratulations!!! You have break your record!!!");
congratulation.setForeground(Color.black);
panel.add(congratulation);
panel.repaint();
}
}
playAgain.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Level.this.playGame(Level.this.getLevel());
}
});
playNextMode.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
int level = Level.this.getLevel();
++level;
Level.this.playGame(level);
}
});
BackHome.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Level.this.backToHome();
}
});
}
public Level(int level1, int mode) {
this.setLevel(level1);
this.setDefaultCloseOperation(3);
this.setVisible(true);
this.setResizable(false);
JPanel panel = new JPanel();
JPanel panel1 = new JPanel();
final JPanel panel2 = new JPanel();
panel1.setBackground(new Color(250, 250, 250));
panel2.setBackground(new Color(250, 250, 250));
panel.setLayout((LayoutManager)null);
panel1.setLayout((LayoutManager)null);
panel2.setLayout((LayoutManager)null);
panel.add(panel1);
panel.add(panel2);
panel.setBounds(0, 0, 1000, 800);
panel1.setBounds(0, 0, 1000, 400);
panel2.setBounds(0, 400, 1000, 400);
Image img1 = null;
BufferedImage img2 = null;
try {
img1 = ImageIO.read(new File("src/images/chess.png"));
img2 = ImageIO.read(new File("src/images/chess.png"));
} catch (IOException var28) {
System.out.println("not find img!!!");
}
ImageIcon chess1 = new ImageIcon(img1);
ImageIcon chess2 = new ImageIcon(img2);
JumpChess jlChess1 = new JumpChess(chess1);
final JumpChess jlChess2 = new JumpChess(chess2);
this.setBounds(350, 300, 1000, 800);
panel1.add(jlChess1);
panel2.add(jlChess2);
Plat plat1 = new Plat();
panel1.add(plat1);
plat1.Random();
Plat plat2 = new Plat((int)((double)plat1.getX() + plat1.xDistance), (int)((double)plat1.getY() - plat1.yDistance), plat1.getWidth(), plat1.getHeight());
panel1.add(plat2);
Plat plat3 = new Plat();
panel2.add(plat3);
plat1.Random();
Plat plat4 = new Plat((int)((double)plat3.getX() + plat1.xDistance), (int)((double)plat1.getY() - plat1.yDistance), plat1.getWidth(), plat1.getHeight());
panel2.add(plat4);
this.add(panel);
Plat thisOne1 = new Plat();
plat2.Asign(thisOne1);
panel1.add(thisOne1);
final Plat thisOne2 = new Plat();
plat4.Asign(thisOne2);
panel2.add(thisOne2);
panel1.repaint();
panel2.repaint();
panel.repaint();
this.repaint();
this.setVisible(true);
this.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
if (Level.keyEnd != 1) {
if (e.getKeyCode() == 83) {
Level.start1p = (double)Long.valueOf(System.currentTimeMillis());
Level.keyEnd = 1;
}
if (e.getKeyCode() == 75) {
Level.start2p = (double)Long.valueOf(System.currentTimeMillis());
System.out.println("start2p:" + Level.start2p);
Level.keyEnd = 1;
}
}
}
public void keyReleased(KeyEvent e) {
Level.keyEnd = 0;
if (e.getKeyCode() == 83) {
Level.end1p = (double)Long.valueOf(System.currentTimeMillis());
System.out.println("end1p:" + Level.end1p);
Level.pressTime1p = Level.end1p - Level.start1p;
System.out.println("1p:" + Level.pressTime1p);
}
if (e.getKeyCode() == 75) {
Level.end2p = (double)Long.valueOf(System.currentTimeMillis());
System.out.println("end2p:" + Level.end2p);
Level.pressTime2p = Level.end2p - Level.start2p;
System.out.println("2p:" + Level.pressTime2p);
}
}
public void keyTyped(KeyEvent e) {
}
});
TimeCounter gameTime1 = new TimeCounter(this);
final TimeCounter gameTime2 = new TimeCounter();
panel1.add(gameTime1);
panel2.add(gameTime2);
panel1.repaint();
final double startTime = gameTime1.recordTime();
(new Thread(new Runnable() {
public void run() {
Level.End2p = 0;
while(true) {
System.out.println(Level.this.getPressTime2p());
double TimeNow = gameTime2.recordTime();
gameTime2.setGameTime(TimeNow - startTime);
if (gameTime2.getGameTime() == Main.getLimitTime(Level.this.level)) {
Level.this.pass2p = 0;
break;
}
if (Level.this.getPressTime2p() > 0.0D) {
jlChess2.jump(Level.this.getPressTime2p(), thisOne2);
if (jlChess2.getState() == 2) {
Level.this.pass2p = 0;
break;
}
Level.this.clearPressTime2p();
thisOne2.Random();
if (thisOne2.getX() + thisOne2.getWidth() + 70 > 1000) {
Level.this.pass2p = 1;
break;
}
Plat nextOne2 = new Plat((int)((double)thisOne2.getX() + thisOne2.xDistance), (int)((double)thisOne2.getY() - thisOne2.yDistance), thisOne2.getWidth(), thisOne2.getHeight());
nextOne2.Asign(thisOne2);
panel2.add(nextOne2);
panel2.repaint();
}
}
Level.End2p = 1;
}
})).start();
double TimeNow;
while(true) {
TimeNow = gameTime1.recordTime();
gameTime1.setGameTime(TimeNow - startTime);
if (gameTime1.getGameTime() == Main.getLimitTime(this.level)) {
this.pass1p = 0;
break;
}
gameTime1.timeChange(this);
panel1.repaint();
if (this.getPressTime1p() > 0.0D) {
jlChess1.jump(this.getPressTime1p(), thisOne1);
if (jlChess1.getState() == 2) {
this.pass1p = 0;
break;
}
this.clearPressTime1p();
thisOne1.Random();
if (thisOne1.getX() + thisOne1.getWidth() + 70 > 1000) {
this.pass1p = 1;
break;
}
Plat nextOne1 = new Plat((int)((double)thisOne1.getX() + thisOne1.xDistance), (int)((double)thisOne1.getY() - thisOne1.yDistance), thisOne1.getWidth(), thisOne1.getHeight());
nextOne1.Asign(thisOne1);
panel1.add(nextOne1);
panel1.repaint();
}
}
do {
TimeNow = gameTime1.recordTime();
gameTime1.setGameTime(TimeNow - startTime);
gameTime1.timeChange(this);
try {
Thread.sleep(1L);
} catch (InterruptedException var27) {
var27.printStackTrace();
}
} while(End2p == 0);
JLabel result = new JLabel();
result.setBounds(450, 0, 200, 40);
result.setFont(new Font("Times New Roman", 1, 20));
result.setForeground(Color.black);
result.setVisible(true);
panel1.add(result);
JButton playAgain = new JButton("Play Again");
JButton playNextMode = new JButton("Play Next Model");
JButton BackHome = new JButton("Back to Home");
playAgain.setBounds(800, 60, 140, 30);
playNextMode.setBounds(800, 100, 140, 30);
BackHome.setBounds(800, 140, 140, 30);
panel1.add(playAgain);
panel1.add(playNextMode);
panel1.add(BackHome);
if (this.pass1p == 0 && this.pass2p == 0) {
playNextMode.setVisible(false);
result.setText("Both of you Lose!");
result.setForeground(Color.black);
panel1.repaint();
} else {
JLabel congratulation;
if (this.pass1p == 1 && this.pass2p == 0) {
result.setText("1P Win!");
result.setForeground(Color.black);
panel1.repaint();
if (gameTime1.getGameTime() <= Main.getMinTime(this.level)) {
Main.setMinTime(this.level, gameTime1.getGameTime());
congratulation = new JLabel();
congratulation.setBounds(275, 50, 500, 40);
congratulation.setFont(new Font("Times New Roman", 1, 20));
congratulation.setForeground(Color.black);
congratulation.setVisible(true);
congratulation.setText("Congratulations!!! 1P have break record!!!");
congratulation.setForeground(Color.black);
panel1.add(congratulation);
panel1.repaint();
}
} else if (this.pass1p == 0 && this.pass2p == 1) {
result.setText("2P Win!");
result.setForeground(Color.black);
panel1.repaint();
if (gameTime2.getGameTime() <= Main.getMinTime(this.level)) {
Main.setMinTime(this.level, gameTime1.getGameTime());
congratulation = new JLabel();
congratulation.setBounds(275, 50, 500, 40);
congratulation.setFont(new Font("Times New Roman", 1, 20));
congratulation.setForeground(Color.black);
congratulation.setVisible(true);
congratulation.setText("Congratulations!!! 2P have break record!!!");
congratulation.setForeground(Color.black);
panel1.add(congratulation);
panel1.repaint();
}
} else if (this.pass1p == 1 && this.pass2p == 1) {
if (this.level == 3) {
playNextMode.setVisible(false);
} else {
playNextMode.setVisible(true);
}
if (gameTime1.getGameTime() < gameTime2.getGameTime()) {
result.setText("1P Win!");
result.setForeground(Color.black);
panel1.repaint();
if (gameTime1.getGameTime() <= Main.getMinTime(this.level)) {
Main.setMinTime(this.level, gameTime1.getGameTime());
congratulation = new JLabel();
congratulation.setBounds(275, 50, 500, 40);
congratulation.setFont(new Font("Times New Roman", 1, 20));
congratulation.setForeground(Color.black);
congratulation.setVisible(true);
congratulation.setText("Congratulations!!! 1P have break record!!!");
congratulation.setForeground(Color.black);
panel1.add(congratulation);
panel1.repaint();
}
} else {
result.setText("2P Win!");
result.setForeground(Color.black);
panel1.repaint();
if (gameTime2.getGameTime() <= Main.getMinTime(this.level)) {
Main.setMinTime(this.level, gameTime1.getGameTime());
congratulation = new JLabel();
congratulation.setBounds(275, 50, 500, 40);
congratulation.setFont(new Font("Times New Roman", 1, 20));
congratulation.setForeground(Color.black);
congratulation.setVisible(true);
congratulation.setText("Congratulations!!! 2P have break record!!!");
congratulation.setForeground(Color.black);
panel1.add(congratulation);
panel1.repaint();
}
}
}
}
playAgain.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Level.this.play2pGame(Level.this.getLevel());
}
});
playNextMode.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
int level = Level.this.getLevel();
++level;
Level.this.play2pGame(level);
}
});
BackHome.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Level.this.backToHome();
}
});
}
public void AddMousePressHandle() {
super.addMouseListener(new MouseListener() {
public void mousePressed(MouseEvent e) {
Level.start = Instant.now();
}
public void mouseReleased(MouseEvent e) {
Level.end = Instant.now();
Level.pressTime = (double)Duration.between(Level.start, Level.end).toMillis();
System.out.println(Level.pressTime);
}
public void mouseExited(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseClicked(MouseEvent e) {
}
});
}
public void AddKeyPressHandle() {
super.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
if (Level.keyEnd != 1) {
if (e.getKeyCode() == 83) {
Level.start = Instant.now();
Level.keyEnd = 1;
}
}
}
public void keyReleased(KeyEvent e) {
Level.keyEnd = 0;
if (e.getKeyCode() == 83) {
Level.end = Instant.now();
Level.pressTime = (double)Duration.between(Level.start, Level.end).toMillis();
}
}
public void keyTyped(KeyEvent e) {
}
});
}
public double getPressTime() {
return pressTime;
}
public double getPressTime1p() {
return pressTime1p;
}
public double getPressTime2p() {
return pressTime2p;
}
public void clearPressTime() {
pressTime = 0.0D;
}
public void clearPressTime1p() {
pressTime1p = 0.0D;
}
public void clearPressTime2p() {
pressTime2p = 0.0D;
}
public int getLevel() {
return this.level;
}
public void setLevel(int level1) {
this.level = level1;
}
public void playGame(final int level1) {
this.clearPressTime();
this.dispose();
(new Thread(new Runnable() {
public void run() {
new Level(level1);
}
})).start();
}
public void play2pGame(final int level) {
this.clearPressTime1p();
this.clearPressTime2p();
this.dispose();
(new Thread(new Runnable() {
public void run() {
new Level(level, 2);
}
})).start();
}
public void backToHome() {
this.clearPressTime();
this.dispose();
(new Thread(new Runnable() {
public void run() {
Main.getToHome();
}
})).start();
}
}
跳跃棋子 jumpChess.class
跳跃棋子类主要负责棋子的初始化和移动。 jumpChess.class继承自JLabel类。其内部Jump函数实现棋子跳跃动画的实现以及棋子跳跃后存活状态的判断。函数中具体的棋子跳跃算法将在后续算法分析中详细阐述。
public class JumpChess extends JLabel
{
private int state=1;//棋子存活状态,其中1为存活,2为死亡
public JumpChess(Icon image)
{
//棋子初始化在左侧
this(null, image);
setBounds(0, 150, image.getIconWidth(), image.getIconHeight());
}
public JumpChess(String text, Icon icon)
{
setText(text);
setIcon(icon);
setHorizontalAlignment(SwingConstants.LEFT);
setVerticalAlignment(SwingConstants.TOP);
updateUI();
setAlignmentX(LEFT_ALIGNMENT);
}
/**
* 跳跃函数,对于跳跃函数,我们
*
* @param MouseTime 从鼠标获取的时间
* @param plat 平台参数,方便确定碰撞
*/
public void jump(double MouseTime,Plat plat)
{
//起跳的速度有点太快,可以考虑分段--HuaCL20210620 2222
System.out.println("get it!!");
//横向速度(匀速运动,初始值和鼠标按压时间成正比)
double Vx=15+MouseTime*0.02;
//纵向加速度(匀加速运动)
double Ay=-0.2;
//纵向速度(初始值和鼠标按压时间成正比)
double Vy=10+MouseTime*0.06;
//时间
int actionTime=0;
//系数,用于将动画更加精细化,10即为/10显示
int Multiplayer=50;
//单位时间,用于控制移动速度
//double standardGapTime=0.5;
//初始位置
int initialX,initialY;
initialX=super.getX();
initialY=super.getY();
//System.out.println(super.getX()+" "+super.getY());
int distance = (int) MouseTime / 20;
//通过不停的刷新棋子的位置实现动画
while(true)
{
setLocation((int)(initialX + (Vx*actionTime)/Multiplayer),
(int)(initialY - (Vy*actionTime+0.5*Ay*actionTime*actionTime)/Multiplayer));
try
{
sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
actionTime+=1;
//actionTime+=standardGapTime;
//Judge返回1,表示正确跳到台上,设定状态为1,继续游戏
if(plat.Judge(this.getX(),this.getWidth(),this.getY(),this.getHeight())==1)
{
this.state=1;
break;
}
//Judge返回2,游戏失败
if(plat.Judge(this.getX(),this.getWidth(),this.getY(),this.getHeight())==2)
{
//棋子掉落下去的动画
do
{
setLocation(super.getX(),super.getY()+1);
try
{
sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}while(super.getY()<400);
this.state=2;
break;
}
//Judge返回3,表示跳到台上,但是重心较远,设定状态为2,游戏失败
if(plat.Judge(this.getX(),this.getWidth(),this.getY(),this.getHeight())==3)
{
//棋子掉落下去的动画
if (this.getX() < plat.getX())//棋子在平台左侧
{
do
{
setLocation(super.getX()-1, super.getY() + 1);
try
{
sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
} while (this.getX() + this.getWidth() >= plat.getX());
do
{
setLocation(super.getX(),super.getY()+2);
try
{
sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}while(super.getY()<400);
}else{
do
{
setLocation((int)(initialX + (Vx*actionTime)/Multiplayer),
(int)(initialY - (Vy*actionTime+0.5*Ay*actionTime*actionTime)/Multiplayer));
try
{
sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
actionTime+=1;
} while (this.getX()<= plat.getX()+plat.getWidth());
do
{
setLocation(super.getX() , super.getY() + 2);
try
{
sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}while(super.getY()<400);
}
this.state=2;
break;
}
//棋子飞出游戏区域外都没有出发碰撞,设定状态为2,游戏失败
if(super.getY()>400)
{
this.state=2;
break;
}
}
}
public int getState()
{
return this.state;
}
}
平台 Plat.class
Plat.class继承自JLabel类,其构造函数实现对其长宽高及位置和颜色的初始化。其内部的Random函数实现下一个平台的随机X向随机距离和Y向随机距离。且该函数会相对于棋子的二次跳跃函数路径进行限制,使得随机刷新新平台的位置处于棋子的抛物线路径上。同时该函数也会对于平台的生成位置进行限制。对于最后一个靠近于终点的平台的刷新会根据距离窗体边缘距离进行判定生成情况。函数中具体的平台生成算法和棋子碰撞到平台的分析判断逻辑将在后续算法分析中详细阐述。
public class Plat extends JLabel {
double xDistance;
double yDistance;
public Plat() {
this.setBounds(0, 200, 70, 40);
switch((int)(Math.random() * 10.0D)) {
case 1:
this.setBackground(new Color(30, 144, 255));
break;
case 2:
this.setBackground(new Color(135, 206, 250));
break;
case 3:
this.setBackground(new Color(148, 0, 211));
break;
case 4:
this.setBackground(new Color(220, 20, 60));
break;
case 5:
this.setBackground(new Color(75, 0, 130));
break;
case 6:
this.setBackground(new Color(0, 128, 128));
break;
case 7:
this.setBackground(new Color(46, 139, 87));
break;
case 8:
this.setBackground(new Color(255, 215, 0));
break;
case 9:
this.setBackground(new Color(255, 165, 0));
break;
case 10:
this.setBackground(new Color(210, 105, 30));
break;
default:
this.setBackground(new Color(178, 34, 34));
}
this.setOpaque(true);
this.setVisible(true);
}
public Plat(int x1, final int y1, int length1, int height1) {
this.setSize(length1, height1);
this.setLocation(x1, 420);
switch((int)(Math.random() * 10.0D)) {
case 1:
this.setBackground(new Color(30, 144, 255));
break;
case 2:
this.setBackground(new Color(135, 206, 250));
break;
case 3:
this.setBackground(new Color(148, 0, 211));
break;
case 4:
this.setBackground(new Color(220, 20, 60));
break;
case 5:
this.setBackground(new Color(75, 0, 130));
break;
case 6:
this.setBackground(new Color(0, 128, 128));
break;
case 7:
this.setBackground(new Color(46, 139, 87));
break;
case 8:
this.setBackground(new Color(255, 215, 0));
break;
case 9:
this.setBackground(new Color(255, 165, 0));
break;
case 10:
this.setBackground(new Color(210, 105, 30));
break;
default:
this.setBackground(new Color(178, 34, 34));
}
this.setOpaque(true);
this.setVisible(true);
(new Thread(new Runnable() {
public void run() {
do {
Plat.this.setLocation(Plat.this.getX(), Plat.this.getY() - 2);
try {
Thread.sleep(5L);
} catch (InterruptedException var2) {
if (Plat.this.getY() >= y1) {
return;
}
}
} while(Plat.this.getY() >= y1);
}
})).start();
this.setLocation(x1, y1);
}
public void Random() {
double height = (double)this.getHeight();
double width = (double)this.getWidth();
do {
do {
this.yDistance = (Math.random() - 0.5D) * 50.0D;
} while((double)this.getY() + height + this.yDistance > 350.0D);
} while((double)this.getY() + this.yDistance < 120.0D);
int Multiplayer = 50;
int MouseTimeMin = 200;
int MouseTimeMax = 600;
double Ay = -0.2D;
double VxMin = 15.0D + (double)MouseTimeMin * 0.02D;
double VxMax = 15.0D + (double)MouseTimeMax * 0.02D;
double VyMin = 10.0D + (double)MouseTimeMin * 0.06D;
double VyMax = 10.0D + (double)MouseTimeMax * 0.06D;
double YtMin = VyMin / Math.abs(Ay) + Math.sqrt(2.0D * Ay * this.yDistance + VyMin * VyMin);
double YtMax = VyMax / Math.abs(Ay) + Math.sqrt(2.0D * Ay * this.yDistance + VyMax * VyMax);
double YtAve = (YtMin + YtMax) / 2.0D;
int platRangeleft = (int)(VxMin * YtMin / (double)Multiplayer);
int platRangeRight = (int)(VxMax * YtMax / (double)Multiplayer);
do {
do {
this.xDistance = (double)platRangeleft + Math.random() * ((double)platRangeRight * 1.2D - (double)platRangeleft);
} while(this.xDistance + width >= 300.0D);
} while(this.xDistance <= width + 50.0D);
if ((double)this.getX() + width + 300.0D > 1000.0D) {
this.xDistance = (double)(1000 - this.getX()) - width;
}
}
public void Asign(Plat plat2) {
plat2.setLocation(this.getX(), this.getY());
}
public int Judge(int chessX1, int chessWidth, int chessY1, int chessHeight) {
int platX1 = this.getX();
int platY1 = this.getY();
int platX2 = this.getX() + this.getWidth();
int platY2 = this.getY() + this.getHeight();
int chessX2 = chessX1 + chessWidth;
int chessY2 = chessY1 + chessHeight;
if (chessX2 >= platX1) {
if (chessY2 <= platY1) {
if (0 <= chessY2 - platY1 && chessY2 - platY1 <= 3) {
if ((double)(chessX2 - platX1) >= 0.75D * (double)chessWidth && (double)(platX2 - chessX1) >= 0.25D * (double)chessWidth) {
return 1;
} else {
return chessX1 <= platX2 ? 3 : 0;
}
} else {
return 0;
}
} else if (chessX1 <= platX2) {
if (chessY2 >= platY1 && chessY1 <= platY2) {
return 2;
} else {
return chessY1 <= platY2 ? 2 : 0;
}
} else {
return 0;
}
} else {
return 0;
}
}
}
倒计时 timeCounter.class
timeCounter.class继承自JLabel类,用于在游戏窗体上实时监测游戏时间以及显示倒计时和和记录历史最快时间的功能。主要利用了System.currentTimeMillis()函数,通过对不同次事件记录间的时间差来获得实时的游戏时间并显示于组件中,并且在每次游戏结束后将与内部记录相应关卡的最快记录进行比较并动态更新。
public class TimeCounter extends JLabel
{ //关卡难度时间
private double gameTime;//单局游戏时间
//当前全部关卡,从1开始
public TimeCounter(Level level)
{
setBounds(800,0,200,40);
double limitTime=Main.getLimitTime(level.getLevel());
double gameTime=0;
double minTime=Main.getMinTime(level.getLevel());
setFont(new Font("Times New Roman",Font.BOLD,15));
setText("<html>Count Down:"+String.format("%.2f", limitTime-gameTime)+"秒<br/>Fastest Record:"+String.format("%.2f", minTime)+"秒</html>");
setForeground(Color.black);
setVisible(true);
}
public TimeCounter()
{
}
public double getGameTime()
{
return gameTime;
}
public void setGameTime(double time)
{
this.gameTime=time;
}
public double recordTime()
{
long TimeNow =System.currentTimeMillis();
double Time=(double)(TimeNow/1000.0);
return Time;
}
public void timeChange (Level level)
{
double limitTime=Main.getLimitTime(level.getLevel());
double minTime=Main.getMinTime(level.getLevel());
setFont(new Font("Times New Roman",Font.BOLD,15));
setText("<html>Count Down:"+String.format("%.2f", (limitTime-gameTime))+"秒<br/>Fastest Record:"+String.format("%.2f", minTime)+"秒</html>");
setForeground(Color.black);
}
}
实现成果
对一些函数算法的介绍
棋子(小人)跳跃算法分析
根据大作业要求,棋子的跳跃距离和高度要和按压时间成正比。为了让棋子的运行轨迹更加流畅,我们使用了二次函数作为棋子的运行轨迹,物理上为加速度向下(屏幕下)的斜抛运动。其中x轴初速度和Y轴初速度与按压时间成正比。由此保证棋子的跳跃距离和高度要和按压时间成正比。
我们定义以下变量:
按压时间:
double MouseTime
横向速度(匀速运动,初始值和鼠标按压时间成正比):
double Vx=15+MouseTime*0.02
纵向加速度(匀加速运动):
double Ay=-0.2
纵向速度(初始值和鼠标按压时间成正比):
double Vy=10+MouseTime*0.06
同时,为了让动画更加的流畅,我们使用超分辨率渲染,定义以下超分辨率系数:
int Multiplayer=50
函数运行在Multiplayer倍的画布上,然后除以Multiplayer倍的系数体现在屏幕上,这样保证棋子运动的丝滑和碰撞判断函数的准确性。
在动画实现上,我们使用每毫秒刷新一次棋子位置来实现。
跳跃平台生成算法分析
根据大作业要求,平台要能够生成在按压后能跳跃到的位置。为此,我们通过给定一个普遍按压时间范围(200-600ms)来计算平台生成范围,并在此范围内随机生成平台位置。
首先我们随机生成平台在Y轴方向上的偏移量,记为yDistance(屏幕向上为正)。
为了计算在此条件下的X轴方向跳跃平台可能生成的范围,我们定义以下变量:
超分辨率系数:
int Multiplayer=50
最短鼠标按压时间:
int MouseTimeMin=200
最长鼠标按压时间:
int MouseTimeMax=600
根据我们在跳跃算法中定义的相关参数:
横向速度(匀速运动,初始值和鼠标按压时间成正比):
Vx=15+MouseTime*0.02
纵向加速度(匀加速运动):
Ay=-0.2
纵向速度(初始值和鼠标按压时间成正比):
Vy=10+MouseTime*0.06
我们计算得出:
最低X轴方向速度:
VxMin=15+MouseTimeMin*0.02
最高X轴方向速度:
VxMax=15+MouseTimeMax*0.02
最低Y轴方向速度:
VyMin=10+MouseTimeMin*0.06
最高Y轴方向速度:
VyMax=10+MouseTimeMax*0.06
由于我们刚才已经随机生成了下一个跳跃平台的Y坐标,根据简单的物理计算,我们可以得到在不同的速度下,跳跃到此高度需要的时间:
最短:
YtMin=VyMin/ Abs(Ay)+ sqrt(2*Ay*yDistance+VyMin*VyMin)
最长:
YtMax=VyMax/ Abs(Ay)+ sqrt(2*Ay*yDistance+VyMax*VyMax)
有了时间之后,我们就能计算X轴方向上能到达的范围:
最短:
VxMin*YtMin/Multiplayer
最长:
VxMax*YtMax/Multiplaye
在此范围内生成的跳跃平台均为可达平台。
碰撞判断算法逻辑分析
判断棋子(小人)和平台的关系,一直是本程序中最为重要的一部分。下面简要分析碰撞判断算法的逻辑:
我们约定如下变量表示棋子和平台的位置:
棋子(chessX1,chessY1,xhessX2,chsssY2)
平台(platX1, platY1, platX2, platY2)
我们设置函数的触发条件为:(chessX2 >= platX1),即当棋子的右侧与平台的左侧处于同一垂直线上。只有在此条件下才有可能出现碰撞。
在此条件下有3种可能:
a. (chessY2 >= platY1 && chessY1 <= platY2)
棋子已经从侧面接触到平台,即棋子跳跃失败;
b. (chessY2 <= platY1)
棋子在平台上方,棋子还有可能跳跃成功,需要继续判断;
c. (chessY1 >= platY2)
棋子在平台下方,棋子跳跃必然失败,但是还需要等待棋子上方接触到平台才能算真正的失败。
对于条件b,我们继续进行判断,当(0 <= chessY2 - platY1 && chessY2 - platY1 <= 3)时,在此情况下棋子底部碰到台子,(同时我们为了防止渲染的坐标问题,设置一个冗余度3)我们继续分类:
a. ((chessX2-platX1) >= 0.75 * chessWidth &&(platX2-chessX1) >= 0.25 * chessWidth)
棋子右侧减去平台左侧大于0.75倍棋子宽度&&平台右侧减去棋子左侧大于0.25倍棋子宽度(视为跳跃成功)
b. (chessX1<=platX2)
棋子左侧小于等于平台右侧,也就是说两个还是连着的。但是重心已经偏移,跳跃失败;
c. (chessX1>=platX2)
棋子跳出踏板X轴范围,跳跃失败。
其中遇到的一些问题
实际上在代码编写前我对java的认知还停留在和C++类似的印象中(事实上也正是因为这个原因以及期末考核方式为大作业导致了我在学习过程中基本是划水的状态。这点各位一定不要学我!!!),对Java swing更是一无所知。从开始写大作业到最终完成只用了五天,而且实际上最关键的部分只集中在三天内,甚至最重要的多线程部分也是临时现学的,所以代码里实际上充满了单线程编程的惯式和面向过程的遗留风格,这也是可以用以改进的部分。
下面列出来了编写过程中的一些问题,有些的解决甚至不是那么完美,也可以是继续优化的空间。
- 使用Java swing 引入图像时无法对图片进行等比压缩,且对于图片质量会进行一定的压缩降低图像质量。
- 写窗口时要实现鼠标点击使棋子跳跃,注册监听器后无响应。
- 进入游戏流程后想要结束当前游戏局重新开始,窗口未按照既定要求进行刷新重绘,且无法响应。
- 写关卡切换时主函数进入分支线程后不再结束,程序结束分支线程后无法回到主线程,程序进入死循环。
- 在新线程中执行修改swing组件语句块时语句执行后组件的属性仍未发生改变的问题。
- 在调用keylistener()来监听键盘事件时,由于windows底层机制,导致keypress事件几乎每过500毫秒都会自动重新响应一次,导致起始时间被不断更新,无法获取到准确的按键长按时间
上述问题综合后集中的方面
Java Swing引入图片的机制
这部分我在网上也查了一段时间,这与其使用的图片压缩算法有关,而且据讲貌似也与图片的文件形式有关。对于这一点在网上统一能找到的图片等比压缩函数代码块如下:
public static ImageIcon scaleImage(ImageIcon icon, int w, int h) {
int nw = icon.getIconWidth();
int nh = icon.getIconHeight();
if (nw > w) {
nw = w;
nh = w * icon.getIconHeight() / icon.getIconWidth();
}
if (nh > h) {
nh = h;
nw = icon.getIconWidth() * h / icon.getIconHeight();
}
icon = new ImageIcon(icon.getImage().getScaledInstance(nw, nh, 4));
return icon;
}
这里是先将图片以文件读的方式读入img文件,再转成ImageIcon,在这个转换中再转为icon返回,最后用icon初始化Jlabel,但是因为我多次尝试无效,图片仍然是质量降低的一个状态,最终没有使用该函数去走这一个过程,直接用ImageIcon初始化了。这里仍然有一定的改进空间,有兴趣的同学可以去试试,说不定你的页面会提升为1080p呢!
Java Swing中的事件监听机制
这部分只能去查到事件监听的过程和相应的冒泡机制(这点和绝大多数的事件监听机制相同),不知道为什么将Jpanel加入监听器后对事件监听无反应,只能将监听器加入JFrame,这也导致了我的level对象从继承Jpanel变成了继承JFrame。如果你有兴趣可以去探究一下!
Java Swing的线程机制
这也是困扰我时间最长,也是最麻烦的一部分了。由于Java Swing是单线程的,这会导致出现许多莫名其妙不可解释的错误,诸如属性变化得靠一句控制台输出的语句来监督变化啦,同样的循环流程第一遍正常,检测了垃圾回收和内存占用都解除了之后第二遍仍然刷新不出来窗口的问题简直是奇奇怪怪折磨人,如同薛定谔的猫一样诡异,许多bug的出现和解决都是充满了诡异色彩,以至于我在写代码过程中多次被搞崩溃。
对于此,我只想评价
这部分我也去找了不少关于Java Swing线程的文章去看。现在罗列下来,如果你感兴趣的话可以去探究!
理解Swing中的事件与线程
java中进度条不能更新问题的研究
恶补Java Swing线程刷新UI机制
事实上本着能用就行的原则,我的代码里面大量的线程切换和关闭难免有一些说不清道不明的混乱逻辑,如果你有志于弄清楚这一块的话,你可以仔细地去调试一下多线程里奇怪的逻辑,我相信你可能会从我的代码里获得比我自己本身从中获得的更多的知识和逻辑,并且提升比我自己多多了。我最高兴地是你能把这些弄清楚然后轻蔑地说这作者什么玩意,写的什么垃圾代码!!!
Java Swing里对键盘事件的监听
实际上这是我在代码后期最耗时的一个问题,我对其的研究也最久,但一度找不到解决方案甚至只能作罢,在大作业提交前夕我才找人问到了解决方式。做个纪念便也写在这里。由于Java Swing对键盘按键的确定依赖的是三个函数key_pressed(),key_released()和key_typed(),而我需要做的就是对某一个指定键从按下开始计录一个时刻,再到松开记录一个时刻,计算之间按压的时间:听起来非常简单对吧!但是问题在于我打死也没想到当你按下某一个键并且一直没有松开后,key_pressed()函数被反复响应,于是我的按键按压时间被不断刷新,甚至可能出现我按的越久反而不如初始按那么一下的时间。而且经我查证,这个还真不是Java的锅,它更像是系统底层对键盘按压时间响应的一种机制,长按的响应更像是不断反复响应一个短暂的按压事件(有点函数的防抖和节流的那味了!),而且因系统的设计不同而异。在windows上面反正是500ms的刷新。而且这部分的介绍网上是少之又少,关于这一点的问题的找出和解决可谓是难上加难。至于最终解决方式,就是加入了一个额外的flag,通过该值的变化去确认哪一次的key_pressed()是我们需要的时刻。详细解决方式见我level.class部分的代码。
关于键盘事件的部分如果你想要了解更多可以看这里:
Qt键盘事件(二)——长按按键反复触发event事件问题解决
关于键盘输入
KeyEvent - Java 11中文版 - API参考文档
Stackoverflow- Java Swing - Pressed key gets not released (sometimes)
最终未能完成部分的解释
由于时间仓促,再加上考试 期限紧张,我的代码工作一度十分痛苦,甚至连一些基本的工作都出问题。我现在仍然还记得的就是对键盘按键时长做获取和一些过度动画的问题,编写过程bug不断,反复横跳。截至大作业的提交我仍然没有完成的部分还剩下了音效的添加。这部分我当时在写代码时请了一个外援协助来处理这部分,后因他的git操作始终不甚熟练和所使用编译器不同造成的差异,这部分没有完成。但是如果你有从我们的git仓库下载源码,仍然能看到他遗留的代码部分,存在了audio层里。如果你乐意仍然可以沿着他的代码往下研究。据讲这部分仍然有着不小的坑和挑战,不过可以肯定的是他的方式是有效的。
结语
实际上自己去利用java写完一个跳一跳小游戏后还是挺有成就感的一件事,除了因为写这样的一项大作业对我后续的复习造成了极大的困扰并且导致了我期末不理想(划去,这是借口),这个项目仍然还有这极大的改进空间并且可以更加完善的。这个完整的一个代码编写过程也让我的Java编写能力有了一定的提升,但是我可以肯定的是这个代码仍然是非常丑陋和不堪的。希望将来有人看到它并且用它借鉴的话能够进一步完善它。