How do you guys architecture your gameplay logic?

I’m deep into the gameplay logic and I feel like I’m coding myself into a corner. I wanted it to be modular (just remove this system if you want to remove x logic) but now I don’t realize if some systems are working against each other (there is no centralized logic) and it’s hard to track who is writing to the component, who is removing it, which system is causing the issue etc…

I’m thinking maybe I should just scrap this and have more strict rules, for example
1- a system only writes to one component (using ref)
2- no other system writes to that component
3- another system handles add/remove of that component
4- no other system adds/removes that component

I could think of more, but what do you think about this? Do you guys have some rules work well for you?

1 Like

I have a very specific methodology that works well for me, but it is a bit too complex for me to fully articulate on these forums.

But to give a little bit of an idea that may or may not make sense, I define input and output components for any particular group of systems that represent the “API” for that group of systems. Any other components those systems touch are exclusively touched by the systems in that group. This is hierarchical, and so I end up with a hierarchical DAG.

3 Likes

This design sounds very much like the idea of a private field in OOP.

I think @DreamingImLatios is proposing a design that utilizing Data(component) As An Interface (instead of OOP way: Function As An Interface), that’s exactly what I am doing in my systems.
People are asking about [Generic/FunctionPointer/CallBack/Reactive/Multiple-loop-Update in one frame] all the time in this forum.
That’s all somehow related to the idea of Function As An Interface. But in DOTS, especially in burst, polymorphism of function itself is not preferred. That’s why many of these approaches hit a dead end in DOTS.(I am not saying features like Generic etc. should be avoided, they are more like a tool to make a better job body instead of a design guideline to your DOTS architecture)

But if you consider Data itself as an interface, that’s exactly what ECS offers.
Do something if the entity has this component do something else if it has that group of components. ComponentType is the nature API of ECS.
Build up ComponentType(in DOTS) as you built those super complex Classes using inheritance and composition (in OOP). Utilize ComponentType/Data(in DOTS) as you register spaghetti event callback systems (in OOP).
That’s what I am doing right now.

4 Likes

Yeah that’s what I’m trying to switch my focus onto right now. For example instead of creating more branches in logic. I just add more tag components to the entities and run many different Entities.ForEach utilizing WithAll, WithAny.

Another thing I tried is one-time systems. Like if I want my PathfindingSystem to run I add RequiresPathfinding component to the Entity, the PathfindingSystem runs through all RequiresPathfinding entities and does some calculations, in the end removes all RequiresPathfinding components. I’m not sure if this is good for performance but I’m getting some simple code in there.

I’ve been attempting to make things as modular as possible, although I haven’t yet tested, I believe someone can make a mod to my game by creating a system that runs in between two existing systems, or one that straight up replaces a system, so long it’s DLL is loaded before the world initialization (which I took control of).
The unfortunate side effect is that I have a f-load of systems doing very small stuff each, not sure if it’s bad or not, but struct systems that will supposedly come out in the next update will help with whichever side effects it have, probably.

Imagine you have a huge OOP method that handles all the complexities of how a plant grows, matures, change stages, get fruits, etc. Instead, I use a whole lot of systems that are strictly ordered to do the same thing.
Some examples of what I ended up with:

Resets all values for the plant, equivalent of a whole bunch of variable initializations at the start of a huge method
Frame Start Reset Values

[UpdateInGroup(typeof(GroupPlantFrameStart))]
public class FrameStartUpkeepCopyCrop : JobComponentSystem {
    public EntityQuery query1;
    public EntityQuery query2;
    protected override void OnCreate() {
        base.OnCreate();
        query1 = GetEntityQuery(ComponentType.ReadOnly<NuAdd>(), ComponentType.ReadOnly<KaAdd>(), ComponentType.ReadOnly<PiAdd>(),
            ComponentType.ReadOnly<ResilienceAdditive>(), ComponentType.ReadOnly<ResilienceMultiplier>(), ComponentType.ReadOnly<PlantTemperatureBufferMax>(),
            ComponentType.ReadOnly<PlantManaBufferMultiplier>(), ComponentType.ReadOnly<PlantWaterBufferMultiplier>(), ComponentType.ReadOnly<PlantNutrientBufferMultiplier>(),
            ComponentType.ReadOnly<PlantTemperatureBufferMultiplier>(), ComponentType.ReadOnly<BasePlantData>(),
            typeof(PlantNuUpkeep), typeof(PlantKaUpkeep), typeof(PlantPiUpkeep), typeof(PlantNutrientBufferMax),
            typeof(PlantWaterMinNeed), typeof(PlantWaterMaxNeed), typeof(PlantWaterBufferMax),
            typeof(PlantManaMinNeed), typeof(PlantManaBufferMax),
            typeof(PlantDamageThisCycle), typeof(ShouldPlantTakeDamage));

        query2 = GetEntityQuery(typeof(CurrentPlantStage), typeof(CurrentStageTempData),
            typeof(PlantTemperatureMinNeed), typeof(PlantTemperatureMaxNeed), typeof(PlantGrowPerDay), typeof(PlantGrowThisCycle));
    }

    #region Systems
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd0 : AutoAddSystem<BasePlantData, CanHarvest> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd1 : AutoAddSystem<BasePlantData, PlantNuUpkeep, PlantKaUpkeep, PlantPiUpkeep> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd2 : AutoAddSystem<BasePlantData, PlantNutrientBufferMax> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd31 : AutoAddSystem<BasePlantData, PlantWaterMinNeed, PlantWaterMaxNeed, PlantWaterBufferMax> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd32 : AutoAddSystem<BasePlantData, PlantManaMinNeed, PlantManaBufferMax> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd33 : AutoAddSystem<BasePlantData, PlantTemperatureBufferMax> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd4 : AutoAddSystemShared<StageData, PlantTemperatureMinNeed, PlantTemperatureMaxNeed> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd5 : AutoAddSystemShared<StageData, PlantGrowPerDay, PlantGrowThisCycle> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd6 : AutoAddSystem<BasePlantData, PlantDamageThisCycle, ShouldPlantTakeDamage> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd7 : AutoAddSystem<BasePlantData, PlantShouldTakeNUDamage, PlantShouldTakeKADamage, PlantShouldTakePIDamage> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd8 : AutoAddSystem<BasePlantData, PlantShouldTakeWATERDamage, PlantShouldTakeMANADamage, PlantShouldTakeTEMPDamage> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd9 : AutoAddSystem<BasePlantData, PlantUsingNUBuffer, PlantUsingKABuffer, PlantUsingPIBuffer> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd10 : AutoAddSystem<BasePlantData, PlantUsingWATERBuffer, PlantUsingMANABuffer, PlantUsingTEMPBuffer> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd14 : AutoAddSystem<BasePlantData, PlantNutrientBufferMultiplier, PlantWaterBufferMultiplier> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd15 : AutoAddSystem<BasePlantData, PlantTemperatureBufferMultiplier, PlantManaBufferMultiplier> { }

    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyAutoAdd11 : AutoAddSystem<TileAdjacency, PlantNeedNutrients, PlantNeedTemperature, PlantNeedWater> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]                
    public class FrameStartUpkeepCopyAutoAdd12 : AutoAddSystem<TileAdjacency, PlantNeedMana, PlantAlertNutrients, PlantAlertTemperature> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]                
    public class FrameStartUpkeepCopyAutoAdd13 : AutoAddSystem<TileAdjacency, PlantAlertWater, PlantAlertMana, CanHarvest> { }

    [UpdateInGroup(typeof(GroupPlantFrameInitialization))]
    public class CopyAutoBuffer1 : AutoCopySystem<PlantNutrientBuffer, PlantNutrientBufferMax> { }
    [UpdateInGroup(typeof(GroupPlantFrameInitialization))]
    public class CopyAutoBuffer2 : AutoCopySystem<PlantWaterBuffer, PlantWaterBufferMax> { }
    [UpdateInGroup(typeof(GroupPlantFrameInitialization))]
    public class CopyAutoBuffer3 : AutoCopySystem<PlantManaBuffer, PlantManaBufferMax> { }
    [UpdateInGroup(typeof(GroupPlantFrameInitialization))]
    public class CopyAutoBuffer4 : AutoCopySystem<PlantTemperatureBuffer, PlantTemperatureBufferMax> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyNuTile : FrameStartUpkeepCopyTile<SoilNuBaseGain, SoilNuGainThisCycle> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyPiTile : FrameStartUpkeepCopyTile<SoilPiBaseGain, SoilPiGainThisCycle> { }
    [UpdateInGroup(typeof(GroupPlantAutoAdd))]
    public class FrameStartUpkeepCopyKaTile : FrameStartUpkeepCopyTile<SoilKaBaseGain, SoilKaGainThisCycle> { }
    #endregion

    [BurstCompile]
    struct FrameStartUpkeepCopyBasePlantDataJob : IJobChunk {
        [NativeDisableContainerSafetyRestriction, ReadOnly] public ComponentTypeHandle<BasePlantData> BasePlantData;
        [NativeDisableContainerSafetyRestriction, ReadOnly] public ComponentTypeHandle<ResilienceAdditive> ResilienceAdd;
        [NativeDisableContainerSafetyRestriction, ReadOnly] public ComponentTypeHandle<ResilienceMultiplier> ResilienceMultiplier;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<NuAdd> NuAdd;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<KaAdd> KaAdd;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PiAdd> PiAdd;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantNuUpkeep> PlantNuUpkeep;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantKaUpkeep> PlantKaUpkeep;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantPiUpkeep> PlantPiUpkeep;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantManaMinNeed> PlantManaMinNeed;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantWaterMinNeed> PlantWaterMinNeed;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantWaterMaxNeed> PlantWaterMaxNeed;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantManaBufferMax> PlantManaBufferMax;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantWaterBufferMax> PlantWaterBufferMax;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantDamageThisCycle> PlantDamageThisCycle;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<ShouldPlantTakeDamage> ShouldPlantTakeDamage;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantNutrientBufferMax> PlantNutrientBufferMax;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantTemperatureBufferMax> PlantTemperatureBufferMax;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantManaBufferMultiplier> PlantManaBufferMultiplier;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantWaterBufferMultiplier> PlantWaterBufferMultiplier;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantNutrientBufferMultiplier> PlantNutrientBufferMultiplier;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantTemperatureBufferMultiplier> PlantTemperatureBufferMultiplier;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
            var BasePlantDataValuesArray = chunk.GetNativeArray(this.BasePlantData);
            var ResilienceAdd = chunk.GetNativeArray(this.ResilienceAdd).Reinterpret<float>();
            var ResilienceMultiplier = chunk.GetNativeArray(this.ResilienceMultiplier).Reinterpret<float>();
            var NuAdd = chunk.GetNativeArray(this.NuAdd).Reinterpret<float>();
            var KaAdd = chunk.GetNativeArray(this.KaAdd).Reinterpret<float>();
            var PiAdd = chunk.GetNativeArray(this.PiAdd).Reinterpret<float>();
            var PlantNuUpkeep = chunk.GetNativeArray(this.PlantNuUpkeep).Reinterpret<float>();
            var PlantKaUpkeep = chunk.GetNativeArray(this.PlantKaUpkeep).Reinterpret<float>();
            var PlantPiUpkeep = chunk.GetNativeArray(this.PlantPiUpkeep).Reinterpret<float>();
            var PlantManaMinNeed = chunk.GetNativeArray(this.PlantManaMinNeed).Reinterpret<float>();
            var PlantWaterMinNeed = chunk.GetNativeArray(this.PlantWaterMinNeed).Reinterpret<float>();
            var PlantWaterMaxNeed = chunk.GetNativeArray(this.PlantWaterMaxNeed).Reinterpret<float>();
            var PlantManaBufferMax = chunk.GetNativeArray(this.PlantManaBufferMax).Reinterpret<float>();
            var PlantWaterBufferMax = chunk.GetNativeArray(this.PlantWaterBufferMax).Reinterpret<float>();
            var PlantDamageThisCycle = chunk.GetNativeArray(this.PlantDamageThisCycle).Reinterpret<float>();
            var ShouldPlantTakeDamage = chunk.GetNativeArray(this.ShouldPlantTakeDamage).Reinterpret<byte>();
            var PlantNutrientBufferMax = chunk.GetNativeArray(this.PlantNutrientBufferMax).Reinterpret<float>();
            var PlantTemperatureBufferMax = chunk.GetNativeArray(this.PlantTemperatureBufferMax).Reinterpret<float>();
            var PlantManaBufferMultiplier = chunk.GetNativeArray(this.PlantManaBufferMultiplier).Reinterpret<float>();
            var PlantWaterBufferMultiplier = chunk.GetNativeArray(this.PlantWaterBufferMultiplier).Reinterpret<float>();
            var PlantNutrientBufferMultiplier = chunk.GetNativeArray(this.PlantNutrientBufferMultiplier).Reinterpret<float>();
            var PlantTemperatureBufferMultiplier = chunk.GetNativeArray(this.PlantTemperatureBufferMultiplier).Reinterpret<float>();
            for (int i = 0; i < chunk.Count; i++) {
                BasePlantData BasePlantData = BasePlantDataValuesArray[i];
                PlantNuUpkeep[i] = BasePlantData.BaseNu + NuAdd[i];
                PlantKaUpkeep[i] = BasePlantData.BaseKa + KaAdd[i];
                PlantPiUpkeep[i] = BasePlantData.BasePi + PiAdd[i];
                PlantManaMinNeed[i] = BasePlantData.BaseMana;
                PlantWaterMinNeed[i] = BasePlantData.BaseWater - (BasePlantData.Resist + ResilienceAdd[i]) * ResilienceMultiplier[i];
                PlantWaterMaxNeed[i] = BasePlantData.BaseWater + (BasePlantData.Resist + ResilienceAdd[i]) * ResilienceMultiplier[i];
                PlantManaBufferMax[i] = BasePlantData.ManaBufferMax;
                PlantWaterBufferMax[i] = BasePlantData.WaterBufferMax;
                PlantDamageThisCycle[i] = 0;
                ShouldPlantTakeDamage[i] = 0;
                PlantNutrientBufferMax[i] = BasePlantData.NutrientBufferMax;
                PlantTemperatureBufferMax[i] = BasePlantData.TemperatureBufferMax;
                PlantManaBufferMultiplier[i] = 1;
                PlantWaterBufferMultiplier[i] = 1;
                PlantNutrientBufferMultiplier[i] = 1;
                PlantTemperatureBufferMultiplier[i] = 1;
            }
        }
    }

    [BurstCompile]
    struct FrameStartUpkeepCopyStageDataJob : IJobChunk {
#if UNITY_EDITOR
        public float Multiplier;
#endif
        [NativeDisableContainerSafetyRestriction, ReadOnly] public ComponentTypeHandle<CurrentPlantStage> CurrentPlantStage;
        [NativeDisableContainerSafetyRestriction, ReadOnly] public ComponentTypeHandle<CurrentStageTempData> CurrentStageTempData;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantTemperatureMinNeed> PlantTemperatureMinNeed;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantTemperatureMaxNeed> PlantTemperatureMaxNeed;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantGrowPerDay> PlantGrowPerDay;
        [NativeDisableContainerSafetyRestriction] public ComponentTypeHandle<PlantGrowThisCycle> PlantGrowThisCycle;
        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
            var CurrentPlantStage = chunk.GetNativeArray(this.CurrentPlantStage).Reinterpret<float>(24);
            var CurrentStageTempData = chunk.GetNativeArray(this.CurrentStageTempData).Reinterpret<float>(8);
            var PlantTemperatureMinNeed = chunk.GetNativeArray(this.PlantTemperatureMinNeed).Reinterpret<float>();
            var PlantTemperatureMaxNeed = chunk.GetNativeArray(this.PlantTemperatureMaxNeed).Reinterpret<float>();
            var PlantGrowPerDay = chunk.GetNativeArray(this.PlantGrowPerDay).Reinterpret<float>();
            var PlantGrowThisCycle = chunk.GetNativeArray(this.PlantGrowThisCycle).Reinterpret<float>();
            for (int i = 0; i < chunk.Count; i++) {
                PlantTemperatureMinNeed[i] = CurrentStageTempData[i * 2 + 0];
                PlantTemperatureMaxNeed[i] = CurrentStageTempData[i * 2 + 1];
#if UNITY_EDITOR
                PlantGrowPerDay[i] = 1f / CurrentPlantStage[i * 6 + 5] * Multiplier;
#else
                PlantGrowPerDay[i] = 1f / CurrentPlantStage[i * 6 + 5];
#endif
                PlantGrowThisCycle[i] = 0;
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDependencies) {
        var job = new FrameStartUpkeepCopyBasePlantDataJob();
        job.BasePlantData = GetComponentTypeHandle<BasePlantData>(true);
        job.ResilienceAdd = GetComponentTypeHandle<ResilienceAdditive>(true);
        job.ResilienceMultiplier = GetComponentTypeHandle<ResilienceMultiplier>(true);
        job.NuAdd = GetComponentTypeHandle<NuAdd>(false);
        job.KaAdd = GetComponentTypeHandle<KaAdd>(false);
        job.PiAdd = GetComponentTypeHandle<PiAdd>(false);
        job.PlantNuUpkeep = GetComponentTypeHandle<PlantNuUpkeep>(false);
        job.PlantKaUpkeep = GetComponentTypeHandle<PlantKaUpkeep>(false);
        job.PlantPiUpkeep = GetComponentTypeHandle<PlantPiUpkeep>(false);
        job.PlantManaMinNeed = GetComponentTypeHandle<PlantManaMinNeed>(false);
        job.PlantWaterMinNeed = GetComponentTypeHandle<PlantWaterMinNeed>(false);
        job.PlantWaterMaxNeed = GetComponentTypeHandle<PlantWaterMaxNeed>(false);
        job.PlantManaBufferMax = GetComponentTypeHandle<PlantManaBufferMax>(false);
        job.PlantWaterBufferMax = GetComponentTypeHandle<PlantWaterBufferMax>(false);
        job.PlantDamageThisCycle = GetComponentTypeHandle<PlantDamageThisCycle>(false);
        job.ShouldPlantTakeDamage = GetComponentTypeHandle<ShouldPlantTakeDamage>(false);
        job.PlantNutrientBufferMax = GetComponentTypeHandle<PlantNutrientBufferMax>(false);
        job.PlantTemperatureBufferMax = GetComponentTypeHandle<PlantTemperatureBufferMax>(false);
        job.PlantManaBufferMultiplier = GetComponentTypeHandle<PlantManaBufferMultiplier>(false);
        job.PlantWaterBufferMultiplier = GetComponentTypeHandle<PlantWaterBufferMultiplier>(false);
        job.PlantNutrientBufferMultiplier = GetComponentTypeHandle<PlantNutrientBufferMultiplier>(false);
        job.PlantTemperatureBufferMultiplier = GetComponentTypeHandle<PlantTemperatureBufferMultiplier>(false);
        var job2 = new FrameStartUpkeepCopyStageDataJob();
#if UNITY_EDITOR
        job2.Multiplier = EverydayEngine.GameEngine.instance.DEBUG.PlantDebugGrowSpeed;
#endif
        job2.CurrentPlantStage = GetComponentTypeHandle<CurrentPlantStage>(true);
        job2.CurrentStageTempData = GetComponentTypeHandle<CurrentStageTempData>(true);
        job2.PlantTemperatureMinNeed = GetComponentTypeHandle<PlantTemperatureMinNeed>(false);
        job2.PlantTemperatureMaxNeed = GetComponentTypeHandle<PlantTemperatureMaxNeed>(false);
        job2.PlantGrowPerDay = GetComponentTypeHandle<PlantGrowPerDay>(false);
        job2.PlantGrowThisCycle = GetComponentTypeHandle<PlantGrowThisCycle>(false);
        return job2.Schedule(query2, job.Schedule(query1, inputDependencies));
    }
}

public abstract class FrameStartUpkeepCopyTile<T, T2> : JobComponentSystem where T : struct, IComponentData where T2 : struct, IComponentData {
    public EntityQuery query;
    protected override void OnCreate() {
        base.OnCreate();
        query = GetEntityQuery(ComponentType.ReadOnly<T>(), typeof(T2));
    }

    [BurstCompile]
    struct FrameStartUpkeepCopyJob : IJobChunk {
        [ReadOnly] public ComponentTypeHandle<T> T;
        public ComponentTypeHandle<T2> T2;
        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
            var T = chunk.GetNativeArray(this.T).Reinterpret<float>();
            var T2 = chunk.GetNativeArray(this.T2).Reinterpret<float>();
            for (int i = 0; i < chunk.Count; i++) {
                T2[i] = T[i];
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDependencies) {
        var job = new FrameStartUpkeepCopyJob();

        job.T = GetComponentTypeHandle<T>(true);
        job.T2 = GetComponentTypeHandle<T2>(false);

        return job.Schedule(query, inputDependencies);
    }
}

Bolting Plant Trait that checks if the plant is taking damage this frame, if so, the plant grows faster, taking even more damage than otherwise, overriding the natural plant growth value before the plant has a chance to grow. Equivalent to two IFS, one for checking if the plant has the bolting trait, and another to check if it is taking damage, then changing the temporary variables to match the effect
Bolting

[UpdateAfter(typeof(CalculatePlantDamageThisCycle))]
[UpdateBefore(typeof(PlantGrow))]
[UpdateInGroup(typeof(PlantGroupAfterUpkeep))]
public class BoltingGrowMod : JobComponentSystem {
    public EntityQuery query;
    protected override void OnCreate() {
        base.OnCreate();
        query = GetEntityQuery(ComponentType.ReadOnly<BasePlantData>(),
            typeof(ShouldPlantTakeDamage), typeof(PlantGrowThisCycle), typeof(PlantDamageThisCycle), typeof(PlantGrowPerDay), typeof(BoltingTrait));
    }

    [BurstCompile]
    struct BoltModJob : IJobChunk {
        [ReadOnly] public ComponentTypeHandle<BoltingTrait> BoltingTrait;
        public ComponentTypeHandle<PlantDamageThisCycle> PlantDamageThisCycle;
        public ComponentTypeHandle<PlantGrowThisCycle> PlantGrowThisCycle;
        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
            var PlantDamageThisCycle = chunk.GetNativeArray(this.PlantDamageThisCycle).Reinterpret<float>();
            var PlantGrowThisCycle = chunk.GetNativeArray(this.PlantGrowThisCycle).Reinterpret<float>();
            var BoltingTrait = chunk.GetNativeArray(this.BoltingTrait).Reinterpret<float>();
            for (int i = 0; i < chunk.Count; i++) {
                if (PlantDamageThisCycle[i] != 0) {
                    PlantDamageThisCycle[i] *= BoltingTrait[i];
                    PlantGrowThisCycle[i] *= BoltingTrait[i];
                }
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDependencies) {
        var job = new BoltModJob();
        job.PlantDamageThisCycle = GetComponentTypeHandle<PlantDamageThisCycle>(false);
        job.PlantGrowThisCycle = GetComponentTypeHandle<PlantGrowThisCycle>(false);
        job.BoltingTrait = GetComponentTypeHandle<BoltingTrait>(true);
        return job.Schedule(query, inputDependencies);
    }
}

Applies the temporary variables to the plant state, equivalent of waiting all of the chances the plants has to modify their plant growth value, then at the end of the huge method, applying the plant plant growth to the plant actual growth state.
Plant Growth

[UpdateInGroup(typeof(PlantGroupAfterUpkeep))]
public class PlantGrow : JobComponentSystem {
    public EntityQuery query;
    EndSimulationEntityCommandBufferSystem entityCommandBufferSystem;
    protected override void OnCreate() {
        base.OnCreate();
        entityCommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
        query = GetEntityQuery(ComponentType.ReadOnly<BasePlantData>(), ComponentType.ReadOnly<CurrentPlantStage>(), typeof(CurrentPlantGrowth),
            typeof(PlantGrowThisCycle), typeof(PlantTotalGrowTime),
            ComponentType.Exclude<RequiresChangingStage>());
    }

    [BurstCompile]
    struct PlantGrowJob : IJobChunk {
        [ReadOnly] public ComponentTypeHandle<PlantGrowThisCycle> PlantGrowThisCycle;
        [ReadOnly] public ComponentTypeHandle<CurrentPlantStage> CurrentPlantStage;
        [ReadOnly] public EntityTypeHandle Entities;
        public ComponentTypeHandle<PlantTotalGrowTime> PlantTotalGrowTime;
        public ComponentTypeHandle<CurrentPlantGrowth> CurrentPlantGrowth;
        public EntityCommandBuffer.ParallelWriter EntityCommandBuffer;
        public float DayFrameDiftime;
        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
            var PlantGrowThisCycle = chunk.GetNativeArray(this.PlantGrowThisCycle).Reinterpret<float>();
            var CurrentPlantGrowth = chunk.GetNativeArray(this.CurrentPlantGrowth).Reinterpret<float>();
            var PlantTotalGrowTime = chunk.GetNativeArray(this.PlantTotalGrowTime).Reinterpret<float>();
            var CurrentPlantStage = chunk.GetNativeArray(this.CurrentPlantStage).Reinterpret<int>(24);
            var CurrentPlantStageF = chunk.GetNativeArray(this.CurrentPlantStage).Reinterpret<float>(24);
            var Entities = chunk.GetNativeArray(this.Entities);
            for (int i = 0; i < chunk.Count; i++) {
                PlantTotalGrowTime[i] = (CurrentPlantGrowth[i] * CurrentPlantStageF[i * 6 + 5] + CurrentPlantStage[i * 6 + 3]) / CurrentPlantStage[i * 6 + 4];
                if ((CurrentPlantGrowth[i] = Mathf.Min(1, CurrentPlantGrowth[i] + PlantGrowThisCycle[i] * DayFrameDiftime)) == 1) {
                    EntityCommandBuffer.AddComponent(chunkIndex, Entities[i], new RequiresChangingStage());
                }
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDependencies) {
        var job = new PlantGrowJob();
        job.DayFrameDiftime = GameEngine.GameDeltaTime / StaticFlags.GameEngine_RealSecondsInDay;
        job.PlantTotalGrowTime = GetComponentTypeHandle<PlantTotalGrowTime>(false);
        job.CurrentPlantGrowth = GetComponentTypeHandle<CurrentPlantGrowth>(false);
        job.PlantGrowThisCycle = GetComponentTypeHandle<PlantGrowThisCycle>(true);
        job.CurrentPlantStage = GetComponentTypeHandle<CurrentPlantStage>(true);
        job.Entities = GetEntityTypeHandle();
        job.EntityCommandBuffer = entityCommandBufferSystem.CreateCommandBuffer().AsParallelWriter();
        JobHandle jobHandle = job.Schedule(query, inputDependencies);

        entityCommandBufferSystem.AddJobHandleForProducer(jobHandle);

        return jobHandle;
    }
}

With this I can easily create more and more traits that changes most aspects of the plant life cycle just by inserting more and more systems that run in between the FrameStartUpkeepCopyCrop and the many systems in the PlantGroupAfterUpkeep system group.

My plant related system groups, I might need to rename them to make more explicit what they do:

[UpdateBefore(typeof(InitializationSystemGroup))]
public class PlantGroupCleanUp : ComponentSystemGroup { }
[UpdateInGroup(typeof(InitializationSystemGroup))]
public class GroupPlantAutoAdd : ComponentSystemGroup { }
[UpdateAfter(typeof(GroupPlantAutoAdd))]
public class GroupPlantFrameStart : ComponentSystemGroup { }
[UpdateAfter(typeof(GroupPlantFrameStart))]
public class GroupPlantFrameInitialization : ComponentSystemGroup { }
[UpdateAfter(typeof(GroupPlantFrameInitialization))]
public class GroupPlantApplyUpkeeps : ComponentSystemGroup { }
[UpdateAfter(typeof(GroupPlantApplyUpkeeps))]
public class PlantGroupAfterUpkeep : ComponentSystemGroup { }
[UpdateAfter(typeof(PlantGroupAfterUpkeep))]
public class PlantGroupEndFrame : ComponentSystemGroup { }

You can use a DynamicBuffer to store entities that need to be processed instead. This reduces structural changes but also provides an existence-based processing criteria which is the buffer length.
If you need to accumulate elements and then process them on a signal, you can add a tag component to the entity containing the buffer.

1 Like