《飞扬的小鸟》 开发教程+个人笔记
《飞扬的小鸟》是一款曾经风靡一时的虐心小游戏,在本教程的帮助下,你可以从零开始,一步一步的开发出这款小游戏。在开发之前,请先明确以下内容:
-
开发语言
Java,主要应用Java的基本语法,Java面向对象的三大特征,以及Java Swing的常见API。
-
开发工具
JDK8,或者更高版本的JDK。
-
注意事项
开发前,先下载游戏素材。开发时,会使用到一些参数,如坐标、尺寸、速度等。这些参数的值是作者根据经验得出的结论,你可以直接使用这些参数值,也可以尝试自定义这些参数值。
-
牛客官方项目源码
https://git.nowcoder.com/999991353/fly-bird.git -
个人项目地址
https://github.com/KYO9700/bird1.0.git
https://git.nowcoder.com/236681084/BookManager.git
目录
1 创建游戏面板
1.1 创建项目
创建Java项目(bird),在src路径下新建resources目录,并导入所有素材图片,如下图:
1.2 绘制面板
创建game包,并在此包下创建游戏面板类BirdGame,在该类中增加如下代码:
package com.KYO9700.game;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
/**
* 游戏界面
*/
public class BirdGame extends JPanel {
// 背景图片
static BufferedImage background;
/**
* 初始化游戏
*/
public BirdGame() throws Exception {
// 初始化背景图片
background = ImageIO.read(getClass().getResource("/resources/bg.png"));
}
/**
* 绘制界面
*/
//重写实现paint方法
//java绘图时,最常使用到的就是paint(Graphics g){...内容...}方法获取画笔,
//然后利用JPanel等容器作为画布,在JFrame内呈现出内容
@Override
public void paint(Graphics g) {
// 绘制背景
g.drawImage(background, 0, 0, null);
}
/**
* 启动方法
*/
public static void main(String[] args) throws Exception {
//JFrame()创建无标题的初始不可见框架
JFrame frame = new JFrame();
//将子组件添加到框架中,frame.add(child); 子组件将被添加到contentPane
//将指定的组件附加到此容器的末尾。
//该方法更改布局相关信息,因此使组件层次结构无效。 如果容器已经被显示,则此后必须验证层次结构,以显示添加的组件。
BirdGame game = new BirdGame();
frame.add(game);
//调整此组件的大小,使其宽度为width ,高度为height 。
//该方法更改布局相关信息,因此使组件层次结构无效。
frame.setSize(440, 670);
//设置组件的位置
//如果组件是null ,或GraphicsConfiguration与此组件关联是null时,窗口被放置在屏幕的中心。 中心点可以使用GraphicsEnvironment.getCenterPoint方法获得。
//如果组件不是null ,但它当前没有显示,则该窗口将放置在与此组件GraphicsConfiguration GraphicsConfiguration定义的目标屏幕的中心。
//如果组件不是null并显示在屏幕上,则窗口的位置使得窗口的中心与组件的中心重合。
frame.setLocationRelativeTo(null);
//设置用户在此框架上启动“关闭”时默认执行的操作。 您指定以下选项之一:
//DO_NOTHING_ON_CLOSE (定义在WindowConstants ):不要做任何事情; 要求程序处理WindowListener对象的windowClosing方法的操作。
//HIDE_ON_CLOSE (在WindowConstants定义):在调用任何已注册的WindowListener对象后自动隐藏框架。
//DISPOSE_ON_CLOSE (在WindowConstants定义):在调用任何已注册的WindowListener对象后自动隐藏和处理该框架。
//EXIT_ON_CLOSE (在JFrame定义):使用System exit方法退出exit程序。 仅在应用程序中使用。
//该值默认设置为HIDE_ON_CLOSE 。 对此属性的值的更改导致触发属性更改事件,属性名称为“defaultCloseOperation”。
//注意 :当Java虚拟机(VM)中的最后一个可显示的窗口被丢弃时,VM可能会终止。
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//显示窗口
frame.setVisible(true);
}
}
笔记:
1、关于java.swing包:
提供所有的windows桌面应用程序包括的控件。比如:Frame , Dialog, Table, List 等等,就是java的图形界面库。
2、# java.awt和javax.swing的联系和区别:
AWT 是Abstract Window ToolKit (抽象窗口工具包)的缩写,这个工具包提供了一套与本地图形界面进行交互的接口。当我们利用 AWT 来构件图形用户界面的时候,我们实际上是在利用操作系统所提供的图形库。由于不同操作系统的图形库所提供的功能是不一样的,在一个平台上存在的功能在另外一个平台上则可能不存在。
Swing 是在AWT的基础上构建的一套新的图形界面系统,它提供了AWT 所能够提供的所有功能,并且用纯粹的Java代码对AWT 的功能进行了大幅度的扩充。由于 Swing 控件是用100%的Java代码来实现的,因此在一个平台上设计的树形控件可以在其他平台上使用。
3、JPanel类是什么
JPanel 是 Java图形用户界面(GUI)工具包swing中的面板容器类,包含在javax.swing 包中,是一种轻量级容器,可以加入到JFrame窗体中。JPanel默认的布局管理器是FlowLayout,其自身可以嵌套组合,在不同子容器中可包含其他组件(component),如JButton、JTextArea、JTextField 等,功能是对窗体上的这些控件进行组合,相当于C++和C#中的Panel类。
4、关于JFrame容器:
https://blog.csdn.net/liujun13579/article/details/7756729?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-2.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-2.control
5、paint(Graphics g){…内容…}方法
java绘图时,最常使用到的就是paint(Graphics g){…内容…}方法获取画笔,
然后利用JPanel等容器作为画布,在JFrame内呈现出内容
https://blog.csdn.net/ye_se_cong_cong/article/details/72729021
https://blog.csdn.net/syhdeclan/article/details/81566789?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control
6、JFrame JPanel 与 paint()的调用
https://blog.csdn.net/qq_43026792/article/details/86557391
1.3 测试一下
运行main方法,效果如下图:
2 设计游戏道具
2.1 设计地面
定义地面类(Ground),代码如下:
package com.KYO9700.game;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
//2 设计游戏道具
//2.1 设计地面
/**
* 地面
*/
class Ground {
// 图片
BufferedImage image;
//地面图片的基本参数:位置、宽高,用于绘图
// 位置
int x, y;
// 宽高
int width, height;
// 初始化地面
public Ground() throws Exception {
image = ImageIO.read(getClass().getResource("/resources/ground.png"));
//宽高通过读取素材确定
width = image.getWidth();
height = image.getHeight();
x = 0;
y = 500;
}
// 地面的动画逻辑,左移、复位,由step()函数实现
// 向左移动一步
public void step() {
x--;
if (x == -109) {
x = 0;
}
}
}
2.2 设计柱子
定义柱子类(Column),代码如下:
package com.KYO9700.game;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.util.Random;
//2 设计游戏道具
//2.2 设计柱子
/**
* 柱子
*/
class Column {
// 图片
BufferedImage image;
// 作图的基本参数,位置和宽高
// 位置
int x, y;
// 宽高
int width, height;
// 柱子之间的缝隙,固定的,与素材图片一致
int gap;
// 柱子之间的距离,这里设置为固定的
int distance;
// 随机数工具,用于确定缝隙的高度偏移
Random random = new Random();
/**
* 初始化第N个柱子
*/
public Column(int n) throws Exception {
image = ImageIO.read(getClass().getResource("/resources/column.png"));
//宽高通过读取素材确定
width = image.getWidth();
height = image.getHeight();
gap = 144;
distance = 245;
x = 550 + (n - 1) * distance;
y = random.nextInt(218) + 132;
}
// 柱子地面的动画逻辑,与地面的动画逻辑类似,左移、复位,由step()函数实现
// 向左移动一步
public void step() {
x--;
//假如柱子已经完全移出画面,再去到画面前方,并重设高度偏移
if (x == -width / 2) {
x = distance * 2 - width / 2;
y = random.nextInt(218) + 132;
}
}
}
2.3 设计小鸟
定义小鸟类(Bird),代码如下:
package com.KYO9700.game;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
//2 设计游戏道具
//2.3 设计小鸟
/**
* 小鸟
*/
class Bird {
// 图片
BufferedImage image;
// 位置
int x, y;
// 宽高
int width, height;
// 大小(用于碰撞检测)
int size;
// 重力加速度
double g;
// 位移的间隔时间
double t;
// 最初上抛速度
double v0;
// 当前上抛速度
double speed;
// 经过时间t之后的位移
double s;
// 小鸟的倾角(弧度)
double alpha;
// 一组图片,记录小鸟的动画帧
BufferedImage[] images;
// 动画帧数组的下标
int index;
// 初始化小鸟
public Bird() throws Exception {
// 初始化基本参数
image = ImageIO.read(getClass().getResource("/resources/0.png"));
width = image.getWidth();
height = image.getHeight();
x = 132;
y = 280;
size = 40;
// 初始化位移参数
g = 4;//重力加速度
v0 = 20;
t = 0.25;
speed = v0;
s = 0;
alpha = 0;
// 初始化动画帧参数
images = new BufferedImage[8];
for (int i = 0; i < 8; i++) {
images[i] = ImageIO.read(
getClass().getResource("/resources/" + i + ".png"));
}
index = 0;
}
// 飞行动作(变化一帧)
public void fly() {
index++;
image = images[(index / 12) % 8];
}
// 移动一步
public void step() {
double v0 = speed;
// 计算上抛运动位移
s = v0 * t + g * t * t / 2;
// 计算鸟的坐标位置
y = y - (int) s;
// 计算下次移动速度
double v = v0 - g * t;
speed = v;
// 计算倾角(反正切函数)
alpha = Math.atan(s / 8);
}
// 向上飞行
public void flappy() {
// 重置速度
speed = v0;
}
// 检测小鸟是否碰撞到地面
public boolean hit(Ground ground) {
boolean hit = y + size / 2 > ground.y;
if (hit) {
y = ground.y - size / 2;
alpha = -3.14159265358979323 / 2;
}
return hit;
}
// 检测小鸟是否撞到柱子
public boolean hit(Column column) {
// 先检测是否在柱子的范围内
if (x > column.x - column.width / 2 - size / 2
&& x < column.x + column.width / 2 + size / 2) {
// 再检测是否在柱子的缝隙中
if (y > column.y - column.gap / 2 + size / 2
&& y < column.y + column.gap / 2 - size / 2) {
return false;
}
return true;
}
return false;
}
}
3 绘制游戏界面
3.1 定义游戏参数
在BirdGame类中增加成员变量,代码如下:
// 开始图片
BufferedImage startImage;
// 结束图片
BufferedImage gameOverImage;
// 地面
Ground ground;
// 柱子
Column column1, column2;
// 小鸟
Bird bird;
// 游戏分数
int score;
// 游戏状态
int state;
// 状态常量
public static final int START = 0; //开始
public static final int RUNNING = 1; //运行
public static final int GAME_OVER = 2; //结束
3.2 初始化游戏参数
在BirdGame的构造方法里,初始化上述成员变量,代码如下:
/**
* 初始化游戏
*/
public BirdGame() throws Exception {
// 初始化背景图片
background = ImageIO.read(getClass().getResource("/resources/bg.png"));
// 初始化开始、结束图片
startImage = ImageIO.read(getClass().getResource("/resources/start.png"));
gameOverImage = ImageIO.read(getClass().getResource("/resources/gameover.png"));
// 初始化地面、柱子、小鸟
ground = new Ground();
column1 = new Column(1);
column2 = new Column(2);
bird = new Bird();
// 初始化分数
score = 0;
// 初始化状态
state = START;
}
3.3 绘制游戏界面
在paint方法内,绘制地面、柱子、小鸟、分数、开始图片、结束图片,代码如下:
/**
* 绘制界面
*/
//重写实现paint方法
//java绘图时,最常使用到的就是paint(Graphics g){...内容...}方法获取画笔,
//然后利用JPanel等容器作为画布,在JFrame内呈现出内容
@Override
public void paint(Graphics g) {
// 绘制背景
g.drawImage(background, 0, 0, null);
// 绘制地面
g.drawImage(ground.image, ground.x, ground.y, null);
// 绘制柱子
g.drawImage(column1.image, column1.x - column1.width / 2, column1.y
- column1.height / 2, null);
g.drawImage(column2.image, column2.x - column2.width / 2, column2.y
- column2.height / 2, null);
// 绘制小鸟(旋转坐标系)
//java.lang.Object
// java.awt.Graphics // java.awt.Graphics2D //该Graphics2D类扩展了Graphics类,以提供对几何,坐标变换,颜色管理和文本布局的更复杂的控制。 这是在Java(tm)平台上呈现二维形状,文字和图像的基础类。
Graphics2D g2 = (Graphics2D) g;
//随后的渲染绕旋转中心以指定弧度旋转,画出的小鸟的旋转角度由bird对象的属性alpha确定
g2.rotate(-bird.alpha, bird.x, bird.y);
g.drawImage(bird.image,
bird.x - bird.width / 2, bird.y - bird.height / 2, null);
//将之前的渲染旋转角度恢复
g2.rotate(bird.alpha, bird.x, bird.y);
// 绘制分数
//java.lang.Object
// java.awt.Font //Font类表示字体,用于以可见的方式呈现文本。 一种字体提供映射字符 字形的序列的序列和上呈现字形序列所需的信息Graphics个Component对象。
//从指定的名称,样式和点大小创建一个新的 Font Font f = new Font(Font.SANS_SERIF, Font.BOLD, 40);//BOLD-大胆的风格常数。SANS_SERIF-逻辑字体“SansSerif”的规范系列名称的字符串常量。
g.setFont(f);
g.drawString("" + score, 40, 60);
g.setColor(Color.WHITE);
g.drawString("" + score, 40 - 3, 60 - 3);
// 绘制开始与结束界面
switch (state) {
case START:
g.drawImage(startImage, 0, 0, null);
break;
case GAME_OVER:
g.drawImage(gameOverImage, 0, 0, null);
break;
}
}
3.4 测试一下
运行main方法,效果如下图:
在构造方法内将状态设置为RUNNING,重新执行main方法,效果如下图:
在构造方法内将状态设置为GAME_OVER,重新执行main方法,效果如下图:
测试完毕,别忘了将构造方法内的状态重置为START。
4 编排游戏动作
4.1 定义开始方法
在BirdGame类中增加开始游戏的方法,并在此方法内编排游戏的动作,代码如下:
// 开始游戏
public void action() throws Exception {
// 不断的移动与重绘
while (true) {
switch (state) {
case START:
// 小鸟做出飞行动作
bird.fly();
// 地面向左移动一步
ground.step();
break;
case RUNNING:
// 地面向左移动一步
ground.step();
// 柱子向左移动一步
column1.step();
column2.step();
// 小鸟做出飞行动作
bird.fly();
// 小鸟上下移动一步
bird.step();
// 计算分数
if (bird.x == column1.x || bird.x == column2.x) {
score++;
}
// 检测是否发生碰撞
if (bird.hit(ground) || bird.hit(column1) || bird.hit(column2)) {
state = GAME_OVER;
}
break;
}
// 重新绘制界面
// 调用repaint方法重新绘制界面
//public void repaint(long tm)重写组件。
//public void repaint()重新编辑这个组件。
//如果此组件是轻量级组件,则此方法将尽快调用此组件的paint方法。
//否则,该方法会尽快调用此组件的update方法。
repaint();
// 休眠 1000/60 毫秒
Thread.sleep(1000 / 60);
}
}
4.2 调用开始方法
在main方法中调用上述方法,代码如下:
/**
* 启动方法
*/
public static void main(String[] args) throws Exception {
JFrame frame = new JFrame();
BirdGame game = new BirdGame();
frame.add(game);
frame.setSize(440, 670);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
game.action();
}
4.3 测试一下
运行main方法,效果如下图:
在构造方法内将状态设置为RUNNING,重新运行main方法,效果如下图:
测试完毕,别忘了将构造方法内的状态重置为START。
5 定义操控逻辑
5.1 增加监听事件
在action方法中,增加鼠标单击事件的监听器,代码如下(增加在while循环的前面):
// 鼠标监听器
// 适配器类已经实现相应接口,MouseAdapter抽象类实现了MouseListener接口,
// 因此可以使用MouseAdapter(的子类)创建的对象做监视器,只需重写需要的接口方法即可。
MouseListener l = new MouseAdapter() {
// 鼠标按下事件
public void mousePressed(MouseEvent e) {
try {
switch (state) {
case START:
// 在开始状态,按下鼠标则转为运行状态。
state = RUNNING;
break;
case RUNNING:
// 在运行状态,按下鼠标则小鸟向上飞行。
bird.flappy();
break;
case GAME_OVER:
// 在结束状态,按下鼠标则重置数据,再次转为开始态。
column1 = new Column(1);
column2 = new Column(2);
bird = new Bird();
score = 0;
state = START;
break;
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
// 将监听器添加到当前的面板上
addMouseListener(l);
笔记:
adapter意为适配器。
我们知道当一个类实现一个接口时,即使不准备使用某个方法,也必须给出接口中所有方法的实现,适配器可以替代接口来处理事件,当java提供处理事件的接口中多于一个方法时,java就相应的提供一个适配器类,比如:MouseAdapter,windowAdapter等等。
适配器已经实现了相应的接口,例如MouseAdapter抽象类实现了MouseListener接口,因此可以使用MouseAdapter的子类创建的对象做监视器,只需重写需要的接口方法即可。
一个是类,一个是接口。
5.2 测试一下
运行main方法,效果如下图:
6 生成可执行.exe文件
6.1 使用exe4j生成.exe文件
在IDEA中build项目的jar包,然后使用exe4j生成.exe文件。
6.2 测试一下
运行.exe文件,效果如下图: