尧图精选

杀戮尖塔 Mod 从零实战:ModTheSpire+BaseMod 自定义卡牌

🕒 发布时间:2026/10/1 9:11:45 📁 来源:尧图网络
1. 为什么从 ModTheSpire 与 Java 这条线切入第一次接触《杀戮尖塔》mod 制作的大多数人脑子里冒出的第一个问题是「我该用什么工具」。《杀戮尖塔》本体是用libGDX框架加 Java 写出来的桌面游戏它并没有像很多现代商业引擎那样提供官方编辑器也没有内置一套可视化 mod 工具链。这就意味着想给这游戏加内容要么去硬改游戏本体文件要么走一条社区已经踩了几年的成熟路线ModTheSpire 加载器 BaseMod API 框架 Java 工程打包。这条路线目前是生态里最稳、资料最全、后续维护成本最低的一条。这套东西能做的事比很多人想象的多。你可以加一张新卡牌、一个新遗物、一个新药水、一个新事件、一次自定义战斗甚至整条新角色线。它也能改数值平衡、改卡池权重、改遗物触发逻辑还能给游戏加自定义的 UI 按钮和设置界面。只要你能用 Java 表达出来的逻辑几乎都能塞进这个 mod 体系里。它解决了玩家「原版内容玩腻了想加料」、以及「有个新机制点子想验证」这两类核心需求。这篇文章面向两类人。第一类是写过一点 Java 或者任意一门面向对象语言、但完全没碰过 mod 开发的人看完能自己跑出一个可加载、可打包、能进游戏的空壳 mod。第二类是已经会照着零散教程复制代码、但搞不清楚「为什么工程要这样组织」「打包为啥老是出问题」的人看完能把整套机制串起来。至于纯粹的新手也没关系涉及到游戏内部类名、资源路径这些地方我都会解释它是干什么的。这次先聚焦从零到「mod 能加载并显示一张自定义卡」这条完整链路后面的战斗逻辑、UI 深度定制、跨 mod 兼容再单独展开。在动手之前我想先把一个判断说在前头别一上来就想着做一个大工程。我见过太多人第一天就要做新角色结果卡在打包环节三天没进游戏热情直接耗尽。正确的节奏是先让一个最小的 mod 出现在游戏里哪怕它什么都没干只要加载器认它、列表里能看到它这一步就值了。剩下的都是在这个基座上加东西。1.1 加载器和 API 框架到底谁管什么这里必须把ModTheSpire和BaseMod的角色分清楚否则后面配依赖会一直懵。ModTheSpire社区简称 MTS是一个外置加载器它的工作是在游戏主程序启动之前先把自己挂进去扫描mods目录读取每个 mod 的描述文件然后把符合版本的 mod 通过它自己的类加载器注入到游戏运行环境里。它管的是「加载、排序、隔离」这些工程层面的事它不关心你写的是卡牌还是遗物。BaseMod 则是一个内容 API 框架它本身也是一个 mod一个特殊的、几乎所有内容 mod 都依赖它的基础 mod。它提供了一大堆抽象类和事件钩子比如卡牌基类、遗物基类、Power 基类、卡牌渲染的注册接口、本地化注册接口等等。它管的是「内容怎么定义、怎么注册给游戏」这些业务层面的事。你写的 mod 通常只直接依赖 BaseMod而 ModTheSpire 是运行时环境通常不写进代码依赖但要保证玩家装了对应版本。一句话总结MTS 负责把你推进游戏BaseMod 负责告诉游戏你是谁。很多新手报「找不到 BaseMod 的类」本质是依赖没配好或者版本对不上报「游戏里不显示我的 mod」往往又是 MTS 没正确读到你的描述文件。把这两层分开看问题定位速度会快很多。1.2 开发环境为什么锁定 JDK 8一个很关键但常被忽略的点《杀戮尖塔》跑在 Java 8 的运行时上。这不是建议是硬约束。如果你用 JDK 11 或更高版本编译打出来的字节码版本号更高游戏自带的 JVM 根本不认加载时直接抛UnsupportedClassVersionError。这个报错不会告诉你「你用了新版 JDK」它只会说类版本不对第一次遇到的人经常一脸茫然。所以第一步就是把编译目标锁死在 Java 8。用 Maven 或 Gradle 的话配sourceCompatibility和targetCompatibility为 1.8用 IDEA 自带的构建则要确认 Project SDK 和 Module SDK 都是 1.8并且 artifact 的编译输出等级也一致。我建议直接装一个JDK 8u专门用于这个项目别和系统里其他新版 JDK 混用能省掉大量莫名其妙的坑。编辑器方面IntelliJ IDEA 社区版就够了它对 Java 工程和依赖管理比较友好也方便后面直接打 jar。Eclipse 也不是不行但配依赖和打 artifact 的体验我个人觉得不如 IDEA 顺。工具链上没有对错选一个你能顺畅打包的就行但不要用轻量文本编辑器硬写因为你会频繁需要跳转到 BaseMod 和游戏本体的类定义去看方法签名没有索引会非常痛苦。2. 工程结构设计与依赖获取这一步在真正写代码之前工程结构决定了你后面会不会反复返工。我见过不少人把代码全堆在一个类里能跑是能跑但一加第二张卡就乱了。合理的结构不是洁癖是为了让注册逻辑集中、本地化文件好找、打包不遗漏资源。所以先把结构定下来再往里填东西事半功倍。典型的 Java mod 工程会分成两大块src/main/java放逻辑代码src/main/resources放资源文件其中最重要的是本地化 JSON 和 mod 的描述清单。打包时这两块会被合并进同一个 jar但运行时的读取路径不一样资源文件走类路径读取代码走类加载。理解这点对排查「游戏里卡牌名字显示成一行英文 ID」这种问题特别重要因为那基本就是本地化资源没打进 jar 或者路径写错了。依赖方面你在编码期需要至少三个 jar 进 classpath游戏本体desktop-1.0.jar、ModTheSpire、BaseMod。游戏本体 jar 一般在游戏安装目录里能直接找到BaseMod 和 MTS 的 jar 从它们的发布页下载对应版本。把这三个作为provided或者本地 lib 引入注意不要打进你自己的 mod jar否则会和玩家的环境冲突打进去反而坏事。2.1 一个不容易返工的目录布局我通常这么组织一个 mod 项目SlayModDemo/ ├── src/main/java/com/example/demo/ │ ├── DemoMod.java // 入口实现 BaseMod 接口 │ ├── cards/ // 自定义卡牌 │ ├── relics/ // 自定义遗物 │ ├── powers/ // 自定义 Power │ └── patches/ // 对原版的代码注入 └── src/main/resources/ ├── ModTheSpire.json // 描述清单 ├── localization/ │ ├── eng/ // 英文 │ ── zhs/ // 简体中文 └── images/ // 卡图、遗物图这个布局不是官方规定是社区里比较通行的做法。按功能分包的好处是当你有几十张卡的时候光靠类名就能定位。patches单独放是因为对原版的注入改动风险最高混在业务代码里容易误伤。资源侧按语言分目录是 BaseMod 的约定zhs是简中eng是英文缺哪个语言游戏里对应语言就会回退显示成 ID。提示图片目录命名和读取路径是新手翻车重灾区卡牌图片的尺寸、文件名规则在 BaseMod 里有约定不符合规则时游戏不报错只是白图或者不显示排查起来很折磨。2.2 ModTheSpire.json 里每个字段的意义描述清单是 MTS 认识你的唯一凭证写错了它连加载都不会尝试。一个标准的清单大致包含这些字段字段作用常见坑modidmod 唯一标识别用中文、别带空格name显示名称可以随意支持中文author作者名用于展示description简介支持中文versionmod 版本号自增用别乱填mts_version要求的 MTS 最低版本填低了行为不确定sts_version要求的游戏版本版本号要为纯数字串dependencies依赖的其他 modid填 BaseMod 的 modidmodid和dependencies一个是「我的身份证」一个是「我依赖谁」。如果你写了依赖某个 mod 但玩家没装MTS 会直接跳过你的 mod 并给出提示这是保护机制不是 bug。sts_version这一栏特别容易填错游戏大版本更新后 base 版本号会变填错会表现为「mod 被忽略」而且提示不一定明显。2.3 依赖引入的两种主流方式第一种是手动放 lib在工程里建一个libs目录把三个 jar 拷进去然后在 IDEA 的模块依赖里加进去scope 设为 provided。这种方式直观、不依赖网络适合新手第一次跑通。缺点是换机器或者团队协作时容易漏文件。第二种是用构建工具声明依赖Gradle 或 Maven 里配置一个仓库地址把 BaseMod、MTS 作为 compileOnly 依赖拉下来。这种方式工程更干净可复现性强适合正儿八经长期维护的 mod。我个人现在更倾向第二种尤其是要发多个 mod 的时候统一由构建工具管理版本能避免很多「我这边能跑你那边不行」的扯皮。不管哪种方式核心原则是这些依赖不能被打进最终 jar。你可以这么理解依赖就像别人家已经有的家具你只需要「用」不需要「搬」搬过去了反而占地方还容易和人家原来的冲突。打包时确认为 provided / compileOnly就对了。3. 从空壳到能进游戏最小可运行 mod 的实现原理捋清楚了现在动手。这一节的目标只有一个让游戏启动后在 mod 列表里能看到你的 mod并且进游戏不崩。别看目标小这一步笼罩着整个流程所有容易出错的点——环境、依赖、清单、打包、加载全都在这条链路上。跑通了它后面加卡加遗物就是往里填内容的事。先说清楚一个概念mod 的「入口类」需要实现 BaseMod 提供的接口游戏在加载时会回调它的一些方法。你不需要自己在main里 new 出什么游戏会替你调用。这个回调机制是理解整个 mod 生命周期的钥匙。你写的所有注册动作绝大多数都发生在入口类被回调的那个时机。3.1 入口类的骨架长什么样一个最简入口大概是这样这里是示意具体接口名以你用的 BaseMod 版本为准package com.example.demo; import basemod.BaseMod; import basemod.interfaces.PostInitializeSubscriber; import com.evacipated.cardcrawl.modthespire.lib.SpireInitializer; SpireInitializer public class DemoMod implements PostInitializeSubscriber { public DemoMod() { BaseMod.subscribe(this); } public static void initialize() { new DemoMod(); } Override public void receivePostInitialize() { // 这里做卡牌、遗物、本地化的注册 } }SpireInitializer这个注解是告诉 MTS「这是我的入口」MTS 会去找名为initialize的静态方法并调用它。构造函数里去subscribe自己等于告诉 BaseMod「这个类订阅了某些事件请在合适时机回调我」。receivePostInitialize就是其中一个回调点通常内容注册都放在这。注意入口类的包名、类名要和清单里别的配置保持一致类改了名忘了改注解会表现为「mod 加载了但什么都没注册」。3.2 打包这一步为什么最容易翻车代码能编译不等于能加载。IDEA 里跑得欢打成 jar 进游戏就崩八成是打包时漏了资源或者混进了不该有的依赖。我建议手动确认三件事jar 根目录下能看到 ModTheSpire.json 吗不是包在某个文件夹里localization 和 images 目录都在吗BaseMod、MTS、游戏本体这三个依赖有没有被误打进去。用 IDEA 打 artifact 的时候选「From modules with dependencies」然后把三个 provided 的依赖从输出里剔除。更稳的方式是直接写构建脚本让打包动作可复现。打包出的 jar 名字随意丢进游戏的mods目录启动游戏看 mod 列表。第一次大概率会遇到各种问题别慌下一节我专门列排查表。3.3 首次验证时该看什么信号启动游戏进入 mod 选择界面看三件事你的 mod 在列表里、能勾选、勾选后启动不报错。如果列表里没有九成是描述文件的问题。如果能勾选但进游戏白屏或者闪退大概率是代码在初始化阶段抛异常。这个时候一定要去看游戏的日志文件MTS 会把加载过程和异常堆栈写进去位置一般在游戏目录下的 logs 文件夹。日志是你唯一可靠的朋友。很多新手习惯靠「猜」猜十次不如看一次堆栈。异常类型和第一行信息基本就锁定了问题方向ClassNotFoundException是依赖没找到NoSuchMethodError是版本不匹配NullPointerException往往是某个注册顺序或者资源读取没处理好。把这几个报错和原因对应上你的排查速度会快一个量级。4. 核心内容实现把一张自定义卡牌塞进游戏空壳跑通之后真正有意思的部分才开始。一张卡牌看似简单实际涉及四件事卡牌逻辑类、卡牌注册、本地化文本、卡牌图片。四者缺一游戏里要么不显示要么显示成红字缺失。很多教程只给你卡牌类代码不告诉你另外三件结果新手拿到代码也不知道为什么游戏里找不到那张卡。我把四件事都过一遍。先说卡牌逻辑类。它一般继承 BaseMod 提供的自定义卡牌基类然后在构造里设置这张卡的一堆属性ID、名字、图片路径、费用、类型、稀有度、目标类型。这些属性决定了游戏怎么显示和怎么使用它。ID 是唯一标识本地化文本就是靠这个 ID 去查的所以 ID 一旦定了就别随便改改了本地化会全部失联。4.1 卡牌类的属性逐个说清楚属性含义设置错误的表现id唯一标识冲突时显示异常或覆盖name默认名常被本地化覆盖img卡图路径路径错就是白图cost费用显示不对或不可用type类型攻击/技能/能力影响提示和兼容判断rarity稀有度影响出现权重target目标类型影响能否指向敌人这里要重点说的是img。卡牌图片不是随便找张图就行它需要满足尺寸和命名约定BaseMod 会按约定去读。我踩过的坑是图片放对了目录但文件名大小写照搬在 Windows 上没事打包后路径匹配在某些环境会出问题所以目录和文件名尽量全小写减少跨环境的意外。另外卡图建议用透明底 PNG别用 JPEG边缘会有白边。费用、类型、稀有度这些相对直接但rarity会影响卡在奖励里出现的频率做平衡的时候要考虑到。如果你做的是罕见卡结果给了个 common那它会满世界刷游戏体验就废了。这个细节教程里很少提但做 mod 做久了会发现平衡感是一张卡好不好用的关键。4.2 注册卡牌与本地化的联动卡牌类写好后要在入口的receivePostInitialize里把它注册给 BaseMod通常是调用注册方法并把类传进去。注册完之后还要把这张卡加进卡池否则它永远不会出现在奖励里。加卡池这一步经常被漏现象就是「卡注册了、用调试命令能拿到但正常游玩永远刷不出来」。注册 ≠ 进卡池这两个概念新手最容易混。本地化是让卡显示中文名和描述的地方。在localization/zhs/cards.json里以卡牌 ID 为键写上名称和描述。描述文本支持一些特殊的格式标记比如伤害数字高亮、关键词提示等这些标记在游戏里会被渲染成相应样式。写本地化时要注意JSON 是严格要求语法的多一个逗号、少一个引号都会导致整个文件解析失败然后你所有卡都显示成 ID。提示本地化文件报错时现象是「整批卡都显示 ID」而不是「某一张卡显示 ID」。一旦出现这种批量现象先怀疑 JSON 语法别一张张去查卡牌类。4.3 图片资源的放置与命名图片通常放在images目录下的子目录里卡图、遗物图、Power 图分开放。命名规则各版本的 BaseMod 略有差异但核心是「文件名要和代码里引用的路径完全一致」。我做过一次专门的验证故意把某张卡的图片文件名改掉一个字母游戏不报错进游戏后那张卡就是空白或者占位图日志里也不写明全靠肉眼对。所以做完图之后建议逐一对照代码里的路径检查一遍。如果你还想让 Powerbuff/debuff显示自定义图标那是另一套图片资源放在专门的 power 图目录。Power 图标一般要求是特定尺寸超了会被拉伸变形。这块我后面单独讲战斗机制时再展开这次先把卡牌这条线走完整。5. 常见问题与排查技巧实录这一节是我觉得整篇文章最值钱的部分因为下面这些问题我都真实遇到过有些坑反复踩了好几次才记住。mod 开发的调试体验不算友好游戏崩了不会给你友好提示只会白屏或者闪退。所以掌握一套系统的排查思路比记住某个具体报错更有用。我先给一个速查表再讲两个典型案例。现象高概率原因排查方向mod 列表里没有描述文件缺失或格式错检查 jar 根目录和 jsonmod 被忽略版本号不匹配核对 sts_version进游戏闪退初始化抛异常看日志堆栈第一行卡显示成 ID本地化未加载查 json 语法和路径卡永远不出现没加进卡池检查卡池注册图片空白路径或格式错对照代码路径逐个查报类找不到依赖没配好检查 classpath 和版本方法不存在BaseMod 版本不符统一版本号5.1 一个「能加载但没反应」的典型排查有段时间我遇到一个特别迷惑的情况mod 在列表里有、能勾选、能进游戏但自定义卡一张都不出现。我一开始怀疑是注册代码没执行加了一堆打印日志里确实打印了说明注册跑了。然后又怀疑是卡池检查后发现卡池也加了。最后定位到是本地化显示问题——卡其实注册成功了只是名字和描述都显示成 ID我在奖励界面上把它当成了「不是我的卡」。这个案例告诉我一个道理「没反应」和「反应了但显示不对」是两种完全不同的故障要先区分开。区分方法很简单用调试命令直接给玩家一张卡看有没有。有就是显示问题没有才是注册问题。这个思路后来帮我省了大量时间。5.2 版本不匹配引发的连锁故障另一个高频问题来自版本。游戏本体更新、BaseMod 更新、MTS 更新三者版本之间是有兼容关系的。你本地开发用的一套版本玩家装了另一套就可能出现NoSuchMethodError之类的报错。这种报错很迷惑因为你的代码明明没改。解决办法是在清单里明确声明要求的版本同时在测试时尽量用「干净环境」验证一遍别只在自己的开发机上测。条件允许的话用另一台没装过任何 mod 的机器跑一次或者把 mods 目录清空重装能发现一大堆只在别人机器上才出现的问题。这个习惯我是吃了亏才养成的。提示做 mod 分发的时候在说明里写清楚「需要的 BaseMod 最低版本和游戏版本」能减少一半以上的用户反馈。5.3 几条能省时间的实操心得第一条改代码后重新打包一定要确认旧的 jar 被覆盖了。我遇到过一次右键 artifact 生成后没生效是因为文件被游戏进程占用没写进去重启游戏后才好。养成打包前先关游戏的习惯。第二条日志文件按时间排序看最新的那一份。日志会滚动容易看串。找最新的、看末尾的堆栈效率最高。第三条善用调试命令。BaseMod 提供了一些控制台命令能直接在游戏里给你卡、给你遗物、进指定战斗比一次次重开游戏快得多。把这些命令记熟调试效率翻几倍。第四条每次只改一个变量。mod 开发里变量太多——代码、本地化、图片、版本——同时改好几样出了问题根本不知道是谁引起的。改一处、测一次虽然慢但总时间更短。说实话我一开始也走过「大干快上」的弯路第一天就想让新角色上线结果一周下来连一个稳定的测试环境都没搭好。后来把节奏放慢先跑通空壳、再加一张卡、再加一个遗物、再做本地化每一步都验证反而在两周内做出了一个自己满意的内容 mod。这种踏实推进的节奏是我现在做任何 mod 项目都会先遵守的。把这张自定义卡完整地、正确地显示在游戏里其实就已经跨过了 mod 制作最难的一道心理门槛。后面加遗物、加 Power、写自定义战斗逻辑都是在这个已经跑通的地基上做加法。形象点说你现在手里已经有一台能发动、能挂挡的发动机了接下来只是决定往车上装什么。等你把这张卡玩熟了可以试试给它加一个原文没有的触发效果或者让它和某个原版遗物产生互动——那种「我写的东西真的在游戏里生效了」的瞬间是这个领域最上瘾的地方。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →