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

[Showcase] DOTS in Production

Discussion in 'Entity Component System' started by echeg, Nov 4, 2020.

  1. echeg

    echeg

    Joined:
    Aug 1, 2012
    Posts:
    90
    Hi everybody!

    I would like to share a case of our last game launch.
    It's our second game which was made with DOTS. The game was developed in 5 months by a team of 5 people. The techlaunch was successful without any serious technical problems.

    The game is Midcore Bricks Breaker - a mix of the arkanoid style battle (like Bricks Breaker games) with heroes and midcore meta which includes town-building, quests and events (like Empires & Puzzles).

    upload_2020-11-4_16-49-58.png

    We have already developed several Match-3 games using our own ECS framework before. We also worked with Entitas-CSharp and Leo ECS.

    The game has two main parts.

    The first is the meta - collection of heroes, building a city, completing quests, etc. This part is created on regular Monobehaviors. The second part is a battle. This battle also has two main modules.

    upload_2020-11-4_17-10-25.png

    The first module is responsible for the logic - an isolated DOTS game logic is in a separate asm.def. This module is covered with unit tests and does not have any connection with Monobehavior\GameObjects. This module is completely deterministic and can make a replay by an player input.

    The second module is responsible for the view. View based on the UGUI and Monobehaviour. Communication goes through the adding of Notification System which is connected with GameObject \ UI elements.


    Code (CSharp):
    1.    
    2.     [UpdateInGroup(typeof(PresentationSystemGroup))]
    3.     public class NotificationHerodHpChangeSystem : SystemBase
    4.     {
    5.         private EntityQuery _notifyGroup;
    6.         protected override void OnCreate()
    7.         {
    8.             RequireForUpdate(_notifyGroup);
    9.         }
    10.  
    11.         protected override void OnUpdate()
    12.         {
    13.             Entities.WithoutBurst().WithStoreEntityQueryInField(ref _notifyGroup).WithChangeFilter<CardResultHeroHp>()
    14.  
    15.                 .ForEach((in CardCoreData cd, in CardResultHeroHp cardSumHeroHp) =>
    16.                 {
    17.                     CardsInHandV2.Instance.GetCardByName(cd.CardId).SetNewHeroHp(cardSumHeroHp);
    18.                 }).Run();
    19.         }
    20.     }

    How we work with DOTS:

    1 We use UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP and manually start the systems from lists, 30-90 ticks per second.

    Code (CSharp):
    1.      
    2.       void Execute(){
    3.             foreach(var sys in simulationSystems)
    4.             {
    5.                 sys.Update();            
    6.             }
    7.         foreach(var sys in presentationSystems)
    8.             {
    9.                 sys.Update();            
    10.             }  
    11. ...
    We have a strict order of execution of all systems. We needed determinism. Also we have extremely bad experience with auto-ordering systems.


    2 from ~600 systems 99% do not use parallel scheduler i.e. it run through .Run()

    Code (CSharp):
    1.  
    2.             Entities.WithAll<CardHeroIsLive>()
    3.                 .WithNone<HeroIsDead>()
    4.                 .ForEach((ref CardCritChanceResult cardCritChanceResult, in CardCritChance cardCritChance,
    5.                 in DynamicBuffer<HeroesBuffsElement> buffBuffer) =>
    6.             {
    7.                 var balls = 0;
    8.                 var buffer = buffBuffer;
    9.  
    10.                 for (var index = 0; index < buffer.Length; index++)
    11.                 {
    12.                     var buff = buffer[index];
    13.                     if (HasComponent<BuffAddCardCritChance>(buff.BuffSourceData))
    14.                     {
    15.                         var c = GetComponent<BuffAddCardCritChance>(buff.BuffSourceData);
    16.                         balls += c.Value;
    17.                     }
    18.                 }
    19.                 cardCritChanceResult.Value = (cardCritChance.Value + balls);
    20.             }).Run();
    In our case launching the parallel scheduler takes more time than the code itself.


    3 We can get performance growth from a parallel scheduler only when we use it in mass physics raycasts. It should be noted that there are about 200 entities in one chunk at most.

    In most cases the system processes only a few(3-4) Entity per tick.

    3 Mostly (90%+) logic systems use Burst.

    4 We try hard to avoid the Add\Remove component. In most cases we use enable\disable flags - it's faster.

    5 To simulate a reactive pattern, we use either a double buffer if it affects a complex object (hero / monster / spell) or tag component, if this Entity is used as an event.

    Double buffer example:

    Code (CSharp):
    1.  
    2. public struct Hp: IComponentData    {
    3.     public int HpPrevios;
    4.     public int HpCurrent;
    5. }
    Tag component example:

    Code (CSharp):
    1. public struct NotifySend : IComponentData    {    }
    6 We have many common components that are used by dozens of different Entities for example

    Code (CSharp):
    1.  
    2. public struct TicksCooldown : IComponentData
    3. {
    4.     public int Value;
    5.     public TicksCooldown(int c)
    6.     {
    7.         Value = c;
    8.     }
    9. }
    Used in 50+ different type "objects" while there is only one system that centrally counts ticks.

    Code (CSharp):
    1.            
    2. protected override void OnCreate()
    3. {
    4.     _endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    5. }
    6.  
    7.     var ecb = _endSimulationEcbSystem.CreateCommandBuffer();          
    8.     Entities.WithNone<TicksDone>().ForEach((Entity entity, ref TicksCooldown ticks) =>
    9.                 {
    10.                     ticks.Value -= 1;
    11.                     if (ticks.Value <= 0)
    12.                     {
    13.                         ecb .AddComponent(entity, new TicksDone());
    14.                     }            
    15.                 }).Run();
    16.  
    17.  
    7 We have a lot of hierarchical relationships.

    Code (CSharp):
    1.  
    2. public struct SpellSource : IComponentData
    3. {
    4.         public Entity SourceHeroCard;
    5. }

    For example, there is a buff with the link to the source → spell (another entity).

    And the spell with the link to the source → hero.

    In this case there will be a lot of code like HasComponent\GetComponent, as we consider, it is almost impossible to make architecture in another way.

    8. We don't use Unity Conversation API. Because we have our own =)

    Since our logic is completely separated from the view, when we work with the view GameObjects\Prefabs it is`t connected with DOTS\Entity. And when we work with data that turns into components and Entity, we don`t work with GameObject \ Prefabs.

    upload_2020-11-4_17-12-16.png

    In Unity game designers may work with Scriptable Objects or with a certain in-game editor. But game designers in 95% of cases work with Google Sheets where they can update game data and send it to the server\client by one click.

    upload_2020-11-4_17-9-25.png

    You can describe a set of components and create new logic right in Google Sheets.
    For example, specify several different triggers or target choices, localization, icons, sounds, etc.
    This allows us to change/add/remove any gameplay data without updating the client. At this moment we have about 10 megabytes of text data in the GoogleSheet.

    If you have any questions please ask) I will try to answer

    Our project is available on Google Play in Russia, USA, England and New Zealand.

    https://play.google.com/store/apps/details?id=com.qcore.monsterbreaker
    IOS version is being approval, but available through Testflight
    https://testflight.apple.com/join/fg8OHw7x
     
  2. jdtec

    jdtec

    Joined:
    Oct 25, 2017
    Posts:
    296
    Congrats on your game launch and thanks for the info!

    Cross-platform determinism? Are you using fixed-point maths?

    I update systems the same way you do for similar reasons.
     
  3. echeg

    echeg

    Joined:
    Aug 1, 2012
    Posts:
    90
    In privios games we use only fixed int math. Self sqrt implementation(by lookup table) and etc
    In this game, we do not have synchronous multiplayer between platforms, so we use floats.
    In this game, we only validate replays
     
    Baggers_ likes this.