Commit 2968bb8e authored by kingyu's avatar kingyu Committed by kingyuluk
Browse files

feat: 优化记分方法

BREAKING CHANGE: 移除了计时器,更改了游戏的记分方法,现在记分更准确了
parent e2cb276d
......@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [1.2.2](https://github.com/kingyuluk/FlappyBird/compare/v1.2.1...v1.2.2) (2020-07-12)
### ⚠ BREAKING CHANGES
* 移除了计时器,更改了游戏的记分方法,现在记分更准确了
### Features
* 优化记分方法 ([9aa9275](https://github.com/kingyuluk/FlappyBird/commit/9aa927537dc9180942c4a982ea46edd840d7c769))
### [1.2.1](https://github.com/kingyuluk/FlappyBird/compare/v1.2.0...v1.2.1) (2020-07-12)
......
File added
......@@ -8,21 +8,26 @@ Flappy Bird for desktop platforms.
## Overview
本项目为Flappy bird的桌面平台版,具备原版的所有功能,且相较于原版优化了游戏难度并加入移动型水管,让游戏更具可玩性。
### 游戏玩法
游戏只需空格键即可操作,每局游戏随机刷新所有元素,小鸟受到重力作用会不断下坠,敲击空格键小鸟就会振翅向上飞,游戏过程中需要玩家控制小鸟不断飞行,并注意躲避随机生成的水管,每飞过一对水管就会得分,飞行过程中如果撞到水管或掉落在地则游戏结束。
本项目为Flappy bird的桌面平台版,具备原版的所有功能,且相较于原版优化了游戏难度并加入移动型水管,增加可玩性。
## How to play
直接运行FlappyBird.jar即可开始游戏。
游戏使用空格键操作。
每局游戏随机刷新所有元素,小鸟受到重力作用会不断下坠,敲击空格键使小鸟振翅向上飞,游戏过程中需要玩家控制小鸟不断飞行,并注意躲避随机生成的水管,每飞过一对水管就会得分,飞行过程中如果撞到水管或掉落在地则游戏结束。
## 游戏界面
### 游戏启动
![image](https://github.com/kingyuluk/flappy-bird/blob/master/examples/start.png)
![image](https://github.com/kingyuluk/FlappyBird/blob/master/resources/readme_img/start.png)
### 运行示例
![image](https://github.com/kingyuluk/flappy-bird/blob/master/examples/play.gif)
![image](https://github.com/kingyuluk/FlappyBird/blob/master/resources/readme_img/play.gif)
### 游戏结束
![image](https://github.com/kingyuluk/flappy-bird/blob/master/examples/over.png)
![image](https://github.com/kingyuluk/FlappyBird/blob/master/resources/readme_img/over.png)
## Package Contents
......@@ -34,6 +39,9 @@ Flappy Bird for desktop platforms.
## Change Log
v1.2.2 - July 12, 2020
* 移除了计时器,优化了游戏的记分方式,现在记分更准确了
v1.2.1 - July 12, 2020
* 使用AudioClip类的方法播放连续的短音频可能会导致线程冲突使游戏卡顿
......@@ -55,4 +63,8 @@ v1.0.0 - July 10, 2020
* sun包在不同操作系统和不同版本的JDK中可能发生变化,因此无法确保工作在所有JAVA平台上
## Contact
* email: <kingyuluk@mail.dlut.edu.cn>
* email: <kingyuluk@hotmail.com>
## License
* 图片与音效资源皆来源于网络,仅供学习交流
......@@ -9,17 +9,19 @@ import com.bird.util.Constant;
import com.bird.util.GameUtil;
import com.bird.util.MusicUtil;
import static com.bird.util.GameUtil.drawTitle;
/**
* 小鸟类,小鸟的绘制与飞行逻辑都在此类
*
* @author Kingyu
*
*/
public class Bird {
public static final int IMG_COUNT = 8; // 图片数量
public static final int STATE_COUNT = 4; // 状态数
private BufferedImage[][] birdImgs; // 小鸟的图片数组对象
private int x, y; // 小鸟的坐标
private final BufferedImage[][] birdImages; // 小鸟的图片数组对象
private final int x;
private int y; // 小鸟的坐标
int wingState; // 翅膀状态
......@@ -37,20 +39,20 @@ public class Bird {
public static final int STATE_FALL = 3;
public static final int STATE_DEAD = 4;
private Rectangle birdRect; // 碰撞矩形
private final Rectangle birdRect; // 碰撞矩形
public static final int RECT_DESCALE = 2; // 补偿碰撞矩形宽高的参数
private GameTime timing; // 飞行时间
private final GameScore countScore; // 计分器
// 在构造器中对资源初始化
public Bird() {
timing = GameTime.getInstance(); // 计
countScore = GameScore.getInstance(); // 计
// 读取小鸟图片资源
birdImgs = new BufferedImage[STATE_COUNT][IMG_COUNT];
birdImages = new BufferedImage[STATE_COUNT][IMG_COUNT];
for (int j = 0; j < STATE_COUNT; j++) {
for (int i = 0; i < IMG_COUNT; i++) {
birdImgs[j][i] = GameUtil.loadBUfferedImage(Constant.BIRDS_IMG_PATH[j][i]);
birdImages[j][i] = GameUtil.loadBufferedImage(Constant.BIRDS_IMG_PATH[j][i]);
}
}
......@@ -58,33 +60,30 @@ public class Bird {
x = Constant.FRAME_WIDTH >> 2;
y = Constant.FRAME_HEIGHT >> 1;
int ImgWidth = birdImgs[state][0].getWidth();
int ImgHeight = birdImgs[state][0].getHeight();
int ImgWidth = birdImages[state][0].getWidth();
int ImgHeight = birdImages[state][0].getHeight();
// 初始化碰撞矩形
int rectX = x - ImgWidth / 2;
int rectY = y - ImgHeight / 2;
int rectWidth = ImgWidth;
int rectHeight = ImgHeight;
birdRect = new Rectangle(rectX + RECT_DESCALE, rectY + RECT_DESCALE * 2, rectWidth - RECT_DESCALE * 3,
rectHeight - RECT_DESCALE * 4); // 碰撞矩形的坐标与小鸟相同
birdRect = new Rectangle(rectX + RECT_DESCALE, rectY + RECT_DESCALE * 2, ImgWidth - RECT_DESCALE * 3,
ImgHeight - RECT_DESCALE * 4); // 碰撞矩形的坐标与小鸟相同
}
// 绘制方法
public void draw(Graphics g) {
fly();
int state_index = state > STATE_FALL ? STATE_FALL : state; // 图片资源索引
int state_index = Math.min(state, STATE_FALL); // 图片资源索引
// 小鸟中心点计算
int halfImgWidth = birdImgs[state_index][0].getWidth() >> 1;
int halfImgHeight = birdImgs[state_index][0].getHeight() >> 1;
int halfImgWidth = birdImages[state_index][0].getWidth() >> 1;
int halfImgHeight = birdImages[state_index][0].getHeight() >> 1;
if (speed > 0)
image = birdImgs[STATE_UP][0];
image = birdImages[STATE_UP][0];
g.drawImage(image, x - halfImgWidth, y - halfImgHeight, null); // x坐标于窗口1/4处,y坐标位窗口中心
if (state == STATE_DEAD) {
drawGameover(g);
} else if (state == STATE_FALL) {
} else {
drawGameOver(g);
} else if (state != STATE_FALL) {
drawScore(g);
}
// 绘制矩形
......@@ -96,7 +95,6 @@ public class Bird {
public static final double g = 9.8; // 重力加速度
public static final double T = 0.2; // 小鸟的下落函数执行的时间
private double h; // 小鸟y轴的位移量
private double speed = 0; // 小鸟的初速度
private boolean keyFlag = true; // 按键状态,true为已释放,使当按住按键时不会重复调用方法
......@@ -117,25 +115,20 @@ public class Bird {
private void fly() {
// 翅膀状态,实现小鸟振翅飞行
wingState++;
image = birdImgs[state > STATE_FALL ? STATE_FALL : state][wingState / 10 % IMG_COUNT];
image = birdImages[Math.min(state, STATE_FALL)][wingState / 10 % IMG_COUNT];
switch (state) {
case STATE_NORMAL:
break;
case STATE_UP:
break;
case STATE_DOWN:
// 物理公式
speed -= g * T;
h = speed * T - g * T * T / 2;
// 小鸟y轴的位移量
double h = speed * T - g * T * T / 2;
y = (int) (y - h);
birdRect.y = (int) (birdRect.y - h);
// 控制坠落的边界,若y坐标 > 窗口的高度 - 地面的高度 - 小鸟图片的高度则死亡
if (birdRect.y >= Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImgs[state][0].getHeight() >> 1)) {
y = Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImgs[state][0].getHeight() >> 1);
birdRect.y = Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImgs[state][0].getHeight() >> 1);
if (birdRect.y >= Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImages[state][0].getHeight() >> 1)) {
y = Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImages[state][0].getHeight() >> 1);
birdRect.y = Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImages[state][0].getHeight() >> 1);
birdFall();
}
......@@ -149,16 +142,18 @@ public class Bird {
birdRect.y = (int) (birdRect.y - h);
// 控制坠落的边界,若y坐标 > 窗口的高度 - 地面的高度 - 小鸟图片的高度则死亡
if (birdRect.y >= Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImgs[state][0].getHeight() >> 1)) {
if (birdRect.y >= Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImages[state][0].getHeight() >> 1)) {
y = Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImgs[state][0].getHeight() >> 1);
birdRect.y = Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImgs[state][0].getHeight() >> 1);
y = Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImages[state][0].getHeight() >> 1);
birdRect.y = Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImages[state][0].getHeight() >> 1);
GameFrame.setGameState(GameFrame.STATE_OVER); // 改变游戏状态
birdDead();
}
break;
case STATE_NORMAL:
case STATE_UP:
case STATE_DEAD:
break;
}
......@@ -199,8 +194,6 @@ public class Bird {
public void birdFall() {
state = STATE_FALL;
MusicUtil.playCrash(); // 播放音效
// 结束计时
timing.endTiming();
}
// 小鸟死亡
......@@ -208,28 +201,23 @@ public class Bird {
state = STATE_DEAD;
// 加载游戏结束的资源
if (overImg == null) {
overImg = GameUtil.loadBUfferedImage(Constant.OVER_IMG_PATH);
scoreImg = GameUtil.loadBUfferedImage(Constant.SCORE_IMG_PATH);
againImg = GameUtil.loadBUfferedImage(Constant.AGAIN_IMG_PATH);
overImg = GameUtil.loadBufferedImage(Constant.OVER_IMG_PATH);
scoreImg = GameUtil.loadBufferedImage(Constant.SCORE_IMG_PATH);
againImg = GameUtil.loadBufferedImage(Constant.AGAIN_IMG_PATH);
}
countScore.isSaveScore(); // 判断是否保存纪录
}
public boolean isDead() {
return state == STATE_FALL || state == STATE_DEAD;
}
// 开始计时的方法
public void startTiming() {
if (timing.isReadyTiming())
timing.startTiming();
}
// 绘制实时分数
private void drawScore(Graphics g) {
g.setColor(Color.white);
g.setFont(Constant.TIME_FONT);
String str = Long.toString(timing.TimeToScore());
int x = Constant.FRAME_WIDTH - GameUtil.getStringWidth(Constant.TIME_FONT, str) >> 1;
g.setFont(Constant.CURRENT_SCORE_FONT);
String str = Long.toString(countScore.getScore());
int x = Constant.FRAME_WIDTH - GameUtil.getStringWidth(Constant.CURRENT_SCORE_FONT, str) >> 1;
g.drawString(str, x, Constant.FRAME_HEIGHT / 10);
}
......@@ -238,7 +226,7 @@ public class Bird {
private int flash = 0; // 图片闪烁参数
// 绘制游戏结束的显示
private void drawGameover(Graphics g) {
private void drawGameOver(Graphics g) {
// 绘制结束标志
int x = Constant.FRAME_WIDTH - overImg.getWidth() >> 1;
int y = Constant.FRAME_HEIGHT / 4;
......@@ -254,28 +242,26 @@ public class Bird {
g.setFont(Constant.SCORE_FONT);
x = (Constant.FRAME_WIDTH - scoreImg.getWidth() / 2 >> 1) + SCORE_LOCATE;// 位置补偿
y += scoreImg.getHeight() >> 1;
String str = Long.toString(timing.TimeToScore());
String str = Long.toString(countScore.getScore());
x -= GameUtil.getStringWidth(Constant.SCORE_FONT, str) >> 1;
y += GameUtil.getStringHeight(Constant.SCORE_FONT, str);
g.drawString(str, x, y);
// 绘制最高分数
if (timing.getBestScore() > 0) {
str = Long.toString(timing.getBestScore());
if (countScore.getBestScore() > 0) {
str = Long.toString(countScore.getBestScore());
x = (Constant.FRAME_WIDTH + scoreImg.getWidth() / 2 >> 1) - SCORE_LOCATE;// 位置补偿
x -= GameUtil.getStringWidth(Constant.SCORE_FONT, str) >> 1;
g.drawString(str, x, y);
}
// 绘制继续游戏
final int COUNT = 30; // 控制闪烁间隔的参数
if (flash++ > COUNT) {
x = Constant.FRAME_WIDTH - againImg.getWidth() >> 1;
y = Constant.FRAME_HEIGHT / 5 * 3;
g.drawImage(againImg, x, y, null);
if (flash == COUNT * 2)
// 绘制继续游戏,使图像闪烁
final int COUNT = 30; // 闪烁周期
if (flash++ > COUNT)
drawTitle(againImg, g);
if (flash == COUNT * 2) // 重置闪烁参数
flash = 0;
}
}
// 重置小鸟
public void reset() {
......@@ -283,10 +269,10 @@ public class Bird {
y = Constant.FRAME_HEIGHT >> 1; // 小鸟坐标
speed = 0; // 小鸟速度
int ImgHeight = birdImgs[state][0].getHeight();
int ImgHeight = birdImages[state][0].getHeight();
birdRect.y = y - ImgHeight / 2 + RECT_DESCALE * 2; // 小鸟碰撞矩形坐标
timing.reset(); // 计时
countScore.reset(); // 重置计分
flash = 0;
}
......
......@@ -3,41 +3,31 @@ package com.bird.main;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import com.bird.util.Constant;
/**
* 云朵类
*
* @author Kingyu
*
*/
public class Cloud {
private int dir; // 方向
public static final int DIR_NONE = 0;
public static final int DIR_LEFT = 1;
public static final int DIR_RIGHT = 2;
private int speed; // 速度
private int x, y; // 坐标
private final int speed; // 速度
private int x; // 坐标
private final int y;
private BufferedImage img;
private final BufferedImage img;
// 云朵图片缩放的比例 1.0~2.0
private double scale;
private int scaleImageWidth;
private int scaleImageHeight;
private final int scaleImageWidth;
private final int scaleImageHeight;
// 构造器
public Cloud(BufferedImage img, int dir, int x, int y) {
public Cloud(BufferedImage img, int x, int y) {
super();
this.img = img;
this.dir = dir;
this.x = x;
this.y = y;
this.speed = 2; //云朵的速度
scale = 1 + Math.random(); // Math.random()返回0.0~1.0的随机值
// 云朵图片缩放的比例 1.0~2.0
double scale = 1 + Math.random(); // Math.random()返回0.0~1.0的随机值
// 缩放云朵图片
scaleImageWidth = (int) (scale * img.getWidth());
scaleImageHeight = (int) (scale * img.getWidth());
......@@ -46,11 +36,9 @@ public class Cloud {
// 绘制方法
public void draw(Graphics g, Bird bird) {
int speed = this.speed;
if(dir == DIR_NONE) //云彩不动
speed = 0;
x = (dir == DIR_LEFT) ? x - speed : x + speed; // 方向逻辑
if (bird.isDead())
speed = 1;
x -= speed;
g.drawImage(img, x, y, scaleImageWidth, scaleImageHeight, null);
}
......@@ -60,21 +48,7 @@ public class Cloud {
* @return 飞出则返回true,否则返回false
*/
public boolean isOutFrame() {
boolean result = false;
if (dir == DIR_LEFT) {
if (x < -1 * scaleImageWidth) {
return true;
}
} else if (dir == DIR_RIGHT) {
if (x > Constant.FRAME_WIDTH) {
return true;
}
}
return result;
return x < -1 * scaleImageWidth;
}
// 改变方向
public void setDir(int dir) {
this.dir = dir;
}
}
......@@ -14,9 +14,9 @@ import com.bird.util.GameUtil;
*/
public class GameBackground {
private static BufferedImage BackgroundImg;// 背景图片
private static final BufferedImage BackgroundImg;// 背景图片
private int speed; // 背景层的速度
private final int speed; // 背景层的速度
private int layerX; // 背景层的坐标
// 在构造器中初始化
......@@ -26,11 +26,9 @@ public class GameBackground {
}
static { //读取背景图片
BackgroundImg = GameUtil.loadBUfferedImage(Constant.BG_IMG_PATH);
BackgroundImg = GameUtil.loadBufferedImage(Constant.BG_IMG_PATH);
}
public static final int BG_IMAGE_HEIGHT = BackgroundImg.getHeight(); //图片的高度
// 绘制方法
public void draw(Graphics g, Bird bird) {
// 绘制背景色
......
......@@ -11,11 +11,10 @@ import com.bird.util.GameUtil;
* 游戏中各种元素层的类
*
* @author Kingyu
*
*/
public class GameElementLayer {
private List<Pipe> pipes; // 水管的容器
private final List<Pipe> pipes; // 水管的容器
// 构造器
public GameElementLayer() {
......@@ -71,35 +70,27 @@ public class GameElementLayer {
} else {
// 判断最后一对水管是否完全进入游戏窗口,若进入则添加水管
Pipe lastPipe = pipes.get(pipes.size() - 1); // 获得容器中最后一个水管
if (lastPipe.isInFrame()) { // 根据游戏分数难度递增
if (GameTime.getInstance().TimeToScore() < Constant.HOVER_MOVING_SCORE) {
if (lastPipe.isInFrame()) {
if (pipes.size() >= Constant.FULL_PIPE - 2)// 若窗口中可容纳的水管已满,说明小鸟已飞到第一对水管的位置,开始记分
GameScore.getInstance().setScore(bird);
try {
if (GameUtil.isInProbability(2, 5)) { // 40%的概率生成悬浮的普通水管
addHoverPipe(lastPipe);
} else {
addNormalPipe(lastPipe);
}
} catch (Exception e) {
e.printStackTrace();
}
int currentScore = (int) GameScore.getInstance().getScore() + 1; // 获取当前分数
// 移动水管刷新的概率随当前分数递增,当得分大于19后全部刷新移动水管
if (GameUtil.isInProbability(currentScore, 20)) {
if (GameUtil.isInProbability(1, 4)) // 生成移动水管和移动悬浮水管的概率
addMovingHoverPipe(lastPipe);
else
addMovingNormalPipe(lastPipe);
} else {
try {
if (GameUtil.isInProbability(1, 4)) { // 1/4的概率生成普通水管
if(GameUtil.isInProbability(1, 2)) // 生成普通水管和悬浮水管的概率
if (GameUtil.isInProbability(1, 2)) // 生成静止普通水管和静止悬浮水管的概率
addNormalPipe(lastPipe);
else
addHoverPipe(lastPipe);
} else {
if(GameUtil.isInProbability(1, 3)) // 生成移动水管和移动悬浮水管的概率
addMovingHoverPipe(lastPipe);
else
addMovingNormalPipe(lastPipe);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
......@@ -113,9 +104,9 @@ public class GameElementLayer {
int topHeight = GameUtil.getRandomNumber(MIN_HEIGHT, MAX_HEIGHT + 1); // 随机生成水管高度
int x = lastPipe.getX() + HORIZONTAL_INTERVAL; // 新水管的x坐标 = 最后一对水管的x坐标 + 水管的间隔
Pipe top = PipePool.get("Pipe"); //从水管对象池中获取对象
Pipe top = PipePool.get("Pipe"); // 从水管对象池中获取对象
//设置x, y, height, type属性
// 设置x, y, height, type属性
top.setAttribute(x, -Constant.TOP_PIPE_LENGTHENING, topHeight + Constant.TOP_PIPE_LENGTHENING,
Pipe.TYPE_TOP_NORMAL, true);
......@@ -130,7 +121,7 @@ public class GameElementLayer {
/**
* 添加悬浮水管
*
* @param lastPipe
* @param lastPipe 传入最后一根水管以获取x坐标
*/
private void addHoverPipe(Pipe lastPipe) {
......@@ -158,7 +149,7 @@ public class GameElementLayer {
/**
* 添加移动的悬浮水管
*
* @param lastPipe
* @param lastPipe 传入最后一根水管以获取x坐标
*/
private void addMovingHoverPipe(Pipe lastPipe) {
......@@ -186,7 +177,7 @@ public class GameElementLayer {
/**
* 添加移动的普通水管
*
* @param lastPipe
* @param lastPipe 传入最后一根水管以获取x坐标
*/
private void addMovingNormalPipe(Pipe lastPipe) {
int topHeight = GameUtil.getRandomNumber(MIN_HEIGHT, MAX_HEIGHT + 1); // 随机生成水管高度
......@@ -205,32 +196,28 @@ public class GameElementLayer {
}
/**
* 判断元素和小鸟是否发生碰撞,若发生碰撞返回true,否则返回false
* 判断元素和小鸟是否发生碰撞
*
* @param bird
* @return
* @param bird 传入小鸟对象
*/
public boolean isCollideBird(Bird bird) {
public void isCollideBird(Bird bird) {
// 若鸟已死则不再判断
if (bird.isDead()) {
return false;
return;
}
// 遍历水管容器
for (int i = 0; i < pipes.size(); i++) {
Pipe pipe = pipes.get(i);
for (Pipe pipe : pipes) {
// 判断碰撞矩形是否有交集
if (pipe.getPipeRect().intersects(bird.getBirdRect())) {
bird.birdFall(); //有交集则小鸟坠落
return true;
bird.birdFall(); // 有交集则小鸟坠落
return;
}
}
return false;
}
// 重置元素层
public void reset() {
for (int i = 0; i < pipes.size(); i++) {
Pipe pipe = pipes.get(i);
for (Pipe pipe : pipes) {
PipePool.giveBack(pipe);
}
pipes.clear();
......
......@@ -12,13 +12,11 @@ import com.bird.util.GameUtil;
* 前景类, 游戏中的遮挡层 包含多朵云
*
* @author Kingyu
*
*/
public class GameForeground {
private List<Cloud> clouds = new ArrayList<>(); // 云朵的容器
private final List<Cloud> clouds; // 云朵的容器
private BufferedImage[] cloudImgs; // 图片资源
private int cloudDir; // 云的方向
private final BufferedImage[] cloudImages; // 图片资源
private long time; // 控制云的逻辑运算周期
public static final int CLOUD_INTERVAL = 100; //云朵刷新的逻辑运算的周期
......@@ -27,21 +25,18 @@ public class GameForeground {
clouds = new ArrayList<>(); //云朵的容器
// 读入图片资源
cloudImgs = new BufferedImage[Constant.CLOUD_IMAGE_COUNT];
cloudImages = new BufferedImage[Constant.CLOUD_IMAGE_COUNT];
for (int i = 0; i < Constant.CLOUD_IMAGE_COUNT; i++) {
cloudImgs[i] = GameUtil.loadBUfferedImage(Constant.CLOUDS_IMG_PATH[i]);
cloudImages[i] = GameUtil.loadBufferedImage(Constant.CLOUDS_IMG_PATH[i]);
}
// 初始化云朵的属性
cloudDir = Cloud.DIR_LEFT;
time = System.currentTimeMillis(); // 获取当前时间,用于控制云的逻辑运算周期
}
// 绘制方法
public void draw(Graphics g, Bird bird) {
cloudLogic();
for (int i = 0; i < clouds.size(); i++) {
clouds.get(i).draw(g, bird);
for (Cloud cloud : clouds) {
cloud.draw(g, bird);
}
}
......@@ -55,18 +50,14 @@ public class GameForeground {
try {
if (GameUtil.isInProbability(Constant.CLOUD_BORN_PERCENT, 100)) { // 根据给定的概率添加云朵
int index = GameUtil.getRandomNumber(0, Constant.CLOUD_IMAGE_COUNT); // 随机选取云朵图片
// 云朵刷新的坐标
int x = Constant.FRAME_WIDTH; // 从屏幕左侧开始刷新
// 根据当前的方向调整云朵刷新的x坐标(暂时无用)
// if (cloudDir == Cloud.DIR_LEFT) {
// } else {
// x = -1 * cloudImgs[index].getWidth();
// }
// y坐标随机在上1/3屏选取
int y = GameUtil.getRandomNumber(Constant.TOP_BAR_HEIGHT, Constant.FRAME_HEIGHT / 3);
//向容器中添加云朵
Cloud cloud = new Cloud(cloudImgs[index], cloudDir, x, y);
Cloud cloud = new Cloud(cloudImages[index], x, y);
clouds.add(cloud);
}
} catch (Exception e) {
......@@ -84,15 +75,6 @@ public class GameForeground {
}
}
/*
* 功能已实现,但不太符合现实,暂时注释掉 // 云朵随机改变方向, try { if (GameUtil.isInProbability(1,
* Constant.CLOUD_DIRCHANGE)) { // 新的云彩方向 int newDir =
* GameUtil.getRandomNumber(Cloud.DIR_LEFT, Cloud.DIR_RIGHT + 1); if (newDir !=
* cloudDir) { // 风向改变,调整云彩状态 cloudDir = newDir; for (int i = 0; i <
* clouds.size(); i++) { // 遍历容器中的云朵 Cloud tempCloud = clouds.get(i);
* tempCloud.setDir(newDir); } } System.out.println(cloudDir); } } catch
* (Exception e) { e.printStackTrace(); }
*/
}
}
}
......@@ -20,7 +20,6 @@ import java.awt.image.BufferedImage;
* 主窗口类,游戏窗口和绘制的相关内容
*
* @author Kingyu
*
*/
public class GameFrame extends Frame implements Runnable {
......@@ -72,7 +71,6 @@ public class GameFrame extends Frame implements Runnable {
bird.birdUp();
bird.birdDown();
setGameState(STATE_START); // 游戏状态改变
bird.startTiming(); // 计时器开始计时
}
break;
case STATE_START:
......@@ -127,12 +125,11 @@ public class GameFrame extends Frame implements Runnable {
// 系统线程:屏幕内容的绘制,窗口事件的监听与处理
// 两个线程会抢夺系统资源,可能会出现一次刷新周期所绘制的内容,并没有在一次刷新周期内完成
// (双缓冲)单独定义一张图片,将需要绘制的内容绘制到这张图片,再一次性地将图片绘制到窗口
private BufferedImage bufImg = new BufferedImage(FRAME_WIDTH, FRAME_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR);
private final BufferedImage bufImg = new BufferedImage(FRAME_WIDTH, FRAME_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR);
/**
* 绘制游戏内容 当repaint()方法被调用时,JVM会调用update(),参数g是系统提供的画笔,由系统进行实例化
* 单独启动一个线程,不断地快速调用repaint(),让系统对整个窗口进行重绘
*
*/
public void update(Graphics g) {
Graphics bufG = bufImg.getGraphics(); // 获得图片画笔
......@@ -141,16 +138,18 @@ public class GameFrame extends Frame implements Runnable {
background.draw(bufG, bird); // 背景层
foreground.draw(bufG, bird); // 前景层
// 鸟
if (gameState == STATE_READY) { // 游戏未开始
ready.draw(bufG);
bird.draw(bufG); // 鸟
} else { // 游戏结束
gameElement.draw(bufG, bird); // 游戏元素层
bird.draw(bufG); // 鸟
}
bird.draw(bufG); // 鸟
g.drawImage(bufImg, 0, 0, null); // 一次性将图片绘制到屏幕上
}
// TODO: 不建议在while循环中使用sleep
@SuppressWarnings("InfiniteLoopStatement")
@Override
public void run() {
while (true) {
......@@ -163,11 +162,6 @@ public class GameFrame extends Frame implements Runnable {
}
}
// 获取、设置游戏状态的方法
public static int getGameState() {
return gameState;
}
public static void setGameState(int gameState) {
GameFrame.gameState = gameState;
}
......
......@@ -2,9 +2,9 @@ package com.bird.main;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import com.bird.util.Constant;
import com.bird.util.GameUtil;
import static com.bird.util.GameUtil.drawTitle;
/**
* 游戏启动界面类
......@@ -14,15 +14,15 @@ import com.bird.util.GameUtil;
*/
public class GameReady {
private BufferedImage titleImg;
private BufferedImage noticeImg;
private final BufferedImage titleImg;
private final BufferedImage noticeImg;
private int flash; // 图像闪烁参数
private int flash = 0; // 图像闪烁参数
// 构造器中进行初始化,装载图像资源
public GameReady() {
titleImg = GameUtil.loadBUfferedImage(Constant.TITLE_IMG_PATH);
noticeImg = GameUtil.loadBUfferedImage(Constant.NOTICE_IMG_PATH);
titleImg = GameUtil.loadBufferedImage(Constant.TITLE_IMG_PATH);
noticeImg = GameUtil.loadBufferedImage(Constant.NOTICE_IMG_PATH);
}
public void draw(Graphics g) {
......@@ -33,14 +33,10 @@ public class GameReady {
// 使notice的图像闪烁
final int COUNT = 30; // 闪烁周期
if (flash++ > COUNT) {
// 计算notice图像的x、y坐标
x = Constant.FRAME_WIDTH - noticeImg.getWidth() >> 1;
y = Constant.FRAME_HEIGHT / 5 * 3;
g.drawImage(noticeImg, x, y, null); // 绘制
if (flash++ > COUNT)
drawTitle(noticeImg, g);
if (flash == COUNT * 2) // 重置闪烁参数
flash = 0;
}
}
}
......@@ -15,35 +15,27 @@ import com.bird.util.MusicUtil;
* @author Kingyu
*
*/
public class GameTime {
private static final GameTime GAME_TIME = new GameTime();
public class GameScore {
private static final GameScore GAME_SCORE = new GameScore();
private int timeState; // 计时器的状态
public static final int STATE_READY = 0; // 计时就绪
public static final int STATE_START = 1; // 计时开始
public static final int STATE_OVER = 2; // 计时结束
private long startTime = 0; // 开始时间 ms
private long endTime = 0; // 开始时间 ms
private long score = 0; // 分数
private long bestScore; // 最高分数
private GameTime() {
timeState = STATE_READY;
private GameScore() {
bestScore = -1;
try {
loadBestTime();
loadBestScore();
} catch (Exception e) {
e.printStackTrace();
}
}
public static GameTime getInstance() {
return GAME_TIME;
public static GameScore getInstance() {
return GAME_SCORE;
}
// 装载最高纪录
private void loadBestTime() throws Exception {
private void loadBestScore() throws Exception {
File file = new File(Constant.SCORE_FILE_PATH);
if (file.exists()) {
DataInputStream dis = new DataInputStream(new FileInputStream(file));
......@@ -53,10 +45,10 @@ public class GameTime {
}
// 保存最高纪录
public void saveBestScore(long time) throws Exception {
public void saveBestScore(long score) throws Exception {
File file = new File(Constant.SCORE_FILE_PATH);
DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));
dos.writeLong(time);
dos.writeLong(score);
dos.close();
}
......@@ -64,51 +56,20 @@ public class GameTime {
return bestScore;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public void setOverTime(long endTime) {
this.endTime = endTime;
}
/**
* 游戏用时,毫秒
*
* @return
*/
public long getTime() {
if (timeState == STATE_READY) {
return startTime;
} else if (timeState == STATE_START) {
return (System.currentTimeMillis() - startTime);
} else {
return (endTime - startTime);
}
}
//游戏时间转换为秒
public long getTimeInSeconds() {
return getTime() / 1000;
public long getScore() {
return score;
}
// 计时器是否就绪
public boolean isReadyTiming() {
return timeState == STATE_READY;
public void setScore(Bird bird) {
if(!bird.isDead()){
MusicUtil.playScore(); //每次得分播放音效
score += 1;
//小鸟没死时记分
}
// 开始计时
public void startTiming() {
startTime = System.currentTimeMillis();
timeState = STATE_START;
}
// 结束计时并判断是否保存记录
public void endTiming() {
endTime = System.currentTimeMillis();
timeState = STATE_OVER;
// 判断本次得分是否为最高分
long score = TimeToScore();
// 判断是否为最高纪录
public void isSaveScore() {
long score = getScore();
if (bestScore < score)
bestScore = score;
try {
......@@ -118,39 +79,78 @@ public class GameTime {
}
}
private static final int FIRST_SCORE_TIME = 6600; // 从游戏开始到通过第一根水管的所需时间
private static final int PER_SCORE_TIME = 2900; // 通过后续每一根水管的间隔的所需时间
//将游戏时间转换为通过水管的数量
public long TimeToScore() {
long time = getTime();
long temp = score;
if (time >= FIRST_SCORE_TIME && time < FIRST_SCORE_TIME + PER_SCORE_TIME) {
score = 1; //time大于FIRST_SCORE_TIME且未到第二对水管
} else if (time >= FIRST_SCORE_TIME + PER_SCORE_TIME) {
score = (int) (time - FIRST_SCORE_TIME) / PER_SCORE_TIME + 1;
}
if (score - temp > 0) {
MusicUtil.playScore(); //每次得分播放音效
}
return score;
}
/**
* 是否正在计时
*
* @return
*/
public boolean isTiming() {
return timeState == STATE_START;
}
//重置计时器
//重置计分器
public void reset() {
timeState = STATE_READY;
startTime = 0;
endTime = 0;
score = 0;
}
// private int timeState; // 计时器的状态
// public static final int STATE_READY = 0; // 计时就绪
// public static final int STATE_START = 1; // 计时开始
// public static final int STATE_OVER = 2; // 计时结束
// 以下为游戏计时的相关方法,因改进了记分方式目前无用
// /**
// * 游戏用时,毫秒
// *
// * @return
// */
// public long getTime() {
// if (timeState == STATE_READY) {
// return startTime;
// } else if (timeState == STATE_START) {
// return (System.currentTimeMillis() - startTime);
// } else {
// return (endTime - startTime);
// }
// }
//
// //游戏时间转换为秒
// public long getTimeInSeconds() {
// return getTime() / 1000;
// }
//
// // 计时器是否就绪
// public boolean isReadyTiming() {
// return timeState == STATE_READY;
// }
//
// // 开始计时
// public void startTiming() {
// startTime = System.currentTimeMillis();
// timeState = STATE_START;
// }
//
// // 结束计时
// public void endTiming() {
// endTime = System.currentTimeMillis();
// timeState = STATE_OVER;
// }
//
// private static final int FIRST_SCORE_TIME = 6600; // 从游戏开始到通过第一根水管的所需时间
// private static final int PER_SCORE_TIME = 2900; // 通过后续每一根水管的间隔的所需时间
//
// //将游戏时间转换为通过水管的数量
// public long TimeToScore() {
// long time = getTime();
// long temp = score;
// if (time >= FIRST_SCORE_TIME && time < FIRST_SCORE_TIME + PER_SCORE_TIME) {
// score = 1; //time大于FIRST_SCORE_TIME且未到第二对水管
// } else if (time >= FIRST_SCORE_TIME + PER_SCORE_TIME) {
// score = (int) (time - FIRST_SCORE_TIME) / PER_SCORE_TIME + 1;
// }
// if (score - temp > 0) {
// MusicUtil.playScore(); //每次得分播放音效
// }
// return score;
// }
//
// /**
// * 是否正在计时
// *
// * @return
// */
// public boolean isTiming() {
// return timeState == STATE_START;
// }
}
......@@ -27,11 +27,11 @@ public class MovingPipe extends Pipe {
/**
* 设置水管参数
*
* @param x
* @param y
* @param height
* @param type
* @param visible
* @param x:x坐标
* @param y:y坐标
* @param height:水管高度
* @param type:水管类型
* @param visible:水管可见性
*/
public void setAttribute(int x, int y, int height, int type, boolean visible) {
this.x = x;
......
......@@ -11,15 +11,15 @@ import com.bird.util.GameUtil;
* 水管类
*
* @author Kingyu
*
*/
public class Pipe {
static BufferedImage[] imgs; // 水管的图片,static保证图片只加载一次
static {// 静态代码块,类加载的时候,初始化图片
final int PIPE_IMAGE_COUNT = 3;
imgs = new BufferedImage[PIPE_IMAGE_COUNT];
for (int i = 0; i < PIPE_IMAGE_COUNT; i++) {
imgs[i] = GameUtil.loadBUfferedImage(Constant.PIPE_IMG_PATH[i]);
imgs[i] = GameUtil.loadBufferedImage(Constant.PIPE_IMG_PATH[i]);
}
}
......@@ -43,15 +43,14 @@ public class Pipe {
public static final int TYPE_HOVER_HARD = 5;
// 水管的速度
public static final int MIN_SPEED = 1;
public static final int MAX_SPEED = 2;
public static final int SPEED = 1;
int speed;
Rectangle pipeRect; // 水管的碰撞矩形
// 构造器
public Pipe() {
this.speed = MIN_SPEED;
this.speed = SPEED;
this.width = PIPE_WIDTH;
pipeRect = new Rectangle();
......@@ -61,11 +60,11 @@ public class Pipe {
/**
* 设置水管参数
*
* @param x
* @param y
* @param height
* @param type
* @param visible
* @param x:x坐标
* @param y:y坐标
* @param height:水管高度
* @param type:水管类型
* @param visible:水管可见性
*/
public void setAttribute(int x, int y, int height, int type, boolean visible) {
this.x = x;
......@@ -78,10 +77,6 @@ public class Pipe {
/**
* 设置碰撞矩形参数
*
* @param x
* @param y
* @param height
*/
public void setRectangle(int x, int y, int height) {
pipeRect.x = x;
......@@ -179,27 +174,7 @@ public class Pipe {
return x + width < Constant.FRAME_WIDTH;
}
// 各参数的写入与获取
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public void setType(int type) {
this.type = type;
}
public void setHeight(int height) {
this.height = height;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
// 获取水管的x坐标
public int getX() {
return x;
}
......@@ -208,4 +183,26 @@ public class Pipe {
public Rectangle getPipeRect() {
return pipeRect;
}
// 各参数的写入与获取
// public void setX(int x) {
// this.x = x;
// }
//
// public void setY(int y) {
// this.y = y;
// }
//
// public void setType(int type) {
// this.type = type;
// }
//
// public void setHeight(int height) {
// this.height = height;
// }
//
// public void setVisible(boolean visible) {
// this.visible = visible;
// }
}
......@@ -6,25 +6,23 @@ import java.util.List;
import com.bird.util.Constant;
/**
* 为了避免反复地创建和销毁对象,使用对象池来提前创建好一些对象,使用时从对象池中获得,使用完归还
* 为了避免反复地创建和销毁对象,使用对象池来提前创建好一些对象,使用时从对象池中获得,使用完归还
*
* @author Kingyu
*
*/
public class PipePool {
private static List<Pipe> pool = new ArrayList<Pipe>(); // 池中对象的容器
private static List<MovingPipe> movingPool = new ArrayList<MovingPipe>(); // 池中对象的容器
private static final List<Pipe> pool = new ArrayList<>(); // 池中对象的容器
private static final List<MovingPipe> movingPool = new ArrayList<>(); // 池中对象的容器
public static final int INIT_PIPE_COUNT = (Constant.FRAME_WIDTH
/ (Pipe.PIPE_HEAD_WIDTH + GameElementLayer.HORIZONTAL_INTERVAL) + 2) * 2; // 根据窗口宽度算得对象池中对象的初始个数
public static final int MAX_PIPE_COUNT = 30; // 对象池中对象的最大个数,自行定义
// 初始化水管容器
// 初始化水管容器,初始化水管的数量的计算方式见常量类中的注释
static {
for (int i = 0; i < INIT_PIPE_COUNT; i++) {
for (int i = 0; i < Constant.FULL_PIPE; i++) {
pool.add(new Pipe());
}
for (int i = 0; i < INIT_PIPE_COUNT; i++) {
for (int i = 0; i < Constant.FULL_PIPE; i++) {
movingPool.add(new MovingPipe());
}
}
......@@ -54,8 +52,6 @@ public class PipePool {
/**
* 归还对象给容器
*
* @param pipe
*/
public static void giveBack(Pipe pipe) {
//判断类的类型
......
......@@ -3,6 +3,9 @@ package com.bird.util;
import java.awt.Color;
import java.awt.Font;
import com.bird.main.GameElementLayer;
import com.bird.main.Pipe;
/**
* 常量类
*
......@@ -18,14 +21,12 @@ public class Constant {
public static final String GAME_TITLE = "Flappy Bird written by Kingyu";
// 窗口位置
public static final int FRAME_X = 1200;
public static final int FRAME_X = 600;
public static final int FRAME_Y = 100;
// 图像资源路径
public static final String BG_IMG_PATH = "resources/img/background.png"; // 背景图片
public static final int HOVER_MOVING_SCORE = 4; //出现移动管道的分数
// 小鸟图片
public static final String[][] BIRDS_IMG_PATH = {
{ "resources/img/0.png", "resources/img/1.png", "resources/img/2.png", "resources/img/3.png",
......@@ -72,9 +73,11 @@ public class Constant {
public static final int CLOUD_BORN_PERCENT = 6; // 云朵生成的概率,单位为百分比
public static final int CLOUD_IMAGE_COUNT = 2; // 云朵图片的个数
public static final int MAX_CLOUD_COUNT = 7; // 云朵的最大数量
public static final int CLOUD_DIRCHANGE = 50; // 云朵随机改变方向的概率,越大表示概率越小
public static final Font TIME_FONT = new Font("华文琥珀", Font.BOLD, 32);// 字体
public static final Font CURRENT_SCORE_FONT = new Font("华文琥珀", Font.BOLD, 32);// 字体
public static final Font SCORE_FONT = new Font("华文琥珀", Font.BOLD, 24);// 字体
// 窗口可容纳的水管数量+2, 由窗口宽度、水管宽度、水管间距算得
public static final int FULL_PIPE = (Constant.FRAME_WIDTH
/ (Pipe.PIPE_HEAD_WIDTH + GameElementLayer.HORIZONTAL_INTERVAL) + 2) * 2;
}
package com.bird.util;
import java.awt.Font;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.IOException;
......@@ -13,7 +13,6 @@ import javax.imageio.ImageIO;
* 工具类,游戏中用到的工具都在此类
*
* @author Kingyu
*
*/
public class GameUtil {
......@@ -24,9 +23,9 @@ public class GameUtil {
* 装载图片的方法
*
* @param imgPath 图片路径
* @return
* @return 图片资源
*/
public static BufferedImage loadBUfferedImage(String imgPath) {
public static BufferedImage loadBufferedImage(String imgPath) {
try {
return ImageIO.read(new FileInputStream(imgPath));
} catch (IOException e) {
......@@ -42,13 +41,13 @@ public class GameUtil {
* @param denominator 分母,不小于0的值
* @return 概率性事件发生返回true,否则返回false
*/
public static boolean isInProbability(int numerator, int denominator)throws Exception{
public static boolean isInProbability(int numerator, int denominator) throws Exception {
// 分子分母不小于0
if (numerator <= 0 || denominator <= 0) {
throw new Exception("传入了非法的参数");
}
//分子大于分母,一定发生
if(numerator >= denominator) {
if (numerator >= denominator) {
return true;
}
......@@ -71,16 +70,22 @@ public class GameUtil {
*/
public static int getStringWidth(Font font, String str) {
AffineTransform affinetransform = new AffineTransform();
FontRenderContext frc = new FontRenderContext(affinetransform,true,true);
int textHeight = (int)(font.getStringBounds(str, frc).getWidth());
return textHeight;
FontRenderContext frc = new FontRenderContext(affinetransform, true, true);
return (int) (font.getStringBounds(str, frc).getWidth());
}
public static int getStringHeight(Font font, String str) {
AffineTransform affinetransform = new AffineTransform();
FontRenderContext frc = new FontRenderContext(affinetransform,true,true);
int textHeight = (int)(font.getStringBounds(str, frc).getHeight());
return textHeight;
FontRenderContext frc = new FontRenderContext(affinetransform, true, true);
return (int) (font.getStringBounds(str, frc).getHeight());
}
// 于屏幕x轴中央、y轴3/5处绘制图像
public static void drawTitle(BufferedImage image, Graphics g) {
int x = Constant.FRAME_WIDTH - image.getWidth() >> 1;
int y = Constant.FRAME_HEIGHT / 5 * 3;
g.drawImage(image, x, y, null);
}
}
package com.bird.util;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
......@@ -11,8 +10,8 @@ import sun.audio.AudioStream;
/**
* 音乐工具类
*
* @author Kingyu wav音频:JDK提供的类可直接解码 mp3音频:JDK没有提供支持,需要使用第三方的工具包
*
* @author Kingyu
* wav音频:JDK提供的类可直接解码 mp3音频:JDK没有提供支持,需要使用第三方的工具包
*/
public class MusicUtil {
......@@ -20,40 +19,33 @@ public class MusicUtil {
private static AudioStream crash;
private static AudioStream score;
private static InputStream flyIn;
private static InputStream crashIn;
private static InputStream scoreIn;
//wav播放
// wav播放
public static void playFly() {
try {
// create an audiostream from the inputstream
flyIn = new FileInputStream("resources/wav/fly.wav");
// create an AudioStream from the InputStream
InputStream flyIn = new FileInputStream("resources/wav/fly.wav");
fly = new AudioStream(flyIn);
} catch (FileNotFoundException fnfe) {
} catch (IOException ioe) {
} catch (IOException ignored) {
}
AudioPlayer.player.start(fly);
}
public static void playCrash() {
try {
// create an audiostream from the inputstream
crashIn = new FileInputStream("resources/wav/crash.wav");
// create an AudioStream from the InputStream
InputStream crashIn = new FileInputStream("resources/wav/crash.wav");
crash = new AudioStream(crashIn);
} catch (FileNotFoundException fnfe) {
} catch (IOException ioe) {
} catch (IOException ignored) {
}
AudioPlayer.player.start(crash);
}
public static void playScore() {
try {
// create an audiostream from the inputstream
scoreIn = new FileInputStream("resources/wav/score.wav");
// create an AudioStream from the InputStream
InputStream scoreIn = new FileInputStream("resources/wav/score.wav");
score = new AudioStream(scoreIn);
} catch (FileNotFoundException fnfe) {
} catch (IOException ioe) {
} catch (IOException ignored) {
}
AudioPlayer.player.start(score);
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment