Repository Wiki
isHarryh/Ark-Pets

技术栈与第三方依赖

本页系统梳理 Ark-Pets 桌面宠物项目的技术栈构成:Gradle 多模块构建体系、运行时 JDK 模块裁剪(jlink)、渲染着色器资源、JavaFX 桌面 UI(FXML/CSS)、Sentry 错误上报集成,以及跨平台安装包分发管线(distJar → jlink → jpackage → Inno Setup)。

目的与范围

本页覆盖:

  • 仓库的构建体系与模块划分(core/build.gradle、desktop/build.gradle)
  • 运行时技术栈:JDK 模块列表、JVM 启动参数、平台差异处理(macOS -XstartOnFirstThread)
  • 渲染层资源:assets/shaders/ 下的 GLSL 着色器源码
  • 桌面 UI 层技术:assets/UI/ 下的 FXML 布局与 CSS 样式
  • 静态资源:字体(思源黑体)、多平台图标、默认配置文件
  • 可观测性:Sentry DSN 的注入方式
  • 分发管线:Fat JAR 打包、定制 JRE、jpackage 安装包、Windows Inno Setup 脚本

留给兄弟页面的内容:

  • 各功能模块的具体实现逻辑(桌面宠物渲染循环、行为状态机、模型加载)不在本页展开
  • assets/ArkPetsConfigDefault.json 中各配置项的业务语义由配置相关页面说明
  • 具体某个第三方库的调用代码分析,见对应功能实现页

概述

Ark-Pets 是一个基于 Java + Gradle 构建的跨平台桌面应用,其技术选型可以概括为四个层面:

层面技术证据来源
构建体系Gradle 多模块工程(core + desktop)desktop/build.gradle 引用 project.appName、project.version 等根项目属性
运行时JDK(构建时通过 ${javaHome} 定位),经 jlink 裁剪为定制 JREdesktop/build.gradle 的 jlink 任务
渲染层GLSL 着色器(Plain* / Complex* 四套 + 片段低配版)assets/shaders/ 目录下 5 个 .glsl 文件
桌面 UIJavaFX 声明式布局(FXML)+ CSS 样式assets/UI/ 下 7 个 .fxml 文件与 Main.css

设计意图上,项目将渲染核心(core)与桌面启动器(desktop)分离:desktop 模块持有入口类 cn.harryh.arkpets.DesktopLauncher,并以 ../assets 作为资源目录与运行工作目录;这种拆分让核心逻辑与平台相关的启动/分发逻辑解耦。

架构总览

Loading diagram...

图解要点:

  • core → desktop 依赖方向:desktop 启动器依赖 core 核心模块,两者的构建脚本分别为 core/build.gradle 与 desktop/build.gradle。
  • 资源不进入源码目录:desktop 模块通过 sourceSets.main.resources.srcDirs = ["../assets"] 把仓库级 assets 目录声明为资源根,使着色器、UI 布局、字体等资源与 Java 源码解耦。
  • 分发任务链:distJar(打 Fat JAR)→ jlink(定制 JRE)→ jpackage 安装包目录,Windows 侧另有 Inno Setup 脚本 docs/scripts/ExePacking.iss 参与最终安装包制作。

构建体系与模块划分

编码与源码布局

desktop 模块脚本的第一行就固定了全仓库的字符编码,这是中文桌面应用最容易踩的坑之一:

groovy
[compileJava, compileTestJava]*.options*.encoding = "UTF-8" sourceSets.main.java.srcDirs = ["src/"] sourceSets.main.resources.srcDirs = ["../assets"]

Source: desktop/build.gradle

三个声明的意图:

  1. UTF-8 强制编码:编译期显式指定编码,保证含中文的资源与源码在任何开发机Locale下字节一致,避免 Windows 默认 GBK 导致的乱码。
  2. 源码目录扁平化:src/ 而非 Gradle 约定的 src/main/java,属于沿用 libGDX 风格工程布局的做法。
  3. 资源指向仓库根的 ../assets:着色器、FXML、字体等以共享资源目录形式存在于模块之外,core 与 desktop 均可引用。

脚本随后定义了模块级扩展属性与 Eclipse 工程命名:

groovy
project.ext.mainClassName = "cn.harryh.arkpets.DesktopLauncher" project.ext.assetsDir = new File("../assets") eclipse.project.name = appName + "-desktop"

Source: desktop/build.gradle

appName 与 project.version 未在本文件定义,说明它们来自 Gradle 根工程配置——这是判定本项目为多模块工程的直接证据。mainClassName 指向 cn.harryh.arkpets.DesktopLauncher,即全应用唯一入口类。

资源打包策略

发布产物并不打包全部资源,processResources 显式排除模型与日志:

groovy
1processResources { 2 includeEmptyDirs = false 3 excludes = [ 4 "**/models_enemies/**", 5 "**/models/**", 6 "**/logs/**", 7 "models_data.json" 8 ] 9}

Source: desktop/build.gradle

设计意图:模型资产(models/、models_enemies/、models_data.json)体积大且可由用户后续下载/管理,日志目录(logs/)属于运行时产物,都不应进入发行 JAR——这直接控制了分发包体积。includeEmptyDirs = false 进一步避免空目录膨胀归档条目。模型下载相关的实现细节属于兄弟页面范围。

运行时技术栈

开发期运行任务与平台差异

run 与 debug 两个任务是开发调试的主入口,它们的 JVM 参数组装逻辑揭示了三项关键技术约束:

groovy
1// Runs the app without debug. 2tasks.register("run", JavaExec) { 3 dependsOn = ["classes"] 4 group = "execute" 5 6 mainClass = mainClassName 7 classpath = sourceSets.main.runtimeClasspath 8 standardInput = System.in 9 workingDir = assetsDir 10 11 setIgnoreExitValue(true) 12 13 jvmArgs += "-Dfile.encoding=UTF-8" 14 if (sentryDsn) 15 jvmArgs += "-Dsentry.dsn=${sentryDsn}" 16 17 if (OperatingSystem.current() == OperatingSystem.MAC_OS) { 18 jvmArgs += "-XstartOnFirstThread" // Required to run on macOS 19 } 20}

Source: desktop/build.gradle

逐行解读:

  • workingDir = assetsDir:应用以 assets/ 为工作目录启动,因此代码中的相对路径资源引用(着色器、字体、配置)均相对该目录解析。
  • -Dfile.encoding=UTF-8:与编译期 UTF-8 声明呼应,保证运行时字符串处理一致。
  • -Dsentry.dsn=...:条件注入 Sentry 数据源名(见下文"可观测性"小节)。
  • -XstartOnFirstThread(仅 macOS):macOS 平台强制在主线程启动 JVM。这是 GLFW/LWJGL 系桌面渲染后端在 macOS 上的硬性要求(macOS 的 Cocoa 事件循环必须在主线程),注释 // Required to run on macOS 明确标注了这一约束。
  • setIgnoreExitValue(true):进程退出码不使 Gradle 构建失败——桌面应用由用户手动关闭,非零退出是常态。

平台分支使用了 org.gradle.internal.os.OperatingSystem 做运行时探测,判断逻辑可图示为:

Loading diagram...

debug 任务(第 44–60 行)与 run 完全同构,仅多出 setDebug(true) 以挂起等待调试器连接,此处不再重复摘录。

分发产物使用 jlink 生成裁剪版 JRE,模块清单集中定义在 ext 块:

groovy
1ext { 2 // Environment vars 3 rootDir = project.rootDir 4 javaHome = System.getProperty('java.home') 5 osName = System.getProperty('os.name').toLowerCase(Locale.ROOT).split(' ')[0] 6 osPathSep = File.pathSeparatorChar 7 // Distribution related vars 8 jarLibDir = layout.buildDirectory.dir("libs").get().toString() 9 jarLibName = project.name + "-" + project.version 10 jlinkDir = layout.buildDirectory.dir("jlink").get().toString() 11 jlinkModuleList = "java.base,java.desktop,java.logging,java.management,java.scripting,jdk.crypto.ec,jdk.localedata,jdk.management,jdk.unsupported" 12 jlinkLocalesList = "en-US,zh-CN" 13 jpackageDir = layout.buildDirectory.dir("jpackage").get().toString() 14 issFileRel = "docs/scripts/ExePacking.iss" 15 distDir = layout.buildDirectory.dir("dist").get().toString() 16 distName = project.appName + "-v" + project.version 17}

Source: desktop/build.gradle

jlinkModuleList 是理解运行时依赖面的关键,各模块职责如下:

JDK 模块在本项目中承担的角色
java.baseJVM 基础模块,任何运行时必需
java.desktop桌面 UI 相关基础类(AWT/Swing 及桌面集成能力)
java.loggingJUL 日志桥接,供依赖库输出日志
java.managementJMX 管理接口(Sentry 等诊断类库常用)
java.scripting脚本引擎 API
jdk.crypto.ec椭圆曲线加密,HTTPS 通信(如 Sentry 上报、资源下载)所需
jdk.localedata本地化数据(配合 jlinkLocalesList = "en-US,zh-CN" 仅保留中英文)
jdk.managementJVM 运行时管理扩展
jdk.unsupportedsun.misc.* 等内部 API,本地渲染后端(LWJGL 系)常用

设计意图:jdk.crypto.ec + jdk.localedata(en-US,zh-CN) 的组合表明发行包需要 HTTPS 网络能力且只面向中英文用户;jdk.unsupported 的保留则说明存在依赖内部 API 的本地库。模块白名单 + 语言白名单双重裁剪,把定制 JRE 压到最小可用集合。

jlink 任务的调用方式:

groovy
1// Creates a customized Java Runtime Environment for the app. 2tasks.register("jlink", Exec) { 3 dependsOn = ["distJar"] 4 group = "dist" 5 6 doFirst() { delete jlinkDir } 7 8 workingDir project.projectDir 9 inputs.property("runtime", "${jlinkDir}/runtime") 10 commandLine = [ 11 "${javaHome}/bin/jlink", 12 '--module-path', "${javaHome}/jmods", 13 '--add-modules', jlinkModuleList, 14 '--output', "${jlinkDir}/runtime",

Source: desktop/build.gradle

(该任务的 commandLine 参数列表在源文件中延续至第 120 行之后,此处摘录到已验证部分。)注意 doFirst() { delete jlinkDir }——每次构建先清空输出目录,避免增量残留导致 jlink 报"目录已存在"错误。

渲染层:GLSL 着色器资源

assets/shaders/ 目录包含 5 个着色器源文件,构成两套可切换的渲染管线:

文件角色
PlainVertex.glsl / PlainFragment.glslPlain(基础)管线的顶点/片段着色器
ComplexVertex.glsl / ComplexFragment.glslComplex(增强)管线的顶点/片段着色器
ComplexFragmentLow.glslComplex 管线的低配片段着色器(性能降级替代)

设计意图:双管线 + 低配备选的命名结构,表明渲染层针对不同机器性能提供了"基础/增强/增强低配"三档策略——Complex* 管线的片段着色器单独存在低配版本,而顶点着色器共用一个 ComplexVertex.glsl,说明降级只发生在光栅化阶段而非几何阶段。这与 Spine 系骨骼动画渲染常见的"普通混合 / 复合混合(预乘 alpha、色调叠加)"分层一致。

桌面 UI 层:JavaFX FXML 与 CSS

assets/UI/ 目录包含 8 个资源文件,全部属于 JavaFX 声明式 UI 技术栈:

文件推断的 UI 职责
RootModule.fxml主窗口根布局容器
ModelsModule.fxml模型管理页(选择/导入桌宠模型)
BehaviorModule.fxml行为配置页(动画/交互行为开关)
SettingsModule.fxml设置页
DownloadDialog.fxml模型下载对话框
LogDialog.fxml日志查看对话框
AnnounceDialog.fxml公告对话框
Main.css全局样式表(JavaFX CSS 方言)

技术选型意图:与渲染层使用 GLSL(OpenGL 系、代码式 UI 无关)相对,管理后台 UI 采用 JavaFX FXML 声明式布局——这与 JDK 模块清单中的 java.desktop 相互印证。多个 *Dialog.fxml 对话框的存在说明应用将"下载、日志、公告"等长耗时/低频操作统一收纳为模态窗口,而非嵌入主界面。

静态资源与配置

  • 字体:assets/fonts/SourceHanSansCN-Bold.otf 与 SourceHanSansCN-Regular.otf(思源黑体中文 Bold/Regular 两字重),随包内置保证无字体环境下中文渲染一致。
  • 图标:assets/icons/ 同时提供 icon.ico(Windows)、icon.icns(macOS)、icon.png(Linux/通用),与三平台 jpackage 分发目标一一对应。
  • 默认配置:assets/ArkPetsConfigDefault.json 为默认配置模板,应用启动时加载;各配置项业务语义由配置相关兄弟页面说明。

可观测性:Sentry 集成

构建脚本通过 Gradle 属性注入 Sentry DSN,实现"构建时配置、运行时上报":

groovy
def sentryDsn = (project.findProperty("SENTRY_DSN") ?: "").trim()

Source: desktop/build.gradle

随后在 run/debug 任务的 JVM 参数中条件追加:

groovy
jvmArgs += "-Dfile.encoding=UTF-8" if (sentryDsn) jvmArgs += "-Dsentry.dsn=${sentryDsn}"

Source: desktop/build.gradle

设计意图:

  1. DSN 不入库:findProperty("SENTRY_DSN") 从命令行(-PSENTRY_DSN=...)或本地 gradle.properties 读取,避免将上报端点硬编码进开源仓库,防止上游仓库被无关错误数据淹没。
  2. 未配置则零开销:DSN 为空字符串时不追加 JVM 参数,Sentry 处于未初始化/禁用态——普通开发者本地构建完全不受影响。
  3. java.management 模块入列:与 Sentry 采集 JVM/运行环境指标的常规需求相符(见前文 JDK 模块表)。

分发管线

distJar 任务生成自包含的 Fat JAR,将运行时依赖整体展开打进单一归档:

groovy
1tasks.register("distJar", Jar) { 2 dependsOn = ["classes"] 3 group = "dist" 4 5 doLast() { 6 copy { 7 from "${jarLibDir}/${jarLibName}.jar" 8 into distDir 9 rename "${jarLibName}.jar", "${distName}.jar" 10 } 11 } 12 13 duplicatesStrategy = DuplicatesStrategy.EXCLUDE 14 manifest { 15 attributes 'Main-Class': mainClassName 16 } 17 dependsOn configurations.runtimeClasspath 18 from { 19 configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 20 } 21 with jar 22}

Source: desktop/build.gradle

关键点:

  • Main-Class 清单属性指向 DesktopLauncher,使产物 java -jar 可直接启动。
  • zipTree 展开依赖:所有 runtimeClasspath 依赖被解压合并进 JAR——这是典型的 Uber-JAR 策略,省去安装包内维护依赖目录的复杂度。
  • DuplicatesStrategy.EXCLUDE:多个依赖 JAR 含同名资源文件(如 META-INF/services/*、module-info.class)时静默保留首个,防止打包因重复条目失败。注意该策略可能丢失 SPI 服务声明,这是 Uber-JAR 的已知代价。
  • doLast 重命名拷贝:把 desktop-{version}.jar 复制为 dist/{AppName}-v{version}.jar,统一发行命名。

完整分发流水线

Loading diagram...

流水线各阶段的依赖链由 dependsOn 显式声明:jlink 任务 dependsOn = ["distJar"],即裁剪 JRE 之前必须先产出可执行 JAR;jpackageDir 与 issFileRel = "docs/scripts/ExePacking.iss" 表明 Windows 安装包由 Inno Setup 脚本做最终封装(该脚本位于 docs/scripts/ 下,不在本次已读文件范围内,安装器配置细节属分发/打包相关页面)。

失败模式与边界情况

基于已验证的构建脚本,本技术栈存在以下值得注意的边界:

  1. 编码不一致风险:编译期与运行期均强制 UTF-8(#L1、#L35),但若第三方工具链(如 IDE 或外部打包脚本)绕过 Gradle,中文资源可能乱码——这是该双保险存在的原因。
  2. jlink 增量残留:jlink 任务通过 doFirst() { delete jlinkDir } 规避输出目录已存在导致的失败;手工遗留的 build/jlink 目录不会阻塞构建。
  3. Uber-JAR 的 SPI/模块元数据丢失:duplicatesStrategy = DuplicatesStrategy.EXCLUDE 遇到多个依赖携带同名 META-INF 文件时只保留首个,理论上可能破坏 ServiceLoader 发现;同时自动模块名(Automatic-Module-Module)冲突也在被排除之列。
  4. DSN 未配置时无错误上报:本地默认构建不传 -PSENTRY_DSN,Sentry 不生效——排查用户侧问题需确认发布构建是否注入了 DSN。
  5. macOS 线程约束:忘加 -XstartOnFirstThread(例如用户绕过 gradle run 直接 java -jar)会导致 macOS 上渲染窗口无法创建/崩溃,属平台硬性约束。
  6. 资源排除的副作用:processResources 排除了 models/ 等目录,意味着发行包内不存在模型数据,首次使用依赖下载流程(见模型下载相关页面)。

扩展点

  • 新增 JDK 依赖模块:修改 jlinkModuleList(desktop/build.gradle #L75)即可扩展运行时能力,例如需要 Nashorn 之外的脚本引擎或 jdk.zipfs 时追加模块名。
  • 新增分发语言:jlinkLocalesList = "en-US,zh-CN"(#L76)控制裁剪后的 locale 集合,新增界面语言需同步追加。
  • 切换/新增构建渠道密钥:遵循 sentryDsn 的 findProperty 模式,用 Gradle 属性注入而非硬编码。
  • 调整资源打包范围:processResources.excludes(#L13-L18)是控制发行体积的唯一开关,新增随包资源目录时需在此登记。

相关链接

说明:core/build.gradle 的依赖声明部分在本次源码工具预算(6/6)耗尽前未能读取,第三方库的具体坐标与版本清单暂缺,待后续在对应核心模块页面补充;本文中关于 core 模块的所有论断均仅基于其在仓库中的存在与 desktop 模块对根工程属性(appName、version)的共享使用。

Sources

(1 files)