跳至正文

创建一个基础城镇NPC

各位读者早午晚上好啊_(:з」∠)_,这里是中学生。之前的城镇NPC教程还停滞于1.3时代而且存在不少疏漏,今天我们就来好好介绍一下如何在1.4 TML 内创建一个基础城镇NPC!如果存在任何建议或疑问都可以随时指出(或者去群里单杀我×)。

!事先声明一点:我可是超强的请确保你观看了前面的基础篇教程,对C#语法以及ModNPC有了一个基础的理解。本篇教程是默认你懂得如何创建ModNPC的。

没看过前置知识的可以在这里走传送门

好了废话不多说,咱们正式开始!

(中学生:来来来各位NPC们又要干活了啊!)


关于分类

在正式学习如何创建之前,我们先来看看原版的城镇NPC可以划分为哪几类,看过了我的1.3版教程的玩家可以选择忽略。

一:按入住方式,可以分为如下四类常驻型旅商型宠物其他型

常驻型,和向导以及原版的绝大多数城镇NPC一样,会入住进房屋,并且在死后会重生的NPC。这是本篇教程的主要讲解对象。

向导,最经典的常驻型NPC,其入住的房屋内会显示带有头像的旗帜

旅商型,和原版的旅商一样,白天有概率刷新,黄昏时如果不在你的视野范围内就会离开的NPC。由于其构成相较常规城镇NPC比较复杂且 Example Mod 的Example Traveling Merchant有提供例子,这里不再多说。

原版唯一的旅商型NPC,也就是旅商本人,注意看城镇列表里没有他的头像

宠物,原版的猫猫狗狗兔兔以及在1.4.4里新加入的各种史莱姆,可以抚摸且可以与常规型城镇NPC同居的NPC。这类NPC在Example Mod内尚无资料,这里不再讲解。

与向导共享一个房间的城镇兔兔,宠物的旗帜为紫色且造型上有别于常规性NPC

其他型,比如原版的地牢老人和骷髅商人这种不同于前两类的NPC(实在没名字可以起了×)。这种NPC较为特殊,但想实现也不难。

骷髅商人,在地下随机刷新,可以交易,离开视野范围就会消失,典型的其他型NPC

二:按攻击方式,可以分为如下五类投掷型远程型魔法型近战型不攻击型

投掷型,原版大多数城镇NPC都属于这个类型。基本特征为四帧攻击动画,发射弹幕且不持有武器。是最基础的NPC攻击类型。

投掷型NPC正在攻击,最典型的即商人

远程型,向导与军火商这类城镇NPC即属于这个类型,基本特征为四到五帧攻击动画但每次只播放一帧,发射弹幕且持有武器。一般而言,使用弓箭的NPC其攻击动画为五帧,而使用枪械的NPC其攻击动画为四帧。绝大多数这类NPC都持有武器但也可以进一步分为两种:像军火商、油漆工这样直接将武器额外绘制在手上的,后文会讲到如何设置;以及像海盗、巫医那样直接将武器画进帧图里的。这里我们主要说的是前者。

远程型NPC正在攻击,最典型的即向导;那把木弓是额外绘制的

魔法型,巫师、裁缝与公主这类城镇NPC属于这个类型,基本特征为两帧攻击动画,发射弹幕,不持有武器但发动攻击时脚下会产生一个(发光发热的)魔法光环,后文会讲到如何设置。

魔法型NPC正在攻击,最典型的即裁缝;他会产生一个紫色的魔法光环

近战型,较为少见的类型,染料商与发型师这类城镇NPC属于这个类型,基本特征为四帧攻击动画,不发射弹幕且持有武器,后文会讲到如何设置。

近战型NPC正在攻击,最典型的即发型师;这类NPC一般只会在敌人非常接近时才发起攻击

不攻击型,所有的城镇宠物都属于这个类型,没有攻击手段,遇见敌怪时只会逃跑。这里不做过多赘述。

为什么要用这么一长串来讲分类?到了后面就明白了。


设置基础属性

进入正题~

首先,创建一个新项,就起名叫TestTownNPC

继承ModNPC类,请确保贴图处在正确的文件路径之下

你可能注意到了我在TeaNPC里做例子上面这个 [AutoloadHead],这一小段代码就是自动加载一个NPC的小地图图标用的,小地图图标的名字为:该NPC的类名_Head(比如TestTownNPC_Head)。

你是否准备齐全了?

按照惯例,先使用SetStaticDefaultsSetDefaults进行基础属性设置,首先来说SetStaticDefaults里需要填的

int npcID = NPCID.Merchant;
public override void SetStaticDefaults()
{
    //游戏内显示的称呼
    DisplayName.SetDefault("测试小伙");

    //总帧数,根据使用贴图的实际帧数进行填写,这里我们直接调用全部商人的数据
    Main.npcFrameCount[Type] = Main.npcFrameCount[npcID];

    //特殊交互帧(如坐下,攻击)的数量,其作用就是规划这个NPC的最大行走帧数为多少,
    //最大行走帧数即Main.npcFrameCount - NPCID.Sets.ExtraFramesCount
    NPCID.Sets.ExtraFramesCount[Type] = NPCID.Sets.ExtraFramesCount[npcID];

    //攻击帧的数量,取决于你的NPC属于哪种攻击类型,如何填写见上文的分类讲解
    NPCID.Sets.AttackFrameCount[Type] = NPCID.Sets.AttackFrameCount[npcID];

    //NPC的攻击方式,同样取决于你的NPC属于哪种攻击类型,投掷型填0,远程型填1,魔法型填2,近战型填3,
    //如果是宠物没有攻击手段那么这条将不产生影响
    NPCID.Sets.AttackType[Type] = NPCID.Sets.AttackType[npcID];

    //NPC的帽子位置中Y坐标的偏移量,这里特指派对帽,
    //当你觉得帽子戴的太高或太低时使用这个做调整(所以为什么不给个X的)         
    NPCID.Sets.HatOffsetY[Type] = NPCID.Sets.HatOffsetY[npcID];

    //这个名字比较抽象,可以理解为 [记录了NPC的某些帧带来的身体起伏量的数组] 的索引值,
    //而这个数组的名字叫 NPCID.Sets.TownNPCsFramingGroups ,详情请在源码的NPCID.cs与Main.cs内进行搜索。
    //举个例子:你应该注意到了派对帽或是机械师背后的扳手在NPC走动时是会不断起伏的,靠的就是用这个进行调整,
    //所以说在画帧图时最好比着原版NPC的帧图进行绘制,方便各种数据调用
    //补充:这个属性似乎是针对城镇NPC的。
    NPCID.Sets.NPCFramingGroup[Type] = NPCID.Sets.NPCFramingGroup[npcID];

    //魔法型NPC在攻击时产生的魔法光环的颜色,如果NPCID.Sets.AttackType不为2那就不会产生光环
    //如果NPCID.Sets.AttackType为2那么默认为白色
    NPCID.Sets.MagicAuraColor[Type] = Color.White;

    //NPC的单次攻击持续时间,如果你的NPC需要持续施法进行攻击可以把这里设置的很长,
    //比如树妖的这个值就高达600
    //补充说明一点:如果你的NPC的AttackType为3即近战型,
    //这里最好选择套用,因为近战型NPC的单次攻击时间是固定的
    NPCID.Sets.AttackTime[Type] = NPCID.Sets.AttackTime[npcID];

    //NPC的危险检测范围,以像素为单位,这个似乎是半径
    NPCID.Sets.DangerDetectRange[Type] = 500;

    //NPC在遭遇敌人时发动攻击的概率,如果为0则该NPC不会进行攻击(待验证)
    //遇到危险时,该NPC在可以进攻的情况下每帧有 1 / (NPCID.Sets.AttackAverageChance * 2) 的概率发动攻击
    //注:每帧都判定
    NPCID.Sets.AttackAverageChance[Type] = 10;

    //图鉴设置部分
    //将该NPC划定为城镇NPC分类
    NPCID.Sets.TownNPCBestiaryPriority.Add(Type);
    NPCID.Sets.NPCBestiaryDrawModifiers drawModifiers = new NPCID.Sets.NPCBestiaryDrawModifiers(0)
    {
        //为NPC设置图鉴展示状态,赋予其Velocity即可展现出行走姿态
        Velocity = 1f,
    };
    //添加信息至图鉴
    NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, drawModifiers);

    //设置对邻居和环境的喜恶,也就是幸福度设置
    //幸福度相关对话需要写在hjson里,见下文所讲
    NPC.Happiness
        .SetBiomeAffection<JungleBiome>(AffectionLevel.Hate)//憎恶丛林环境
        .SetBiomeAffection<UndergroundBiome>(AffectionLevel.Dislike)//讨厌地下环境
        .SetBiomeAffection<SnowBiome>(AffectionLevel.Like)//喜欢雪地环境
        .SetBiomeAffection<OceanBiome>(AffectionLevel.Love)//最爱海洋环境
        .SetNPCAffection(NPCID.Angler, AffectionLevel.Dislike)//讨厌与渔夫做邻居
        .SetNPCAffection(NPCID.Guide, AffectionLevel.Like)//喜欢与向导做邻居
                                                          //邻居的喜好级别和环境的AffectionLevel是一样的
    ;
}

看着很多?没关系,后面还有更多的只是注释比较多罢了。

接下来是SetDefaults里需要填写的,这个不会太复杂。

public override void SetDefaults()
{
    //判断该NPC是否为城镇NPC,决定了这个NPC是否拥有幸福度对话,是否可以对话以及是否会被地图保存
    //当然以上这些属性也可以靠其他的方式开启或关闭,我们日后再说
    NPC.townNPC = true;
    //该NPC为友好NPC,不会被友方弹幕伤害且会被敌对NPC伤害
    NPC.friendly = true;
    //碰撞箱宽,不做过多解释,此处为标准城镇NPC数据
    NPC.width = 18;
    //碰撞箱高,不做过多解释,此处为标准城镇NPC数据
    NPC.height = 40;
    //套用原版城镇NPC的AIStyle,这样我们就不用自己费劲写AI了,
    //同时根据我以往的观测结果发现这个属性也决定了NPC是否会出现在入住列表里,还请大佬求证
    NPC.aiStyle = NPCAIStyleID.Passive;
    //伤害,由于城镇NPC没有体术所以这里特指弹幕伤害(虽然弹幕伤害也是单独设置的所以理论上这个可以不写?)
    NPC.damage = 10;
    //防御力
    NPC.defense = 15;
    //最大生命值,此处为标准城镇NPC数据
    NPC.lifeMax = 250;
    //受击音效
    NPC.HitSound = SoundID.NPCHit1;
    //死亡音效
    NPC.DeathSound = SoundID.NPCDeath1;
    //抗击退性,数据越小抗性越高
    NPC.knockBackResist = 0.5f;
    //模仿的动画类型,这样就不用自己费劲写动画播放了
    AnimationType = NPCID.Merchant;
}

在前面设置了如此多的属性,为的就是不用再亲自写AIFindFrame这两个函数,所以这俩可以不用添加

※ 注意:如果你的NPC在前面的SetStaticDefaults里套用了其他原版NPC的属性诸如 ExtraFrameCount,我建议所有的可套用的属性都套用那个NPC的,因为许多属性都是相互对应的,如果一个NPC的 AttackType 是模仿树妖但 AttackFrameCount 是模仿向导,那极有可能出现帧图错位的情况。再比如你使用的贴图是模仿渔夫画的,但 AnimationType 模仿的是商人,也会导致错位。


设置身份信息

图鉴

填完了基础属性,我们发现他还需要一些在图鉴中展示的信息 … 欸等等?上面不是设置过了吗?

有些信息是不能在SetStaticDefaults里设置的,所以TML才会给我们这个东西。

这里就请出我们下一个要介绍的重写函数:SetBestiary

//设置图鉴内信息
public override void SetBestiary(BestiaryDatabase database, BestiaryEntry bestiaryEntry)
{
    bestiaryEntry.Info.AddRange(new IBestiaryInfoElement[] {
        //设置所属环境,一般填写他最喜爱的环境
        BestiaryDatabaseNPCsPopulator.CommonTags.SpawnConditions.Biomes.Ocean,
        //图鉴内描述
        new FlavorTextBestiaryInfoElement("一个人见人爱,花见花开,月总见了直接裂开的测试小伙")
    });
}

一个常驻型城镇NPC肯定要有自己的名字与入住方式,如果一个NPC没有入住条件,那么在不依靠非正常手段的情况下是不可能遇见他的;如果一个NPC没有姓名那么当邻居在对话中提及他时只能说那个谁谁谁

因此,我们需要为这个NPC添加入住条件姓名

入住条件

城镇NPC的入住条件是依靠一个重写函数实现的:CanTownNPCSpawn

//设置入住条件
public override bool CanTownNPCSpawn(int numTownNPCs, int money)
{
    //返回条件为:拥有两个或以上的城镇居民,并且持有的钱币数量大于等于1金币
    return numTownNPCs >= 2 && money >= Item.buyPrice(0, 1, 0, 0);
}

numTownNPCs 就是当前地图存在的城镇NPC的数量,money 就是玩家持有的钱币数。

当然,你也可以用别的条件判定比如是否击败了克苏鲁之眼、玩家背包内是否存在某样特定物品甚至天气是否为大风天或者玩家是否处在某个特定环境里。这里你可以随意展开写的太复杂被玩家冲了别赖我

姓名

城镇NPC的姓名也是靠一个重写函数实现的:SetNPCNameList

蛤?前面那个 DisplayName 不算数吗?当然不算,那是一个NPC的称呼,比如“向导”、“树妖”,“冰之妖精”;再比如“学生”、“老师”、“司机”等等。

这里指的就是我们普遍意义上的姓名,比如一个向导叫“Colin”,一个树妖叫”Tina”,一个专门霍霍城镇NPC的屑叫“中学生”。当你想要调用时可以使用 NPC.GivenName

//设置姓名
public override List<string> SetNPCNameList()
{
    //所有可能出现的名字
    return new List<string>() {
        "Frisk",
        "Den-o",
        "Tiga",
        "Steve?",
        "Cirno",
        "Cocoa",
        "Lea",
        "Fei",
        "Quirrel"
    };
}

使用一个 List<string> 来决定NPC在每次生成时都会获得一个什么样的名字

通过这串名字你可以窥见这个人的成分到底有多复杂且有多偏僻

性别

一个城镇NPC怎么能没有性别呢你说是吧骷髅商人?只不过我们给城镇NPC设置性别的方法不太寻常,我们是通过决定他是否会被国王雕像传送来决定他的性别的:CanGoToStatue

//决定NPC会被哪座雕像传送
public override bool CanGoToStatue(bool toKingStatue)
{
    //可以被国王雕像传送
    return toKingStatue;
}

toKingStatue 也就是可以被国王雕像传送,这样一来测试小伙就是个男生了;如果你想让她是个女生则可以返回 !toKingStatue,也就是不能被国王雕像传送、但可以被王后雕像传送。如果你想要它无性别,那就直接返回false,双性别(??)则可以直接返回true。

除了这个,我们还可以在NPC被传送到雕像时搞点动作,需要的就是这个函数:OnGoToStatue

//当NPC在被雕像传送时会发生什么
public override void OnGoToStatue(bool toKingStatue)
{
    //在左下角弹出一句话
    if (toKingStatue)
        Main.NewText("测试小伙受到了国王雕像的召唤!");
    else
        Main.NewText("测试小伙受到了王后雕像的召唤!");
}

这样一来,当NPC被传送的那一瞬间,左下角就会出现这句话了。

如果你先前的CanGoToStatue返回了false,那么这段内容将不会被执行。


对话与商店

对话

一个标准的城镇NPC肯定会有属于自己的对话。同样的,TML为我们提供了一个重写函数:GetChat

//设置对话
public override string GetChat()
{
    //声明一个int类型变量,查找一个whoAmI最靠前的、种类为向导的NPC并返回他的whoAmI
    int guide = NPC.FindFirstNPC(NPCID.Guide);
    WeightedRandom<string> chat = new WeightedRandom<string>();
    {
        //当血月和日食都没有发生时
        if (!Main.bloodMoon && !Main.eclipse)
        {
            //无家可归时
            if (NPC.homeless)
            {
                chat.Add("我想我们已经认识对方了,所以....能给个住的地方吗?");
            }
            else
            {
                //自我介绍,NPC.FullName就是带上称呼的姓名,比如“测试小伙Den-o”
                chat.Add($"你好!我是{NPC.FullName}");
                //当查找到向导NPC时
                if (guide != -1)
                {
                    //GivenName上面有提
                    chat.Add($"{Main.npc[guide].GivenName}博学多识,是个人才。");
                }
                //正在举行派对时
                if (BirthdayParty.PartyIsUp)
                {
                    chat.Add("老兄,我最喜欢派对了!");
                }
            }
        }
        //日食时
        if (Main.eclipse)
        {
            chat.Add("我相信你可以挺过去,让我没事就行!");
        }
        //血月时
        if (Main.bloodMoon)
        {
            chat.Add("不!不要血月!!!");
        }
        return chat;
    }
}

这里我们使用了一个叫 WeightedRandom 的类型,这是一个很好用的随机选取器,可以有权重的进行随机选择,想要了解的玩家可以去查看Microsoft的C#官方文档

对话按钮

你想让你的NPC卖点东西?当然可以,不过在直接设置商店之前,我们肯定要为他设置打开商店的方式,也就是对话按钮。这里有两个我们需要认识的重写函数:SetChatButtonsOnChatButtonClicked

首先是SetChatButtons,设置这个NPC的按钮属性。

//设置对话按钮的文本
public override void SetChatButtons(ref string button, ref string button2)
{
    //直接引用原版的“商店”文本
    button = Language.GetTextValue("LegacyInterface.28");
    //设置第二个按钮
    button2 = "按钮2";
}

button 就是第一个按钮,button2 就是第二个。

虽然叫第二个,但实际的排序上“关闭”永远是第二个,button2 是第三个,第四个自然就是幸福度了

接下来是OnChatButtonClicked,设置按钮被按下后会发生什么发生什么事儿了?

//设置当对话按钮被摁下时会发生什么
public override void OnChatButtonClicked(bool firstButton, ref bool shop)
{
    //当第一个按钮被按下时
    if (firstButton)
    {
        //打开商店
        shop = true;
    }
    //如果是第二个按钮被按下时
    else
    {
        //出现一句对话,使用这个属性可以直接设置NPC要说的话。
        Main.npcChatText = "这是第二个按钮哦!";
    }
}

firstButton 就是我们上面刚说的 button,如果我们不做判定,那么这里面所有的操作都会在 buttonbutton2 被按下时执行。

虽然按钮数量有限,但只要通过一定的写法就可以实现按钮的切换,这里不做深入探讨说白了就是懒

按钮说了这么多,终于到了设置商店的部分了。

商店

设置商店需要用到的重写函数为:SetupShop

//设置商店内容
public override void SetupShop(Chest shop, ref int nextSlot)
{
    //将小型血瓶加入商店内
    shop.item[nextSlot].SetDefaults(ItemID.LesserHealingPotion);
    //进入下一个物品栏
    nextSlot++;
    //将小型魔瓶加入商店内
    shop.item[nextSlot].SetDefaults(ItemID.LesserManaPotion);
    nextSlot++;
    //将土块加入商店内
    shop.item[nextSlot].SetDefaults(ItemID.DirtBlock);
    //以一个金币的价格卖出(倒爷说的就是你是吧)
    //如果不对这个进行设置,那么商品的售价默认按照该物品的Item.value设置
    shop.item[nextSlot].value = Item.buyPrice(0, 1, 0, 0);
    nextSlot++;
    //当 击败克苏鲁之眼 且 处于专家模式 时
    if (NPC.downedBoss1 && Main.expertMode)
    {
        //将克苏鲁之盾加入商店内
        shop.item[nextSlot].SetDefaults(ItemID.EoCShield);
        nextSlot++;
    }
}

nextSlot 是一个很重要的参数,代表了下一个商品在格子中的位置,你可以用这个决定下一件商品出现在哪个格子里。不过如果你不写nextSlot++,那么后面设置的商品会直接覆盖上一个设置的商品。一定要注意!

这样一来,你的NPC就可以与你聊天和进行PY交易了!


NPC自护能力

你正在地下挖着矿,突然左下角一条消息弹出:

“测试小伙易者被杀死了…”

这简直是开荒中再常见不过的事了,每次有NPC死亡我们都需要等待他的复活,我们很需要提高他们的生存能力。怎么提高?加血加防御?我们可以换个思路,赋予他还击的手段,毕竟“进攻就是最好的防护”嘛。

这里列出的重写函数较多,我们按照攻击方式分类分开来讲:

常规设置

这部分包含了所有NPC都需要具备的:

TownNPCAttackStrength, NPC的攻击力

//设置NPC的攻击力
public override void TownNPCAttackStrength(ref int damage, ref float knockback)
{
    //伤害,直接调用NPC本体的伤害
    damage = NPC.damage;
    //击退力,中规中矩的数据
    knockback = 3f;
}

TownNPCAttackCooldown,NPC的攻击冷却

//设置每次攻击完毕后的冷却时间
public override void TownNPCAttackCooldown(ref int cooldown, ref int randExtraCooldown)
{
    //基础冷却时间
    cooldown = 60;
    //额外冷却时间
    randExtraCooldown = 30;
}

这两段没啥难点,只要懂得英文单词的意思就能轻易理解。

需要说明的是 randExtraCooldown ,一个NPC的实际冷却时间为等于大于cooldown 而小于 cooldown + randExtraCooldown 这个区间内的一个随机数。如果你想要一个精确的冷却时间那么第二个参数可以不填或者填0。

弹幕设置

这部分包含了投掷型远程型魔法型的通用函数,且全部在近战型不可用

TownNPCAttackProj,也就是设置NPC发射的弹幕。

//设置发射的弹幕
public override void TownNPCAttackProj(ref int projType, ref int attackDelay)
{
    //射弹种类,这里用火枪子弹的弹幕
    projType = ProjectileID.Bullet;
    //弹幕发射延迟,最好只给魔法型NPC设置较高数据
    attackDelay = 10;
}

attackDelay 就是当你的NPC做出攻击动画后需要延迟多少才会实际将弹幕发射出去,比如原版的裁缝在攻击前会先进行大约1s的施法动作(见分类中的GIF)。这个数值是必填的并且要大于等于1,否则NPC将无法发射弹幕。

TownNPCAttackProjSpeed,设置发射弹幕的向量。

//设置发射弹幕的向量
public override void TownNPCAttackProjSpeed(ref float multiplier, ref float gravityCorrection, ref float randomOffset)
{
    //发射速度
    multiplier = 6f;
    //射击角度额外向上偏移的量
    gravityCorrection = 0f;
    //射击时产生的最大额外向量偏差
    randomOffset = 0.5f;
}

gravityCorrection 是什么呢?如果你的弹幕带有重力(比如木箭),那么在发射时可能就需要稍微往上抬一抬,不然就打不准了,这个变量就是让你调整上抬角度的虽然我感觉没啥用

值得一提的是,这个数值似乎是按角度制计算的,并且最大上限大约在120°到140°之间。

randomOffset 偏移的不仅仅是角度,还有射弹的速度,如果你希望创造一个稳定的弹道就不要填这个了。

远程型设置

这个部分的函数仅限远程型NPC使用

TownNPCAttackShoot…一言难尽的函数,TML标注的说明是“告诉游戏你的NPC在单次攻击中已经创造了一个弹幕并且在后面还会创造更多弹幕,这样游戏可以对正确的设置NPC的攻击动画”,但翻阅源码时我愣是看不出它到底发挥了什么作用,TML也没有提供什么专门创造多重射弹的钩子…有搞懂了的可以去群里扣我,十分感谢。

不过难道我们就无法像蒸汽朋克人或巫师那样一次创造多个弹幕了吗?当然不是不行,日后我们会说说怎么用最简单的办法来实现。

DrawTownAttackGun,顾名思义,它负责控制绘制在NPC手上的武器,当然如果你选择把武器画进帧图里可以不带这个。

//设置绘制在NPC手上的远程武器
public override void DrawTownAttackGun(ref float scale, ref int item, ref int closeness)
{
    //贴图尺寸
    scale = 1f;
    //贴图样式,这里用燧发枪
    item = ItemID.FlintlockPistol;
    //绘制位置到NPC中心的距离,数值越大越靠近
    closeness = 6;
}

item 这个参数是 Item 类型的,也就是说你必须拥有一个物品实例而不能只有一张贴图。

魔法型设置

这个部分的函数仅限魔法型NPC使用

TownNPCAttackMagic,调整施法时脚下光环的亮度。

//设置NPC施法时魔法光环的亮度
public override void TownNPCAttackMagic(ref float auraLightMultiplier)
{
    //光亮程度,这里写2倍
    auraLightMultiplier = 2f;
}

看起来挺没用的就…也不是完全没用,谁不喜欢一个超级手电筒呢?×

近战型设置

这个部分的函数仅限近战型NPC使用,且其他类型的NPC不可用

TownNPCAttackSwing,用来设置NPC挥动武器时的实际碰撞箱

//设置NPC挥动武器时的实际碰撞箱
public override void TownNPCAttackSwing(ref int itemWidth, ref int itemHeight)
{
    //宽与高,合称尺寸
    itemWidth = itemHeight = 80;
}

DrawTownAttackSwing,用来设置绘制该武器时的各种属性。

//设置绘制在NPC手上挥动的武器
public override void DrawTownAttackSwing(ref Texture2D item, ref int itemSize, ref float scale, ref Vector2 offset)
{
    //贴图样式,破坏者大剑hhh
    int id = ItemID.BreakerBlade;
    //如果选择使用原版贴图,这里必须要提前Load一遍,否则贴图将无法呈现
    Main.instance.LoadItem(id);
    item = TextureAssets.Item[id].Value;
    //碰撞箱尺寸,用于调整持握位置
    itemSize = 80;
    //贴图挥动起点到NPC中心的距离比例,值太大可能会导致武器脱手
    //!注意这不是调整贴图大小的
    scale = 0.1f;
    //绘制偏移量,绝对值越大距离NPC越近
    offset = new Vector2(2 * -NPC.direction, 0);
}

这里的 item 参数是 Texture2D 类型,也就是说只要有贴图就可以使用。

为什么这里又有一个 itemSize?是不是和上一个函数重复了?不是的。这个参数是用来规划挥动时贴图的旋转与位置等诸多信息的。一般而言这个数据需要和贴图尺寸对应。

scale,一个欺骗性极强的参数。它的实际用途是调整武器的挥动起点到NPC的中心的距离比例,比如说铁剑,它调整的就是剑柄位置到NPC的中心的距离比例。如果这个值过大会导致武器挥舞起来和NPC的手产生脱节。此外,它不能调整贴图的大小,一定要注意!

offset 那里进行了一个 * -NPC.direction 的操作,因为这个偏移量不会根据你的NPC的朝向而自动调整

设置完毕

好了!经过上面这些设置,我们的测试小伙终于拥有了还击的能力。无论他是一个战士、射手还是个法师,他都可以更好的保护自己了!

(测试小伙在水中溺亡…)

………..

(╯‵□′)╯︵┻━┻


幸福度对话

我们都知道大多数城镇NPC都会有幸福度对话(也就是 “快乐” 按钮)。但似乎并没有一个为之提供的重写函数?这是因为幸福度对话是自动获取的,你需要在hjson文件里提前写好。

所谓hjson,就是现阶段进行文本翻译时使用的、类似json的文件。其文件名称决定了语言版本,英文就是en-US.hjson,中文就是zh-Hans.hsjon。这里我们仅使用中文作为示例,首先建立一个名为Localization的文件夹用于存放翻译文件,文件夹位置无固定要求:

大概就像这样

由于 Hjson 和 json 并不同,一般情况下VS并不能对其进行高亮显示。这无疑是一个很要命的问题,因为Hjson中出现格式错误会导致Mod编译失败。那咋办?我们可以使用 Visual Studio Code 读取 Hjson。

首先肯定是要下载一个 Visual Studio Code。然后,在左侧的一列图标中点击最下面的 “拓展” 选项。搜索 “Hjson”,即可找到相应的插件,然后下载该插件,即可对 Hjson 进行高亮显示。

VSC,你好强大

很遗憾目前为止VS还没有任何与 Hjson 有关的拓展插件,我们要是需要使用 Hjson 大概只能双开了。

更多关于Hjson的讲解从这里跳转。

前面准备就绪,接下来打开你的 zh-Hans.Hjson。需要输入的内容如下:

TownNPCMood_Princess: 
{
	//公主关于测试小伙的对话
	LoveNPC_TownNPCExample/TestTownNPC: 
	'''
	{NPCName}愿意为他人传授知识,我很欣赏他!
	'''
}
Mods: 
{
	TownNPCExample: 
    {
		TownNPCMood: 
		{
		    TestTownNPC: 
			{
				//正好满足
				Content: 现在的一切都恰到好处。
				//需要房屋
				NoHome: 我想我需要一个家。
				//不拥挤
				LoveSpace: 我喜欢这样充足的个人空间。
				//离家太远
				FarFromHome: 我似乎离家太远了。
				//略微拥挤
				DislikeCrowded: 这里的人有点多...
				//十分拥挤
				HateCrowded: 这里的人太多了!
				//喜欢公主的对话
				LikeNPC_Princess: 
				'''
				{NPCName}深受大家欢迎,包括我。
				'''
				//喜爱的环境
				LoveBiome: 
				'''
				{BiomeName}宽广无垠,我爱这里。
				'''
				//喜欢的环境
				LikeBiome: 
				'''
				我很喜欢{BiomeName}银装素裹的美景。
				'''
				//讨厌的环境
				DislikeBiome: 
				'''
				{BiomeName}非常昏暗,我不喜欢这里。
				'''
				//厌恶的环境
				HateBiome: 
				'''
				{BiomeName}又潮湿又闷热,真是糟糕透了!
				'''
				//喜欢的NPC
				LikeNPC: 
				'''
				{NPCName}见多识广,我很崇拜他。
				'''
				//不喜欢的NPC
				DislikeNPC: 
				'''
				{NPCName}太喜欢恶作剧了,我不想和他当邻居。
				'''
			}
		}
	}
}

看着很长,实际上只是缩进比较大开大合。

Mods \ 模组类名 \ TownNPCMood \ NPC类名 这个路径一定要对,如果你不知道哪些选项该怎么写可以在不加入Hjson的情况下点击NPC的幸福度对话查看路径。


展示我们的测试小伙

经过我们的不懈努力,我们的测试小伙终于可以出现在这个世界上了!让我们一起来看看吧:

一位叫Lea的测试小伙,材质来自ExampleMod
关于自己的对话
关于向导的对话
按下第二个按钮后
幸福度对话
商店一览
攻击敌怪(手搓子弹?)
派对粉我们后续会说如何“正确”的切换派对材质

拓展与延伸

我们在做Mod时,最重要的就是不要被既有的内容所束缚,在学会了翻阅源码的情况下我们会很轻松的发现这些函数的其他“非主流”用途。

举个例子:TownNPCAttackStrength是会在一个NPC进行攻击时持续执行的,也就是说你完全可以把它当作一个只在NPC攻击时执行的AI,进而搞出一些骚操作来。比如:

多重弹幕,当然我在这里使用的是一种比较麻烦的办法

即使正常来说近战型NPC无法发射射弹,我们依旧可以在TownNPCAttackSwing里动手脚以实现发射剑气的效果。

另外补充一些城镇NPC的冷知识:

1、城镇NPC的攻击计时器是.localAI[3],稍微运用一下可以轻易做到蒸汽朋克人那样的三连发效果。

2、NPC的.ai[0]在攻击时的状态会根据NPC的攻击类型的不同而产生区别,投掷型NPC在攻击时的ai[0]为10、远程型为12,魔法型为14,近战型为15。灵活运用的话可以实现更多仅依靠模板做不到的效果。

3、城镇NPC存在一个“自杀开关”,当你把一个城镇NPC的.ai[3]设置为1时,他就会暴毙自动死亡,原版地牢老人变身为骷髅王时就是用了这个。


总结

只要你有足够的想法,加上扎实的基础开阔的思维,一定可以在这套模板之上开发出更多有意思的事物。这篇教程的作用就是为你提供基础,并且在一定程度上开拓一下思维。相信你能够打造出自己满意的城镇NPC!

本篇完结之后,我会考虑后续跟进几篇进阶教程,诸如如何创建一个旅商NPC或者如何绘制城镇NPC贴图等(提前挖坑填不填随机),如果条件允许,那就敬请期待后续的文章吧~我是中学生,一个专门霍霍城镇NPC的屑仍在学习的模组作者,我们下次再见_(:з」∠)_

《创建一个基础城镇NPC》有3个想法

  1. Pingback: 认识ITownNPCProfile - 裙中世界

  2. Pingback: 如何绘制城镇NPC贴图 - 裙中世界

  3. Pingback: 创建一个骷髅商人NPC - 裙中世界

发表回复