Skip to content

状态效果和特效

合金气旋,已逝的神

状态效果是Mindustry融合性的一大体现,而炫酷的游戏效果离不开特效与音效的加持。本节将介绍状态效果与特效两者,同时还会介绍音效和音乐的相关内容,

声明一个StatusEffect

状态效果,在其他游戏可能被称为buff,是一种可以应用在单位上,使单位获得增益或减益效果的一种内容类型(Content Type)。你可以按照先前的组织架构,为状态效果新建一个存储文件:

java
new StatusEffect("tutorial-status-effect-1");
kotlin
StatusEffect("tutorial-status-effect-1")

和往常一样,你需要给它分配贴图和文本。

properties
status.tutorial-mod-tutorial-status-effect-1.name = 演示状态效果1
status.tutorial-mod-tutorial-status-effect-1.description = 原版甚至没有给状态效果做过描述。
status.tutorial-mod-tutorial-status-effect-1.details =
properties
status.tutorial-mod-tutorial-status-effect-1.name = Tutorial Status Effect 1
status.tutorial-mod-tutorial-status-effect-1.description = There is no description of a status effect in vanilla.
status.tutorial-mod-tutorial-status-effect-1.details =

状态效果有很多属性可以设置,给状态效果中的某些属性赋予负值是有意义的;作为对比,方块的某些属性为负值可能代表忽略该属性:

  • damageMultiplier:单位伤害倍率;
  • healthMultiplier:单位生命值倍率;
  • sppedMultiplier:单位速度倍率;
  • reloadMultiplier:单位开火速度倍率;
  • buildSpeedMultiplier:单位建造速度倍率;
  • dragMultiplier:单位运动阻力倍率,原版中没有状态效果使用此属性,但有些方块设置了同名属性,比如“冰”;
  • damage:大于0是指对单位每帧造成的伤害;小于0时指对单位每帧修复的生命值;
  • transitionDamage:发生状态影响时每秒造成的伤害,只供核心数据库显示;
  • disarm:是否使单位无法使用武器;
  • color:状态效果的颜色;
  • effect:状态生效时单位产生的特效。

冲突与反应

StatusEffect类中有oppositeaffinity字段。不过,这些字段的类型并不是简单的数组或Seq,而是ObjectSet,一种对象的“集合”类型,并且,这个字段只是用于显示,没有实际的冲突或反应功能。若想要设置冲突或反应功能,需要如下的语法:

java
wet = new StatusEffect("wet"){{
    color = Color.royal;
    speedMultiplier = 0.94f;
    effect = Fx.wet;
    effectChance = 0.09f;
    transitionDamage = 14;

    init(() -> {
        affinity(shocked, (unit, result, time) -> {
            unit.damage(transitionDamage);
        });
        opposite(burning, melting);
    });
}};
kotlin
val wet = StatusEffect("wet").apply {
    color = Color.royal
    speedMultiplier = 0.94f
    effect = Fx.wet
    effectChance = 0.09f
    transitionDamage = 14

    `init` {
        affinity(shocked) { unit, result, time ->
            unit.damage(transitionDamage)
        }
        opposite(burning, melting)
    }
}

先来看两个方法在Java中的使用方式。opposite()是一个拥有变长参数的方法,接收一系列状态效果,然后将它们设置为与本状态效果冲突。affinity()的第一个参数是另一个状态效果,而第二个参数是完全没有见过的结构,有如下的形式:

java
(参数列表) -> {函数体}

我们把这种表达式称为Lambda表达式,又叫匿名函数。在这里你可以认为,传递函数其实就是传递“我要干什么”的信息。affinity()方法的含义是,“当本状态效果遇到了第一个参数对应的状态效果时,游戏会帮你执行一段你写的代码,同时游戏在这里给你的代码提供三条信息,用unit代表受影响的单位,用time代表第一个参数对应的状态效果的持续时长。在这里我们想让“潮湿”和“电击”在反应时给予单位14伤害,那我们只需要写unit.damage(transitionDamage);即可(因为在此作用域中transitionDamage是可见的)。

init 方法的作用是实现一种延迟初始化机制。由于原版游戏中的状态效果之间关系复杂,且在声明冲突和反应时必须确保所引用的字段已加载完成,因此很难找到一个绝对安全的初始化顺序。为解决这一问题,可以将冲突与反应等属性的设置从构造函数推迟到专门的 init() 方法中执行。init() 方法是一个约定俗成的执行节点,它会在所有内容(即原版及所有模组的 loadContent() 方法)完全加载完毕后被调用。在这个阶段,所有内容都已注册并存于内容管理器中,因此在此设置内容之间的相互引用和关系属性是非常合适的。总结来说:建议在 loadContent() 阶段只完成内容(物品、方块等)的注册声明,而将内容属性的设置(尤其是涉及相互引用的部分)安排在 init() 阶段进行。

在 Kotlin 语言中,提供了一项称为尾随 Lambda 的语法特性。当一个函数的最后一个参数是 Lambda 表达式时,可以将其移至函数调用的小括号外部。此外,如果该 Lambda 表达式没有参数,则可以省略其参数声明,使代码更加简洁直观。

Kotlin 中的trans怎么办?

这个方法是protected,在Java中可以直接使用,因为匿名类继承了父类。在Kotlin中,如果使用apply()等作用域函数,由于没有发生继承,无法直接访问protected方法。此时可以考虑以下两种方式:

kotlin
//法一:使用匿名类
val wet1 = object : StatusEffect("wet") {
    init{
        `init`{
            trans(melting){ unit, result, time ->
                unit.damage(transitionDamage)
            }
        }
    }
}
//法二:包装类
class TutorialStatusEffect(name: String?) : StatusEffect(name){
    fun transHelper(statusEffect: StatusEffect, transitionHandler: TransitionHandler){
        trans(statusEffect, transitionHandler)
    }
}

val wet2 = TutorialStatusEffect("wet").apply{
    `init`{
        transHelper(melting){ unit, result, time ->
            unit.damage(transitionDamage)
        }
    }
}

特效

特效(Effect)是一些短而简单的、可在任意时刻绘制的小片段。建造方块时、炮塔开火时、钍反应堆爆炸时都伴随着特效的出现。Effect又名Fx,因为两者读音接近。尽管状态效果(StatusEffect)这一单词中也有Effect,但它和特效是完全不同的两个概念。

原版有很多地方都有特效,例如工厂就有craftEffect updateEffect placeEffect breakEffect destroyEffect多种特效。各个特效字段的名称通常反映了其应用时机,如craftEffect为生产后可能产生的特效。原版内置的所有音效都存储在mindustry.content.Fx这个类中,例如Fx.pulverizeMedium等。

在Java模组中,我们一般会直接使用Effect的基类构造方法:

java
pulverizeMedium = new Effect(30, e -> {
    randLenVectors(e.id, 5, 3f + e.fin() * 8f, (x, y) -> {
        color(Pal.stoneGray);
        Fill.square(e.x + x, e.y + y, e.fout() + 0.5f, 45);
    });
})

这个构造方法的第一个参数是特效的寿命,第二个参数也是一个Lambda表达式,用于放置具体的绘制方法。我们将在第五章深入介绍各种绘制方法的使用。

Effect有一些模板化的子类,如ExplosionEffect ParticleEffect SoundEffect WaveEffect。这些子类的初衷是为了方便JSON模组创建特效的,但在Java模组中也可以使用:

java
despawnEffect = hitEffect = new ExplosionEffect(){{
    waveColor = Pal.surge;
    smokeColor = Color.gray;
    sparkColor = Pal.sap;
    waveStroke = 4f;
    waveRad = 40f;
}};

以下是对各个模板化子类的字段介绍:

ParticleEffect是粒子特效,能够以多种形状(锥形、直线等)生成和渲染粒子,支持颜色、大小、旋转、光照等属性的动态变化:

  • colorFrom:粒子效果的起始颜色;会随时间过渡到 colorTo,用于创建颜色渐变。
  • colorTo:粒子效果的结束颜色;与 colorFrom 配合,定义粒子寿命的颜色变化。
  • particles:每次触发效果时生成的粒子数量;数值越大,粒子越密集。
  • randLength:是否使用随机的发射长度;为 true 时,每个粒子的飞行距离会在指定范围内随机。
  • casingFlip:是否为弹壳类特效启用翻转兼容;当父对象(如炮台)朝左时,此属性可确保粒子方向正确。
  • cone:粒子发射的锥形角度范围(单位:度);180度表示向前方半圆区域发射。
  • length:粒子从发射点飞出的基础距离;与 cone 共同决定粒子的初始位置。
  • baseLength:在动态长度基础上附加的固定长度;用于微调粒子的起始位置。
  • interp:控制粒子尺寸(如大小、长度)随时间变化的插值函数;默认为线性变化。
  • sizeInterp:专门控制粒子大小变化的插值函数;若为 null,则自动使用 interp
  • offsetX:粒子发射点在X轴上的固定偏移;基于效果的旋转中心。
  • offsetY:粒子发射点在Y轴上的固定偏移;基于效果的旋转中心。
  • lightScl:粒子光照范围的缩放系数;乘以粒子尺寸后得到实际的光照半径。
  • lightOpacity:粒子光照的透明度;影响光照的可见程度。
  • lightColor:粒子发出的自定义光照颜色;若为 null,则使用粒子本身的绘制颜色。
  • spin:粒子每秒自旋的角度(单位:度);正值表示逆时针旋转。
  • sizeFrom:粒子在寿命开始时的初始大小。
  • sizeTo:粒子在寿命结束时的最终大小。
  • sizeChangeStart:粒子开始改变大小的延迟时间(单位:帧);在此时间之前保持 sizeFrom
  • useRotation:粒子是否继承父对象(如单位、炮台)的旋转角度;为 false 时使用世界角度。
  • offset:粒子绘制的固定旋转角度偏移(单位:度);叠加在其他旋转之上。
  • region:用于绘制粒子的纹理名称;需要是游戏图集中有效的纹理名称。
  • line:是否将粒子渲染为线段而非贴图;开启后将启用线段相关的参数。
  • strokeFrom:线段在寿命开始时的宽度(粗细)。
  • strokeTo:线段在寿命结束时的宽度(粗细)。
  • lenFrom:线段在寿命开始时的长度。
  • lenTo:线段在寿命结束时的长度。
  • cap:绘制线段时是否为其两端添加圆头端点;为 false 时线段两端是方的。

ExplosionEffect用于模拟爆炸视觉效果的专用类,能同时生成冲击波、烟雾和火星三种元素,通过调整参数可以创建从小火花到大型爆炸的不同表现:

  • waveColor:爆炸冲击波的颜色;视觉效果上表现为向外扩散的圆形波纹。
  • smokeColor:爆炸产生的烟雾粒子的颜色。
  • sparkColor:爆炸产生的火星粒子的颜色。
  • waveLife:冲击波效果的视觉寿命(单位:帧);控制其从出现到消失的持续时间。
  • waveStroke:冲击波圆环线条的初始粗细。
  • waveRad:冲击波最终达到的最大半径。
  • waveRadBase:冲击波的初始基础半径;冲击波半径会从waveRadBase过渡到waveRad
  • sparkStroke:火星线条的初始粗细;会随着粒子寿命衰减。
  • sparkRad:火星粒子从爆炸中心向外飞散的最大距离半径。
  • sparkLen:火星线条可以达到的最大长度。
  • smokeSize:烟雾粒子可以达到的最大尺寸。
  • smokeSizeBase:烟雾粒子的初始基础尺寸;粒子大小会从smokeSizeBase过渡到smokeSize
  • smokeRad:烟雾粒子从爆炸中心向外扩散的最大距离半径。
  • smokes:每次爆炸生成的烟雾粒子数量。
  • sparks:每次爆炸生成的火星粒子数量。

RadialEffect用于放射状重复特效,能够将指定的子效果围绕一个中心点按固定角度间隔重复创建多次,常用于创建圆形分布的攻击特效、光环效果或多方向发射效果。

  • effect:需要被重复创建的子效果实例;决定了放射状效果中每个分支的具体表现。
  • rotationSpacing:每个子效果之间的旋转角度间隔(单位:度);控制着子效果在圆周上的分布密度。
  • rotationOffset:整体放射效果的旋转角度偏移(单位:度);用于调整放射图案的初始朝向。
  • effectRotationOffset:每个子效果自身的旋转角度偏移(单位:度);叠加在放射角度的基础上。
  • lengthOffset:子效果从中心点向外偏移的固定距离;用于创建空心或带有半径的放射图案。
  • amount:放射状效果中创建的子效果数量;即围绕中心点重复创建的效果分支数。

WaveEffect用于渲染基础的冲击波效果,通常表现为一个从中心向外扩散的圆圈或多边形,并带有颜色、大小和描边的动态变化,同时可伴随光照效果。

  • colorFrom:冲击波起始颜色;会随时间过渡到colorTo,形成颜色渐变。
  • colorTo:冲击波结束颜色;与colorFrom共同定义冲击波生命周期的颜色变化。
  • lightColor:冲击波光照的自定义颜色;若为null,则使用当前绘制颜色作为光照颜色。
  • sizeFrom:冲击波的初始半径或大小。
  • sizeTo:冲击波最终扩散到的半径或大小。
  • lightScl:光照范围的缩放系数;乘以冲击波半径得到实际的光照半径。
  • lightOpacity:光照的透明度;影响光照的可见程度。
  • sides:冲击波多边形的边数;若小于等于0,则自动根据半径计算圆形顶点数,从而渲染为圆形。
  • rotation:冲击波的固定旋转角度(单位:度);用于调整多边形(非圆形)的初始朝向。
  • strokeFrom:冲击波线条的初始粗细。
  • strokeTo:冲击波线条的最终粗细。
  • interp:控制冲击波大小和线条粗细变化的插值函数;默认为线性变化。
  • lightInterp:控制光照透明度变化的插值函数;默认与interp相反,可实现光照先强后弱等效果。
  • offsetX:冲击波中心在X轴上的固定偏移;基于效果的旋转中心。
  • offsetY:冲击波中心在Y轴上的固定偏移;基于效果的旋转中心。

音效与音乐

音效(Sound)与特效所处的地位类似。音效是一段较短的、起提示作用的音频。原版中音效应用也比较广泛,如placeSound breakSound destroySound等。和特效一样,这些音频的作用时机也可以从字段名称上看出。

原版中所有可用的音频都存放在mindustry.gen.Sounds。这个类位于mindustry.gen这个包中,说明这个类是在编译过程中由程序自动生成,而非手写由程序员编写的。如果你查阅的源代码使用的是Mindustry项目本身,则你必须运行过一次编译流程才会在core/build/generated/source/kapt/main/mindustry/gen/文件夹下找到这个类的“原件”。如果你是通过IDEA的依赖管理查阅源代码,那么直接就可以在IDEA的依赖项中找到这个类。这个类中的字段是根据core/build/assets/sounds文件夹下的文件名生成的。

要想加载你自己的音效文件,你需要将后缀名为oggmp3的音效文件放入你的项目中的assets/sounds/文件夹下,然后在你想要加载位置填入以下内容,括号里不用填入文件的路径和后缀名:

java
Vars.tree.loadSound("example-sound")

Sound还有一个子类RandomSound,拥有一个sounds属性,可以接受一个Sound的列表。在播放此音效时,会在sounds中随机选择一个音效进行播放。

音乐(Music)与音效的加载过程是类似的,只需要把loadSound变为loadMusic即可。