我们使用 Maven 来管理项目,以下为 POM 的参考配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lusifer</groupId>
<artifactId>airplane</artifactId>
<version>0.0.1</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>17.0.2</javafx.version>
</properties>
<dependencies>
<!-- JavaFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>在 src/main/java 目录下创建 module-info.java 文件
module com.lusifer.airplane {
requires javafx.graphics;
requires javafx.media;
requires javafx.controls;
exports com.lusifer.airplane to javafx.graphics;
}创建程序的入口程序
package com.lusifer.airplane;
import javafx.application.Application;
import javafx.stage.Stage;
/**
* 游戏启动器
*
* @author Lusifer
* @since 2024/6/21
*/
public class GameLauncher extends Application {
@Override
public void start(Stage stage) throws Exception {
// 设置标题
stage.setTitle("地球防卫军");
// 关闭窗口时退出程序
stage.setOnCloseRequest(windowEvent -> {
System.exit(0);
});
// 显示窗口
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lusifer</groupId>
<artifactId>airplane</artifactId>
<version>1.1.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>2.0.13</slf4j.version>
<javafx.version>17.0.2</javafx.version>
<lombok.version>1.18.20</lombok.version>
<logback.version>1.4.12</logback.version>
<snakeyaml.version>2.2</snakeyaml.version>
</properties>
<dependencies>
<!-- JavaFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- Log -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- YAML -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<!-- 解决使用 MVN 编译时无法编译 Lombok 的问题 -->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- JavaFX 打包 bat 插件 -->
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<executions>
<execution>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<configuration>
<stripDebug>true</stripDebug>
<compress>2</compress>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
<!-- 启动器名称,此处会生成 .bat 文件 -->
<launcher>airplane</launcher>
<!-- 此处会生成 airplane 文件夹存放生成后的文件 -->
<jlinkImageName>airplane</jlinkImageName>
<!-- 此处会生成 .zip 文件 -->
<!--<jlinkZipName>airplane</jlinkZipName>-->
<mainClass>com.lusifer.airplane.GameLauncher</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>config.yaml
app:
name: 地球防卫军
version: 1.1.0
display:
width: 1920
height: 1080
fullscreen: truelogback.xml
<configuration>
<!-- 控制台输出的日志格式 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 设置日志级别 -->
<root name="com.lusifer.airplane" level="debug" additivity="false">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>module-info.java
module com.lusifer.airplane {
requires javafx.graphics;
requires static lombok;
requires org.yaml.snakeyaml;
requires org.slf4j;
requires javafx.media;
requires java.desktop;
requires javafx.controls;
exports com.lusifer.airplane to javafx.graphics;
opens com.lusifer.airplane.config to org.yaml.snakeyaml;
}package com.lusifer.airplane;
import com.lusifer.airplane.component.ReVideoView;
import com.lusifer.airplane.config.AppAssets;
import com.lusifer.airplane.config.AppConfig;
import com.lusifer.airplane.config.ConfigLoader;
import com.lusifer.airplane.game.menu.StartMenuScene;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
/**
* 游戏启动器
*
* @author Lusifer
* @since 2024/6/21
*/
@Slf4j
public class GameLauncher extends Application {
private static final AppConfig config = ConfigLoader.getInstance().getConfig();
@Override
public void start(Stage stage) throws Exception {
log.debug("游戏启动");
// 设置标题
stage.setTitle(config.getApp().get("name"));
// 设置图标
stage.getIcons().add(new Image(AppAssets.IMAGES.get("logo")));
// 禁止窗口最大化和拉伸
stage.setResizable(false);
// 隐藏全屏后显示【按 ESC 退出全屏】的提示
stage.setFullScreenExitHint("");
// 禁止使用 ESC 退出全屏功能
stage.setFullScreenExitKeyCombination(KeyCodeCombination.NO_MATCH);
// 关闭窗口时退出程序
stage.setOnCloseRequest(windowEvent -> {
log.debug("退出");
System.exit(0);
});
// 设置默认空白场景
stage.setScene(createScene());
// 设置分辨率
int width = Integer.parseInt(config.getDisplay().get("width"));
int height = Integer.parseInt(config.getDisplay().get("height"));
boolean isFullscreen = Boolean.parseBoolean(config.getDisplay().get("fullscreen"));
stage.setFullScreen(isFullscreen);
if (!isFullscreen) {
stage.setWidth(width);
stage.setHeight(height);
}
// 播放启动动画
new ReVideoView(stage, AppAssets.VIDEOS.get("movie_story_start"), () -> {
new StartMenuScene(stage);
});
// 显示窗口
stage.show();
}
/**
* 创建空白场景
*/
private Scene createScene() {
StackPane stackPane = new StackPane();
Scene scene = new Scene(stackPane);
scene.setFill(Color.BLACK);
return scene;
}
public static void main(String[] args) {
launch(args);
}
}CustomEvent.java
package com.lusifer.airplane.common;
/**
* 回调接口
* @author Lusifer
* @since 2024/6/25
*/
public interface CustomEvent {
void onCallback();
}ReLoopImageView.java
package com.lusifer.airplane.component;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.TranslateTransition;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;
import lombok.Getter;
/**
* 无尽图。
* 循环滚动图片的自定义组件,用于创建一个具有无限循环滚动效果的图像视图。
* 该组件可以在指定的容器中创建两个相同的图片视图,使其在垂直方向上交替滚动,以实现连续的滚动效果。
* 此类中的滚动速度和图片路径可以根据需要进行自定义。
*
* @author Lusifer
* @since 2024/6/26
*/
@Getter
public class ReLoopImageView {
/**
* 滚动的间隔时间(秒)
*/
private static final int SCROLL_SPEED = 20;
/**
* 视图的实际宽度
*/
private final Double fitWidth;
private final Double translateX;
/**
* 初始化无尽图
*
* @param stage 窗口
* @param container 容器,放置无尽图的容器
* @param filePath 图片路径
*/
public ReLoopImageView(Stage stage, Pane container, String filePath) {
Image image = new Image(filePath);
// 创建两个 ImageView 并设置相同的背景图片
ImageView topImageView = new ImageView(image);
ImageView bottomImageView = new ImageView(image);
// 设置 ImageView 的高度
topImageView.fitHeightProperty().bind(stage.heightProperty());
bottomImageView.fitHeightProperty().bind(stage.heightProperty());
// 设置 preserveRatio 为 true,保持图片的宽高比例不变
topImageView.setPreserveRatio(true);
bottomImageView.setPreserveRatio(true);
// 设置第二张图片的位置在第一张图片的下方
bottomImageView.setTranslateY(bottomImageView.getFitHeight());
// 实现背景图片垂直循环滚动
double scrollDistance = topImageView.getFitHeight(); // 图片滚动的距离
TranslateTransition topTransition = new TranslateTransition(Duration.seconds(SCROLL_SPEED), topImageView);
TranslateTransition bottomTransition = new TranslateTransition(Duration.seconds(SCROLL_SPEED), bottomImageView);
// 设置动画插值器为线性
topTransition.setInterpolator(Interpolator.LINEAR);
bottomTransition.setInterpolator(Interpolator.LINEAR);
// 设置第一张图片的滚动距离(从上往下)
topTransition.setFromY(-scrollDistance);
topTransition.setToY(0);
// 设置第二张图片的滚动距离(从上往下)
bottomTransition.setFromY(0);
bottomTransition.setToY(scrollDistance);
// 设置循环次数为无限次
topTransition.setCycleCount(Animation.INDEFINITE);
bottomTransition.setCycleCount(Animation.INDEFINITE);
// 播放动画
topTransition.play();
bottomTransition.play();
// 将 ImageView 添加到容器中
container.getChildren().addAll(topImageView, bottomImageView);
// 计算 ImageView 的实际宽度
this.fitWidth = calcFitWidth(topImageView);
this.translateX = topImageView.getTranslateX();
}
/**
* 计算视图的实际宽度
*
* @param imageView 图片视图
* @return 视图的实际宽度
*/
private Double calcFitWidth(ImageView imageView) {
Image image = imageView.getImage();
double originalWidth = image.getWidth(); // 获取原始图像的宽度
double scaledWidth = imageView.getFitWidth(); // 获取 ImageView 的适应宽度
double scaledHeight = imageView.getFitHeight(); // 获取 ImageView 的适应高度
double actualWidth = scaledWidth; // 未设置 preserveRatio,实际宽度等于适应宽度
if (imageView.isPreserveRatio()) {
actualWidth = scaledHeight * (originalWidth / image.getHeight()); // 计算实际宽度
}
return actualWidth;
}
}ReVideoView.java
package com.lusifer.airplane.component;
import com.lusifer.airplane.common.CustomEvent;
import com.lusifer.airplane.utils.ScreenUtil;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import java.net.MalformedURLException;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 自定义组件(视频播放)
*
* @author Lusifer
* @since 2024/6/25
*/
@Slf4j
public class ReVideoView {
private StackPane container;
private MediaPlayer mediaPlayer;
private MediaView mediaView;
private final Stage stage;
private final String filePath;
private final CustomEvent event;
/**
* 初始化视频播放组件
*
* @param stage 窗口
* @param filePath 视频路径
* @param event 回调方法(视频播放放完成后的回调)
*/
public ReVideoView(Stage stage, String filePath, CustomEvent event) {
this.stage = stage;
this.filePath = filePath;
this.event = event;
initScene();
}
/**
* 初始化场景
*/
private void initScene() {
container = new StackPane();
Scene scene = new Scene(container);
scene.setFill(Color.BLACK);
scene.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
// 手动触发播放完成(跳过动画)
if (mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING || mediaPlayer.getStatus() ==
MediaPlayer.Status.PAUSED) {
mediaPlayer.seek(mediaPlayer.getTotalDuration());
}
}
});
stage.setScene(scene);
stage.setFullScreen(ScreenUtil.isFullscreen());
initMediaPlayer();
}
// -------------------- 场景细节 Begin --------------------
/**
* 初始化播放器
*/
private void initMediaPlayer() {
Path path = Paths.get(filePath);
try {
Media media = new Media(path.toUri().toURL().toString());
mediaPlayer = new MediaPlayer(media);
// 播放视图自适应窗口
mediaView = new MediaView(mediaPlayer);
mediaView.fitHeightProperty().bind(stage.widthProperty());
mediaView.fitHeightProperty().bind(stage.heightProperty());
// 追加进容器
container.getChildren().add(mediaView);
container.setAlignment(Pos.TOP_LEFT);
mediaPlayer.play(); // 开始播放
// 播放监听
mediaPlayer.setOnReady(() -> log.debug("视频准备就绪 {}", filePath));
mediaPlayer.setOnEndOfMedia(() -> {
log.debug("视频播放完成");
// 将视频跳回播放的开头
Platform.runLater(() -> mediaPlayer.seek(Duration.ZERO));
mediaPlayer.stop(); // 停止播放
// 调用回调
event.onCallback();
});
// 捕获播放错误
mediaPlayer.setOnError(() -> {
log.error("视频播放出错", mediaPlayer.getError());
// 如果发生错误,尝试重新播放
log.warn("尝试重新播放 {}", filePath);
container.getChildren().remove(mediaView);
initMediaPlayer(); // 调用自身重新尝试播放
});
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
}AppAssets.java
package com.lusifer.airplane.config;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.net.URI;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 应用资源
*/
@Slf4j
public final class AppAssets {
private AppAssets() {
}
public static final Map<String, String> IMAGES = new HashMap<>();
public static final Map<String, String> VIDEOS = new HashMap<>();
public static final Map<String, String> AUDIOS = new HashMap<>();
static {
loadResources();
}
/**
* 加载资源
*/
private static void loadResources() {
File folder = new File(location());
File[] files = folder.listFiles();
if (!Objects.isNull(files)) {
for (File file : files) {
if (file.exists() && file.isDirectory()) {
loadResourcePath(file);
}
}
}
}
/**
* 加载资源路径
*/
private static void loadResourcePath(File folder) {
for (File file : Objects.requireNonNull(folder.listFiles())) {
String name = file.getName();
int suffixIndex = name.lastIndexOf(".");
String fileName = name.substring(0, suffixIndex);
switch (folder.getName()) {
case "image" -> {
IMAGES.put(fileName, file.getAbsolutePath());
}
case "audio" -> {
AUDIOS.put(fileName, file.getAbsolutePath());
}
case "video" -> {
VIDEOS.put(fileName, file.getAbsolutePath());
}
}
}
}
private static URI location() {
return Paths.get("assets").toAbsolutePath().toUri();
}
}AppConfig.java
package com.lusifer.airplane.config;
import lombok.Data;
import java.util.Map;
/**
* 应用配置
* @author Lusifer
* @since 2024/6/21
*/
@Data
public class AppConfig {
private Map<String, String> app;
private Map<String, String> display;
}ConfigLoader.java
package com.lusifer.airplane.config;
import lombok.Getter;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import java.io.InputStream;
/**
* 应用配置加载器
* @author Lusifer
* @since 2024/6/21
*/
@Getter
public class ConfigLoader {
private static volatile ConfigLoader instance;
private AppConfig config;
private ConfigLoader() {
loadConfig();
}
public static ConfigLoader getInstance() {
if (instance == null) {
synchronized (ConfigLoader.class) {
if (instance == null) {
instance = new ConfigLoader();
}
}
}
return instance;
}
private void loadConfig() {
LoaderOptions loaderOptions = new LoaderOptions();
loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE);
Yaml yaml = new Yaml(new Constructor(AppConfig.class, loaderOptions));
InputStream inputStream = ConfigLoader.class.getClassLoader().getResourceAsStream("config.yaml");
if (inputStream == null) {
throw new RuntimeException("Failed to load config.yaml");
}
config = yaml.load(inputStream);
}
}BossPlane.java
package com.lusifer.airplane.game.enemy;
import com.lusifer.airplane.common.CustomEvent;
import com.lusifer.airplane.component.ReLoopImageView;
import com.lusifer.airplane.config.AppAssets;
import com.lusifer.airplane.game.NodeMetaData;
import com.lusifer.airplane.utils.Mp3Util;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
/**
* BOSS 飞机类
*
* @author Lusifer
* @since 2024/6/27
*/
@Slf4j
public class BossPlane {
private final Pane container;
private final ReLoopImageView levelMapView;
private final CustomEvent callback;
private final ImageView bossPlaneView;
private final Random random = new Random();
private Timeline moveTimeline;
private Timeline shootTimeline;
/**
* 初始化 BOSS
*
* @param container 容器,放置 BOSS 飞机的容器
* @param levelMapView 关卡地图视图
* @param callback 回调方法(BOSS 摧毁后的回调)
*/
public BossPlane(Pane container, ReLoopImageView levelMapView, CustomEvent callback) {
this.container = container;
this.levelMapView = levelMapView;
this.callback = callback;
this.bossPlaneView = new ImageView(new Image(AppAssets.IMAGES.get("plane_boss_1")));
initBossPlane();
}
private void initBossPlane() {
log.debug("BOSS 进入战场");
bossPlaneView.setTranslateX(0);
bossPlaneView.setTranslateY(-bossPlaneView.getImage().getHeight() - container.getHeight() / 2);
NodeMetaData nodeMetaData = new NodeMetaData();
nodeMetaData.setName("bossPlane");
nodeMetaData.setHealth(30); // 设置 BOSS 初始生命值
bossPlaneView.setUserData(nodeMetaData);
container.getChildren().add(bossPlaneView);
// BOSS 出现动画
TranslateTransition transition = new TranslateTransition(Duration.seconds(2), bossPlaneView);
transition.setToY(-bossPlaneView.getImage().getHeight() - 100);
transition.setOnFinished(event -> startMoveAndShoot());
transition.play();
// 启用碰撞检测
detection();
}
/**
* 移动与发射子弹
*/
private void startMoveAndShoot() {
moveTimeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> moveRandomly()));
moveTimeline.setCycleCount(Timeline.INDEFINITE);
moveTimeline.play();
shootTimeline = new Timeline(new KeyFrame(Duration.seconds(0.5), event -> shootBullets()));
shootTimeline.setCycleCount(Timeline.INDEFINITE);
shootTimeline.play();
}
private void moveRandomly() {
double minX = levelMapView.getTranslateX() - bossPlaneView.getImage().getWidth() / 2;
double maxX = levelMapView.getTranslateX() + bossPlaneView.getImage().getWidth() / 2;
double newX = minX + random.nextDouble() * (maxX - minX);
double newY = random.nextDouble() * (-container.getHeight() / 2); // 保持在屏幕上半部分
log.debug("BOSS 坐标,x:{} y:{}", newX, newY);
TranslateTransition transition = new TranslateTransition(Duration.seconds(1), bossPlaneView);
transition.setToX(newX);
transition.setToY(newY);
transition.play();
}
private void shootBullets() {
for (int i = -2; i <= 2; i++) {
ImageView bullet = new ImageView(new Image(AppAssets.IMAGES.get("bullet_enemy_1")));
bullet.setTranslateX(bossPlaneView.getTranslateX() + i * 30);
bullet.setTranslateY(bossPlaneView.getTranslateY() + bossPlaneView.getImage().getHeight() / 2 + 20);
NodeMetaData bulletMetaData = new NodeMetaData();
bulletMetaData.setName("bossBullet");
bullet.setUserData(bulletMetaData);
container.getChildren().add(bullet);
TranslateTransition bulletTransition = new TranslateTransition(Duration.seconds(5), bullet);
bulletTransition.setToY(container.getHeight());
bulletTransition.setOnFinished(event -> container.getChildren().remove(bullet));
bulletTransition.play();
}
}
public void stop() {
if (moveTimeline != null) {
moveTimeline.stop();
}
if (shootTimeline != null) {
shootTimeline.stop();
}
}
// -------------------- 碰撞检测 Begin --------------------
/**
* 碰撞检测
*/
private void detection() {
Timeline collisionCheck = new Timeline(new KeyFrame(Duration.millis(20), event -> {
List<ImageView> bulletsToRemove = new ArrayList<>();
for (javafx.scene.Node node : container.getChildren()) {
if (node instanceof ImageView bullet && node != bossPlaneView) {
try {
if (!Objects.isNull(bullet.getUserData())) {
NodeMetaData bulletUserData = (NodeMetaData) bullet.getUserData();
if (!Objects.isNull(bulletUserData) && bulletUserData.getName().equals("playerBullet")) {
NodeMetaData enemyUserData = (NodeMetaData) bossPlaneView.getUserData();
int enemyHealth = enemyUserData.getHealth();
if (enemyHealth > 0) {
if (bossPlaneView.getBoundsInParent().intersects(bullet.getBoundsInParent())) {
bulletsToRemove.add(bullet);
decreaseHealth();
break;
}
}
}
}
} catch (Exception ignored) {
}
}
}
container.getChildren().removeAll(bulletsToRemove);
}));
collisionCheck.setCycleCount(Timeline.INDEFINITE);
collisionCheck.play();
}
private void explode() {
Mp3Util.playOnce(AppAssets.AUDIOS.get("effect_explosion_type_1"));
Timeline explosionTimeline = new Timeline();
for (int i = 1; i <= 6; i++) {
final int index = i;
explosionTimeline.getKeyFrames().add(new KeyFrame(Duration.seconds(0.1 * i),
e -> bossPlaneView.setImage(new Image(AppAssets.IMAGES.get("explosion" + index)))));
}
explosionTimeline.getKeyFrames().add(new KeyFrame(Duration.seconds(0.7),
e -> container.getChildren().remove(bossPlaneView)));
explosionTimeline.play();
}
public void decreaseHealth() {
NodeMetaData bossMetaData = (NodeMetaData) bossPlaneView.getUserData();
int health = bossMetaData.getHealth();
health--;
bossMetaData.setHealth(health);
if (health <= 0) {
explode();
stop();
callback.onCallback();
}
bossPlaneView.setUserData(bossMetaData);
}
}EnemyPlane.java
package com.lusifer.airplane.game.enemy;
import com.lusifer.airplane.common.CustomEvent;
import com.lusifer.airplane.component.ReLoopImageView;
import com.lusifer.airplane.config.AppAssets;
import com.lusifer.airplane.game.NodeMetaData;
import com.lusifer.airplane.utils.Mp3Util;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
/**
* 敌人飞机类
*
* @author Lusifer
* @since 2024/6/26
*/
@Slf4j
public class EnemyPlane {
private final Pane container;
private final ReLoopImageView levelMapView;
/**
* 随机数生成器,用于随机化敌机生成的位置和属性
*/
private final Random random = new Random();
/**
* 标记敌机生成位置,控制生成在左侧还是右侧
*/
private boolean isLeft = false;
/**
* 时间轴,用于控制敌机的生成
*/
private Timeline timeline;
/**
* 敌机摧毁后的回调
*/
private final CustomEvent callback;
/**
* 初始化敌人飞机
*
* @param container 容器,放置敌人飞机的容器
* @param levelMapView 关卡地图视图
* @param callback 回调方法(敌机摧毁后的回调)
*/
public EnemyPlane(Pane container, ReLoopImageView levelMapView, CustomEvent callback) {
this.container = container;
this.levelMapView = levelMapView;
this.callback = callback;
}
// -------------------- 生成敌机 Begin --------------------
/**
* 生成敌机
* 使用一个时间轴定时任务,每隔 2 秒生成一个敌机
*/
public void startGenerate() {
this.timeline = new Timeline(new KeyFrame(Duration.seconds(2), event -> createEnemyPlane()));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
/**
* 停止生成敌机
*/
public void stopGenerate() {
if (timeline != null) {
timeline.stop();
log.debug("已停止生成敌机");
}
}
/**
* 生成敌机
*/
private void createEnemyPlane() {
// 确定敌机的生成位置
double startX = 0;
if (isLeft) {
isLeft = false;
startX = random.nextDouble() * (container.getWidth() / 2 - levelMapView.getFitWidth()); // 右侧
} else {
isLeft = true;
startX = random.nextDouble() * (-container.getWidth() / 2 + levelMapView.getFitWidth()); // 左侧
}
// 创建敌机并设置其初始属性
ImageView enemyPlaneView = new ImageView(new Image(AppAssets.IMAGES.get("plane_enemy_" + (random.nextInt(4) + 1))));
enemyPlaneView.setTranslateX(startX);
enemyPlaneView.setTranslateY(-container.getHeight()); // 初始位置在屏幕下方
NodeMetaData nodeMetaData = new NodeMetaData();
nodeMetaData.setName("enemyPlane");
nodeMetaData.setHealth(random.nextInt(6) + 3); // 设置敌机的初始生命值
enemyPlaneView.setUserData(nodeMetaData);
// 将敌机添加到容器中
container.getChildren().add(enemyPlaneView);
log.debug("生成敌机 x:{}", startX);
// 设置敌机的移动动画
TranslateTransition transition = new TranslateTransition(Duration.seconds(random.nextInt(15) + 10), enemyPlaneView);
transition.setToY(container.getHeight() + 50); // 敌机移动到屏幕顶部外
transition.setOnFinished(event -> container.getChildren().remove(enemyPlaneView)); // 敌机飞出屏幕后移除
transition.play();
// 启用碰撞检测
detection(enemyPlaneView);
}
// -------------------- 碰撞检测 Begin --------------------
/**
* 碰撞检测
*/
private void detection(ImageView enemyPlaneView) {
Timeline collisionCheck = new Timeline(new KeyFrame(Duration.millis(20), event -> {
List<ImageView> bulletsToRemove = new ArrayList<>();
for (javafx.scene.Node node : container.getChildren()) {
if (node instanceof ImageView bullet && node != enemyPlaneView) {
try {
if (!Objects.isNull(bullet.getUserData())) {
NodeMetaData bulletUserData = (NodeMetaData) bullet.getUserData();
if (!Objects.isNull(bulletUserData) && bulletUserData.getName().equals("playerBullet")) {
NodeMetaData enemyUserData = (NodeMetaData) enemyPlaneView.getUserData();
int enemyHealth = enemyUserData.getHealth();
if (enemyHealth > 0) {
if (enemyPlaneView.getBoundsInParent().intersects(bullet.getBoundsInParent())) {
bulletsToRemove.add(bullet);
decreaseHealth(enemyPlaneView);
break;
}
}
}
}
} catch (Exception ignored) {
}
}
}
container.getChildren().removeAll(bulletsToRemove);
}));
collisionCheck.setCycleCount(Timeline.INDEFINITE);
collisionCheck.play();
}
/**
* 减少敌机生命值
*
* @param enemyPlaneView 敌机视图
*/
private void decreaseHealth(ImageView enemyPlaneView) {
NodeMetaData enemyUserData = (NodeMetaData) enemyPlaneView.getUserData();
int enemyHealth = enemyUserData.getHealth();
enemyHealth--;
enemyUserData.setHealth(enemyHealth);
if (enemyHealth <= 0) {
enemyUserData.setName(""); // 清空名称防止碰撞
explode(enemyPlaneView);
callback.onCallback();
}
enemyPlaneView.setUserData(enemyUserData);
}
/**
* 敌机爆炸效果
*/
private void explode(ImageView enemyPlaneView) {
Mp3Util.playOnce(AppAssets.AUDIOS.get("effect_explosion_type_1"));
Timeline explosionTimeline = new Timeline(
new KeyFrame(Duration.ZERO, e -> enemyPlaneView.setImage(new Image(AppAssets.IMAGES.get("explosion1")))),
new KeyFrame(Duration.seconds(0.1), e -> enemyPlaneView.setImage(new Image(AppAssets.IMAGES.get("explosion2")))),
new KeyFrame(Duration.seconds(0.2), e -> enemyPlaneView.setImage(new Image(AppAssets.IMAGES.get("explosion3")))),
new KeyFrame(Duration.seconds(0.3), e -> enemyPlaneView.setImage(new Image(AppAssets.IMAGES.get("explosion4")))),
new KeyFrame(Duration.seconds(0.4), e -> enemyPlaneView.setImage(new Image(AppAssets.IMAGES.get("explosion5")))),
new KeyFrame(Duration.seconds(0.5), e -> enemyPlaneView.setImage(new Image(AppAssets.IMAGES.get("explosion6")))),
new KeyFrame(Duration.seconds(0.6), e -> container.getChildren().remove(enemyPlaneView))
);
explosionTimeline.play();
}
}GameLevel.java
package com.lusifer.airplane.game.level;
import com.lusifer.airplane.component.ReLoopImageView;
import com.lusifer.airplane.game.enemy.BossPlane;
import com.lusifer.airplane.game.enemy.EnemyPlane;
import com.lusifer.airplane.game.player.PlayerPlane;
import com.lusifer.airplane.game.record.GameScore;
import com.lusifer.airplane.utils.Mp3Util;
import com.lusifer.airplane.utils.ScreenUtil;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
/**
* 游戏关卡
*
* @author Lusifer
* @since 2024/6/26
*/
@Slf4j
public abstract class GameLevel {
protected Stage stage;
protected StackPane container;
protected Scene scene;
protected ReLoopImageView levelMapView;
protected Mp3Util mp3;
protected PlayerPlane player;
protected EnemyPlane enemy;
protected BossPlane boss;
protected Label scoreLabel;
/**
* 初始化游戏关卡
*
* @param stage 窗口
*/
public GameLevel(Stage stage) {
log.debug("进入游戏关卡");
this.stage = stage;
initScene();
}
/**
* 初始化场景
*/
private void initScene() {
container = new StackPane();
scene = new Scene(container);
stage.setScene(scene);
stage.setFullScreen(ScreenUtil.isFullscreen());
initBackground();
initLevelMap();
initBGM();
initPlayer();
initRecord();
}
// -------------------- 场景细节 Begin --------------------
/**
* 初始化背景
*/
protected abstract void initBackground();
/**
* 初始化关卡地图
*/
protected abstract void initLevelMap();
/**
* 初始化背景音乐
*/
protected abstract void initBGM();
/**
* 初始化玩家
*/
protected abstract void initPlayer();
/**
* 初始化游戏计分
*/
protected void initRecord() {
scoreLabel = new Label(String.valueOf(GameScore.score));
scoreLabel.setFont(Font.font("微软雅黑", FontWeight.BOLD, 24));
scoreLabel.setTextFill(Color.YELLOW);
scoreLabel.setTranslateX(-levelMapView.getFitWidth() / 2 + 50);
scoreLabel.setTranslateY(-scene.getHeight() / 2 + 50);
container.getChildren().add(scoreLabel);
}
}GameLevel1.java
package com.lusifer.airplane.game.level;
import com.lusifer.airplane.component.ReLoopImageView;
import com.lusifer.airplane.component.ReVideoView;
import com.lusifer.airplane.config.AppAssets;
import com.lusifer.airplane.game.enemy.EnemyPlane;
import com.lusifer.airplane.game.menu.StartMenuScene;
import com.lusifer.airplane.game.player.PlayerPlane;
import com.lusifer.airplane.game.record.GameScore;
import com.lusifer.airplane.utils.Mp3Util;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
/**
* 游戏关卡(第一关)
*
* @author Lusifer
* @since 2024/6/26
*/
@Slf4j
public class GameLevel1 extends GameLevel {
/**
* 初始化游戏关卡
*
* @param stage 窗口
*/
public GameLevel1(Stage stage) {
super(stage);
}
@Override
protected void initBackground() {
Image image = new Image(AppAssets.IMAGES.get("bg_game_1"));
ImageView imageView = new ImageView(image);
// 图片大小自适应窗口
imageView.fitWidthProperty().bind(stage.widthProperty());
imageView.fitHeightProperty().bind(stage.heightProperty());
container.getChildren().add(imageView);
}
@Override
protected void initLevelMap() {
levelMapView = new ReLoopImageView(stage, container, AppAssets.IMAGES.get("map_level_1"));
}
@Override
protected void initBGM() {
mp3 = new Mp3Util(AppAssets.AUDIOS.get("bgm_level_1"));
mp3.setLoop(true);
mp3.play();
}
@Override
protected void initPlayer() {
// 创建玩家,最后一个参数为玩家被摧毁后的回调
player = new PlayerPlane(scene, container, levelMapView, () -> {
log.debug("玩家被摧毁");
// 回到菜单界面
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(3), new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
mp3.stop();
enemy.stopGenerate();
new StartMenuScene(stage);
}
}));
// 设置定时器播放一次后停止
timeline.setCycleCount(1);
timeline.play();
});
player.in(() -> {
log.debug("玩家进入战场");
// 创建敌机,最后一个参数为敌机被摧毁后的回调
enemy = new EnemyPlane(container, levelMapView, () -> {
log.debug("敌机被摧毁");
GameScore.add();
scoreLabel.setText(String.valueOf(GameScore.score));
if (GameScore.score == 1000) {
player.out(() -> {
log.debug("玩家离开战场");
mp3.stop();
enemy.stopGenerate();
// 进入下一关
new ReVideoView(stage, AppAssets.VIDEOS.get("movie_story_2"), () -> {
new GameLevel2(stage);
});
});
}
});
enemy.startGenerate(); // 生成敌机
});
}
}GameLevel3.java
package com.lusifer.airplane.game.level;
import com.lusifer.airplane.component.ReLoopImageView;
import com.lusifer.airplane.component.ReVideoView;
import com.lusifer.airplane.config.AppAssets;
import com.lusifer.airplane.game.enemy.BossPlane;
import com.lusifer.airplane.game.enemy.EnemyPlane;
import com.lusifer.airplane.game.menu.StartMenuScene;
import com.lusifer.airplane.game.player.PlayerPlane;
import com.lusifer.airplane.game.record.GameScore;
import com.lusifer.airplane.utils.Mp3Util;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
/**
* 游戏关卡(第三关)
*
* @author Lusifer
* @since 2024/6/26
*/
@Slf4j
public class GameLevel3 extends GameLevel {
/**
* 初始化游戏关卡
*
* @param stage 窗口
*/
public GameLevel3(Stage stage) {
super(stage);
}
@Override
protected void initBackground() {
Image image = new Image(AppAssets.IMAGES.get("bg_game_3"));
ImageView imageView = new ImageView(image);
// 图片大小自适应窗口
imageView.fitWidthProperty().bind(stage.widthProperty());
imageView.fitHeightProperty().bind(stage.heightProperty());
container.getChildren().add(imageView);
}
@Override
protected void initLevelMap() {
levelMapView = new ReLoopImageView(stage, container, AppAssets.IMAGES.get("map_level_4"));
}
@Override
protected void initBGM() {
mp3 = new Mp3Util(AppAssets.AUDIOS.get("bgm_level_3"));
mp3.setLoop(true);
mp3.play();
}
@Override
protected void initPlayer() {
// 创建玩家,最后一个参数为玩家被摧毁后的回调
player = new PlayerPlane(scene, container, levelMapView, () -> {
log.debug("玩家被摧毁");
// 回到菜单界面
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(3), new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
mp3.stop();
enemy.stopGenerate();
boss.stop();
new StartMenuScene(stage);
}
}));
// 设置定时器播放一次后停止
timeline.setCycleCount(1);
timeline.play();
});
player.in(() -> {
log.debug("玩家进入战场");
// 创建敌机,最后一个参数为敌机被摧毁后的回调
enemy = new EnemyPlane(container, levelMapView, () -> {
log.debug("敌机被摧毁");
GameScore.add();
scoreLabel.setText(String.valueOf(GameScore.score));
if (GameScore.score == 3000) {
// BOSS 战
mp3.stop();
mp3.setFilePath(AppAssets.AUDIOS.get("bgm_boss"));
mp3.play();
boss = new BossPlane(container, levelMapView, () -> {
GameScore.score += 5000;
player.out(() -> {
log.debug("玩家离开战场");
mp3.stop();
boss.stop();
enemy.stopGenerate();
// 进入下一关
new ReVideoView(stage, AppAssets.VIDEOS.get("movie_story_end"), () -> {
new StartMenuScene(stage);
});
});
});
}
});
enemy.startGenerate(); // 生成敌机
});
}
}StartMenuScene.java
package com.lusifer.airplane.game.menu;
import com.lusifer.airplane.component.ReVideoView;
import com.lusifer.airplane.config.AppAssets;
import com.lusifer.airplane.game.level.GameLevel1;
import com.lusifer.airplane.game.record.GameScore;
import com.lusifer.airplane.utils.Mp3Util;
import com.lusifer.airplane.utils.ScreenUtil;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
/**
* 开始菜单
*
* @author Lusifer
* @since 2024/6/25
*/
@Slf4j
public class StartMenuScene {
private final Stage stage;
private StackPane container;
private Mp3Util mp3;
/**
* 初始化开始菜单
*
* @param stage 窗口
*/
public StartMenuScene(Stage stage) {
log.debug("进入启动菜单");
this.stage = stage;
initScene();
}
/**
* 初始化场景
*/
private void initScene() {
container = new StackPane();
Scene scene = new Scene(container);
stage.setScene(scene);
stage.setFullScreen(ScreenUtil.isFullscreen());
initBackground();
initCopyright();
initMenu();
initBGM();
}
// -------------------- 场景细节 Begin --------------------
/**
* 初始化背景
*/
private void initBackground() {
Image image = new Image(AppAssets.IMAGES.get("bg_menu_start"));
ImageView imageView = new ImageView(image);
// 图片大小自适应窗口
imageView.fitWidthProperty().bind(stage.widthProperty());
imageView.fitHeightProperty().bind(stage.heightProperty());
container.getChildren().add(imageView);
}
/**
* 初始化底部版权
*/
private void initCopyright() {
HBox hBox = new HBox();
hBox.setAlignment(Pos.BOTTOM_CENTER);
hBox.setPadding(new Insets(20));
Label label = new Label("Copyright 2024 源本科技李卫民. All Rights Reserved.");
label.setFont(Font.font("微软雅黑", FontWeight.BOLD, 28));
label.setTextFill(Color.YELLOW);
hBox.getChildren().add(label);
container.getChildren().add(hBox);
}
/**
* 初始化菜单
*/
private void initMenu() {
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
vBox.setPadding(new Insets(200));
vBox.getChildren().add(createButton("拯救地球", mouseEvent -> {
GameScore.score = 0; // 计分清零
mp3.stop();
// 进入游戏关卡
new ReVideoView(stage, AppAssets.VIDEOS.get("movie_story_1"), () -> {
new GameLevel1(stage);
});
}));
vBox.getChildren().add(createButton("择日再战", mouseEvent -> {
System.exit(0);
}));
container.getChildren().add(vBox);
}
/**
* 初始化背景音乐
*/
private void initBGM() {
mp3 = new Mp3Util(AppAssets.AUDIOS.get("bgm_menu"));
mp3.setLoop(true);
mp3.play();
}
private Label createButton(String text, EventHandler<MouseEvent> event) {
Label label = new Label(text);
label.setStyle("-fx-padding: 0 0 25 0; -fx-effect: dropshadow(one-pass-box, black, 20, 0.0, 0, 0);");
label.setFont(Font.font("微软雅黑", FontWeight.BOLD, 56));
label.setTextFill(Color.WHITE);
label.setOnMouseClicked(event);
return label;
}
}PlayerPlane.java
package com.lusifer.airplane.game.player;
import com.lusifer.airplane.common.CustomEvent;
import com.lusifer.airplane.component.ReLoopImageView;
import com.lusifer.airplane.config.AppAssets;
import com.lusifer.airplane.game.NodeMetaData;
import com.lusifer.airplane.utils.Mp3Util;
import com.lusifer.airplane.utils.ScreenUtil;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.SequentialTransition;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
/**
* 玩家飞机类
*
* @author Lusifer
* @since 2024/6/26
*/
@Slf4j
public class PlayerPlane {
private final Scene scene;
private final Pane container;
private final ReLoopImageView levelMapView;
private final Image playerPlaneImage;
private final ImageView playerPlaneView;
private final CustomEvent event;
private boolean spacePressed = false; // 标识空格键是否被按下
private int health = 1; // 玩家生命值
private boolean isOut = false; // 是否为离开状态(如果为离开状态则表示无敌不启用碰撞检测)
private boolean isBroker = false; // 是否为摧毁状态
/**
* 飞机的移动速度
*/
public static final int SPEED = 5;
/**
* 初始化玩家飞机
*
* @param scene 场景
* @param container 容器,放置玩家飞机的容器
* @param levelMapView 关卡地图视图
* @param event 玩家被摧毁的事件
*/
public PlayerPlane(Scene scene, Pane container, ReLoopImageView levelMapView, CustomEvent event) {
this.scene = scene;
this.container = container;
this.levelMapView = levelMapView;
this.event = event;
this.playerPlaneImage = new Image(AppAssets.IMAGES.get("plane_player_1"));
this.playerPlaneView = new ImageView(playerPlaneImage);
}
// -------------------- 飞机动画 Begin --------------------
/**
* 飞机入场动画
*
* @param callback 回调方法(飞机入场后的回调)
*/
public void in(CustomEvent callback) {
// 播放飞机飞过的声音
Mp3Util.playOnce(AppAssets.AUDIOS.get("effect_plane_flying_overhead"));
// 设置初始位置为屏幕下方
playerPlaneView.setTranslateY(ScreenUtil.getHeight());
container.getChildren().add(playerPlaneView);
// 实现飞机从屏幕下方飞到屏幕中间
TranslateTransition flyInTransition = new TranslateTransition(Duration.seconds(2), playerPlaneView);
flyInTransition.setFromY(ScreenUtil.getHeight());
flyInTransition.setToY(0);
flyInTransition.setInterpolator(Interpolator.EASE_OUT);
// 实现飞机从屏幕中间飞到底部居中
TranslateTransition flyDownTransition = new TranslateTransition(Duration.seconds(2), playerPlaneView);
flyDownTransition.setFromY(0);
flyDownTransition.setToY(ScreenUtil.getHeight() / 2 - playerPlaneImage.getHeight() / 2 - 20);
flyDownTransition.setInterpolator(Interpolator.EASE_IN);
// 创建 SequentialTransition 将两个 TranslateTransition 连接起来
SequentialTransition sequentialTransition = new SequentialTransition(flyInTransition, flyDownTransition);
sequentialTransition.setOnFinished(event -> {
// 初始化键盘控制
initPlayerControl();
// 初始化飞机移动
initPlayerAnimation();
// 调用回调
callback.onCallback();
// 启用碰撞
detection();
});
// 播放动画
sequentialTransition.play();
}
/**
* 飞机出场动画
*
* @param callback 回调方法(飞机出场后的回调)
*/
public void out(CustomEvent callback) {
// 飞离状态不启用碰撞检测
isOut = true;
// 移除键盘监听
scene.removeEventHandler(KeyEvent.KEY_PRESSED, keyPressHandler);
// 获取当前飞机的位置
double currentY = playerPlaneView.getTranslateY();
// 飞机飞到屏幕顶部外
TranslateTransition flyOutTransition = new TranslateTransition(Duration.seconds(2), playerPlaneView);
flyOutTransition.setFromY(currentY);
flyOutTransition.setToY(-scene.getHeight() - 100);
flyOutTransition.setInterpolator(Interpolator.EASE_IN);
flyOutTransition.setOnFinished(event -> {
// 移除飞机
container.getChildren().remove(playerPlaneView);
// 调用回调
callback.onCallback();
});
// 播放动画
flyOutTransition.play();
}
// -------------------- 飞机控制 Begin --------------------
/**
* 键盘按键事件处理程序,处理方向键和空格键的按下事件
*/
private final EventHandler<KeyEvent> keyPressHandler = new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
KeyCode keyCode = event.getCode();
if (keyCode == KeyCode.UP) {
playerPlaneView.setUserData(KeyCode.UP);
} else if (keyCode == KeyCode.DOWN) {
playerPlaneView.setUserData(KeyCode.DOWN);
} else if (keyCode == KeyCode.LEFT) {
playerPlaneView.setUserData(KeyCode.LEFT);
} else if (keyCode == KeyCode.RIGHT) {
playerPlaneView.setUserData(KeyCode.RIGHT);
} else if (keyCode == KeyCode.SPACE && !spacePressed) {
spacePressed = true;
}
}
};
/**
* 初始化键盘控制,添加键盘事件监听器
*/
private void initPlayerControl() {
scene.addEventHandler(KeyEvent.KEY_PRESSED, keyPressHandler);
scene.setOnKeyReleased(event -> {
KeyCode keyCode = event.getCode();
if (keyCode == KeyCode.UP && playerPlaneView.getUserData() == KeyCode.UP) {
playerPlaneView.setUserData(null);
} else if (keyCode == KeyCode.DOWN && playerPlaneView.getUserData() == KeyCode.DOWN) {
playerPlaneView.setUserData(null);
} else if (keyCode == KeyCode.LEFT && playerPlaneView.getUserData() == KeyCode.LEFT) {
playerPlaneView.setUserData(null);
} else if (keyCode == KeyCode.RIGHT && playerPlaneView.getUserData() == KeyCode.RIGHT) {
playerPlaneView.setUserData(null);
} else if (keyCode == KeyCode.SPACE) {
spacePressed = false; // 释放空格键
fire(); // 发射子弹
}
});
}
/**
* 创建飞机移动动画的时间线,根据方向键的按下状态移动飞机
*/
private void initPlayerAnimation() {
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(16), event -> {
KeyCode keyCode = (KeyCode) playerPlaneView.getUserData();
if (keyCode != null) {
double newX = playerPlaneView.getTranslateX();
double newY = playerPlaneView.getTranslateY();
switch (keyCode) {
case UP:
newY -= SPEED;
break;
case DOWN:
newY += SPEED;
break;
case LEFT:
newX -= SPEED;
break;
case RIGHT:
newX += SPEED;
break;
default:
break;
}
double imageWidth = playerPlaneView.getImage().getWidth();
double imageHeight = playerPlaneView.getImage().getHeight();
double minX = -levelMapView.getFitWidth() / 2 + imageWidth / 2;
double maxX = levelMapView.getFitWidth() / 2 - imageWidth / 2;
double minY = -ScreenUtil.getHeight() / 2 + imageHeight / 2;
double maxY = ScreenUtil.getHeight() / 2 - imageHeight / 2;
if (newX >= minX && newX <= maxX) {
playerPlaneView.setTranslateX(newX);
}
if (newY >= minY && newY <= maxY) {
playerPlaneView.setTranslateY(newY);
}
log.debug("玩家坐标 x:{} y:{}", newX, newY);
}
}));
timeline.setCycleCount(Timeline.INDEFINITE); // 设置时间线无限循环
timeline.play();
}
// -------------------- 飞机子弹 Begin --------------------
/**
* 发射子弹
*/
private void fire() {
if (isBroker) { // 摧毁状态无需发射子弹
return;
}
// 创建子弹的图片视图
ImageView bullet = new ImageView(new Image(AppAssets.IMAGES.get("bullet_player_type_a_1")));
double bulletHeight = bullet.getImage().getHeight();
// 计算子弹的初始位置
double startX = playerPlaneView.getTranslateX();
double startY = playerPlaneView.getTranslateY() - (playerPlaneImage.getHeight() / 2) + (bulletHeight / 2) - 25;
log.debug("玩家子弹坐标 x:{} y:{}", startX, startY);
// 创建子弹的图片视图
NodeMetaData nodeMetaData = new NodeMetaData();
nodeMetaData.setName("playerBullet");
bullet.setUserData(nodeMetaData); // 设置用户数据标识为玩家子弹,用于碰撞检测
bullet.setTranslateX(startX);
bullet.setTranslateY(startY);
container.getChildren().add(bullet); // 将子弹添加到容器中
// 播放子弹发射的声音
Mp3Util.playOnce(AppAssets.AUDIOS.get("effect_bullet_hero_type_a_1"));
// 创建子弹的移动动画
TranslateTransition transition = new TranslateTransition(Duration.seconds(2), bullet);
transition.setToY(-400); // 设置子弹的终点位置
// transition.setToY(-ScreenUtil.getHeight()); // 设置子弹的终点位置
transition.setOnFinished(event -> container.getChildren().remove(bullet)); // 子弹飞出屏幕后移除
transition.play(); // 播放动画
}
// -------------------- 碰撞检测 Begin --------------------
/**
* 碰撞检测
*/
private void detection() {
Timeline collisionCheck = new Timeline(new KeyFrame(Duration.millis(20), event -> {
for (javafx.scene.Node node : container.getChildren()) {
if (node instanceof ImageView enemy) {
try {
if (!Objects.isNull(enemy.getUserData())) {
NodeMetaData enemyUserData = (NodeMetaData) enemy.getUserData();
if (!Objects.isNull(enemyUserData)
&& (
enemyUserData.getName().equals("enemyPlane")
|| enemyUserData.getName().equals("bossPlane")
|| enemyUserData.getName().equals("bossBullet"))
&& !isOut) {
if (health > 0) {
if (playerPlaneView.getBoundsInParent().intersects(enemy.getBoundsInParent())) {
decreaseHealth();
break;
}
}
}
}
} catch (Exception ignored) {
}
}
}
}));
collisionCheck.setCycleCount(Timeline.INDEFINITE);
collisionCheck.play();
}
/**
* 减少玩家生命值
*/
private void decreaseHealth() {
health--;
if (health <= 0) {
// 移除键盘监听
isBroker = true;
scene.removeEventHandler(KeyEvent.KEY_PRESSED, keyPressHandler);
explode();
event.onCallback();
}
}
/**
* 玩家爆炸效果
*/
private void explode() {
Mp3Util.playOnce(AppAssets.AUDIOS.get("effect_explosion_type_1"));
Timeline explosionTimeline = new Timeline(
new KeyFrame(Duration.ZERO, e -> playerPlaneView.setImage(new Image(AppAssets.IMAGES.get("player_bom_1")))),
new KeyFrame(Duration.seconds(0.1), e -> playerPlaneView.setImage(new Image(AppAssets.IMAGES.get(
"player_bom_2")))),
new KeyFrame(Duration.seconds(0.2), e -> playerPlaneView.setImage(new Image(AppAssets.IMAGES.get(
"player_bom_3")))),
new KeyFrame(Duration.seconds(0.3), e -> playerPlaneView.setImage(new Image(AppAssets.IMAGES.get(
"player_bom_4")))),
new KeyFrame(Duration.seconds(0.6), e -> container.getChildren().remove(playerPlaneView))
);
explosionTimeline.play();
}
}GameScore.java
package com.lusifer.airplane.game.record;
/**
* 游戏计分板
*
* @author Lusifer
* @since 2024/6/26
*/
public class GameScore {
private GameScore() {
}
/**
* 积分默认从 0 开始
*/
public static int score = 0;
/**
* 追加分数
*/
public static void add() {
score += 100;
}
}NodeMetaData.java
package com.lusifer.airplane.game;
import lombok.Data;
/**
* 元素元数据
*
* @author Lusifer
* @since 2024/6/26
*/
@Data
public class NodeMetaData {
/**
* 名称
*/
private String name;
/**
* 健康值
*/
private Integer health;
}Mp3Util.java
package com.lusifer.airplane.utils;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.util.Duration;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.net.URI;
/**
* MP3 播放工具
*
* @author Lusifer
* @since 2024/6/26
*/
@Slf4j
@Setter
public class Mp3Util {
private MediaPlayer player;
private boolean isPaused;
private Duration pausedTime;
private boolean loop;
private String filePath;
public Mp3Util(String filePath) {
this.filePath = filePath;
}
public void play() {
if (isPaused) {
resume();
return;
}
stop();
URI uri = new File(filePath).toURI();
Media media = new Media(uri.toString());
player = new MediaPlayer(media);
player.setOnEndOfMedia(() -> {
if (!isPaused) {
if (loop) {
play(); // Recursive call to replay the audio
} else {
pausedTime = Duration.ZERO;
}
}
});
player.setOnError(() -> log.debug(player.getError().getMessage()));
player.play();
}
public void pause() {
if (player != null) {
isPaused = true;
pausedTime = player.getCurrentTime();
player.pause();
}
}
public void resume() {
if (isPaused) {
isPaused = false;
player.seek(pausedTime);
player.play();
}
}
public void stop() {
if (player != null) {
player.stop();
pausedTime = Duration.ZERO;
isPaused = false;
}
}
public boolean isPlaying() {
return player != null && player.getStatus() == MediaPlayer.Status.PLAYING;
}
public static void playOnce(String filePath) {
URI uri = new File(filePath).toURI();
Media media = new Media(uri.toString());
MediaPlayer staticPlayer = new MediaPlayer(media);
staticPlayer.setOnError(() -> log.debug(staticPlayer.getError().getMessage()));
staticPlayer.play();
}
}ScreenUtil.java
package com.lusifer.airplane.utils;
import com.lusifer.airplane.config.AppConfig;
import com.lusifer.airplane.config.ConfigLoader;
import javafx.geometry.Rectangle2D;
import javafx.stage.Screen;
/**
* 屏幕工具
*
* @author Lusifer
* @since 2024/6/25
*/
public class ScreenUtil {
/**
* 游戏配置
*/
private static final AppConfig config = ConfigLoader.getInstance().getConfig();
/**
* 存储屏幕主显示器的边界矩形。
* 此变量用于后续操作,以了解主显示器的尺寸和位置,以便进行窗口布局或其它需要考虑屏幕边界的操作。
* 通过 Screen.getPrimary().getBounds() 进行初始化,获取的是主显示器的矩形区域,包括宽度、高度和位置。
*/
public static final Rectangle2D bounds = Screen.getPrimary().getBounds();
/**
* 获取屏幕宽度
*
* @return 屏幕宽度
*/
public static double getWidth() {
if (isFullscreen()) {
return bounds.getWidth();
} else {
return Double.parseDouble(config.getDisplay().get("width"));
}
}
/**
* 获取屏幕高度
*
* @return 屏幕高度
*/
public static double getHeight() {
if (isFullscreen()) {
return bounds.getHeight();
} else {
return Double.parseDouble(config.getDisplay().get("height"));
}
}
/**
* 判断是否是全屏状态
*
* @return true 全屏/false 窗口
*/
public static boolean isFullscreen() {
return Boolean.parseBoolean(config.getDisplay().get("fullscreen"));
}
}