状态效果和特效
合金气旋,已逝的神
状态效果是Mindustry融合性的一大体现,而炫酷的游戏效果离不开特效与音效的加持。本节将介绍状态效果与特效两者,同时还会介绍音效和音乐的相关内容,
声明一个StatusEffect
状态效果,在其他游戏可能被称为buff,是一种可以应用在单位上,使单位获得增益或减益效果的一种内容类型(Content Type)。你可以按照先前的组织架构,为状态效果新建一个存储文件:
new StatusEffect("tutorial-status-effect-1");StatusEffect("tutorial-status-effect-1")和往常一样,你需要给它分配贴图和文本。
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 =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类中有opposite和affinity字段。不过,这些字段的类型并不是简单的数组或Seq,而是ObjectSet,一种对象的“集合”类型,并且,这个字段只是用于显示,没有实际的冲突或反应功能。若想要设置冲突或反应功能,需要如下的语法:
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);
});
}};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()的第一个参数是另一个状态效果,而第二个参数是完全没有见过的结构,有如下的形式:
(参数列表) -> {函数体}我们把这种表达式称为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方法。此时可以考虑以下两种方式:
//法一:使用匿名类
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的基类构造方法:
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模组中也可以使用:
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文件夹下的文件名生成的。
要想加载你自己的音效文件,你需要将后缀名为ogg或mp3的音效文件放入你的项目中的assets/sounds/文件夹下,然后在你想要加载位置填入以下内容,括号里不用填入文件的路径和后缀名:
Vars.tree.loadSound("example-sound")Sound还有一个子类RandomSound,拥有一个sounds属性,可以接受一个Sound的列表。在播放此音效时,会在sounds中随机选择一个音效进行播放。
音乐(Music)与音效的加载过程是类似的,只需要把loadSound变为loadMusic即可。