简介面向零基础玩家的《杀戮尖塔》Mod开发入门教程适合想为游戏添加自定义卡牌、角色或新机制的玩家与Java初学者。教程以ModTheSpire、BaseMod、StSLib三大支持库为主线覆盖从Steam创意工坊订阅依赖、安装游戏本体到IntelliJ IDEA开发环境搭建、Maven环境变量配置、创建Maven项目及编辑pom.xml依赖的完整流程帮助读者跨过首个可运行Mod之前的全部门槛。资源打包为单个PDF文档压缩包约1.5MB内容按简介、准备工作、构建项目、测试构建效果、作业练习与Mod开发语法基础等章节组织适合边读边练也方便作为开发手册随时查阅。目前已有2121人学习下载常被用作社区入门参考。除项目构建外教程还侧重讲解控制台输出调试、基础变量与函数、类作用域、监听函数与继承等面向对象知识在动手过程中建立对Mod代码结构的理解为后续自制卡牌、遗物或完整角色打下扎实基础。1. 杀戮尖塔mod制作入门从创意工坊的旁观者到能改规则的开发者你还记得第一次在创意工坊刷到一张只改了数值的卡牌mod吗我当时的反应是这也能算mod我自己上是不是也行杀戮尖塔mod制作入门没有想象中那么高它本质上是一场Java工程实战游戏本体由Java编写mod的开发链路也围绕Java展开。我们要做的只有一件事——用modthespire、basemod、stslib这三个支持库提供的API把自己的代码接进游戏原本的逻辑里去加卡牌、改规则、甚至重塑角色。这份笔记适合两类人没写过Java的新手能照着一路把第一个jar跑起来有编程基础的熟手能顺着环境链路的拆解快速定位到可以下手的位置。2. 环境搭建与依赖配置三大支持库和Maven工程是怎么组合的2.1 三大支持库的分工加载器、基础库、扩展库杀戮尖塔的mod并不是一个孤立的jar丢进游戏目录就能跑。真正启动时游戏会以带mod的方式拉起ModTheSpire再由它逐个扫描、加载mod目录下的jar文件。动手之前先把三个库的身份搞清楚否则后面看到代码里的import语句会一直疑惑某个包到底来自哪里。ModTheSpire是加载器本体。它负责读jar里的ModTheSpire.json、决定加载顺序、在游戏内弹出mod选择界面。没有它我们的jar根本不会进入游戏进程换种说法它才是真正调用我们initialize()方法的宿主。BaseMod是事件与注册中心。我们要在“玩家抽卡”“战斗结束”这类事件上挂钩子不是去改游戏源码而是向BaseMod注册一个订阅者——你的类实现某个Subscriber接口BaseMod在对应时机回调你。它也统一管理卡牌、遗物、药水的注册API。没有它mod和游戏之间的交互就没有落点。stslib是BaseMod上的扩展库社区常称STS Lib。它提供预置的好用组件比如常见卡牌效果、字符串工具、UI组件。只做单张卡牌mod时stslib不一定直接用上但做角色mod时它能把大量通用效果变成现成调用省下不少重复代码。一句话记住分工ModTheSpire管加载BaseMod管事件与注册stslib管常用组件。后面写代码时import的包名基本就在这三者之间转。2.2 IDE与Maven环境准备我推荐IntelliJ IDEA社区版加Maven的组合。IDEA社区版免费对Java工程的支持足够完整Eclipse也完全可以只是下文示例的菜单路径以IDEA为例。Maven负责把依赖声明、编译、打包串成一条流水线这正是mod工程需要的用pom.xml声明依赖用package命令输出jar。建议先把Maven装好再打开IDE。Windows下的步骤不算复杂但有两处容易踩坑下载Maven并解压到本地目录比如E:\Maven\apache-maven-3.3.9。路径尽量不要有中文和空格后续命令行解析会少很多麻烦。进入“计算机 - 属性 - 高级系统设置”打开环境变量窗口。新建系统变量MAVEN_HOME变量值填Maven所在目录。再在系统变量Path里追加一行%MAVEN_HOME%\bin。注意一定是“系统变量”不是用户变量因为后续IDEA和命令行都要读取这个值。改完环境变量后旧的命令行窗口不会自动刷新必须新开一个窗口输入mvn -version验证。输出里能看到Maven版本和Java版本说明环境已生效。如果执行mvn提示“不是内部或外部命令”多半是Path没配好或追加时路径前后多了空格。处理完再新开窗口不要在旧窗口里反复试那是很多人最容易忽略的一步。2.3 依赖获取与配置思路三个支持库的获取方式比较直接在创意工坊里搜索 stslib、basemod、modthespire 并订阅。订阅成功后它们会出现在创意工坊的内容目录下形如workshop/content/646570/xxx的文件夹中。订阅完成后先别急着开发把游戏本体完整启动一次确认能以mod模式进入游戏、并在启动器界面看到BaseMod和ModTheSpire。这一步的意义是验证游戏和库的版本搭配没问题。如果连游戏本身都进不去后面所有开发动作都没有载体。这里有一个常见的失败顺序只订阅了basemod忘掉ModTheSpire或者下载了加载器但游戏没识别出来。我的习惯是先把三件套完整订阅、把游戏跑通一次再回来搭建开发环境。顺序反了配置了半天IDE最后被一个缺失依赖卡住排查起来很影响心情。到pom.xml里三个库jar会以system依赖的方式被引用所以现在就把workshop目录下的jar路径抄下来。抄写要完整从workshop开始一直到jar文件名少一级目录都会编译失败。后续如果游戏更新这些jar的路径或文件名也可能变化需要重新确认。3. 工程骨架拆解pom.xml、ModTheSpire.json和打包链路3.1 新建Maven工程与目录结构在IDEA中新建工程选择Maven不勾选archetype模板随后填写GroupId和ArtifactId。为了让示例能直接对号入座ArtifactId可以填tutorial工程名称也取tutorial。生成后的目录结构大致是这样tutorial/ ├── pom.xml └── src/ ├── main/ │ ├── java/ │ └── resources/ └── test/.idea是IDE的辅助配置与mod本体无关src/main/java是存放代码的地方src/main/resources用来放图片、json等资源ModTheSpire.json就要放在这里src/test是Maven自动生成的测试目录构建时Maven会先生成测试产物再把测试目录生成的文件拷贝到目标文件夹。这里有个值得提前说明的点如果不希望测试代码被打进最终jar最好保持测试目录为空或者在打包前执行mvn clean package。很多新手第一次打出来的jar体积异常多半就是测试类被带了进去。jar里多出无用的class不影响加载但会干扰排查尤其是当你怀疑某个类是不是被旧版本覆盖时。3.2 pom.xml关键配置逐段解释pom.xml是整个工程的命脉。下面是去掉注释后的最小可用版本?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdorg.example/groupId artifactIdtutorial/artifactId version0.1/version packagingjar/packaging nametutorialMod/name description杀戮尖塔mod入门示例工程/description properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding steam.pathE:\SteamLibrary\steamapps/steam.path /properties dependencies dependency groupIdbin/groupId artifactIdBaseMod/artifactId scopesystem/scope version1.0/version systemPath${steam.path}/workshop/content/646570/1605833019/BaseMod.jar/systemPath /dependency dependency groupIdbin/groupId artifactIdSlayTheSpire/artifactId scopesystem/scope version1.0/version systemPath${steam.path}/common/SlayTheSpire/desktop-1.0.jar/systemPath /dependency dependency groupIdbin/groupId artifactIdModTheSpire/artifactId scopesystem/scope version1.0/version systemPath${steam.path}/workshop/content/646570/1605060445/ModTheSpire.jar/systemPath /dependency /dependencies build finalNametutorialMod/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.7.0/version configuration source1.8/source target1.8/target /configuration /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-antrun-plugin/artifactId version1.8/version executions execution phasepackage/phase configuration target copy filetarget/tutorialMod.jar tofile${steam.path}/common/SlayTheSpire/mods/tutorialMod.jar/ /target /configuration goals goalrun/goal /goals /execution /executions /plugin /plugins /build /projectproperties里的project.build.sourceEncoding设为UTF-8保证代码里能直接用中文写字符串打包后不乱码steam.path指向游戏平台的库目录如果你的游戏装在其他盘这个值必须跟着改。我的做法是先把游戏本体所在路径完整复制到记事本再粘贴进pom.xml避免手打路径漏掉一级目录。dependencies里三个依赖的scope都写成了system配合systemPath直接定位到jar文件。原因是ModTheSpire、BaseMod这些jar不是中央仓库里的公共制品没必要推到私有仓库直接引用本地文件最省事。desktop-1.0.jar是游戏主程序BaseMod.jar和ModTheSpire.jar是两个支持库。如果你之后订阅了stslib可以按同样格式再追加一条dependency。build部分做了两件事maven-compiler-plugin把源码编译级别固定在Java 1.8这和游戏本身的编译版本保持一致避免出现class版本过新导致加载失败maven-antrun-plugin在package阶段把生成的jar复制到游戏目录下的mods文件夹。这个mods文件夹是游戏识别mod的标准目录如果不存在需要先手动创建。3.3 ModTheSpire.json配置说明在src/main/resources下新建文件ModTheSpire.json内容如下{ modid: tutorial, name: tutorialMod, author_list: [A, B], description: 新手教程mod, version: 0.0.1, sts_version: 03-29-2018, mts_version: 2.6.0, dependencies: [basemod] }modid是整个mod在加载器中的唯一标识尽量简洁小写后续代码里可能通过它做资源路径引用name是界面上显示的名称author_list是数组可以填多个作者dependencies声明了本mod依赖basemod加载器会优先把BaseMod排在前面。json里最容易出问题的是大括号和方括号的配对。见过不止一次开头花括号写成方括号加载器直接忽略这个mod。文件不大建议用带json语法高亮的编辑器写不要用记事本硬写。一旦语法出错加载器不会给任何弹窗提示表现就是mod列表里没有你。3.4 package打包与游戏内验证配置写完后展开IDEA右侧的Maven面板在Lifecycle里双击package即可打包也可以在终端执行mvn package。构建成功后target目录下会出现tutorialMod.jarantrun插件会自动把它复制到游戏目录下的mods文件夹。用mod模式启动游戏加载器界面里应该能看到刚才写入的信息。此时勾选BaseMod、ModTheSpire和你自己的mod然后运行。进入游戏后打开mod菜单能看到json里写的标识但因为还没写任何代码这个mod暂时没有任何功能。第一次验证时多留意加载器弹出的log窗口。这里面每一行都有含义哪个mod先加载、耗时多少毫秒、有没有依赖缺失。后面排查问题一半的答案都在log里。如果log里提示找不到某个类优先怀疑pom.xml里的依赖路径或版本配错了。4. 语法实战变量、函数、类、作用域与监听函数4.1 变量int、float、boolean、String怎么选mod开发中最常用的是四类变量。int表示整数占4字节float表示小数字面量后要加fboolean只有true和falseString表示字符串。选错类型很常见比如把费用写成float之后做数值比较时会出现精度问题把命中率写成int计算概率时又丢失小数。在杀戮尖塔的mod场景里对应关系很直观卡牌费用用int概率计算用float是否被某种状态影响用boolean描述文本用String。public int number; private float dot 2.4f; public static boolean judge true; public final String HELLO Hello world!;声明变量的语法是“属性 类型 名称 初始化”。public表示类外可访问private表示仅当前类可访问static表示这个变量归属类而非对象final表示常量值不可修改按习惯常量名全大写并用下划线分隔。这里有一个常见误用把不该为static的成员变量设置成static。一个卡牌的伤害如果写成static所有同名卡牌的伤害会互相覆盖战斗中就出现“一张被buff其他全跟着变”的灵异现象。判断标准很简单——这个值究竟是“每个对象各自一份”还是“整个类型共享一份”。4.2 函数return、参数和作用域的协作函数是类里面的方法有输入有输出。声明时用返回值类型表示输出用括号里的参数列表表示输入。private int print(String string, int num) { System.out.print(string); return num; } private void hello() { System.out.print(Hello world!); return; }第一个函数传入一个字符串和一个整数输出字符串后把整数返回给调用者第二个函数返回值类型是void表示不返回任何东西这里的return只是跳出函数体。括号里的string和num属于形式参数调用时真正传入的值叫实际参数两者的名字没有任何关系。初学者最容易把“形式参数”和“外部变量”搞混。写mod时我们会频繁在监听函数里收到游戏传入的对象比如一张卡牌。此时不要试图通过修改形参去影响外部变量先看看这个形参对象本身提供了哪些字段再决定怎么改。这是面向对象设计和面向过程思维的分水岭。4.3 类、对象与引用类是Java的基本代码单元。在杀戮尖塔里类有很强的现实对应你手上打出的每张“打击”都是打击类的一个对象生成的每个电球都属于电球类。函数和变量都放在类里讨论对象之间互不干扰。A a new A();这行代码中A是类名a是对象引用new A()创建一个真正的对象并执行构造函数。注意a本身不是对象只是指向对象的“把手”。后面把a传给其他函数传的是引用不是对象副本。创建新卡牌时构造函数往往要传入费用、基础伤害这些初始值这就是为什么new一个卡牌对象时经常要写参数。卡牌对象在场上被修改攻击力其他同名卡牌不受影响因为这些成员变量属于各自的实例。反过来说如果想要某种数据在所有对象间共享就得用static。4.4 作用域与static作用域决定了哪些代码能访问某个成员。public可以被任何类访问private只能在该类内部访问。IDE通常强制公有类以单独文件存在其他类要import它的包路径才能使用。import的格式是“包名.类名”最后一个单词是类名前面的单词是包所在的目录路径这种结构很像文件与文件夹。Strike a new Strike(); int dam a.damage; // 某一张打击对象a的伤害 int summary Strike.sum; // 整个Strike类累计生成的卡牌数damage是实例字段不同打击卡各存各的sum是static字段所有打击卡共享同一个值。访问方式也不同实例字段要通过对象访问static字段可以直接用类名访问。后面作业里统计战斗总伤害就要用static字段来跨对象累计。4.5 监听函数mod和游戏互动的最低成本方案有了上面的基础可以写出第一个有实际行为的mod了。扩充入口类让它监听“抽卡”事件package tutorial; import basemod.BaseMod; import basemod.interfaces.PostDrawSubscriber; import com.evacipated.cardcrawl.modthespire.lib.SpireInitializer; import com.megacrit.cardcrawl.cards.AbstractCard; SpireInitializer public class Tutorial implements PostDrawSubscriber { public Tutorial() { BaseMod.subscribe(this); } public static void initialize() { new Tutorial(); System.out.print(hello world! 5*630); System.out.print(5 * 6 30); System.out.print(end of line!\n22 4); } Override public void receivePostDraw(AbstractCard abstractCard) { System.out.println(abstractCard.name); } }整个流程ModTheSpire通过SpireInitializer找到initialize()initialize里new出Tutorial对象构造函数调用BaseMod.subscribe(this)把自己注册成事件订阅者之后游戏里每次抽卡加载器都会回调receivePostDraw方法传入被抽到的卡牌对象。方法体里只做了一件事——打印卡牌名称但这已经是mod与游戏互动的完整闭环。System.out.print和println的区别在于后者输出后自动换行字符串拼接里的表示文本相连\n是换行符。这些输出会出现在ModTheSpire的log窗口里是早期调试最重要的观察点。加载mod时控制台显示的时间消耗信息也是来自同一条输出通道。5. 避坑排查从环境到打包最容易翻车的五个点写mod最痛苦的不是概念不会而是环境、依赖、打包这几件事连环翻车。下面五条都是新手期高频出现的问题按“现象—原因—解决”整理。5.1 现象mvn package成功但游戏mod目录里没有新jar原因antrun插件的copy路径写错。常见的是steam.path指向了游戏库根目录游戏本体其实装在另一个路径或者mods目录不存在时copy静默失败。解决先在文件管理器里确认${steam.path}/common/SlayTheSpire/mods/是否存在不存在就手动创建。把pom.xml里的tofile路径改准确后重新执行package留意构建日志里是否出现copy相关的源文件和目标文件。如果copy一直没有打印检查antrun插件的phase和goal是否配置正确。5.2 现象加载器界面里看不到自己的mod原因ModTheSpire.json缺失、放错位置、或json格式错误。json必须放在src/main/resources根目录下不是java目录下。大括号和方括号配对错误时加载器会静默跳过。解决先用带格式校验的编辑器打开json确认花括号和方括号成对再检查打出的jar确认json位于jar包根路径不在某个子目录里。用解压工具打开jar看结构是最直接的验证方式比反复猜原因省时得多。5.3 现象加载mod时报ClassNotFound或NoSuchMethod原因游戏版本、BaseMod版本、ModTheSpire版本的匹配出了问题。你引用的desktop-1.0.jar里的类名或方法签名和实际运行的游戏版本对不上。解决把pom.xml里依赖的三个jar换成同一时期、同一批更新下的版本。最稳妥的操作是先不带mod启动一次游戏确认三件套都能正常加载再同步更新工程里的依赖路径。如果某个方法名报错说明你参考的代码版本和当前环境不一致上游戏对应版本的源码里检索方法签名。5.4 现象Java代码改了重开游戏还是旧行为原因只改了源码没有重新打包。IDEA里修改代码不会自动触发maven packagemods目录里躺着的还是上一次的旧jar。解决把“改源码—执行package—开游戏验证”变成固定节奏。条件允许的话在IDEA里给mvn package配一个外部工具快捷键每次保存代码后顺手执行一次。这个坑几乎人人都会踩而且踩完一次还会踩第二次因为游戏启动本身就有延迟等你意识到没更新时前几分钟的验证全白做了。5.5 现象控制台中文乱码原因pom.xml没有声明UTF-8编码或者源文件本身用系统默认编码保存。Windows下默认编码经常是GBK中文注释和字符串在打包后全乱了。解决保留properties里的project.build.sourceEncoding为UTF-8并在IDEA的File编码设置里把项目编码、模块编码、默认编码都切到UTF-8。中文字符串在json和Java代码里都会出现这一步不处理好后面做卡牌描述时会非常崩溃。排查这些问题的通用路径是先看log再看jar最后回头看pom。log里有加载顺序、耗时、异常栈jar能确认json和class是否到位pom决定了依赖和复制路径是否准确。按这个顺序走绝大多数玄学问题都能落回一个具体的配置错误。6. 二次开发思路把作业题改出一个真正有用的功能到这里工程能跑、代码能写剩下的就是知识落进实战。原教程留了两道作业稍微改动就能变成突破口。第一道作业仿照前面的流程输出一个jar并在游戏中运行给自己mod取名、在作者栏写上自己的ID。它逼着你走完环境、依赖、配置、打包、加载全链路别跳过。第二道作业更有价值给定一个只完成了一半的监听类要求统计每场战斗玩家受到的总伤害。我补全后的版本如下package tutorial; import basemod.BaseMod; import basemod.interfaces.OnPlayerDamagedSubscriber; import basemod.interfaces.PostBattleSubscriber; import com.evacipated.cardcrawl.modthespire.lib.SpireInitializer; import com.megacrit.cardcrawl.cards.DamageInfo; import com.megacrit.cardcrawl.rooms.AbstractRoom; SpireInitializer public class Tutorial implements OnPlayerDamagedSubscriber, PostBattleSubscriber { private static int totalDamage 0; public Tutorial() { BaseMod.subscribe(this); } public static void initialize() { new Tutorial(); } Override public int receiveOnPlayerDamaged(int damage, DamageInfo damageInfo) { totalDamage damage; return damage; } Override public void receivePostBattle(AbstractRoom r) { System.out.println(本场战斗玩家受伤总量: totalDamage); totalDamage 0; } }两个关键点totalDamage声明为static因为每次回调都在不同对象上下文里只有类级字段能在多次回调之间保持累计receivePostBattle在战斗结束后执行打印后把totalDamage重置为0确保下一场战斗重新统计。从这个问题往外走一步就能触及mod开发的真正玩法。把totalDamage存起来在某个时机触发增益或者在receivePostDraw里判断抽到的是不是特定卡牌修改它的伤害更进一步调用BaseMod的注册API把一张自制卡牌加进卡池。凡是能实现的效果本质上都是先订阅一个事件、再在回调里操作游戏对象。我自己的习惯是不直接上手做新mod先把作业拆成“事件来源、回调时机、修改对象”三个要素用log验证回调时机再动手改数据。从那以后每次写完代码我都强制走一遍“改源码—mvn package—开log验证”的闭环这个习惯帮我躲过了大量“改了半天发现没生效”的尴尬时刻。希望这个流程也能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?