Commit 4e8f8dab authored by kingyu's avatar kingyu Committed by kingyuluk
Browse files

feat: 随机刷新可自行上下移动的水管

BREAKING CHANGE: 移动型水管的刷新概率会随着当前游戏分数递增
parent 5e5ba4bf
......@@ -3,6 +3,19 @@
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.0 (2020-07-11)
### ⚠ BREAKING CHANGES
* 移动型水管的刷新概率会随着当前游戏分数递增
### Features
* 随机刷新可自行上下移动的水管 ([6c72b96](https://github.com/kingyuluk/FlappyBird/commit/6c72b968c8f7d953f2a34bc795674dcfe570c688))
* 添加悬浮型水管 ([3f84810](https://github.com/kingyuluk/FlappyBird/commit/3f84810429e0e2dd0932ff325845fc10daebde88))
* 具备原版的游戏功能 ([264a7c3](https://github.com/kingyuluk/FlappyBird/commit/264a7c320c894851aa3e6c8c30ffcddf3ce1f78a))
## 1.1.0 (2020-07-11)
......
# Flappy Bird
Flappy Bird for desktop platforms.
基于java语言编写的Flappy Bird,只用了基本库
基于Java和JDK基本库编写
开发难度不高,适合JAVA刚入门的同学,注释写得很详细,欢迎交流
开发平台为macOS 10.15.5,开发工具为Eclipse IDE (4.16.0),Java SE 8[1.8.0_251]
开发平台为macOS 10.15.5,开发工具为Eclipse IDE (4.16.0), Java SE 8[1.8.0_251]
## Overview
# Package Contents
com.bird.app 游戏的入口
本项目为Flappy bird的桌面平台版,具备原版的所有功能,且相较于原版优化了游戏难度的梯度并加入移动型水管,使游戏的可玩性更高。
### 游戏玩法
每局游戏随机刷新所有元素,游戏只需空格键即可操作,敲击按键小鸟就会振翅向上飞,且受到重力作用会不断下坠,需要玩家控制小鸟不断飞行,并注意躲避随机生成的水管,每飞过一对水管就会得分,飞行过程中如果撞到水管或掉落在地则游戏结束。
com.bird.main 游戏的内容
com.bird.util 自定义的工具
## 游戏界面
### 游戏启动
![image](https://github.com/kingyuluk/flappy-bird/blob/master/examples/start.png)
# Version History
1.0.0 - July 10, 2020
* 具备完整的游戏功能
### 运行示例
![image](https://github.com/kingyuluk/flappy-bird/blob/master/examples/play.gif)
### 游戏结束
![image](https://github.com/kingyuluk/flappy-bird/blob/master/examples/over.png)
## Package Contents
* com.bird.app 游戏的入口
* com.bird.main 游戏的内容
* com.bird.util 自定义的工具
1.1.0 - July 11, 2020
## Change Log
v1.2.0 - July 11, 2020
* 现在水管可以移动了,随着游戏分数的上升会提升游戏难度
v1.1.0 - July 11, 2020
* 添加了悬浮型的水管
# Notes
v1.0.0 - July 10, 2020
* 具备完整的游戏功能
## Notes
* 文本编码格式为UTF-8,若注释出现乱码请修改编译器的文本编码格式
* 由于使用了sun.*包,不同版本的JDK中sun包中的类可能发生变化,因此无法确保工作在所有JAVA平台上
文本编码格式为UTF-8,若注释出现乱码请修改编译器的文本编码格式
# Contact
* email: <kingyuluk@hotmail.com>
## Contact
* email: <kingyuluk@mail.dlut.edu.cn>
sources/img/title.png

1.72 KB | W: | H:

sources/img/title.png

5.22 KB | W: | H:

sources/img/title.png
sources/img/title.png
sources/img/title.png
sources/img/title.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -72,7 +72,7 @@ public class Bird {
// 绘制方法
public void draw(Graphics g) {
Fly();
fly();
int state_index = state > STATE_FALL ? STATE_FALL : state; // 图片资源索引
// 小鸟中心点计算
int halfImgWidth = birdImgs[state_index][0].getWidth() >> 1;
......@@ -85,10 +85,8 @@ public class Bird {
drawGameover(g);
} else if (state == STATE_FALL) {
} else {
drawTime(g);
drawScore(g);
}
timing.TimeToScore();
// 绘制矩形
// g.setColor(Color.black);
// g.drawRect((int) birdRect.getX(), (int) birdRect.getY(), (int) birdRect.getWidth(), (int) birdRect.getHeight());
......@@ -116,7 +114,7 @@ public class Bird {
}
// 小鸟的飞行逻辑
private void Fly() {
private void fly() {
// 翅膀状态,实现小鸟振翅飞行
wingState++;
image = birdImgs[state > STATE_FALL ? STATE_FALL : state][wingState / 10 % IMG_COUNT];
......@@ -126,8 +124,6 @@ public class Bird {
break;
case STATE_UP:
// 控制上边界
break;
case STATE_DOWN:
......@@ -136,7 +132,13 @@ public class Bird {
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);
birdFall();
}
break;
case STATE_FALL:
......@@ -146,13 +148,14 @@ public class Bird {
y = (int) (y - h);
birdRect.y = (int) (birdRect.y - h);
// 控制坠落的边界
if (birdRect.y > Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (birdImgs[state][0].getHeight() >> 1)) {
// 控制坠落的边界,若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);
GameFrame.setGameState(GameFrame.STATE_OVER);
BirdDead();
GameFrame.setGameState(GameFrame.STATE_OVER); // 改变游戏状态
birdDead();
}
break;
......@@ -166,38 +169,42 @@ public class Bird {
y = -1 * Constant.TOP_PIPE_LENGTHENING / 2;
}
// 控制下方边界
if (birdRect.y > Constant.FRAME_HEIGHT - Constant.GROUND_HEIGHT - (image.getHeight() >> 1)) {
BirdFall();
birdFall();
}
}
// 鸟的状态
public void BirdUp() {
// 小鸟振翅
public void birdUp() {
if (keyIsReleased()) { // 如果按键已释放
if (state == STATE_DEAD || state == STATE_FALL || state == STATE_UP)
return;
state = STATE_UP;
speed = SPEED_UP;
if (state == STATE_DEAD || state == STATE_UP || state == STATE_FALL)
return; // 小鸟死亡或坠落时返回
MusicUtil.playFly(); // 播放音效
state = STATE_UP;
speed = SPEED_UP; // 每次振翅将速度改为上升速度
wingState = 0; // 重置翅膀状态
keyPressed();
}
}
public void BirdDown() {
// 小鸟下降
public void birdDown() {
if (state == STATE_DEAD || state == STATE_FALL)
return;
return; // 小鸟死亡或坠落时返回
state = STATE_DOWN;
}
public void BirdFall() {
// 小鸟坠落(已死)
public void birdFall() {
state = STATE_FALL;
MusicUtil.playCrash(); // 播放音效
// 结束计时
timing.endTiming();
}
public void BirdDead() {
// 小鸟死亡
public void birdDead() {
state = STATE_DEAD;
// 加载游戏结束的资源
if (overImg == null) {
......@@ -218,10 +225,9 @@ public class Bird {
}
// 绘制实时分数
private void drawTime(Graphics g) {
private void drawScore(Graphics g) {
g.setColor(Color.white);
g.setFont(Constant.TIME_FONT);
// String str = Long.toString(timing.getTimeInSeconds()); 时间分数
String str = Long.toString(timing.TimeToScore());
int x = Constant.FRAME_WIDTH - GameUtil.getStringWidth(Constant.TIME_FONT, str) >> 1;
g.drawString(str, x, Constant.FRAME_HEIGHT / 10);
......@@ -230,8 +236,8 @@ public class Bird {
private static final int SCORE_LOCATE = 5; // 位置补偿参数
private int flash = 0; // 图片闪烁参数
// 绘制游戏结束的显示
// 绘制游戏结束的显示
private void drawGameover(Graphics g) {
// 绘制结束标志
int x = Constant.FRAME_WIDTH - overImg.getWidth() >> 1;
......
......@@ -6,7 +6,7 @@ import java.awt.image.BufferedImage;
import com.bird.util.Constant;
/**
* 云朵类,在屏幕的上半部飘动
* 云朵类
*
* @author Kingyu
*
......
......@@ -99,23 +99,23 @@ public class GameElementLayer {
e.printStackTrace();
}
}
}
}
}
/**
* 添加普通水管
*
* @param lastPipe
* @param lastPipe 传入最后一根水管以获取x坐标
*/
private void addNormalPipe(Pipe lastPipe) {
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属性
top.setAttribute(x, -Constant.TOP_PIPE_LENGTHENING, topHeight + Constant.TOP_PIPE_LENGTHENING,
Pipe.TYPE_TOP_NORMAL, true);
......@@ -215,13 +215,12 @@ public class GameElementLayer {
if (bird.isDead()) {
return false;
}
// 遍历水管容器
for (int i = 0; i < pipes.size(); i++) {
Pipe pipe = pipes.get(i);
// 判断碰撞矩形是否有交集
if (pipe.getPipeRect().intersects(bird.getBirdRect())) {
bird.BirdFall();
bird.birdFall(); //有交集则小鸟坠落
return true;
}
}
......
......@@ -94,6 +94,5 @@ public class GameForeground {
* (Exception e) { e.printStackTrace(); }
*/
}
}
}
\ No newline at end of file
package com.bird.main;
import com.bird.util.MusicUtil;
import static com.bird.util.Constant.FRAME_HEIGHT;
import static com.bird.util.Constant.FRAME_WIDTH;
import static com.bird.util.Constant.FRAME_X;
......@@ -15,7 +17,6 @@ import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import com.bird.util.MusicUtil;
/**
* 主窗口类,游戏窗口和绘制的相关内容
......@@ -69,31 +70,34 @@ public class GameFrame extends Frame implements Runnable {
switch (gameState) {
case STATE_READY:
if (keycode == KeyEvent.VK_SPACE) {
bird.BirdUp();
bird.BirdDown();
setGameState(STATE_START);
bird.startTiming();
// 游戏启动界面时按下空格,小鸟振翅一次并开始受重力影响
bird.birdUp();
bird.birdDown();
setGameState(STATE_START); // 游戏状态改变
bird.startTiming(); // 计时器开始计时
}
break;
case STATE_START:
if (keycode == KeyEvent.VK_SPACE) {
bird.BirdUp();
bird.BirdDown();
//游戏过程中按下空格则振翅一次,并持续受重力影响
bird.birdUp();
bird.birdDown();
}
break;
case STATE_OVER:
if (keycode == KeyEvent.VK_SPACE) {
//游戏结束时按下空格,重新开始游戏
resetGame();
}
break;
}
}
// 定义重新开始游戏的方法
// 重新开始游戏
private void resetGame() {
setGameState(STATE_READY);
gameElement.reset();
bird.reset();
}
// 按键松开,更改按键状态标志
......@@ -114,30 +118,26 @@ public class GameFrame extends Frame implements Runnable {
gameElement = new GameElementLayer();
foreground = new GameForeground();
ready = new GameReady();
MusicUtil.load(); // 装载音乐资源
bird = new Bird();
MusicUtil.load();
setGameState(STATE_READY);
// 启动用于刷新窗口的线程
new Thread(this).start();
}
// 项目中存在两个线程:系统线程,自定义的线程:调用repaint()。
// 系统线程:屏幕内容的绘制,窗口事件的监听与处理
// 项目中存在两个线程:系统线程;自定义线程,调用repaint()。
// 两个线程会抢夺系统资源,可能会出现一次刷新周期所绘制的内容,并没有在一次刷新周期内完成
// (双缓冲)单独定义一张图片,将需要绘制的内容绘制到这张图片,再一次性地将图片绘制到窗口
private BufferedImage bufImg = new BufferedImage(FRAME_WIDTH, FRAME_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR);
/**
* 绘制需要屏幕内容 当repaint()方法被调用时,JVM会调用update() 不要主动调用update 参数g是系统提供的画笔,由系统进行实例化
*
* 绘制游戏内容 当repaint()方法被调用时,JVM会调用update(),参数g是系统提供的画笔,由系统进行实例化
* 单独启动一个线程,不断地快速调用repaint(),让系统对整个窗口进行重绘
*
*/
public void update(Graphics g) {
Graphics bufG = bufImg.getGraphics(); // 获得图片画笔
// 使用图片画笔将需要绘制的内容绘制到图片
......@@ -166,10 +166,11 @@ public class GameFrame extends Frame implements Runnable {
}
}
//获取、设置游戏状态的方法
// 获取、设置游戏状态的方法
public static int getGameState() {
return gameState;
}
public static void setGameState(int gameState) {
GameFrame.gameState = gameState;
}
......
......@@ -7,7 +7,7 @@ import com.bird.util.Constant;
import com.bird.util.GameUtil;
/**
* 游戏未开始的内容
* 游戏启动界面类
*
* @author Kingyu
*
......@@ -17,24 +17,28 @@ public class GameReady {
private BufferedImage titleImg;
private BufferedImage noticeImg;
private int flash;
private int flash; // 图像闪烁参数
// 构造器中进行初始化,装载图像资源
public GameReady() {
titleImg = GameUtil.loadBUfferedImage(Constant.TITLE_IMG_PATH);
noticeImg = GameUtil.loadBUfferedImage(Constant.NOTICE_IMG_PATH);
}
public void draw(Graphics g) {
int x = Constant.FRAME_WIDTH - titleImg.getWidth() >> 1;
int y = Constant.FRAME_HEIGHT / 5 << 1;
g.drawImage(titleImg, x, y, null);
// 计算title图像的x、y坐标
int x = Constant.FRAME_WIDTH - titleImg.getWidth() >> 1; //x坐标为窗口中央
int y = Constant.FRAME_HEIGHT / 3; //y坐标为游戏窗口的1/3处
g.drawImage(titleImg, x, y, null); // 绘制
final int COUNT = 30;
// 使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 * 2)
g.drawImage(noticeImg, x, y, null); // 绘制
if (flash == COUNT * 2) // 重置闪烁参数
flash = 0;
}
}
......
......@@ -10,7 +10,7 @@ import com.bird.util.Constant;
import com.bird.util.MusicUtil;
/**
* 游戏计时类,单例类,方便调用
* 游戏计时类, 单例类,方便调用
*
* @author Kingyu
*
......@@ -31,7 +31,6 @@ public class GameTime {
private GameTime() {
timeState = STATE_READY;
bestScore = -1;
try {
loadBestTime();
} catch (Exception e) {
......@@ -54,7 +53,7 @@ public class GameTime {
}
// 保存最高纪录
public void saveBestTime(long time) throws Exception {
public void saveBestScore(long time) throws Exception {
File file = new File(Constant.SCORE_FILE_PATH);
DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));
dos.writeLong(time);
......@@ -104,7 +103,7 @@ public class GameTime {
timeState = STATE_START;
}
// 结束计时
// 结束计时并判断是否保存记录
public void endTiming() {
endTime = System.currentTimeMillis();
timeState = STATE_OVER;
......@@ -113,21 +112,21 @@ public class GameTime {
if (bestScore < score)
bestScore = score;
try {
saveBestTime(bestScore);
saveBestScore(bestScore);
} catch (Exception e) {
e.printStackTrace();
}
}
private static final int FIRST_SCORE_TIME = 6600; // 从游戏开始到通过第一根水管的所需时间
private static final int PER_SCORE_TIME = 2880; // 通过后续每一根水管的间隔的所需时间
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;
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;
}
......
package com.bird.main;
import java.awt.Color;
import java.awt.Graphics;
import com.bird.util.Constant;
......@@ -70,8 +69,8 @@ public class MovingPipe extends Pipe {
pipeLogic();
// 绘制碰撞矩形
g.setColor(Color.black);
g.drawRect((int) pipeRect.getX(), (int) pipeRect.getY(), (int) pipeRect.getWidth(), (int) pipeRect.getHeight());
// g.setColor(Color.black);
// g.drawRect((int) pipeRect.getX(), (int) pipeRect.getY(), (int) pipeRect.getWidth(), (int) pipeRect.getHeight());
}
// 绘制移动的悬浮水管
......@@ -89,7 +88,7 @@ public class MovingPipe extends Pipe {
g.drawImage(imgs[1], x - ((PIPE_HEAD_WIDTH - width) >> 1), y + dealtY, null);
}
// 绘制从上往下的普通水管
// 绘制从上往下的移动水管
private void drawTopHard(Graphics g) {
// 拼接的个数
int count = (height - PIPE_HEAD_HEIGHT) / PIPE_HEIGHT + 1; // 取整+1
......@@ -102,7 +101,7 @@ public class MovingPipe extends Pipe {
height - Constant.TOP_PIPE_LENGTHENING - PIPE_HEAD_HEIGHT + dealtY, null);
}
// 绘制从下往上的普通水管
// 绘制从下往上的移动水管
private void drawBottomHard(Graphics g) {
// 拼接的个数
int count = (height - PIPE_HEAD_HEIGHT) / PIPE_HEIGHT + 1;
......@@ -118,13 +117,14 @@ public class MovingPipe extends Pipe {
* 可动水管的运动逻辑
*/
private void pipeLogic() {
//x坐标的运动逻辑与普通水管相同
x -= speed;
pipeRect.x -= speed;
if (x < -1 * PIPE_HEAD_WIDTH) {// 水管完全离开了窗口
visible = false;
}
//水管上下移动
//水管上下移动的逻辑
if (dir == DIR_DOWN) {
dealtY++;
if (dealtY > MAX_DEALY) {
......@@ -137,6 +137,6 @@ public class MovingPipe extends Pipe {
}
}
pipeRect.y = this.y + dealtY;
}
}
......@@ -106,7 +106,6 @@ public class Pipe {
case TYPE_HOVER_NORMAL:
drawHoverNormal(g);
break;
}
// //绘制碰撞矩形
// g.setColor(Color.black);
......
......@@ -32,7 +32,7 @@ public class PipePool {
/**
* 从对象池中获取一个对象
*
* @return
* @return 传入对象的类型,以判断从哪个对象池中获取
*/
public static Pipe get(String className) {
if ("Pipe".equals(className)) {
......
......@@ -15,7 +15,7 @@ public class Constant {
public static final int FRAME_HEIGHT = 640;
// 游戏标题
public static final String GAME_TITLE = "Flappy Bird";
public static final String GAME_TITLE = "Flappy Bird written by Kingyu";
// 窗口位置
public static final int FRAME_X = 1200;
......
......@@ -18,7 +18,7 @@ import javax.imageio.ImageIO;
public class GameUtil {
private GameUtil() {
} // 私有化,不让其他类实例化
} // 私有化,防止其他类实例化
/**
* 装载图片的方法
......
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