在泰拉瑞亚中,Dust是一种很常用的视觉效果,很多时候使用原版的粒子就能够有非常不错的效果了,但是或许你会想要有自己独特的效果,于是就可以使用ModDust
来制作。
需要注意的是本文中大部分时候提到的粒子都是指Dust。在泰拉中有另外一种粒子叫做Particle,关于它的内容会在结尾处稍微提一下。
本次教程中所使用到的粒子贴图是长这个样子的↓↓↓↓↓
创建一个ModDust
就和创建物品,弹幕或者其他东西那样,新建一个文件,然后写下你的类名,再继承一下ModDust
这个类。
public class MyDust : ModDust { }
对于模组粒子来说,最常用的东西是OnSpawn
和Update
这两个方法。
OnSpawn
在粒子生成时被调用,所以可以在其中设置Dust
中一些字段的初始值,具体的如下。
public override void OnSpawn(Dust dust) { //类似于弹幕的aiStyle和AIType,把这个东西赋值为粒子类型就能使用对应的原版粒子的AI //在本期教程内不会用到这个东西,都是直接重写粒子AI的所以让它是默认值-1就行。 //UpdateType = -1; //粒子是否受到重力效果,只有启用了原版的粒子AI,并且这个原版的粒子AI使用了这个东西的情况下才有用 //如果Update返回false了那么需要自己手动判断然后更改速度。 dust.noGravity = true; //粒子的颜色 dust.color = Color.White; //粒子的旋转角度 dust.rotation = 0; //粒子的帧图,用在绘制里 //同时这里的Texture2D属性就是粒子的贴图,它会自动加载不需要自己给它赋值 dust.frame = Texture2D.Frame(1,6,0,0); //粒子使用的盔甲shader,这里就不多展开细讲了,想了解如何使用的可以看它的注释或查看源码 //一些粒子能够使用玩家装备的染料的shader,就是设置了这个东西 //dust.shader }
在生成了之后就是要更新这个粒子了,来到Update
方法中,其实在这里面就和写弹幕AI比较类似,只不过可用的东西少一些。现在让这个粒子原地自转一下并不断修改它的帧图吧。
public override bool Update(Dust dust) { //旋转每帧增加0.05f dust.rotation += 0.05f; //fadeIn这个东西可以把它当成计时器来用,当然也不只是能当成计时器,有需要的可以自行修改。 dust.fadeIn++; //每5帧变更一下帧图 if (dust.fadeIn % 5 == 0) { dust.frame.Y += Texture2D.Height() / 6; if (dust.frame.Y >= Texture2D.Height()) dust.frame.Y = 0; } if (dust.fadeIn > 60 * 3)//设置3秒钟后消失。原版的粒子基本上都是scale小于一定值就自动消失的 dust.active = false;//直接让粒子消失 return false; }
这样一个基础的粒子就做好了,我们来尝试生成一下这个粒子。我在一个测试物品的CanUseItem
这个方法中生成粒子,你可以在你喜欢的地方来生成它。要想获取模组粒子的type
和其他的弹幕物品什么的类似,使用ModContent.DustType
。
//在鼠标位置生成一个粒子,缩放调到了2来方便看得更清楚 Dust.NewDustPerfect(Main.MouseWorld, ModContent.DustType<MyDust>(), Scale: 2);
接下来进入游戏来看看效果吧!
看上去,似乎旋转中心不太对劲?这是因为原版默认粒子是8*8大小的,旋转中心写死在了(4, 4),对于这个问题我们可以重写PreDraw
自己绘制来解决。(1.4.4的tml才增加的这个,以前都是莫有的)
public override bool PreDraw(Dust dust) { Texture2D dustTex = Texture2D.Value; //这里偷个懒,直接拿粒子的位置当作中心来用 var position = dust.position - Main.screenPosition; //使用粒子中心的位置获取亮度 Color lightColor = Lighting.GetColor((int)dust.position.X / 16, (int)dust.position.Y / 16); //粒子里也有GetAlpha这个重写方法,有需要的可以自行修改并且在此调用而不是用这个东西 // 我这里是偷懒了( 👇 Main.EntitySpriteDraw(dustTex, position, dust.frame, dust.color.MultiplyRGBA(lightColor) , dust.rotation, dust.frame.Size() / 2, dust.scale, SpriteEffects.None); return false; }
如果你不知道上面这一堆是在写些什么东西的话我建议先看一看绘制教程和绘制教程2号。
再进入到游戏内就可以发现粒子现在能够正常的自转了。
来点特殊的行为
一个粒子就这样原地旋转似乎是有些太单调了,于是在这里来做一下特殊的效果吧!
原版的部分粒子是能够和物块产生一些互动的效果的,比如雪粒子,让我们来康康源码是怎么写的。
于是经过一些小调整我们的Update
里面就变成了这样
public override bool Update(Dust dust) { //旋转每帧增加0.2f //dust.rotation += 0.05f; //fadeIn这个东西可以把它当成计时器来用,当然也不只是能当成计时器,有需要的可以自行修改。 dust.fadeIn++; //每5帧变更一下帧图 if (dust.fadeIn % 5 == 0) { dust.frame.Y += Texture2D.Height() / 6; if (dust.frame.Y >= Texture2D.Height()) dust.frame.Y = 0; } if (dust.fadeIn > 60 * 3)//设置3秒钟后消失。原版的粒子基本上都是scale小于一定值就自动消失的 dust.active = false;//直接让粒子消失 //如果粒子不是无视重力的就给它加一点Y方向速度 if (!dust.noGravity) dust.velocity.Y += 0.05f; //检测碰撞 if (Collision.SolidCollision(dust.position - Vector2.One * 5f, 10, 10)) { dust.scale *= 0.9f; dust.velocity *= 0.25f; } //由于我们直接把原版的粒子AI给阻挡掉了所以需要自己更新粒子位置,千万别忘了这一点!!!! dust.position += dust.velocity; return false; }
需要注意的是我这里取消了粒子更新里的自转并且在OnSpawn
里将粒子旋转随机赋值了。
并且删掉了OnSpawn
里的对dust.noGravity
的赋值,别忘了这个!!
接下来就是生成粒子时候的调整了,Dust.NewDustPerfect
直接返回Dust实例,可以直接使用,如果使用的是Dust.NewDust
的话返回的就是这个粒子在Main.dust
数组里的索引,这一点和弹幕基本一致。
//于是在生成粒子的时候就变成了这样,不过其实noGravity默认就是false Dust dust = Dust.NewDustPerfect(Main.MouseWorld, ModContent.DustType<MyDust>(), Scale: 2); dust.noGravity = false;
进入游戏看一下效果叭~
(一个小小的冷知识就是原版在生成粒子时会随机给速度赋值,就像图里一样明明没有在生成时改粒子的速度但它却有一个随机的速度)
接下来制作一个能够跟随玩家的粒子吧,就像原版的魔力值恢复满的时候生成的那个粒子一样,需要使用粒子中一个特殊的东西叫做customData
,它是一个object
类型的变量,可以直接使用它来传入玩家。理论上来说甚至能用它来记录粒子位置,做出有真正的拖尾的粒子。
爆改一下Update
,效果如下。
public override bool Update(Dust dust) { dust.fadeIn++; //每5帧变更一下帧图 if (dust.fadeIn % 5 == 0) { dust.frame.Y += Texture2D.Height() / 6; if (dust.frame.Y >= Texture2D.Height()) dust.frame.Y = 0; } if (dust.fadeIn > 60 * 3)//设置3秒钟后消失。原版的粒子基本上都是scale小于一定值就自动消失的 dust.active = false;//直接让粒子消失 //如果粒子不是无视重力的就给它加一点Y方向速度 if (!dust.noGravity) dust.velocity.Y += 0.05f; //这里使用玩家位置减去玩家的旧位置来更新粒子的位置 if (dust.customData != null && dust.customData is Player player) dust.position += player.position - player.oldPosition; return false; }
这样的话在生成粒子的时候就需要多手动传入一个东西了,并且我们让粒子在玩家中心附近随机生成。
Dust dust = Dust.NewDustPerfect(player.Center+Main.rand.NextVector2Circular(40,40) , ModContent.DustType<MyDust>(), Scale: 2); dust.customData = player;//这个player是CanUseItem提供的
进入游戏查看效果,如下。
其他杂项
MidUpdate
这个重写方法,只有在Update
返回true
的时候才有可能调用到这个东西,如果返回false
那么这个粒子的速度每帧就会乘以0.99f,就……没啥大用。
ModDust
里的Texture
属性直接给它重写赋值成null
的话就会让这个粒子使用原版的粒子贴图。
使用ChildSafety.SafeDust
可以判断这个粒子是否是个血腥的粒子,为false
的话就会在更新中将它变为云的粒子或者直接让它active
变为false
,源码里是这样写的
真正的“粒子”?
其实Dust,一般来说叫做灰尘,对于粒子,一般叫做Particle。
原版的Particle需要使用和粒子完全不一样的东西生成,是使用ParticleOrchestrator.RequestParticleSpawn
来生成,但是很不幸的是目前并没有ModParticle这样的东西,所以只能用原版的。
对于使用原版的Particle以及所有Particle的样子可以看这个链接~
尾声
为什么一直没有ModDust
的教程呢?因为它确实是非常简单(
简单到自己看一看ExampleMod就能解决的程度
所以,下期见~
额外挑战
试试来写一个带拖尾的粒子!
如果有兴趣可以试着自己钻研一下,主要内容其实都是绘制,和粒子什么的已经关系不大了。这里的拖尾使用的是一张黑底的灰度图。
参考代码
public override void OnSpawn(Dust dust) { dust.color = Color.White; dust.frame = Texture2D.Frame(1, 6, 0, 0); Vector2[] oldPos= new Vector2[20]; for (int i = 0; i < oldPos.Length; i++) oldPos[i] = dust.position; dust.customData = oldPos; } public override bool Update(Dust dust) { dust.fadeIn++; //每5帧变更一下帧图 if (dust.fadeIn % 5 == 0) { dust.frame.Y += Texture2D.Height() / 6; if (dust.frame.Y >= Texture2D.Height()) dust.frame.Y = 0; } if (dust.fadeIn > 60 * 5) dust.active = false; dust.velocity = Vector2.SmoothStep(dust.velocity, (Main.MouseWorld - dust.position).SafeNormalize(Vector2.Zero) * 8, 0.4f); dust.rotation = dust.velocity.ToRotation()+MathHelper.PiOver2; dust.position += dust.velocity; //更新拖尾数组 if (dust.customData is Vector2[] oldPos) { for (int i = 0; i < oldPos.Length - 1; i++) oldPos[i] = oldPos[i + 1]; oldPos[^1] = dust.position; dust.customData = oldPos; } else dust.active = false; return false; } public override bool PreDraw(Dust dust) { Texture2D Texture = ModContent.Request<Texture2D>("ExampleUI/Trail").Value; //这个CustomVertexInfo,在很多拖尾绘制教程里都有,就不再写一遍了 List<CustomVertexInfo> bars = new List<CustomVertexInfo>(); //随便填的颜色 Color c = new Color(255, 174, 33) * 0.9f; c.A = 0; if (dust.customData is Vector2[] oldPos) { int trailCachesLength = oldPos.Length; for (int i = 0; i < trailCachesLength - 1; i++) { float factor = (float)i / trailCachesLength; Vector2 Center = oldPos[i] - Main.screenPosition; Vector2 normal = (oldPos[i + 1] - oldPos[i]).SafeNormalize(Vector2.Zero).RotatedBy(MathHelper.PiOver2); Vector2 Top = Center + normal * 14; Vector2 Bottom = Center - normal * 14;//<---这样的魔法数字写法只是偷懒,不建议学习 var Color = c * factor; bars.Add(new(Top, Color, new Vector3(factor, 0, 0))); bars.Add(new(Bottom, Color, new Vector3(factor, 1, 0))); } Vector2 normal2 = (dust.rotation + 1.57f).ToRotationVector2(); bars.Add(new(dust.position-Main.screenLastPosition + normal2 * 14, c, new Vector3(1, 0, 0))); bars.Add(new(dust.position - Main.screenLastPosition - normal2 * 14, c, new Vector3(1, 1, 0))); Main.graphics.GraphicsDevice.Textures[0] = Texture; Main.graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, bars.ToArray(), 0, bars.Count - 2); Main.graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, bars.ToArray(), 0, bars.Count - 2); } Texture2D dustTex = Texture2D.Value; var position = dust.position - Main.screenPosition; Color lightColor = Lighting.GetColor((int)dust.position.X / 16, (int)dust.position.Y / 16); Main.EntitySpriteDraw(dustTex, position, dust.frame, dust.color.MultiplyRGBA(lightColor) , dust.rotation, dust.frame.Size() / 2, dust.scale, SpriteEffects.None); return false; }
请问customData究竟是什么啊?
如果没有设置customData的话,粒子就不能跟着玩家跑了吗,前面不是已经将玩家位置方向赋给了dust了吗,那么这与customData有什么关系吗?
或者说customData就是一个单纯用来存数据的字段吗?