Skip to content

植物深度教程

基础植物教程能做出“种下去、长出来、收获”的流程。进阶植物要处理更多行为:灌溉、休眠、枯萎、特殊收获、范围效果、图鉴文本和自定义状态机。

这一章重点讲植物系统常见扩展点:灌溉、休眠、枯萎、特殊收获、范围效果和自定义状态机。

生长参数

核心仍然是 ExtendEntityToBasicPlant()

csharp
EntityTemplates.ExtendEntityToBasicPlant(
    plant,
    temperature_lethal_low: 179.15f,
    temperature_warning_low: 189.15f,
    temperature_warning_high: 373.15f,
    temperature_lethal_high: 383.15f,
    safe_elements: new[]
    {
        SimHashes.Oxygen,
        SimHashes.ContaminatedOxygen,
        SimHashes.CarbonDioxide
    },
    pressure_sensitive: true,
    pressure_lethal_low: 0f,
    pressure_warning_low: 0.15f,
    crop_id: MyFruitConfig.ID,
    max_age: 2400f,
    baseTraitId: "MyPlantOriginal",
    baseTraitName: STRINGS.CREATURES.SPECIES.MYPLANT.NAME);

max_age 是生长周期,crop_id 是收获物 ID。温度全部用开尔文。

灌溉

消耗液体或气体用 ExtendPlantToIrrigated()

csharp
EntityTemplates.ExtendPlantToIrrigated(
    plant,
    new[]
    {
        new PlantElementAbsorber.ConsumeInfo
        {
            tag = GameTags.Water,
            massConsumptionRate = 0.033333335f
        }
    });

0.033333335f 约等于每秒 33.3g。数值太大时,农砖供给看起来会像坏了一样。先用小数值跑通。

作物产量

作物类型要注册进 CROPS.CROP_TYPES

csharp
[HarmonyPatch(typeof(Db), "Initialize")]
public static class Db_Initialize_Patch
{
    public static void Postfix()
    {
        CROPS.CROP_TYPES.Add(new Crop.CropVal(
            MyFruitConfig.ID,
            300f,
            5,
            true));
    }
}

参数大致是作物 ID、生长周期、产量、是否可食用。不同版本签名可能变动,编译不过就对照当前源码里的 Crop.CropVal

枯萎和休眠

植物可以接 BlightVulnerable

csharp
plant.AddOrGet<BlightVulnerable>();

如果没有做枯萎动画,可以先不要加。加了组件却没有对应动画,植物可能在枯萎阶段显示异常。

休眠逻辑由 Growing 状态机内部处理,通过 GameHashes.CropSleepGameHashes.CropWakeUp 事件驱动。自定义状态机接入休眠可以读取 Growing.StatesInstance

csharp
private static bool IsSleeping(MyPlant.StatesInstance smi)
{
    Growing.StatesInstance growing = smi.master.GetSMI<Growing.StatesInstance>();
    return growing != null && growing.IsStalledByCustomCondition();
}

状态跳转:

csharp
alive.idle
    .EventTransition(GameHashes.CropSleep, alive.sleeping, IsSleeping);

alive.sleeping
    .EventTransition(GameHashes.CropWakeUp, alive.idle, smi => !IsSleeping(smi));

自定义植物状态机

当原版 StandardCropPlant 不够用时,可以写自己的植物状态机:

csharp
public class MyPlant : StateMachineComponent<MyPlant.StatesInstance>
{
    [MyCmpGet] public Growing growing;
    [MyCmpGet] public Harvestable harvestable;
    [MyCmpGet] public Crop crop;

    protected override void OnSpawn()
    {
        base.OnSpawn();
        smi.StartSM();
    }

    protected override void OnCleanUp()
    {
        smi.StopSM("OnCleanUp");
        base.OnCleanUp();
    }
}

状态:

csharp
public class States : GameStateMachine<States, StatesInstance, MyPlant>
{
    public State idle;
    public State fruiting;
    public State harvest;

    public override void InitializeStates(out StateMachine.BaseState defaultState)
    {
        defaultState = idle;

        idle
            .PlayAnim("grow", KAnim.PlayMode.Paused)
            .Update((smi, dt) => smi.RefreshGrowthAnim(), UpdateRate.SIM_4000ms)
            .EventTransition(GameHashes.Grow, fruiting,
                smi => smi.master.growing.ReachedNextHarvest());

        fruiting
            .PlayAnim("idle_full", KAnim.PlayMode.Loop)
            .Enter(smi => smi.master.harvestable.SetCanBeHarvested(true))
            .EventTransition(GameHashes.Harvest, harvest);

        harvest
            .PlayAnim("harvest", KAnim.PlayMode.Once)
            .Enter(smi => smi.SpawnFruit())
            .Exit(smi => smi.master.harvestable.SetCanBeHarvested(false))
            .OnAnimQueueComplete(idle);
    }
}

这只是骨架。完整植物还要处理死亡、枯萎、休眠、被连根拔起等状态。先把主链路跑通,再补边缘状态。

特殊收获

如果想根据环境改变产物,可以在收获时重新配置作物:

csharp
public void SpawnFruit()
{
    string cropId = IsHotEnough()
        ? HotFruitConfig.ID
        : ColdFruitConfig.ID;

    Crop.CropVal cropVal = CROPS.CROP_TYPES.Find(crop => crop.cropId == cropId);
    master.crop.Configure(cropVal);
    master.crop.SpawnConfiguredFruit(null);
}

类似的特殊植物可以在收获阶段根据环境条件选择产物。

范围效果

植物如果要影响周围,可以加 RangeVisualizer 方便玩家理解范围:

csharp
RangeVisualizer visualizer = plant.AddOrGet<RangeVisualizer>();
visualizer.OriginOffset = new Vector2I(0, 0);
visualizer.RangeMin.x = -4;
visualizer.RangeMin.y = -4;
visualizer.RangeMax.x = 4;
visualizer.RangeMax.y = 4;
visualizer.BlockingTileVisible = true;

真正的范围逻辑不要每帧扫。用 ISim4000ms 或状态机里的限频 Update()

csharp
public void Sim4000ms(float dt)
{
    int center = Grid.PosToCell(gameObject);
    // 扫附近格子,改变温度、气体或给复制人加效果。
}

图鉴和文本

植物要补植物文本和种子文本:

csharp
Strings.Add($"STRINGS.CREATURES.SPECIES.{ID.ToUpper()}.NAME", "氧树");
Strings.Add($"STRINGS.CREATURES.SPECIES.{ID.ToUpper()}.DESC", "一种会影响周围环境的植物。");
Strings.Add($"STRINGS.CREATURES.SPECIES.SEEDS.{SEED_ID.ToUpper()}.NAME", "氧树种子");

如果要进图鉴,还要看 CodexCache 和植物注册流程。先保证游戏内名称、种子、收获物都正常,再做图鉴。

常见坑

温度写错单位是植物教程第一大坑。20f 不是 20 摄氏度,是 -253.15 摄氏度。

自定义状态机不要和 StandardCropPlant 同时抢同一套状态。要么用原版标准植物,要么明确替换成自己的状态机。

Harvestable.SetCanBeHarvested(true) 后,收获完成一定要关掉。不然复制人可能一直尝试收获。

特殊产物要先注册进 CROPS.CROP_TYPES,否则 crop.Configure() 找不到配置。

范围效果要限频。植物数量一多,全图扫描会非常明显地拖慢游戏。