跳至正文

召唤武器实战:僚机(1.4再版)

来移植了,下方源码文件。

介绍

这次教程我打算写一个召唤武器的制作方式,这个召唤武器来自四十九落星的僚机法杖。这个法杖经过我的魔改以后放进了模板MOD里面,源码在github上可以看到。但是本教程主要想讲的是一个简单的召唤武器的制作过程,从构思状态图到最后编写出来的全过程。

魔改了的僚机召唤杖
效果展示

召唤武器其实本质上就是物品带个智能一点的弹幕,但是为了表示玩家的召唤物的状态,我们还需要一个Buff以及一个自定义玩家属性来保证召唤物能正常工作以及消失。

// 自定义玩家属性
public class GliderPlayer : ModPlayer
{
    public bool Gliders;
    public override void ResetEffects()
    {
        Gliders = false;
    }
}

状态图

制作一个比较复杂的AI最好能先从它的状态图入手,分析一下这个弹幕在每个状态下的行为。我们先从这个召唤物的状态图说起,从图中我们可以看出这个召唤物会不断追着敌人射出弹幕,但是又不会靠的太近。于是我们可以画出一个这样的状态图:

一个召唤物的状态图

对了,这个状态图是用draw.io这个网站绘制的,比之前PPT绘制的方便,好看多了。

物品参数

有了状态图就可以应用在弹幕上了,但是在此之前我们还有一些准备工作。大家如果去GliderStaff这个文件里看就会发现召唤武器有几个属性是需要设置的:

Item.DamageType = DamageClass.Summon;// 召唤伤害类型
//Item.buffType = ModContent.BuffType<GliderBuff>();
//Item.buffTime = 3600;
Item.shoot = ModContent.ProjectileType<GliderPro>();
Item.shootSpeed = 10f;

buffType 和 buffTime,会在武器使用的时候给玩家加上一个3600帧的召唤物buff,这个值设置成多少都行(设置成0当我没说),因为我们会在buff里面调整成无限存活时间。这个buff重要的地方在于玩家可以通过取消这个buff来让召唤物消失。

逻辑是这么个逻辑,但是按照上方这么写会导致这个武器上显示“一分钟持续时间”的介绍(乐),所以我们把这个添加buff改成放到召唤物AI中去。

接下来就是buff的代码了:

public class GliderBuff : ModBuff
{
	public override void SetStaticDefaults()
	{
		DisplayName.SetDefault("僚机部队");
		Description.SetDefault("僚机群会为你战斗");
		Main.buffNoSave[Type] = true;
		// 不显示buff时间
		Main.buffNoTimeDisplay[Type] = true;
	}

	public override void Update(Player player, ref int buffIndex)
	{
		GliderPlayer modPlayer = player.GetModPlayer<GliderPlayer>();
		// 如果当前有属于玩家的僚机的弹幕
		if (player.ownedProjectileCounts[ModContent.ProjectileType<GliderPro>()] > 0)
		{
			modPlayer.Gliders = true;
		}
		// 如果玩家取消了这个召唤物就让buff消失
		if (!modPlayer.Gliders)
		{
			player.DelBuff(buffIndex);
			buffIndex--;
		}
		else
		{
			// 无限buff时间
			player.buffTime[buffIndex] = 9999;
		}
	}
}

Buff代码没什么特别的,就是维护modPlayer.Gliders这个属性,代表玩家有没有开启僚机召唤物。接下来,我们就可以进入重头戏弹幕的编写了。

召唤物弹幕

我写弹幕什么的一般不会套用任何原版aiStyle以及aitype,所以大家写的时候也要注意,不要让原版AI影响了你的代码。

召唤物的初始设定:

public override void SetDefaults()
{
    Projectile.width = 16;
    Projectile.height = 16;
    Projectile.friendly = true;
    Projectile.aiStyle = -1;
    Projectile.timeLeft = 3;
    Projectile.penetrate = -1;
    Projectile.ignoreWater = true;
    Projectile.tileCollide = false;
    Projectile.scale = 1.1f;
    // 召唤物必备的属性
    //Main.projPet[Type] = true;
    Projectile.netImportant = true;
    Projectile.minionSlots = 1;
    Projectile.minion = true;
    ProjectileID.Sets.MinionSacrificable[Type] = true;
    ProjectileID.Sets.CultistIsResistantTo[Type] = true;
}

你会发现召唤物弹幕的设定要比其他弹幕复杂一些,有一些必要的属性比如 minion ,可以防止弹幕因为跟玩家距离太远被刷掉,minionSlots 决定召唤物占用几个召唤物位置,设置的比较大能召唤出来的数量就会比较少。

projPet会让TR系统标记这个弹幕为宠物,而且会屏蔽召唤物的接触伤害(因为僚机按理来说不应该有接触伤害)。当然这个属性也可以通过

public override bool MinionContactDamage()
{
    return false;
}

来改变。剩下两个属性都是TR系统定义的,这里就不多说了,有兴趣的同学可以通过TR源码来解读。

召唤物AI

召唤物的AI其实还应该有一个检测玩家是否激活了召唤物的判断,如果玩家关闭了这个召唤物,那么这个弹幕应该立即消失。代码如下:

Player player = Main.player[Projectile.owner];
var modPlayer = player.GetModPlayer<GliderPlayer>();
// 玩家死亡会让召唤物消失
if (player.dead)
{
    modPlayer.Gliders = false;
}
if (modPlayer.Gliders)
{
    // 如果Gliders不为true那么召唤物弹幕只有两帧可活
    Projectile.timeLeft = 2;
}
player.AddBuff(ModContent.BuffType<GliderBuff>(), 2);// 把之前说的添加buff放在这里

然后我们就可以检测周围敌人并且运行我们的状态图了。这次我还给召唤物加了一个功能:如果拿着法杖右键可以改变召唤物的射击地点。

可控打击

这个的实现方法就是给弹幕加上一个Vector2变量,每次寻找最近NPC的时候先判断这个变量是否有值,如果有值就认为找到了打击目标,弹幕射击的时候也按照这个目标去射击。这样做就要避免直接用NPC是否为空来判断目标,而是和这个新变量联合判定。

实现右键改变位置也很简单,只要 AI 重写函数里面写:

// 这段在弹幕主人的客户端执行,需要发给服务器端来同步给其他客户端
if (Main.myPlayer == Projectile.owner)
{
    if (Main.mouseRight)
    {
        // 传入目标位置
        TargetLocation = Main.MouseWorld;
        Projectile.netUpdate = true;
    }
    if (TargetLocation != Vector2.Zero && Main.mouseRightRelease)
    {
        TargetLocation = Vector2.Zero;
        Projectile.netUpdate = true;
    }
}

关于这里的联机同步,只要给这个传入的位置写上Send和Recieve就好。

// 是写在GliderPro里
public override void SendExtraAI(BinaryWriter writer)
{
    writer.WriteVector2(TargetLocation);
}
public override void RecieveExtraAI(BinaryWriter reader)
{
    TargetLocation = reader.ReadVector2();
}

要注意,由于MOD弹幕一定继承于 ModProjectile 这个基类,所以将 projectile.modProjectile 强制转换为该MOD弹幕的类型(这里是GliderPro )是可以得到这个MOD弹幕的实例的。这算是一个非常有用的小技巧,可以用来实现很多骚操作。为了让召唤物接近地方目标的时候更加平滑,我们仍然可以参考跟踪弹幕那一章的算法,通过渐进的方式把弹幕的速度变成追击速度。弹幕攻击完毕回到玩家身边也可以用这个算法,这样召唤物的运动不会那么突兀。由此可见,这个渐进算法用途是真的广。

Projectile.velocity = (Projectile.velocity * 20f + diff * 5) / 21f;

最后再介绍一个很有用的小AI,接近玩家的写法。有时候我们不希望弹幕接近玩家以后速度就减慢到与玩家重合,我们更希望接近玩家以后能够刹不住车的越过玩家,然后重新接近玩家,如图:

这应该怎么起名……临近?

这样能让召唤物行为更接近TR的标准召唤物行为,当然你也可以让它停留在玩家身边,甚至以一定的阵型。

如果不想以射击的方式攻击而是近战冲刺敌人,那么就要把接触伤害打开,并且自己实现冲刺的代码。像星辰龙的攻击方式其实本质上就是冲刺,只不过判定范围以及弹幕的AI更加复杂,但是其实原理相当简单。

其他的一些基本实现细节,比如发射弹幕,状态机实现什么的,我就不仔细讲了,如果有兴趣可以去看TemplateMod的源码,能看到这里的同学对于这些内容都基本能掌握的吧。

我以后的教程都会主要讲思路而不是具体实现了,如果对具体实现不太了解的同学可以趁着这段时间好好练习一下自己的实现能力。

希望大家能够多动手实践,只有多写,多思考,才能成为出色的MOD制作者!

《召唤武器实战:僚机(1.4再版)》有2个想法

  1. 在召唤物弹幕的SetStaticDefaults()里加上ProjectileID.Sets.MinionTargettingFeature[Type] = true;可以让发射物为这个召唤物的法杖可以直接用右键标记敌人

发表回复