简介:面向《杀戮尖塔》Mod开发初学者的入门教程,以PDF文档形式呈现,聚焦零基础玩家从搭建环境到创建首个自定义Mod的完整路径。教程围绕modthespire、stslib、basemod三大支持库展开,说明如何通过第三方API在游戏本体中注入自定义代码,并逐步讲解IntelliJ IDEA安装、Windows下Maven环境变量配置及pom.xml依赖管理。文档还涉及控制台输出函数、基础变量与类作用域,引入监听函数和继承等面向对象概念,配有作业练习,帮助读者理解从普通Java语法到游戏Mod开发的思维转换。资源为单个PDF文件,压缩包仅1.5MB,轻量易携;已有2121人学习/下载。按文档步骤操作,即可独立搭建Maven项目并完成基础MOD开发,为后续扩展卡牌、角色或新机制打下扎实基础。
1. 杀戮尖塔Mod开发:不只是换皮,是把整张战斗桌搬进代码里
不少玩家玩杀戮尖塔玩到几百小时之后,第一反应是去创意工坊找卡牌包,第二反应就是——这些东西到底是怎么做出来的?这篇教程给我的第一感受是:它不教你写花哨的卡牌,而是先把“Mod到底怎么被游戏加载”这件事拆得明明白白。杀戮尖塔本体是Java写的,Mod本质上是往游戏进程里塞一段第三方代码,让游戏在启动时通过ModTheSpire这个加载器把你的代码并进去。整套东西依赖三个库:ModTheSpire、BaseMod,以及经常被提到的stslib。适合谁看?适合有一点点Java基础、想从零做出第一个能进游戏、能被识别、能产生日志的Mod的开发者。教程把从Steam创意工坊订阅依赖,到IDEA里建Maven工程,再到写第一行“hello world”日志、写监听函数感知抽卡行为的全过程串了一遍,属于那种“照着点就能跑通”的入门路线。
2. 核心技术栈拆解:三大依赖库和Mod的加载链路
先把这个Mod的运行机制说透。杀戮尖塔的Mod不是独立程序,它是寄生在游戏进程里的代码包,游戏本体不会主动加载你的代码,全靠ModTheSpire在启动时扫描指定目录下的jar包,读取其中的标识信息,再把jar里的类加载进JVM。BaseMod则提供了一套事件订阅机制,让Mod能“听到”游戏里发生了什么,比如抽牌、受伤、战斗结束。stslib在这套体系里属于扩展库,提供更多基础API,很多成品Mod会用到,新手阶段可以先订阅但不一定直接用。
2.1 三个库各管哪一段
ModTheSpire是加载器,负责识别和加载Mod;BaseMod是事件总线,负责把游戏事件广播给订阅者;stslib是功能扩展,提供比如“新角色UI框架”这类高级封装。教程里的配置过程是:先在Steam创意工坊搜索并订阅这三个库,订阅之后它们会出现在Steam的workshop目录下,路径类似steamapps/workshop/content/646570/,每个库对应一串数字ID的文件夹,里面放着jar包。之后做项目时,pom.xml里直接通过systemPath指向这几个jar。
E:\SteamLibrary\steamapps\workshop\content\646570\1605833019\BaseMod.jar E:\SteamLibrary\steamapps\workshop\content\646570\1605060445\ModTheSpire.jar E:\SteamLibrary\steamapps\common\SlayTheSpire\desktop-1.0.jar这三个路径分别是BaseMod、ModTheSpire和游戏主程序。这里有个细节值得注意:desktop-1.0.jar是游戏本体打包后的jar,里面是游戏全部类代码,Mod编译时要依赖它,否则代码里引用com.megacrit.cardcrawl.*就会报找不到符号。也就是说,这个游戏本体jar相当于Mod的“JDK”,BaseMod和ModTheSpire相当于“框架库”。
2.2 Maven工程里的依赖声明和构建链路
教程用Maven管理项目,pom.xml里除了定义坐标,还做了三件事:指定编译级别Java 1.8,声明上述三个system scope的依赖,以及配置构建后将jar复制到Steam的mods目录。maven-antrun-plugin这个插件负责把target/tutorialMod.jar拷贝到Steam的mods文件夹,这就是“一键打包并部署”的效果。
mvn package执行后,Maven会先编译src/main/java下的代码,再把资源文件打包进jar,最后执行antrun插件把jar复制到ModTheSpire的mods目录。这里“mods”目录指的是游戏根目录下的mods文件夹,ModTheSpire启动时就会扫描这个目录加载jar。
2.3 ModTheSpire.json:Mod的身份证
jar包里的resources/ModTheSpire.json是Mod的标识文件,ModTheSpire靠它识别Mod名称、作者、版本、依赖哪些库。这个文件如果写错,Mod会直接在加载器界面里消失。
3. 从零构建一个可运行的Mod工程:环境、配置、打包
这一章是可抄作业的部分。很多新手在环境搭建环节就翻车,主要翻在两个地方:一个是Maven环境变量没配好,另一个是Steam路径写死导致jar路径解析失败。
3.1 环境准备:JDK、Maven、IDEA三件套
教程推荐IntelliJ IDEA社区版,免费且对Maven支持好。JDK建议装1.8,因为游戏本身是老版本Java构建的,高版本JDK编译出的class文件可能在游戏加载时出现版本不兼容。Maven需要配置环境变量:新建MAVEN_HOME指向Maven安装目录(例如E:\Maven\apache-maven-3.3.9),再在Path里追加%MAVEN_HOME%\bin。配置好之后命令行输入mvn -v验证。
mvn -v出现Maven版本信息就说明环境OK。这一步如果失败,多半是Path变量没生效,重开命令行窗口即可。
3.2 创建Maven工程并手写pom.xml
在IDEA里新建Maven项目,不勾选模板,填好groupId和artifactId。然后打开pom.xml,把教程里的依赖声明和构建配置粘进去。这里需要注意<steam.path>的值必须改成你自己的Steam库路径。
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <steam.path>E:\SteamLibrary\steamapps</steam.path> </properties>这段配置里steam.path是个变量,后面的${steam.path}/workshop/...会自动拼接。有人把路径写错成D:\Steam\steamapps\common\...结果日志直接报文件不存在,原因就是Steam库不一定装在C盘默认路径,需要到Steam设置里查看游戏库位置。
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是加载器界面显示的名字;dependencies声明依赖BaseMod,ModTheSpire会先加载依赖再加载本Mod。然后点Maven侧边栏Lifecycle里的package,或在IDEA终端执行mvn package,看到BUILD SUCCESS后去Steam用ModTheSpire模式启动游戏,加载器界面会出现你的Mod。
3.4 用@SpireInitializer创建第一个类
只有json还不够,ModTheSpire加载了jar,但不知道从哪里切入游戏代码。这时需要一个带@SpireInitializer注解的入口类:
package tutorial; import com.evacipated.cardcrawl.modthespire.lib.SpireInitializer; @SpireInitializer public class Tutorial { public Tutorial() { } public static void initialize() { new Tutorial(); } }@SpireInitializer告诉ModTheSpire:这个类的initialize()静态方法就是入口。ModTheSpire加载时会调用它,从而创建Mod对象并注册进BaseMod。没有这个类,Mod在加载器里能被看到,但进入游戏后不会有任何行为。
4. 核心代码拆解:监听、引用传递与继承,一个都不能少
教程中段从控制台输出讲到监听函数,再深入到引用传递和继承。这一章是理解Mod能不能“感知游戏”“修改游戏”的关键。
4.1 控制台输出:Mod的第一个可观测行为
在initialize()方法里加一行System.out.print("hello world!"),打包加载后在ModTheSpire的log窗口里就能看到输出。这里隐藏着一个调试技巧:游戏运行时控制台输出的日志,是排查Mod运行状态的最直接手段。加载时看不到日志要么是jar没复制成功,要么是入口类没被识别。
System.out.print("hello world! 5*6+30="); System.out.print(5 * 6 + 30); System.out.print("end of line!\n2+2=" + 4);注意System.out.print不换行,如果想每条日志独立一行,用println。日志里带上表达式值,是最原始的断点式调试。
4.2 监听函数:让Mod听见游戏事件
监听是Mod与游戏互动的核心机制。BaseMod提供了一套订阅接口,比如PostDrawSubscriber在玩家抽牌后触发。教程示例:
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(); } @Override public void receivePostDraw(AbstractCard abstractCard) { System.out.println(abstractCard.name); } }这段代码的逻辑:构造函数里调用BaseMod.subscribe(this)把自己注册为订阅者;游戏里每次抽牌,BaseMod遍历所有订阅了PostDrawSubscriber的对象,回调receivePostDraw方法,传入抽到的那张卡对象。拿到AbstractCard对象后,就能访问它的name、cost、damage等属性。这就是Mod修改游戏的第一步——不只是看日志,还能改卡的属性,比如把抽到的牌名字改成“NULL”。这里值得注意的是@Override注解,它声明这是一个重写的方法,编译器会帮你检查接口签名是否匹配。
4.3 引用传递的坑:基本类型传值、对象传引用
教程里设计了一个经典的交换实验:在initialize()里定义两个int变量,调用静态方法swap(a, b)交换它们,函数内部确实交换了,但调用方再次打印时,a和b的值没有变化。原因很明确:Java基本类型(int、float、boolean)传参是值传递,函数拿到的是副本,怎么改都不影响外部。
public static void swap(int a, int b) { int temp = a; a = b; b = temp; }但同样的逻辑换到对象上就完全不一样了。receivePostDraw(AbstractCard abstractCard)里直接把abstractCard.name改成“NULL”,游戏里的卡牌名字真的变了。原因就是对象类型的形参是引用传递,形参和实参指向同一个对象。新手最容易在这里产生困惑:为什么int改不动而对象能改掉?记住一条判断规则:基本类型是复制值,对象类型是复制引用,只要引用指向同一个对象,你改的就是原对象。
4.4 继承:Mod设计的地基
教程用能量球和卡牌举例:机器人有多种能量球,但都有共同行为,比如能被激发、能执行回合操作。这种归纳就靠继承。创建父类AbstractOrb定义共有的行为,具体球类继承它,各自实现独有的逻辑。卡牌也一样,游戏中所有卡牌都是AbstractCard的子类。做Mod时新增自定义卡牌,最常见做法就是继承AbstractCard,重写use方法、upgrade方法等。理解继承之后,看游戏源码里的类结构会轻松很多,因为整个游戏本体就是一堆类继承体系搭起来的。
5. 避坑记录:环境配置、依赖路径和日志排查的踩坑实录
这一章写几条真实项目中高频遇到的坑,每个都是“现象→原因→解决”的记录。
5.1 Maven编译报“找不到BaseMod”
现象:执行mvn package时报错,找不到com.evacipated.cardcrawl.modthespire.*包。原因:pom.xml里的systemPath指向的BaseMod.jar路径不存在,可能是Steam库路径配错了,也可能是创意工坊下载位置不在默认路径。解决:先到Steam的创意工坊文件夹确认jar实际位置,再把pom.xml里的<steam.path>改成绝对路径,重新reimport Maven工程。
5.2 游戏内加载器里看不到自己的Mod
现象:启动游戏后ModTheSpire界面只有BaseMod和ModTheSpire,自己打包的jar没出现。原因:antrun插件的拷贝目标目录不对,或者jar包没有成功生成。解决:先检查target目录下有没有tutorialMod.jar,再确认拷贝目标路径是不是游戏根目录的mods文件夹。教程里的拷贝目标是${steam.path}/common/SlayTheSpire/mods,注意是游戏目录下的mods,不是创意工坊目录。
5.3 加载时log报错,但不清楚具体原因
现象:加载Mod时log框里红字报错,代码看不出问题。原因:多半是ModTheSpire.json里的dependencies声明了basemod,但游戏运行时没有勾选BaseMod。解决:在ModTheSpire加载器界面必须同时勾选BaseMod和ModTheSpire,再勾选自己的Mod。另外检查json里modid不能含中文和空格,否则解析失败。
5.4 修改代码后重新打包,但游戏里行为没变
现象:改了代码、重新package、启动了游戏,但行为还是旧的。原因:jar被复制到了mods目录,但ModTheSpire加载时可能读取的是缓存,或者游戏没有完全退出。解决:完全关闭游戏再重新用ModTheSpire模式启动,同时确认target/tutorialMod.jar的修改时间是否更新了。另一个隐蔽原因是多个Mod的modid重复,加载器只加载了第一个。
5.5 控制台输出中文乱码
现象:日志里中文变成问号或乱码。原因:Maven工程的默认编码不是UTF-8,老版本Windows环境变量导致控制台编码不符。解决:在pom.xml的properties里保持project.build.sourceEncoding为UTF-8,同时在IDEA的Settings里设置File Encodings为UTF-8。教程里特意强调UTF-8允许变量名用中文定义,也保证了输出代码的准确性。
6. 二次开发进阶:从日志监控到第一个自定义卡牌
到了这一步,你已经能把Mod跑起来了。接下来的方向是把“能输出日志”变成“真正改变游戏体验”。我建议按优先级做三件事:第一,把监听函数和游戏事件的关系梳理清楚,搞清楚每一个事件回调什么时候触发、什么时候拿到什么对象,这是Mod能稳定工作的前提;第二,找一个游戏自带的简单卡牌类(比如打击)作为参考,照着它的结构写一张自己的卡;第三,理解继承和重写的关系,把自定义卡挂进游戏卡池。
public class CustomStrike extends AbstractCard { public CustomStrike() { super("custom_strike", "自定义打击", "red/attack/strike", 1, 6, 6, CardType.ATTACK, CardColor.RED, CardRarity.BASIC, CardTarget.ENEMY); } @Override public void use(AbstractPlayer p, AbstractMonster m) { this.addToBot(new DamageAction(m, new DamageInfo(p, this.damage, DamageInfo.DamageType.NORMAL))); } @Override public void upgrade() { if (!this.upgraded) { this.upgradeName(); this.upgradeDamage(3); } } }这段代码参考的是“打击”的骨架:构造方法里指定卡牌ID、显示名、费用、基础伤害;use方法描述打出卡牌时的动作,这里是对目标施加伤害;upgrade方法定义升级后的变化。写完之后需要注册卡牌,通常是用BaseMod.addCard(new CustomStrike()),这样卡池里才会出现它。整个流程走完,你会意识到Mod开发的本质不是“写新代码让游戏变复杂”,而是“在正确的事件点插入自己的代码逻辑”。从那以后我做任何Mod改动,都会强制先走一遍“改代码→package→看log→进游戏验证”这条完整链路,确保每次改动都能被观察、被定位。这个习惯帮我避免了很多次“改了但没生效”的翻车场景。希望这篇解析能给准备入坑杀戮尖塔Mod开发的你,一条能直接踩上去的路径。
本文还有配套的精品资源,点击获取