跳至正文

创建一个骷髅商人NPC

各位读者早午晚上好啊_(:з」∠)_,这里是中学生。

你是否想过要创造一个 “流浪商人” 或 “神秘商人” 这种,可以随机遇见的,像原版的骷髅商人那样的NPC?但直接塞一个常规城镇NPC肯定行不通,这一篇,我们将讲解一下如何创建一个 “类骷髅商人” NPC。

!注意:讲解之前请确保你已熟读这篇文章,因为本篇的很多内容都是基于这篇文章讲解的。

废话不多说,开始正片!


特征

如果骷髅商人的构成和常规城镇NPC一样,也就不会单独拉出来讲,所以我们首先要认识一下骷髅商人的独有特征以及其与常规NPC的区别,这样我们才能更好的理解骷髅商人的写法。

特征一:没有小地图头像

骷髅商人并不会呈现在地图上,这也算是游戏常识。

特征二:不会被地图保存

骷髅商人的一个特征就是如果你退出地图后,它都会像普通生物一样消失不见。这就和一直存在于地图上的常规城镇NPC很不一样了。

特征三:刷新机制不一样

骷髅商人遵循和普通生物一样的刷新机制,在特定环境下随机刷新,离开屏幕太远会消失,并且不会像常规NPC一样达成一定条件后入住。

看完这三条你就会发现:骷髅商人不就是个可以对话、可以卖东西的普通生物么?只要把城镇NPC中关于对话和商店的部分搬过去不就好了?可没有那么简单。接下来,咱们进入实操环节。


设置基础属性

首先,创建一个新项,我们这次叫他 TestSkeletonMerchant

熟悉的开头

你发现 [AutoloadHead] 不见了?毕竟骷髅商人不需要这个功能,自然也就不会用到。

首先我们来看看 SetStaticDefaults 里的变化:

int npcID = NPCID.Merchant;
public override void SetStaticDefaults()
{
    DisplayName.SetDefault("测试骷髅小伙");
    Main.npcFrameCount[Type] = Main.npcFrameCount[npcID];
    NPCID.Sets.ExtraFramesCount[Type] = NPCID.Sets.ExtraFramesCount[npcID];
    NPCID.Sets.AttackFrameCount[Type] = NPCID.Sets.AttackFrameCount[npcID];
    NPCID.Sets.AttackType[Type] = NPCID.Sets.AttackType[npcID];
    NPCID.Sets.HatOffsetY[Type] = NPCID.Sets.HatOffsetY[npcID];
    NPCID.Sets.NPCFramingGroup[Type] = NPCID.Sets.NPCFramingGroup[npcID];
    NPCID.Sets.AttackTime[Type] = NPCID.Sets.AttackTime[npcID];
    NPCID.Sets.DangerDetectRange[Type] = 500;
    NPCID.Sets.AttackAverageChance[Type] = 10;

    //让一个NPC在AI、动画等方面表现的像一个城镇NPC,但是并非一个标准城镇NPC,
    //如果你想要实现骷髅商人那种可以对话并交易,但是不会出现在入住菜单里、没有幸福度选项、且不会出现在小地图上的NPC,
    //可以在 NPC.townNPC 设置为 false 时将此选项设置为true。
    //若 NPC.townNPC 设置为 true。则这条将不再需要设置。
    NPCID.Sets.ActsLikeTownNPC[Type] = true;

    //该NPC生成时是否会顺便生成一个GivenName,
    //如果设置了false,那么该NPC生成时将会和普通NPC一样只显示DisplayName
    NPCID.Sets.SpawnsWithCustomName[Type] = true;

    NPCID.Sets.TownNPCBestiaryPriority.Add(Type);
    NPCID.Sets.NPCBestiaryDrawModifiers drawModifiers = new NPCID.Sets.NPCBestiaryDrawModifiers(0)
    {
        Velocity = 1f,
    };
    NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, drawModifiers);
}

由于骷髅商人不需要幸福度选项,我们也就不用再为他设置 NPC.Happiness

乍一看好像没啥变化,但这次出现了两个新的属性: ActsLikeTownNPCSpawnsWithCustomName。咱们分别来说

首先是 ActsLikeTownNPC,顾名思义,“让NPC表现得像一个城镇NPC”。这里特指允许让该NPC拥有类似城镇NPC的AI动画攻击方式,但是并不会伴生其他城镇NPC特有的属性,比如幸福度按钮呈现在小地图上。这就是骷髅商人的核心所在。你也可以利用这一条创造各种各样的“非常规城镇NPC”。

其次是 SpawnsWithCustomName,当这个设置为true时你的NPC在生成时就会随机获得一个SetNPCNameList重写函数内包含的名字。

上面这两个属性好像是城镇NPC自带的功能啊?为什么还要手动设置?这就要牵扯到接下来我们要填写的部分了:SetDefaults

public override void SetDefaults()
{
    NPC.friendly = true;
    NPC.width = 18;
    NPC.height = 40;
    NPC.aiStyle = NPCAIStyleID.Passive;
    NPC.damage = 10;
    NPC.defense = 15;
    NPC.lifeMax = 250;
    NPC.HitSound = SoundID.NPCHit1;
    NPC.DeathSound = SoundID.NPCDeath1;
    NPC.knockBackResist = 0.5f;
    NPC.rarity = 2;//设置稀有度

    AnimationType = npcID;
}

和普通NPC相比,这里缺少了一个对城镇NPC而言十分重要的东西:NPC.townNPC。正是这个属性为我们自动(或者说强制)设置了很多城镇NPC的特性比如幸福度、生成姓名、刷新入住机制等等。这些好一部分都是骷髅商人不需要的,我们也就没必要再设置了,只需要把我们需要的属性手动打开即可。

NPC.rarity 用来表示NPC的稀有度。数字越高,在稀有生命体分析仪上显示的权重就越大。

由于骷髅商人需要对话功能,但我们又没有设置 NPC.townNPC,所以我们需要一个重写函数手动打开对话功能:CanChat

//NPCID.Sets.ActsLikeTownNPC不会自动设置这里
//仍然需要手动设置为true
public override bool CanChat()
{
    return true;
}

注意一下:这个重写函数并非基于NPC的Type而是基于NPC的单独实例。也就是说,只要你的NPC的这一块设置为true,无论后续它的Type怎么变,都将是可以对话的。(想想被缚NPC)


生成

既然骷髅商人是在环境内随机刷新,那我们就不能再使用CanTownNPCSpawn这个函数了。应当选择普通生物生成时需要的函数:SpawnChance

public override float SpawnChance(NPCSpawnInfo spawnInfo)
{
    //当 玩家处于洞穴层 且 世吞or克脑 被击败时 以地下敌怪的概率生成
    if ((spawnInfo.Player.ZoneDirtLayerHeight || spawnInfo.Player.ZoneRockLayerHeight)
        && NPC.downedBoss2)
        return SpawnCondition.Underground.Chance;

    return 0f;
}

ZoneDirtLayerHeight 就是泥土层,ZoneRockLayerHeight 就是岩石层,是构成泰拉世界的地下(除了地狱)的两个基本层次。


商店

上回不是说了商店了吗?没关系,你应该知道骷髅商人会按月相卖东西对吧?咱们就来看看怎么根据月相设置商品:

public override void SetupShop(Chest shop, ref int nextSlot)
{
    shop.item[nextSlot].SetDefaults(ItemID.SpelunkerGlowstick);
    nextSlot++;
    //满月时 出售挖掘药水
    if (Main.GetMoonPhase() == MoonPhase.Full)
    {
        shop.item[nextSlot].SetDefaults(ItemID.MiningPotion);
        nextSlot++;
    }
    //新月时 出售危险感药水
    if (Main.GetMoonPhase() == MoonPhase.Empty)
    {
        shop.item[nextSlot].SetDefaults(ItemID.TrapsightPotion);
        nextSlot++;
    }
    //任意 峨眉月时 出售探矿药水
    if (Main.GetMoonPhase() is MoonPhase.QuarterAtLeft or MoonPhase.QuarterAtRight)
    {
        shop.item[nextSlot].SetDefaults(ItemID.SpelunkerPotion);
        nextSlot++;
    }
    //任意 弦月时 出售狩猎药水
    if (Main.GetMoonPhase() is MoonPhase.HalfAtLeft or MoonPhase.HalfAtRight)
    {
        shop.item[nextSlot].SetDefaults(ItemID.HunterPotion);
        nextSlot++;
    }
    //任意 凸月时 出售建筑工药水
    if (Main.GetMoonPhase() is MoonPhase.ThreeQuartersAtLeft or MoonPhase.ThreeQuartersAtRight)
    {
        shop.item[nextSlot].SetDefaults(ItemID.BuilderPotion);
        nextSlot++;
    }
}

Main.GetMoonPhase 就是原版的获取月相的方法。虽然还有一个 Main.moonPhase 同样可用,但RE并没有提供MoonPhaseID这类的东西,不过为了可读性,还是加入了 MoonPhase 这个枚举类。同时我们也仅仅是想要获取月相,所以综合考虑,使用这个方法是更佳的选择。


Profile?

骷髅商人这类NPC一般不需要设置TownNPCProfile,如果你有需求那加上也无妨。

其他方面就和常规城镇NPC基本一致了,稍微设置一下姓名啦,攻击属性啦。一个类骷髅商人NPC就组装完毕了!


展示我们的测试骷髅小伙

喜闻乐见的展示环节:

在地下发现测试骷髅小伙,材质依旧来自ExampleMod
没有幸福度按钮
大致月相为峨眉月

总结

是不是很简单?只需要用一个常规城镇NPC稍加改造即可!

我是中学生,一个专门霍霍城镇NPC的屑仍在学习的模组作者,我们下次再见_(:з」∠)_

(这个屑已经越来越懒得写总结了)

《创建一个骷髅商人NPC》有5个想法

  1. 我認為其實特徵三應該再細分成三個特徵來看,
    1.可否入住
    2.是否會自動消失
    3.生成與活動特性

    因為要細分NPC的種類,目前共有四五種。

    常駐型:(寵物型也很類似這種,故這兩類放在一起)
    1.有 小地圖頭像 (且會被淹死)
    2.會 被地圖保存
    3.可 入住
    4.不會 自動消失 (除非手動或代碼消失)
    5.會在一定範圍內活動,但可能被手動改變位置 (生成條件說來複雜)
    [這一兩類就像平常那樣就不必多說。]

    骷髏商人型:
    1.沒有 小地圖頭像 (也不會被淹死)
    2.不會 被地圖保存
    3.不可 入住
    4.會 自動消失 (只要玩家遠離一段距離)
    5.會在一定範圍內活動,生成條件就像隨機生物一樣
    [所以這類可以看成跟常駐類是幾乎為相對特性的]

    然而之後的兩類 才是作mod代碼比較困難的。

    旅商型:
    1.有 小地圖頭像 (且會被淹死)
    2.不會 被地圖保存 (一旦離開遊戲後只能等下次自動生成)
    3.不可 入住
    4.(玩家遠離)不會 自動消失 (但離開遊戲或代碼有條件的會讓他消失,可再次生成)
    5.只會在一定範圍內活動,但出現位置與生成條件卻很扭,在城鎮NPC附近生成,但一旦決定位置後就不會被改變
    [這一類還蠻尷尬的,不屬於前兩類的定義,然而之後的類型更加奧秘與困難]

    老人型:
    1.沒有 小地圖頭像 (也不會被淹死)
    2.會 被地圖保存
    3.不可 入住
    4.不會 自動消失 (但代碼有條件的會讓他消失,可能無法再次生成)
    5.只會在一定範圍內活動,且生成位置永遠就在固定位置,也無法被手動改變位置
    [這一類似乎更尷尬,不屬於前兩類的定義,卻跟旅商類卻是部分相對特性的]

    然而老人型因為tmod API的緣故,想用代碼實現老人類就變得非常困難了。

    而目前原版的玩家是沒辦法召喚出任何村人類型的NPC來,能召喚的僕從也只是彈幕罷了。
    但Boss生物卻可能召喚出更多敵對的NPC生物來。

    如果想做出玩家可召喚的NPC,卻又不想變成常駐型NPC,該採取怎樣的策略呢?

    1. Middle School Student

      首先,第一个,关于特征三。
      这里的特征划分主要是为了和普通城镇NPC区分,因而没有进一步细分。
      第二个,关于老人NPC。
      要实现其实不难,只需要固定住NPC.homeTile即可,ModNPC内存在一个叫做NeedSaving的重写函数,用这个即可实现任何NPC的保存。
      第三个。
      你是想实现城镇NPC样式的召唤物么?
      手动召唤的话我认为你应该去基础NPC或Boss篇查看相关资料
      不可入住这一点可以参考现在的骷髅商人和将来我要写的旅商NPC(或者直接去ExampleMod看)
      如果你还有其他问题可以加群询问。

      1. 喔,感謝回覆。不知道這個網站的IP是否在中國,之前發表過的回覆不知何故會失敗。
        另外很抱歉,本人非中國人,故沒有所謂的QQ群。

        說實在的原版雖然有我說的這四五類,但並非能應付任何情況。

        例如:如果希望NPC能像原神的凱瑟琳那樣,該如何實現?看起來最類似老人NPC,因為希望
        能 被地圖保存,不可 入住,不會 自動消失,生成位置在固定位置(且無法被玩家改變),
        但又希望 能 在小地圖出現頭像,
        這又該如何實現呢?
        (不過如果一個世界有太多的凱瑟琳的話,會很佔記憶體吧)

        其實我是想實現可被玩家召喚的戰鬥型村人NPC,但又希望不同玩家召喚的NPC各自獨立運作(各一個),
        也就是有不同玩家召喚專屬的NPC,以避免出現爭搶召喚的情況,但如何聯機同步又是另外的問題。

        而我所希望被玩家召喚的村人NPC,我思考一番希望符合的條件有
        1.沒有 小地圖頭像
        2.不會 被地圖保存
        3.不可 入住
        4.當玩家遠離一段距離 不會 立刻自動消失,但一直忽略的話會在必要時自動消失,避免佔記憶體
        5.生成條件為玩家召喚,改變位置為玩家召喚處,活動範圍由玩家選擇的策略決定

        看起來這些條件很類似骷髏商人型,但第4、5點就不符合骷髏商人型。

        然而如何決定活動範圍是個複雜的問題,有時希望他一直跟著玩家,但寫AI尋找路徑是很困難的事,
        有時希望他能自動打敵對怪(成為玩家的戰鬥夥伴),當生命值太低時能尋找遮蔽處躲避敵人等。

        但我認為最困難的莫過於AI尋路,時常有太高而跳不上去,寫得不好的話,甚至可能落入深淵起不來,
        遇到牆壁或阻礙時如何尋找可走的路徑是個相當困難的問題。(當然可穿牆的就另當別論)

        1. Middle School Student

          首先,方便说一下你是哪里人吗?这样有助于我们找到一个更方便沟通的方法。
          其次,你的需求可能并不是三言两语就能解决的,评论区里肯定不适合。
          想办法建立一个更方便的沟通渠道吧。

          1. 如果我談論的對象不是中國人,就不必那麼難以啟齒了。但我可以給你提示讓你猜。

            慣用繁體中文,使用Google搜尋引擎不會遇到被牆的情況。

            至於聯絡方式,我目前有的其中一組e-mail: gatanada@protonmail.com

            如果你問我為什麼用protonmail,其中一個原因是考慮到是否會被中國的防火長城擋住,
            (如果使用的是gmail,那麼就會有大概率會被牆。儘管protonmail用起來沒那麼方便。)
            相信你應該會比我更清楚中國的情況吧。

            如果你身在中國而有些話不方便說的話,請告知我一聲,你是否目前身在中國。

发表回复