Search Unity

Question A potentially unsolvable problem with the ECS...

Discussion in 'Entity Component System' started by DacunaZuke, Jun 11, 2022.

  1. DacunaZuke

    DacunaZuke

    Joined:
    Apr 30, 2022
    Posts:
    10
    Couldn't think of a good title. Anyways, I was able to do something with classic Unity which might be impossible with ECS due to its non-OOP nature, but I thought I would post this here in case someone had a workaround that I haven't thought of.

    I want a projectile that moves in a straight line, spins around for a bit, and then moves in a straight line again.

    This is obviously very possible with both classic and DOTS Unity by hardcoding a couple movement functions and calling them based on a timer, but what if I could generalize this a bit?

    In classic Unity, I created a component called SequentialMovement which got every component of a type attached to the same GameObject, say IMoveable, and put them into an array. Then, I also had an array of start times. The logic was basically:

    • If the timer reaches the next start time in the array, increment the current movement component index.
    • Run the current movement component at that index. This update's the projectile's position.

    With the ECS, this seems pretty much impossible. The best I could think of is adding and removing components at runtime with a "component switcher" system, but even then, I wouldn't know what to give the system so that it knows what to switch to. Not to mention I would have to tweak the component's variables after switching to get the right outcome.

    I know that everything generally runs better hardcoded, and this breaks rules that make the ECS as powerful as it is, but sometimes the ease of use of some abstraction makes game development simpler, so I thought I'd give it a shot.
     
  2. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    967
    Huh? Impossible?
    It would work pretty much 1:1 as you have described your OOP approach. The array data would live in a DynamicBuffer or a blob.
    ECS is not just about IComponentData.
     
    charleshendry and nekelund_unity like this.
  3. DacunaZuke

    DacunaZuke

    Joined:
    Apr 30, 2022
    Posts:
    10
    Sorry, could you explain a bit further? I've used DynamicBuffers before, but not blobs. Even so, I'm not sure how I would tell a component what component I would like to switch to at runtime.

    Would I call AddComponent? What would I give it if I can't refer to some generic movement component?
     
  4. vectorized-runner

    vectorized-runner

    Joined:
    Jan 22, 2018
    Posts:
    398
    It's easy, you could just use tag components for describing your movements and use a timer to switch state. It seems like you're trying to generalize (and maybe overengineer) a simple problem. Data oriented design forces you to think concrete.
     
  5. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    This looks like a state machine problem.

    Simplest solution to it - don't store logic and data as entities. Use state machine separately to attach / remove required components with data. To which then systems would react to.

    Alternatively, you go with mega-struct approach & modifiers.
    For example, store all required data modifiers in a struct, some kind of MovementModifiersData.

    Then, have a DynamicBuffer<MovementModifiersData> built and attached based on designer's nodes (authored data).

    Each time index switches, take values for movement from that DynamicBuffer by index, set all movement variables based on that MovementModifiersData.

    So basically, swap out values each X seconds.
    ProjectileMovementSystem will then process every value set, and apply proper movement without branches, and in a job. This approach is somewhat identical to shader branchless techniques. Multiply by zero - and you've got no movement. Multiply by X or 1 and you've got a speed boost, enabled rotation speed, etc.
     
    Last edited: Jun 12, 2022
    DacunaZuke likes this.
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    You can look at this tool, and the sample that comes with it (a state machine example): https://forum.unity.com/threads/sou...m-in-dots-now-with-source-generators.1264616/

    This tool will allow you to have a DynamicBuffer<ProjectileState> on your projectile entity, where "ProjectileState" can be all kinds of different states with different data, sizes, and logic. In other words, in OOP you'd have a List<ProjectileState> where "ProjectileState" is an abstract class that can have many implementations, and this tool will give you an equivalent of that in DOTS. You can remember a state to switch to in a generic way, by remembering the index of the desired state struct in the DynamicBuffer<ProjectileState>. So your Projectile component can remember the index of each state, and switch to the correct one depending on some conditions. Alternatively, each projectile prefab could be given a unique sequence of states stored in its dynbuffer, and you simply transition to the next state in the buffer once a state is finished. All you'd need is a way to author states in a way where they are all converted to PolymorphicStructs and added to a dynamic buffer during conversion

    If your "states" change relatively often, this solution performs very significantly better than a "adding/removing components" approach or a "enabled components" approach, on top of being much simpler to setup and much more versatile. And even if states don't change often, the performance disadvantage of this approach will be very small, because the very large majority of the frame cost of your projectile updates will be hit detection logic either way. I'd guess the difference would barely be noticeable on the profiler unless you reach over 10k simultaneous projectiles in flight at the same time. At low projectile counts (a few hundreds), the 1-job-per-state scheduling cost of the structural changes approach might even cost more than the cost of "polymorphism", which only needs one job for all states. One last note on performance; the polymorphic approach makes sure that there are no performance spikes during state changes, which can often be more desirable than an alternative that has a lower constant cost but with big structural change spikes once in a while. A stable 60fps average will feel a lot more fluid than a 60fps average where most frames are at 80fps but big spikes happen here and there

    While I'd recommend the solution above, there still is a way to solve this without "PolymorphicStructs". Your projectile entity would have a DynamicBuffer<StateEntity>, where StateEntity is a reference to an entity that holds the state data. When you switch to a certain projectile state, you'd remove the current state component on the projectile entity and copy that state entity component onto the main projectile entity. This way, you can have multiple states of the same component type, but with different parameters that are remembered. But chances are this approach will only have disadvantages compared to the PolymorphicStructs approach, both in terms of performance and convenience
     
    Last edited: Jun 12, 2022
    DacunaZuke, xVergilx and tmonestudio like this.