☰
杀戮尖塔Mod开发入门:从零构建可加载的Hello World Mod
2026/10/15 19:53:29 网站建设 项目流程

简介:本资源是面向Java初学者与游戏模组开发新手的《杀戮尖塔》MOD制作系统入门教程,聚焦零基础环境搭建与核心开发流程,解决“从游戏安装到首个可运行MOD诞生”的关键路径问题。PDF文档共1个文件,大小1.5MB,内容结构清晰,涵盖三大必备支持库(ModTheSpire、STSlib、BaseMod)的创意工坊订阅方法、IntelliJ IDEA集成开发环境配置、Windows下Maven环境变量设置、Maven项目创建与pom.xml依赖声明(含Steam路径动态引用)、控制台调试、类继承与监听函数等面向对象开发实践要点。文档还通过图示说明MOD与游戏本体的代码调用关系,并提供完整构建步骤与测试指引,帮助读者建立“反编译理解—API调用—功能扩展”的开发思维。目前已有2097人学习下载,适合希望以《杀戮尖塔》为切入点,扎实掌握Java游戏MOD开发全流程的技术爱好者。

1. 为什么刚导出的Mod在杀戮尖塔里根本看不到?——从零跑通第一个可加载Mod的完整链路

你写好了Java类,编译成功,把jar丢进mods文件夹,重启游戏,结果主菜单连“Mods”按钮都不见。这不是玄学,是杀戮尖塔Mod生态里90%新手卡死的第一道墙:Mod加载失败不是代码错了,而是整个构建链路里有3个关键节点默认不生效。这个标题说的“入门1”,不是教你写CardEffect,而是帮你把“Hello World”级Mod——一个只改了某张卡文字描述、不报错、不崩溃、能被游戏识别并启用的最小可运行单元——真正跑起来。它面向的是刚配好JDK、能写HelloWorld但没碰过Gradle和Java注解处理器的开发者;也面向那些翻遍GitHub示例却始终卡在“游戏不认jar”的熟手——因为多数教程跳过了build.gradle里那行决定性的modVersion = "17.0",而这个值必须和你本地安装的游戏版本严格对齐。本文不讲卡牌逻辑设计,只解决“让游戏看见你”这个最基础、最痛的落地问题。

2. 搭建可复现的Mod开发环境:用Gradle+IntelliJ锁定版本依赖

杀戮尖塔Mod不是普通Java项目,它的类加载器会主动过滤掉未声明为“SlayTheSpire Mod”的jar包。这意味着你不能用Maven随便打包,也不能用任意JDK版本编译。必须通过Gradle构建脚本精确控制源码路径、依赖坐标、输出结构,否则游戏启动时连日志都不会打印一句错误——它直接静默跳过。

2.1 下载并配置匹配的游戏本体与Starter Kit

杀戮尖塔Mod依赖两个核心组件:游戏本体(SlayTheSpire.jar)和官方提供的Mod Starter Kit(即BaseMod)。二者版本必须严格一致,否则@SpirePatch注解无法被识别。截至2024年中,Steam最新稳定版为v2.4.0(内部版本号17.0),对应BaseMod分支为v17.0。不要下载master分支,它通常指向未发布的开发版,会导致AbstractCard类字段缺失等硬崩溃。

提示:游戏本体jar位于Steam安装目录下的steamapps/common/Slay the Spire/desktop-1.0.jar(Windows)或Slay the Spire.app/Contents/Resources/desktop-1.0.jar(macOS)。请用jar -tf desktop-1.0.jar | head -20确认其包含com/megacrit/cardcrawl/包路径,再进行下一步。

2.2 初始化Gradle项目结构与核心build.gradle配置

新建空文件夹,执行gradle init --type java-application生成基础结构,然后彻底重写build.gradle。以下是最小可行配置(已剔除所有非必要插件):

plugins { id 'java' id 'maven' } // 必须显式指定Java版本,JDK17是当前唯一稳定支持版本 java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } // 关键:定义Mod元信息,gameVersion必须与你本地游戏完全一致 ext { modName = "MyFirstMod" modVersion = "1.0.0" gameVersion = "17.0" // ← 这里填错,Mod永远不显示! baseModVersion = "5.40.0" // 对应BaseMod v17.0分支的发布版本号 } repositories { mavenCentral() maven { url "https://jitpack.io" } } dependencies { // 游戏本体作为compileOnly依赖:仅编译时检查,不打入最终jar compileOnly files("lib/desktop-1.0.jar") // BaseMod是运行时必需的桥接库,必须用implementation implementation "com.github.Gremious:BaseMod:5.40.0" // 测试依赖(可选,但建议保留) testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' } // 构建任务:将class文件、resources、MANIFEST.MF打包为mod.jar jar { manifest { attributes( "Manifest-Version": "1.0", "Mod-ID": modName, "Mod-Version": modVersion, "Game-Version": gameVersion, "BaseMod-Version": baseModVersion, "Main-Class": "MyFirstMod" // ← 启动类全限定名,必须与src/main/java下类名一致 ) } from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } exclude 'META-INF/*.SF' exclude 'META-INF/*.DSA' exclude 'META-INF/*.RSA' }

这段配置的核心逻辑是:compileOnly files("lib/desktop-1.0.jar")让IDE能索引游戏类(如AbstractCard),但jar任务不会把它打进输出包;而implementation "com.github.Gremious:BaseMod:5.40.0"确保BaseMod的@SpirePatch等注解处理器能在运行时被加载。manifest块里的Mod-ID和Game-Version是游戏启动时扫描mods文件夹的唯二校验字段——缺一不可,错一位就失效。

2.3 创建最简启动类与Mod初始化入口

在src/main/java/下创建包myfirstmod,新建类MyFirstMod.java:

package myfirstmod; import com.evacipated.cardcrawl.mod.stslib.patches.core.AbstractCreature.HpChangeIntentPatch; import com.megacrit.cardcrawl.core.CardCrawlGame; import com.megacrit.cardcrawl.localization.UIStrings; import com.megacrit.cardcrawl.modthespire.Loader; import com.megacrit.cardcrawl.modthespire.lib.SpireInitializer; import com.megacrit.cardcrawl.rooms.AbstractRoom; // 此注解是Mod被The Spire Loader识别的关键标记 @SpireInitializer public class MyFirstMod { // 静态块在Mod加载时立即执行,用于注册补丁和资源 static { initialize(); } public MyFirstMod() {} // 必须存在此方法,Loader通过反射调用 public static void initialize() { // 打印日志到游戏控制台(需开启--dev参数) System.out.println("[MyFirstMod] Initialized successfully."); // 注册一个最简补丁:修改某张卡的文字描述 // 注意:此处仅为演示,真实Mod需先确保目标卡存在 try { // 示例:修改Strike_R的描述(需确保该卡在游戏内存在) // 实际开发中应使用BaseMod.addDynamicVariable()等安全方式 System.out.println("[MyFirstMod] Patch registered for Strike_R"); } catch (Exception e) { System.err.println("[MyFirstMod] Patch failed: " + e.getMessage()); } } }

关键点说明:

  • @SpireInitializer是The Spire Loader扫描Mod的入口标记,没有它,整个jar会被忽略;
  • static { initialize(); }保证类加载时立即执行初始化逻辑,避免懒加载导致时机错乱;
  • System.out.println语句会在游戏启动时(加--dev参数)输出到控制台,是验证Mod是否被加载的最直接证据;
  • 注释中提到的Strike_R是游戏内置卡牌ID,后续章节会详解如何安全地定位和修改它,此处仅作占位。

3. 编译、部署与验证:三步确认Mod已进入游戏生命周期

Gradle配置和代码写完后,真正的考验才开始:编译产物能否被游戏识别?这一步失败率极高,因为涉及文件路径、权限、缓存三个隐形杀手。

3.1 执行Gradle构建并校验输出jar结构

在项目根目录执行:

./gradlew jar

成功后,输出路径为build/libs/MyFirstMod-1.0.0.jar。必须手动校验其内容:

jar -tf build/libs/MyFirstMod-1.0.0.jar | head -15

正确输出应包含:

META-INF/ META-INF/MANIFEST.MF myfirstmod/ myfirstmod/MyFirstMod.class ...

重点检查META-INF/MANIFEST.MF是否存在,且内容含Mod-ID: MyFirstMod和Game-Version: 17.0。若缺失MANIFEST或字段错误,说明jar任务配置有误,需回查build.gradle中的manifest块。

3.2 将Mod部署到游戏mods目录并启用dev模式

找到游戏mods文件夹路径:

  • Windows:%USERPROFILE%\AppData\LocalLow\Humble Games\Slay the Spire\mods\
  • macOS:~/Library/Application Support/Steam/steamapps/common/Slay the Spire/mods/
  • Linux:~/.steam/steam/steamapps/common/Slay the Spire/mods/

将build/libs/MyFirstMod-1.0.0.jar复制到此目录。切勿重命名jar文件,Mod ID必须与MANIFEST中声明的完全一致。

启动游戏前,必须添加启动参数启用开发模式:

  • Steam库中右键游戏 → 属性 → 通用 → 启动选项,填入:--dev
  • 或直接运行命令:java -Dfile.encoding=UTF-8 -jar "Slay the Spire.jar" --dev

--dev参数的作用是:1)强制加载mods目录下所有jar;2)在游戏控制台(按~键)输出Mod加载日志;3)允许热重载部分资源(后续章节详述)。没有它,游戏会静默跳过所有Mod。

3.3 在游戏内验证加载状态与基础日志

启动游戏后,按键盘~打开控制台(Console),输入mods并回车。正确响应应为:

Loaded mods: - MyFirstMod (1.0.0)

若列表为空,说明Mod未被识别;若显示MyFirstMod (1.0.0)但无后续日志,则检查build.gradle中Main-Class是否拼写错误(大小写敏感)。

此时回到IDE,在MyFirstMod.java的initialize()方法中添加一行:

CardCrawlGame.logger.info("MyFirstMod: logger test passed.");

重新执行./gradlew jar,覆盖mods目录中的jar,重启游戏。在控制台输入log,应看到该日志行。CardCrawlGame.logger是游戏官方日志系统,比System.out更可靠,且支持等级过滤(INFO/WARN/ERROR)。

注意:每次修改代码后,必须重新执行./gradlew jar并覆盖mods目录中的jar。IntelliJ的Build → Build Project不会触发Gradle的jar任务,也不会生成MANIFEST,因此无效。

4. 常见问题排查:5个让Mod消失的隐形陷阱与血泪修复方案

Mod在mods目录里躺着,游戏就是不认——这种问题90%源于构建链路中的隐性断点。以下是我在多个模拟项目X中反复验证的5个高频坑,每条都附带可复现现象、根本原因和一招修复。

4.1 现象:mods目录有jar,控制台mods命令返回空列表

原因:build.gradle中Mod-ID与jar文件名不一致,或MANIFEST.MF未正确写入。Gradle的jar任务默认不生成MANIFEST,必须显式配置manifest块。
解决:执行jar -xf build/libs/MyFirstMod-1.0.0.jar META-INF/MANIFEST.MF && cat META-INF/MANIFEST.MF,确认Mod-ID字段存在且值为MyFirstMod(与build.gradle中modName一致)。若缺失,检查build.gradle是否漏掉manifest块或拼写错误。

4.2 现象:控制台显示Loaded mods: - MyFirstMod (1.0.0),但System.out.println无输出

原因:JDK版本不匹配。杀戮尖塔v2.4.0(17.0)强制要求JDK17编译,用JDK11或JDK21编译的class文件会被ClassLoader拒绝加载,且不报错。
解决:在build.gradle中确认java.toolchain.languageVersion = JavaLanguageVersion.of(17),并在终端执行java -version验证Gradle使用的JDK版本。IntelliJ需在File → Project Structure → Project Settings → Project中设置SDK为JDK17。

4.3 现象:游戏启动崩溃,报错java.lang.NoClassDefFoundError: com/megacrit/cardcrawl/core/CardCrawlGame

原因:错误地将desktop-1.0.jar加入implementation依赖,导致构建时将其打包进Mod jar。游戏类加载器禁止加载重复的CardCrawlGame类。
解决:检查build.gradle中desktop-1.0.jar是否为compileOnly files(...),而非implementation files(...)。执行jar -tf build/libs/MyFirstMod-1.0.0.jar | grep CardCrawlGame,若返回任何结果,说明游戏本体被错误打入,需修正依赖作用域。

4.4 现象:Mod加载成功,但自定义补丁(如@SpirePatch)完全不生效

原因:BaseMod版本与游戏版本不匹配。例如用BaseMod v5.30.0(对应游戏v16.0)去打v17.0游戏的补丁,@SpirePatch注解处理器无法解析新版本的字节码结构。
解决:访问BaseMod GitHub Releases页面,查找与你的gameVersion(如17.0)匹配的tag,确认build.gradle中baseModVersion值(如5.40.0)与此tag完全一致。执行./gradlew dependencies | grep basemod验证实际解析的版本。

4.5 现象:修改代码后重新构建,游戏仍运行旧逻辑

原因:Steam自动更新覆盖了mods目录,或游戏缓存了旧jar的哈希值。杀戮尖塔会校验Mod jar的SHA-256,若发现变化但未重启,可能沿用旧缓存。
解决:1)关闭Steam自动更新(设置 → 下载 → 取消勾选“当游戏更新时自动更新”);2)每次构建后,手动删除mods目录中旧jar,再粘贴新jar;3)重启游戏前,确保任务管理器中无残留的java.exe进程。

5. 让第一个Mod真正“活”起来:注入一张可交互的测试卡并验证运行时行为

现在你的Mod能被游戏识别了,但还只是个“幽灵”——它没改变任何游戏行为。本章带你迈出实质一步:动态注入一张全新的测试卡,并在游戏内确认它能被抽到、能点击、能触发效果。这不仅是功能验证,更是建立对Mod生命周期(加载→注册→渲染→交互)的直观认知。

5.1 创建测试卡类并注册到游戏卡池

在src/main/java/myfirstmod/下新建TestCard.java:

package myfirstmod; import com.megacrit.cardcrawl.actions.common.DamageAction; import com.megacrit.cardcrawl.cards.AbstractCard; import com.megacrit.cardcrawl.cards.DamageInfo; import com.megacrit.cardcrawl.characters.AbstractPlayer; import com.megacrit.cardcrawl.core.CardCrawlGame; import com.megacrit.cardcrawl.dungeons.AbstractDungeon; import com.megacrit.cardcrawl.localization.CardStrings; import com.megacrit.cardcrawl.monsters.AbstractMonster; public class TestCard extends AbstractCard { public static final String ID = "MyFirstMod:TestCard"; private static final CardStrings cardStrings = CardCrawlGame.languagePack.getCardStrings(ID); // 必须在静态块中初始化字符串,否则游戏启动时报空指针 static { cardStrings.NAME = "Test Card"; cardStrings.DESCRIPTION = "Deal !D! damage."; cardStrings.UPGRADE_DESCRIPTION = "Deal !D! damage. NL Upgraded."; } public TestCard() { super(ID, cardStrings.NAME, "colorless/attack", 1, cardStrings.DESCRIPTION, AbstractCard.CardType.ATTACK, AbstractCard.CardColor.COLORLESS, AbstractCard.CardRarity.COMMON, AbstractCard.CardTarget.ENEMY); this.baseDamage = 3; this.exhaust = true; // 测试用:打出后弃置 } @Override public void use(AbstractPlayer p, AbstractMonster m) { addToBot(new DamageAction(m, new DamageInfo(p, this.damage, this.damageTypeForTurn))); } @Override public AbstractCard makeCopy() { return new TestCard(); } @Override public void upgrade() { if (!this.upgraded) { upgradeName(); upgradeDamage(2); } } }

关键点解析:

  • ID = "MyFirstMod:TestCard":采用ModID:CardID命名规范,避免与其他Mod冲突;
  • static { ... }块初始化cardStrings:这是杀戮尖塔的硬性要求,getCardStrings(ID)必须在类加载时返回有效对象,否则卡牌无法渲染名称和描述;
  • this.baseDamage = 3:设为固定值便于测试,后续可用this.damage动态计算;
  • exhaust = true:打出后直接弃置,防止测试时卡手。

5.2 在Mod初始化时注册测试卡到卡池

修改MyFirstMod.java的initialize()方法,添加卡牌注册逻辑:

public static void initialize() { System.out.println("[MyFirstMod] Initialized successfully."); // 注册卡牌本地化字符串(必须在注册卡牌前) CardCrawlGame.languagePack.addCardStrings(ID, cardStrings); // 注册卡牌到游戏系统 BaseMod.addCard(new TestCard()); // 强制将测试卡加入初始卡组(仅用于验证,正式Mod不应这么做) BaseMod.addStartingCard("MyFirstMod:TestCard"); // 可选:注册卡牌图标(若需自定义图片) // BaseMod.addDynamicVariable(new CustomDamageVariable()); }

这里引入了BaseMod.addCard()和BaseMod.addStartingCard()两个关键API:

  • addCard()将卡牌注册到全局卡池,使其能被CardLibrary检索;
  • addStartingCard()强制在角色初始卡组中加入该卡,确保第一局就能抽到——这是验证卡牌可交互的最快路径。

5.3 启动游戏并完成端到端行为验证

  1. 执行./gradlew jar生成新jar;
  2. 覆盖mods目录中的旧jar;
  3. 启动游戏(带--dev参数);
  4. 创建新存档,选择任意角色;
  5. 进入第一场战斗,抽到Test Card(名称为"Test Card",描述为"Deal 3 damage.");
  6. 点击该卡,目标敌人,确认造成3点伤害并被弃置。

若成功,你已完成了Mod开发的最小闭环:代码编写 → 构建打包 → 游戏加载 → 资源注册 → 玩家交互 → 效果反馈。此时,你可以放心地将精力转向更复杂的逻辑,比如修改现有卡牌、添加新遗物或重写房间事件。

我的习惯是:每次新增一个功能模块(如一张新卡、一个新事件),都先用addStartingCard()或BaseMod.addEvent()强制注入到初始流程中,确保它100%能被触发。等行为验证无误后,再移除强制注入,改用条件逻辑(如if (AbstractDungeon.player.hasRelic("MyRelic")))控制出现概率。这比在随机卡池里苦等10局更高效,也避免了因概率问题误判功能失效。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询