Search Unity

Unity's Survival Shooter recreated with the ECS

Discussion in 'Entity Component System' started by gamevanilla, Mar 28, 2018.

  1. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    For the past few days, and as a learning experience, I have been working on recreating Unity's Survival Shooter using the new ECS. I wanted to share it here in case someone finds it useful.



    I wrote some notes about it here and you can find the GitHub repository here.
     
    Last edited: Feb 15, 2022
  2. Emre_U

    Emre_U

    Joined:
    Jan 27, 2015
    Posts:
    49
    Please do the roll a ball too. I want to start very very basic :)
     
    Gametyme, gamevanilla and FROS7 like this.
  3. MitchStan

    MitchStan

    Joined:
    Feb 26, 2007
    Posts:
    568
    Thank you for your write up. Yours is the clearest and most enlightening explanation of ECS on Unity I’ve come across. Thank you so much for sharing!
     
    FROS7 and gamevanilla like this.
  4. MechEthan

    MechEthan

    Joined:
    Mar 23, 2016
    Posts:
    166
    I love insights like these from your blog post:
     
    sharkapps, gamevanilla, osss and 2 others like this.
  5. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Great tutorial @gamevanilla . This is a very good introduction.
     
    gamevanilla likes this.
  6. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Thank you for your feedback! I would love to do more of them in the future.
     
  7. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Thank you, Joachim! It means a lot coming from you.

    And thank you for your work on the ECS; it truly is a remarkable system and I cannot wait to write more and more code with it.
     
  8. buFFalo94

    buFFalo94

    Joined:
    Sep 14, 2015
    Posts:
    273
    That's really interesting dude. So how is the performance compared to the original?
     
    Gametyme likes this.
  9. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    I have not checked it myself, as the focus was more on learning the concepts behind ECS and how to express them in code. One thing to note is the hybrid mode, which the project uses, does not reach the maximum level of performance attained by the pure mode (this is explained in more detail here). It is still a great way to introduce the new paradigm in your codebase, though.
     
    buFFalo94 likes this.
  10. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    451
    Very nice example, I will use some of your ideas in my game.

    I can't find how you are restarting the session, I can't find any reference to RestartLevel().
     
  11. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Thank you! I did not implement the restart functionality.
     
  12. starikcetin

    starikcetin

    Joined:
    Dec 7, 2017
    Posts:
    340
    Clean and well-written.
     
    gamevanilla likes this.
  13. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    We are in 2019 already, so I figured it would be a good time to update this project to reflect the latest changes in the ECS. I wrote some notes here and created a new GitHub repository that you can find here.

    Happy new year!
     
    MechEthan, Gametyme and IsaiahKelly like this.
  14. IsaiahKelly

    IsaiahKelly

    Joined:
    Nov 11, 2012
    Posts:
    418
    @gamevanilla Thanks for making such a nice example and updating it!

    I actually already removed injection from my own fork of your project but hadn't gotten around to implementing jobs yet. Was somewhat distracted with figuring out how best to implement per enemy type settings. Right now I just have a score value on enemy component itself and a new CharacterConfig MonoBehaviour on each prefab to store hurt and death sound effects for each type.
     
    gamevanilla likes this.
  15. IsaiahKelly

    IsaiahKelly

    Joined:
    Nov 11, 2012
    Posts:
    418
    Actually you can use typeof for MonoBehaviour components too, as that is actually what I did at first. Using ComponentType.Create<> appears to be just slightly more efficient according to the docs.

    Edit: Might be confusing classic Component with MonoBehaviour. Need to verify this....
    Edit 2: Yep, I can confirm typeof(CharacterConfig) works. At least for me anyway.
     
    Last edited: Jan 2, 2019
    gamevanilla likes this.
  16. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Thank you very much, Isaiah! :)

    Generally speaking, I intend to keep the project up-to-date as the ECS evolves. Hopefully in a few months we will be able to jobify most of it as the work on rewriting core systems like rendering and physics for the ECS progresses.

    That is a very interesting area! It looks like there is a really nice pattern waiting to emerge with prefabs + scriptable objects feeding into the ECS world. Without having researched this too much as of yet, what you are doing sounds exactly like what I would do myself.
     
  17. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    You are definitely right, thank you! I will fix the article right away.

    With ComponentType.Create<> being more efficient and also symmetrical with the Subtractive and ReadOnly variants, I will also update the code to use it everywhere.
     
  18. IsaiahKelly

    IsaiahKelly

    Joined:
    Nov 11, 2012
    Posts:
    418
    When studying your older source code I kind of disliked how many things some systems would touch. Like how the player health system would access audio, animator, particles, etc. This seemed to go against the single responsibility principle. I see now you've separated some of these aspects into more systems like PlayerHitFxSystem, which is a good improvement.

    However I've also thought about the idea of just creating something like a CharacterEffects MonoBehaviour that you attach to both enemy and player prefabs that handle all special effects the "classic" way. Then you could just have a single damage or effects system call this CharacterEffects script on anything with a CharacterEffects, Health and Damage component on it.

    This encapsulation would mean you don't need a system for each kind of entity that can get hit. The system doesn't need to know or care about this aspect. It also means you can set per prefab configs in the editor and we let "classic" Unity handle all the visual presentation aspects of the game. Now I realize this goes completely against ECS principles and is probably slower but I think it might be a fine solution right now for hybrid ECS since so many features of Unity remain in OOP land. You should also still be able to easily upgrade the project as ECS evolves. Just an interesting idea anyway.

    I've been toying around with using ScriptableObjects for settings but have run into some issues. It looks like @tertle has a really nice system for this but it probably depends on third-party frameworks and I hope to find a more simple and "vanilla" Unity solution.

    Yes, that is what I ended up doing myself. I only started out using typeof because the Create keyword confused me and I didn't realize it served the same purpose.
     
    Last edited: Jan 11, 2019
    gamevanilla likes this.
  19. ghtx1138

    ghtx1138

    Joined:
    Dec 11, 2017
    Posts:
    114
    Click! :D

    Hey @gamevanilla thanks so much for your notes. A lightbulb has just gone off in my head. Great info for newbs.

    Cheers.
     
    gamevanilla likes this.
  20. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Hey there! Just wanted to let you know I have upgraded the project to work with the latest beta of Unity 2019.1 and the latest version of the ECS (and also updated the article to reflect these changes).
     
    IsaiahKelly, pcg, FROS7 and 1 other person like this.
  21. MechEthan

    MechEthan

    Joined:
    Mar 23, 2016
    Posts:
    166
    Awesome! Will check them out.

    Sharing your updated direct links for everyone's convenience, because they've changed since the OP:
     
    gamevanilla likes this.
  22. Deleted User

    Deleted User

    Guest

    Bookmarked! I hope I'll be able to get back to it before you remove it! :p

    I need your advice: how useful would ECS be in a project like the 2018 Unite Berlin 2D platformer tutorial? Videos are here, the first one shows a demo: https://www.youtube.com/playlist?list=PLX2vGYjWbI0REfhDHPpdIBjjrzDHDP-xT

    Thank you. :)

    Edit: :D I hadn't noticed that this post was a year old!
     
    gamevanilla likes this.
  23. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Do not worry! I do not plan to remove it. If anything, I want to keep on improving it as the DOTS evolves. :)

    With regard to your question: I feel by now we all have a pretty good grasp of how the ECS is a performance enabler but, as I gain more experience applying it in the real world, I think it also has other benefits such as the quality of the resulting code in more qualitative, software engineering terms. I am finding that, when applying the paradigm to the entirety of a project*, my code is easier to test, maintain and has fewer bugs.

    I would like to be able to verbalize this in a better way, because just stating something is univocally better and leaving it at that feels like an over-simplified generalization that may be hard to swallow for some (and understandably so). It feels new and it feels refreshing, but maybe that is only because I personally have always found it incredibly hard to write excellent object-oriented code. What is funny is that, when you truly get into the theory behind data-oriented programming, there is actually very little that is new to it. It is back to basics time**. It is caring about the hardware your code will run on. It is about putting the data back at the center of your thought process. Back to where it always was and where it always should have been. My guess is that we, as a community, will also gain more insight on this other side of data-oriented programming as our understanding of the paradigm evolves.

    I know this does not answer your question, but it really depends on what you mean by useful. If all you want to do is to ship a game today, you may as well wait until the system is more mature and has editor facilities that are somewhat comparable to classic Unity. If all you want to do is to learn, it will definitely be an excellent way to do so.

    [*] Technically speaking, not truly a completely pure ECS project. But pretty much as close as you can reasonably get today, by having the simulation running in ECS and with only the presentation layer relying on traditional Game Objects.

    [**] Which does not mean it is necessarily simpler. It certainly presents challenges and, as always, hard problems are hard.
     
    Last edited: Mar 13, 2019
    pcg likes this.
  24. Deleted User

    Deleted User

    Guest

    Thanks for your answer. What I meant by useful was:
    • would converting the project to a hybrid ecs version make the project better in terms of performance,
    • is it worth the trouble for us to convert it to hybrid ecs (the answer could be yes because it would be informative for us but maybe it would not be that informative).
    ;)
     
  25. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Hybrid ECS will not give you a big jump (orders of magnitude) in performance by itself; pure ECS, jobified systems and Burst will get you there. But using hybrid ECS today gives you conceptual exposure to the paradigm and provides a good starting point that you can later extend upon as more engine features become available to use from within pure ECS.
     
    Deleted User likes this.
  26. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    966
    I was a bit overwhelmed by the new ForEach and [Inject] replacements and you deliver a full project to learn from! Big thanks!
     
    gamevanilla likes this.
  27. alexandre-fiset

    alexandre-fiset

    Joined:
    Mar 19, 2012
    Posts:
    715
    Not all heroes wear capes :) That's awesome, thanks for sharing (and updating)!

    Looking at the code shows how things are moving really fast with the ECS package.

    For instance:
    • ComponentDataProxy should now be "Monobehaviour, IConvertGameObjectToEntity"
    • SurvivalShooterSettings could now be moved to several IConvertGameObjectToEntity scripts to pass the default values, as shown here in the samples
    I also have a question: Why is there no defined execution order in your systems? For instance, inputs should be read before everything interactive and Death should happen after everything linked to health, shouldn't they?

    Otherwise it really is great work and it's nice to have more examples to compare to.

    Cheers!
     
  28. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Thank you!

    And thank you for your excellent feedback! Migrating everything to IConvertGameObjectToEntity is definitely on my list. As for the systems order, there is no actual reason other than things working just fine without enforcing one. But I agree it would be a good idea to make it explicit in any case; it is something that I have been doing more recently in my real-world work using the ECS.
     
    pcg and alexandre-fiset like this.
  29. Dragonarkon

    Dragonarkon

    Joined:
    Jun 4, 2017
    Posts:
    5
    @gamevanilla I love this project! You made it super easy for me to understand how ECS works and thanks for keeping updated! My question is how do you overcome the problem with the Update() vs FixedUpdate()? In your ECS project the Player seems to stutter vs the regular MonoBehaviour. I can only conclude that it is because ECS using Onupdate() and there is no OnFixedUpdate(). What can be done to fix this so that the player position is update more than once a frame?

    *Edit: This also applies to the camera follow.
     
    Last edited: Apr 9, 2019
  30. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    Thank you! I have just pushed a commit that makes the player movement and camera follow systems run in FixedUpdate as in the original code (using the approach described here).
     
    LaneFox and Dragonarkon like this.
  31. Dragonarkon

    Dragonarkon

    Joined:
    Jun 4, 2017
    Posts:
    5
    Awesome!
     
  32. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    I wrote some notes on why I believe data-oriented design is more than just about performance here.
     
    MechEthan, RaL and SugoiDev like this.
  33. Naviual

    Naviual

    Joined:
    May 8, 2018
    Posts:
    2
    bookmarked this really helped me understand data oriented design better

    I never did pure/ideal OOP in my games because it never feels right. Say I would have a Fireball class that extends Action class and use a static function like Fireball.action(actor, target,...), so I don't have to write a player.fireball() and then a monster.fireball(). I would chop player class into smaller subclasses for different usages etc.

    back to the topic, when I'm trying to remake it data oriented, I feel like its just adding in entities and converting Player.Xfunction() into PlayerXSystem (or FireballSystem in my case). I noticed alot more lines of code and when the player/monsters have like a few dozens of different actions/spells/abilities, another few dozens of active items... every line of code adds up to be a lot more to do, while the readability of the code seems.. bad (perhaps its just me not used to ECS/DOP or too used to OOP).

    I just have to ask myself does it worth using ECS if performance isn't really an issue? I feel OOP or my semi OOP approach seems alot easier to read (closer to human logic) and maintain(? not too sure but I feel it would be a pain in the ass to take over someone's data oriented program even with proper documentation).
     
    Last edited: Jul 20, 2019
  34. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,266
    Porting OOP code to DoD will lead to a messier and longer implementation. Design a DoD codebase right from the start will lead to a much less code, much more readability, and far fewer edge cases especially as the codebase grows in complexity.
     
  35. Naviual

    Naviual

    Joined:
    May 8, 2018
    Posts:
    2
    would you happen to know any other code example of games using DoD?
     
  36. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    Awesome Thank you!
    but i think it can be much better by changing some logic.

    Example:
    1) CameraFollowSystem, EnemySpawnSystem ..... can use JobComponentSystem instead to maximize the parallelism.
    2) EnemyHealthSystem should use the DynamicBuffer instead of adding and removing components for every hit.you can also use the ChangeFilter to minimize system execution to only when a hit is performed.
     
  37. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    I plan to jobify more things in the future as more core engine systems get rewritten for DOTS; most of the systems in this project still need to eventually sync things with the main-threaded, GameObject-based side of things, so they cannot really go pure easily as of yet. In particular, I am not sure why you would ever want to parallelize a camera following system, considering you only have one camera in the game and this is one of the few very frame-sensitive parts in a codebase where you do not really want to introduce any kind of delays.

    Generally speaking, though, the goal here was to provide a tutorial rather than a benchmark.
     
    Opeth001 likes this.
  38. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    as i said that's Awesome and thank you!
    but from my point of view, it's always better to get the most efficient examples.
    the CameraFollowSystem running into the main thread means that other systems with totally separated logic and components will wait for it to Complete before starting and that's exactly why ECS and Jobs are trying to prevent.
    example, why creating a sync point between healthSystem and CameraFollowSystem when both can run in parallel ? ^_^
     
  39. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    As mentioned earlier, that is what most systems in the project are doing already (because they need to, because many features of the engine are not rewritten for DOTS yet).
     
    Opeth001 likes this.
  40. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    that's my Jobified CameraFollowSystem ^_^

    Code (CSharp):
    1.  
    2. namespace CWBR.Client.Systems
    3. {
    4.  
    5.     /// <summary>
    6.     /// Manage Camera Follow Logic
    7.     /// </summary>
    8.     //[DisableAutoCreation]
    9.     [UpdateInGroup(typeof(PresentationSystemGroup))]
    10.     public class CameraFollowSystem : JobComponentSystem
    11.     {
    12.         private EntityQuery m_LocalPlayer;
    13.         Entity localPlayer;
    14.    
    15.  
    16.         protected override void OnCreateManager()
    17.         {
    18.             // This Query will be Used Once
    19.             m_LocalPlayer = GetEntityQuery(ComponentType.ReadOnly<LocalPlayer>());
    20.             localPlayer = Entity.Null;
    21.         }
    22.    
    23.    
    24.         [BurstCompile]
    25.         [RequireComponentTag(typeof(CameraComponent))]
    26.         public struct CameraFollowJobAfterCache : IJobForEach<Translation, Rotation>
    27.         {
    28.             [ReadOnly]
    29.             public Entity entityPlayer;
    30.  
    31.             [ReadOnly][NativeDisableContainerSafetyRestriction][NativeDisableParallelForRestriction]
    32.             public ComponentDataFromEntity<Translation> targetPlayerPositionGet;
    33.  
    34.             [ReadOnly]
    35.             public float3 cameraOffeset;
    36.             [ReadOnly]
    37.             public float cameraSmoothSpeed;
    38.             [ReadOnly]
    39.             public float deltaTime;
    40.             public void Execute(ref Translation camPosition, ref Rotation camRotation)
    41.             {
    42.                 var playerPosition = targetPlayerPositionGet[entityPlayer].Value;
    43.                 // Follow the Player
    44.                 float3 desiredPosition = playerPosition + cameraOffeset;
    45.                 float3 targetCameraPositionSmoothed = math.lerp(camPosition.Value, desiredPosition, cameraSmoothSpeed * deltaTime);
    46.  
    47.                 // Set Camera Position
    48.                 camPosition.Value = targetCameraPositionSmoothed;
    49.  
    50.                 // Rotate Camera to the Player
    51.                 float3 lookVector = playerPosition - camPosition.Value;
    52.                 Quaternion rotation = Quaternion.LookRotation(lookVector);
    53.                 camRotation.Value = rotation;
    54.             }
    55.         }
    56.  
    57.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    58.         {
    59.             if(localPlayer == Entity.Null )
    60.             {
    61.                 // one time cache
    62.                 if( m_LocalPlayer.CalculateLength() == 0)
    63.                     return inputDeps;
    64.  
    65.  
    66.                 var players = m_LocalPlayer.ToEntityArray(Allocator.TempJob);
    67.  
    68.                 localPlayer = players[0];
    69.                 // Caching LocalPlayerEntity (can be used for fast access from other systems)
    70.                 ClientGameBootstrap.LocalPlayerEntity = localPlayer;
    71.                 players.Dispose();
    72.             }
    73.          
    74.             var job = new CameraFollowJobAfterCache
    75.             {
    76.                 entityPlayer = localPlayer,
    77.                 targetPlayerPositionGet = GetComponentDataFromEntity<Translation>(true),
    78.                 cameraOffeset = ClientGameBootstrap.instance.CameraOffset,
    79.                 cameraSmoothSpeed = ClientGameBootstrap.instance.CameraSmoothSpeed,
    80.                 deltaTime = Time.deltaTime
    81.             }.Schedule(this, inputDeps);
    82.  
    83.             return job;
    84.         }
    85.     }
    86.  
    87. }
     
  41. gamevanilla

    gamevanilla

    Joined:
    Dec 28, 2015
    Posts:
    968
    This is one of those situations in which I believe the code is trivial enough that it may be just faster to run it on the main thread rather than scheduling a job for it. Also, the code is not exactly equivalent to the original because it does not seem to care about the camera not being necessarily up-to-date on a per-frame basis. On the other hand, I can understand the reasoning that mixing regular systems with jobified systems may introduce undesirable sync points here and there. Plus, jobified code can be Burst-ed, which is just great.

    Personally, I feel some common sense should be applied when deciding whether a given piece of code should be jobified or not (as opposed to 'jobify everything'). But I may be wrong; as more engine features get rewritten for DOTS, more of our code will run in jobs without us even noticing it. But I am getting really off-topic here... :)
     
  42. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,266
    I unfortunately don't have any example in particular that I can recommend looking at. For me it is personal experience. I've been doing weekend game jams every other weekend or so (including this weekend which I should probably be working on atm, lol), and I am reaching the point where I can make just as complex of a game in DOTS as I could with the classical approach.

    Though I should mention that I do write more lines of code. A good chunk of that is the authoring components. The other chunk is using Jobs to get massive speedups. But I also end up with code I would be much more likely to use in a commercial game because it is significantly more organized and readable by default.