Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Factorio like game

Discussion in 'Entity Component System' started by dennis124, Nov 8, 2022.

  1. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    Hello everyone,

    I'm right now working on a factorio like game with conveyor's and constructor's. Right now the conveyor's are working and also is the smelter. For my smelter entity I have an InputComponent:
    Code (CSharp):
    1. public struct InputComponent : IComponentData
    2.     {
    3.         public Entity item;
    4.         public int2 pos;
    5.         public bool occupied;
    6.         public Entity filter;
    7.     }
    And an OutputComponent:
    Code (CSharp):
    1. public struct OutputComponent : IComponentData
    2.     {
    3.         public Entity outputEntity;
    4.         public int2 pos;
    5.         public Entity item;
    6.     }
    Also there is a move system that transfers the items from an output to an input.
    So every building process the item when its in their input slot and instantiate the output item in the output slot.

    Now I want to create constructor's with multiple input slots. How do I do that?
    You can't have the same ComponentData multiple time on an entity and when I think of a dynamic buffer it get's complicated. Should I just have a ConstructorInputComponent with pos1 and pos2 as well as item1 and item2?

    Has anyone a good idea for this problem?

    Thank you very much for helping!
    Dennis
     
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,574
    You could perhaps attack problem in multiple ways.
    But dynamic buffer may Ne your friend here.
     
  3. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    The dynamic buffers didn't work really well. I've now spent a lot of time thinking about how I could improve my move system. Currently I'm moving an item from one entity to another which are a lot of structural changes. Does anyone have any good ideas on how to do this better?
    Thank you for the answers.
    Dennis
     
  4. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    113
    Based on the info in this thread, here's how I'd see it:
    • Each "Building" (smelter) has a DynamicBuffer<Input> and a DynamicBuffer<Output>. These Input/Output IBufferElementDatas contain the exact same fields as your InputComponent/OutputComponent in the first post.
    • A building that has 3 input ports and 2 output ports would have a DynamicBuffer<Input> of length 3, and a DynamicBuffer<Output> of length 2
    • When an item entity on a conveyor reaches an input port, it gets assigned as the "item" Entity in that port. So if the conveyor connects to your smelter's input port at index 1, then you'll get the smelter entity's DynamicBuffer<Input>, get the element at index 1 in that buffer, and write the item entity to it. You'd also probably disable rendering of the item entity at this point
    • When the smelter detects that its input ports have a valid set of items, it begins the transformation process on these items
    • When the transformation process is done, it destroys the input item entities, and creates output items that are immediately assigned to the "item" entity fields of the output ports
    So with this strategy, the only structural changes that happen are when a set of input items are transformed into a set of output items (destruction of the input items and creation of output items).
     
    charleshendry and toomasio like this.
  5. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    That's a good point here is some more information:
    This is my move system: (not working)
    Code (CSharp):
    1. Entities.WithReadOnly(itemComponentData).WithReadOnly(filterData).WithNone<OutputNotFoundTag>().WithNone<UnregisteredTag>().ForEach((Entity e, int entityInQueryIndex, ref OutputComponent output) =>
    2.                 {
    3.                     //output empty
    4.                     if(output.item == Entity.Null) return;
    5.                     var inputs = ecbParallel.SetBuffer<InputComponent>(entityInQueryIndex, output.outputEntity);
    6.                     Debug.Log(inputs.Length);
    7.                     for (int i = 0; i < inputs.Length; i++)
    8.                     {
    9.                         var input = inputs[i];
    10.                         if (input.pos.Equals(output.pos))
    11.                         {
    12.                             //next input is full or occupied
    13.                             if(input.item != Entity.Null || input.occupied) return;
    14.                             var itemComponent = itemComponentData[output.item];
    15.                             if (filterData.TryGetBuffer(input.filter, out var itemFilter))
    16.                             {
    17.                                 foreach (var filter in itemFilter)
    18.                                 {
    19.                                     if (itemComponent.itemID == filter.itemID)
    20.                                     {
    21.                                         ecbParallel.SetComponent(entityInQueryIndex ,output.item, new ItemComponent{itemID = itemComponent.itemID, pos = input.pos, hasMoved = true, timeToMine = itemComponent.timeToMine});
    22.                                         input.item = output.item;
    23.                                         inputs[i] = input;
    24.                                         ecbParallel.SetComponent(entityInQueryIndex ,e, new OutputComponent{item = Entity.Null, pos = output.pos, outputEntity = output.outputEntity});
    25.                                         return;
    26.                                     }
    27.                                 }
    28.                             }else {
    29.                                 Debug.Log("No filter");
    30.                                 ecbParallel.SetComponent(entityInQueryIndex ,output.item, new ItemComponent{itemID = itemComponent.itemID, pos = input.pos, hasMoved = true, timeToMine = itemComponent.timeToMine});
    31.                                 input.item = output.item;
    32.                                 inputs[i] = input;
    33.                                 ecbParallel.SetComponent(entityInQueryIndex ,e, new OutputComponent{item = Entity.Null, pos = output.pos, outputEntity = output.outputEntity});
    34.                                 return;
    35.                             }
    36.                         }
    37.                     }
    38.                 }).ScheduleParallel();
    39.             Dependency.Complete();
    40.             ecb.Playback(EntityManager);
    41.             ecb.Dispose();
    The conveyor are also entity's that only set their item from the input component to the output component:
    Code (CSharp):
    1. timeToMove -= Time.DeltaTime;
    2.             if (timeToMove <= 0)
    3.             {
    4.                 Entities.ForEach((ref ItemComponent itemComponentData) => { itemComponentData.hasMoved = false; })
    5.                     .ScheduleParallel();
    6.                 var itemComponentData = GetComponentDataFromEntity<ItemComponent>(true);
    7.                 Entities.WithAll<ConveyorComponent>().WithReadOnly(itemComponentData).WithNone<UnregisteredTag>().WithNone<OutputNotFoundTag>().ForEach((ref OutputComponent output, ref DynamicBuffer<InputComponent> inputs) =>
    8.                 {
    9.                     for (int i = 0; i < inputs.Length; i++)
    10.                     {
    11.                         var input = inputs[i];
    12.                         // output occupied
    13.                         if (output.item != Entity.Null)
    14.                         {
    15.                             input.occupied = true;
    16.                             inputs[i] = input;
    17.                             break;
    18.                         }
    19.                         input.occupied = false;
    20.                         // no input
    21.                         if (input.item == Entity.Null)
    22.                         {
    23.                             inputs[i] = input;
    24.                             break;
    25.                         }
    26.                         var itemComponent = itemComponentData[input.item];
    27.                         if (!itemComponent.hasMoved)
    28.                         {
    29.                             output.item = input.item;
    30.                             input.item = Entity.Null;
    31.                         }
    32.                         inputs[i] = input;
    33.                     }
    34.                 }).ScheduleParallel();
    35.                 timeToMove += 2f;
    36.             }
    So moving items only means transfering from one entity to another.
    I thought that would be more performant
     
    Morvar likes this.
  6. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    113
    It's a bit tricky to give further advice on this without trying to code the whole thing in practice, but some general suggestions would be:
    • Try to rearrange things so that no EntityCommandBuffer is needed at any point in the movement system code above. I'm fairly confident this is possible:
      • No need to "ecb.SetBuffer<InputComponent>", since the quantity of input buffer elements should always stay the same
      • No need to "ecb.SetComponent" on the ItemComponent, since item entities don't need to be changed by the buildings
      • No need to "ecb.SetComponent" on the OutputComponent, since your job already declared "ref" access to it (unless I'm mistaken?). You can change it directly in the job
      • etc...
    • While it can be tempting to give individual conveyor tiles the same Input/Output approach that regular buildings have, this may not the most efficient approach. You could have entities that represent a full chain of conveyor tiles (instead of individual conveyor tiles), and those entities could have a buffer of all item Entities that are on it. This way, a job can go "for each conveyor chain, move all items on it by x displacement". There probably exist even better strategies than this
     
    Last edited: Nov 21, 2022
    toomasio likes this.
  7. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    This is a very good idea. I will definitly try this.
    Thanks you very much!

    If there are other ideas please let me now
     
  8. scottjdaley

    scottjdaley

    Joined:
    Aug 1, 2013
    Posts:
    152
    I agree that DynamicBuffers are probably what you should be using to store inputs and outputs. While you might only need 2 inputs or 2 outputs right now, its easy to imagine later needing 3 or more.

    What exactly isn't working?

    I believe this is more or less what Factorio does. They have some other interesting optimizations as well. For example, instead of storing how far along each item is on the belt, they store the distance between items. This means that only the front item's distance has to be updated each frame. Source

    Although if the items are represented as entities with their own mesh, you'll need to write to each translation each frame anyways in order to render them in the correct position. So this probably wouldn't be worth implementing unless you had a custom item rendering solution.

    I'd recommend reading through their blog posts for other implementation and optimization ideas: https://www.factorio.com/blog/

    Not exactly relevant to your question, but I'm also making an automation game with ECS. However, my requirements are quite a bit different than a game like Factorio and Satisfactory. Specifically
    • Each entity can only ever hold a single item
    • Each entity can only have one input and/or output. No splitters or mergers (you build these up from other primitives)
    • All entities can be pushed around and rotated.
    Example of the kinds of things that are possible:


    For these reasons, I decided to represent an Input and Output as IComponentData's, and then separately store a Slot IComponentData to hold the item. Initially, I implemented the "conveyor chain" optimization, but later ripped it out since it made everything much more complicated and the conveyor chains are constantly changing in my game.

    Anyways, it sounds like your game is much closer to something like Satisfactory so I'd stick with the DynamicBuffer approach. That way, conveyor belts, smelters, and constructors can all be thought of as entities with N inputs and M outputs.
     
  9. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    The setBuffer from the entityCommandBuffer is always empty.

    I also did read the blog post a few times and they have really good approaches but the whole algorithm behind the conveyor segments, especially when you set and remove one conveyorbelt is quiet heavy I think.

    I also think that the important part of my game is the performance of all conveyor's and item's. But if I get the dynamic buffer to work it's still a step in the right direction.

    P.S: Your game looks really cool!
     
  10. scottjdaley

    scottjdaley

    Joined:
    Aug 1, 2013
    Posts:
    152
    ECB.SetBuffer is for when you want to replace the entire buffer on an entity. It returns an empty buffer that you can then add things too. But in general, you should avoid using an ECB unless you need structural changes. Like @philsa-unity mentioned, its seems possible to write this code without an ECB.

    Instead of
    ECB.SetBuffer<>
    , you should something that returns the buffer immediately and doesn't defer anything. If you are using entities 1.0, the replacement would be
    SystemAPI.GetBuffer<>
    . If you are on 0.51 or earlier, you'll need to use something like this). This will return the actual buffer on the entity and allow you to read and write to it.
     
  11. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    Thats really good to know. Thank you very much!
    Do you recommend upgrading to ecs 1.0 or should I wait til ecs 1.0 releases fully?
     
  12. scottjdaley

    scottjdaley

    Joined:
    Aug 1, 2013
    Posts:
    152
    If you're just starting out, I'd definitely recommend upgrading now. If not, it will be more painful to do so later. I'd also encourage you to start using IJobEntity instead of Entities.ForEach. There have been hints that Entities.ForEach will be removed at some point in a future release. Also, IJobEntity is pretty similar, especially when you leverage the SystemAPI shortcuts for ConponentLookups and the like.
     
  13. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    Thanks, then I will do this!
     
  14. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    Hello I'm back.
    I did managed to upgrade my project fully to ecs 1.0.
    I also moved my Entities.ForEach into IJobEntity jobs.
    I have now beltPath Entites instead of an entity for a single belt but now I have another (maybe small?) problem.
    If I have entities that I want to move around on my beltPath I have to use SystemAPI (SetComponent) which is very slow. Every time I want to move it I need to set the new position of that item. So if there is a solution for this problem it's good but I think that there is a even better solution:

    There is another option but I don't now if it works:
    Can I create a system that goes parallel through all belt paths and only draws the different items an the right position? So that there are no real item entities and just numbers that are identifiers for an item. Does anybody know if something like this exsists? The system would somehow have access to materials and meshes of the right entity.

    Thanks for your response and help
    Dennis
     
  15. scottjdaley

    scottjdaley

    Joined:
    Aug 1, 2013
    Posts:
    152
    Nice work!

    While SystemAPI.SetComponent() is slower than iterating the items directly, it's still pretty fast. Yes, it is random memory access which isn't great for leveraging cache prefetching, but if your items are actual entities, it is impossible to avoid some amount of random memory access. It is worth profiling at really large scales to see if it will actually be bottleneck for your game.

    There are a few other ideas you could try if you really want to optimize this further. Instead of iterating the belt paths and setting the position on each item, you could set some kind of velocity on the item, but only when the velocity changes. If your belts follow a grid, you could have different kinds of "velocities" to represent when an item is moving on a straight section or corner section or blocked.

    Or perhaps each item stores a copy of the spline of the belt that it is on. Then it only needs to know when to start and stop moving. So you would iterate the belt paths and update an IsBlocked component on all the items that just started or stopped moving.

    You could also try iterating the items instead of the belt paths. This avoids the need for random memory access in order to set the item positions, but it does now require random memory access to lookup information about the state of the belt paths it is on. For example, maybe each belt path has a component that stores the number of items that are blocked and stationary at the end. From this, you can determine the position or progress along the belt path's spline where the blockage starts. Now, when iterating items, you lookup this blockage position of the belt path it is on and compare it to the item's position along the spline and use this to determine how the item should move. This still requires the random lookup of the blockage position, but since there are much fewer belt paths than items, it's more likely to already be in the cache.

    Yeah, this is what i was referring to in my initial post about a custom item rendering solution. If you really want to go down this path, I think BatchRendererGroup is what you want. I haven't used it but I think it gives a lower level access to drawing meshes on the screen directly.

    In general, I would caution you from going to deep into optimization if you are just starting on this game. It is quite easy to get carried away and make something so complex that it makes everything else harder to do. And the requirements for your game might change. For my own game, I started simple, then went super deep into optimizing the conveyor belts, and then threw all of that away when I realized it wasn't compatible with the direction to take the game. So now I'm back to something simple and flexible and still fast enough for the scale of the game I'm making. And I can still optimize it later.

    Good luck!
     
    mbalmaceda likes this.
  16. MicCode

    MicCode

    Joined:
    Nov 19, 2018
    Posts:
    58
    Nice to see fellow game dev here making more factory game, there's is just not enough automation game out there imho.
    I love factory and automation game, I think coder is more susceptible to this type of game, and when you play it, you think about how to code it, then you want to make your own!
    And with unity DOTS and ECS, there's is just no more excuse not making one.
    (Beside the fact that entities is experimental:p)

    It's interesting to see that the most complex game in terms of programming effort are usually make by really small team. Still it's a huge work for a solo dev, totally agree on the point that one should start simple and optimize later. When your game is already build on top of entities, you already have a head start, getting the game play right is more important then anything else at the beginning.
    Just some thought from my experience doing similar attempt. Actually I think of it this way, when you are mentally prepare to rewrite the whole thing at some point, you can progress faster and allow you to write "dirtier" code. In case you are a OCD perfectionist coder like me.

    BTW, I joined the AutomationStation Discord, and there's quite a few interesting discussion about Unity and DOTS there. @scottjdaley Have you consider doing a devblog about your game?
    edit: Sorry, you already did! Need to learn to google
    https://automationstationgame.com/blog/devlog-3-crafting-a-crafting-system/
     
    Last edited: Dec 10, 2022
  17. scottjdaley

    scottjdaley

    Joined:
    Aug 1, 2013
    Posts:
    152
    Greetings! Totally agree about there not being enough automation games! (Btw, technically ECS is in preview as of the latest 1.0.0-pre.15 version)

    I think its difficult to compare genres in terms of the programming effort. A lot of "simple" games have a crazy amount of programming effort in order to make them run well of low end hardware. But I totally agree that making a game solo is a ton of work. Better to save complex optimizations until they are strictly needed.

    Glad you found the Discord! There are a few other members making games in DOTS so its a good place to ask questions. I'd love to have more people to chat with about automation game dev.

    Unfortunately, I haven't kept up with the blog posts but it is something I'd like to revisit. Right now it's just a couple of posts about some game design stuff, but I'd like to write about the more technical aspects as well (ECS architecture, shaders, proc gen, etc.)
     
  18. dennis124

    dennis124

    Joined:
    Oct 9, 2022
    Posts:
    17
    I think that is a good advice. I will work on my game and if I need to optimize my belts further I will do this then.

    That's exactly why I started to work on my own game!

    I joined now too and it's really cool. You even made a Satisfactory bot. That's really nice.
    Thank you for all the help!