Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECS Game Architecture with Unity and Entitas #610

Closed
sschmid opened this issue Feb 16, 2018 · 82 comments
Closed

ECS Game Architecture with Unity and Entitas #610

sschmid opened this issue Feb 16, 2018 · 82 comments

Comments

@sschmid
Copy link
Owner

sschmid commented Feb 16, 2018

ECS Game Architecture with Unity and Entitas

This is my mental model based on which I decide where to put my code.
Feel free to comment so we can improve this model further

Entitas-Game-Architecture

@sschmid
Copy link
Owner Author

sschmid commented Feb 16, 2018

If I strictly apply this model to Match One, I'd have to remove the EmitInputSystem and turn it to a MonoBehaviour that emit input entities.
I would have to remove the AddViewSystem and the ViewComponent and have a ViewController or ViewService that listens to AssetComponent, then creates a view and binds it to an entity. The View would update itself with position listeners and destroyed listeners.
If I'm really strict I'd also remove the AssetComponent and have a mapping from entity type to asset name in the View Service, eg. isPlayer -> "Views/Player.prefab"
In my other games I'd also have to replace the PlayAudioSystem with and AudioController or AudioService. There would be no audio context and no AudioComponent anymore.

How does this sound? Am I going crazy? :)

@sschmid
Copy link
Owner Author

sschmid commented Feb 16, 2018

UI and Views visualize the current game state. They don't affect the game simulation. They can emit input like button clicks, but the game simulation doesn't know if this input came from a button, from the network or from some other program. That's why we don't need Entitas systems for them and can decouple them from the core logic as observers of the game state. UI and Views know how to represent the current game state. This allows us to start with placeholder assets which later can be replaced with the real game assets without touching the game logic.

@sschmid sschmid changed the title My mental model Game Architecture with Entitas Feb 16, 2018
@ghost
Copy link

ghost commented Feb 16, 2018

We are doing it exactly like shown in the graphic. Furthermore we splitted the game logic into different parts: Action -> Command -> Core -> View. The View or other external inputs are able to create actions. The systems there are doing checks if it's possible and emit commands. The command systems will change the state of the core. Core systems only change core state. View is reacting on core and creating view state. Listeners are getting called, because something changed directly in core or in view. We also have a meta context which is just loaded balance values (from json just for easier access). This is done because action and view are part of unity (client side). Command and core are part of the simulation and will run not only on the client, but also on the server (we have a simulation game).

@sschmid
Copy link
Owner Author

sschmid commented Feb 16, 2018

1st result of applying this strictly with TDD (Audio):
Before:

  • AudioComponent
  • PlayAudioSystem
  • AudioService
  • no abstraction, dependency to unity

After:

  • No Component / System / IListener / EventSystem
  • AudioService (half the code)
  • Abstraction (IAudioService, IAudioClip)
  • Testable and no dependencies to Unity

Less complex and almost certainly more efficient

I guess I'm going down this road further :)

@RomanZhu
Copy link

Can you post your experiments into some repository?

@chiuan
Copy link

chiuan commented Feb 16, 2018

i thinks it's good to use react system to tell ui & view logic.
by the way , sometimes good to use event + iListener to make it better right now. good !

@c0ffeeartc
Copy link

Model looks nice and very intuitive, should be in wiki somewhere 😄

I wouldn't place Physics Collision alongside Button Click, because Unity runs physics in separate cycle N times before Update FixedUpdate > InternalPhysics > OnTriggerXXX > OnCollisionXXX > yield WaitForFixedUpdate.
Bullet may bounce of wall during few physics cycles before it gets destroyed in Game Logic, if we mark Bullet as trigger it gives no collisionInfo, and if we add BounceComponent to Bullet marked as trigger, it will travel a bit inside objects before GameLogic corrects its direction.

@sschmid
Copy link
Owner Author

sschmid commented Feb 17, 2018

I also refactored my views now. All testable and very nice :)

  • removed ViewComponent
  • Removed all view systems
  • Views updated themselves using the new Entitas events
  • ViewService listsnes to asset event and creates view from object pool
  • View reacts to destroyed and pushes itself back to the object pool
  • Added IView and IViewInstantiator to wrap monobehaviours and view creation. This was a result of TDD with TestView impl and TestViewInstantiator impl. The game uses UnityView (wraps MonoBehaviour) and UnityViewInstantiator (loads from Resources but can be swapped with UnityAssetBundleViewInstantiator). This way you can simply swap from creating view from resources to creating views from asset bundles at a later point of time. Object pooling is in ViewService

@sschmid
Copy link
Owner Author

sschmid commented Feb 17, 2018

I really start loving the new events. It feels so clean now :)
(We introduced the idea in 2016, but now that we can generate it's even nicer)

@optimisez
Copy link

optimisez commented Feb 18, 2018

Awesome. Btw @sschmid does it support loading some game objects first at loading time? For example, I want to load 10 enemies into game object pool first before starting game. I expect there is an API to preload the game object first into game object pool.

@sschmid
Copy link
Owner Author

sschmid commented Feb 18, 2018

Yes :)

@sschmid
Copy link
Owner Author

sschmid commented Feb 18, 2018

Updated diagram to include commands.
External input should be checked first and if valid creates commands which affect the simulation.

Invalid input could be for example buying an item, but you don't have enough resources. Commands don't verify, they only act.

@sschmid
Copy link
Owner Author

sschmid commented Feb 18, 2018

I recently built a few smaller games following the "Game Architecture with Entitas" diagram very strictly and I was really happy with the results. After the latest Video Tutorial about TDD #611 I started with strict testing again, and I have to tell you:
TDD really had a big impact on my apis and the general outcome. I'm so much happier with the result now. The code is way more reusable, which I was aiming for to be able to reuse as much code as possible in the next game. The apis are much cleaner and I got rid of unnecessary complexity. Plus, It just feels so much better to see all the green passing tests :) Really happy now! TDD FTW!

@sschmid
Copy link
Owner Author

sschmid commented Feb 18, 2018

As another consequence I could get rid of the following contexts: Input, UI, Audio. I only have game and command context left.
Feels like everything becomes simpler 👍

@sschmid
Copy link
Owner Author

sschmid commented Feb 18, 2018

ViewService, AudioService and all the other services are all less than 50 lines of code. Things became veeery simple now :)

@koden-km
Copy link

This sounds very interesting. Will you be sharing your game examples using this structure?

I've been struggling a bit in my current project to convert input actions/intents into validated commands and that apply the state changes. I was trying to follow a design discussion you had on gitter a few months ago.

@koden-km
Copy link

Would you do A.I. in the "External Input" section or "Game Logic" section?
I was planning to do A.I. as Entitas systems just like player input. Each creature/NPC would create intent actions (just like input, eg. MoveFoward) to be validated and converted into Commands. I then run all command systems in the simulation step before render.

Which logic should be done directly in systems and which should be passed off to do in View's?

@thygrrr
Copy link

thygrrr commented Feb 19, 2018

I don't understand why you'd want to move Input out of a system if it could exist in a system.

If it lives inside the "view/presenter" domain, i.e. in a Monobehaviour, you have much less control over it.

I generally try to keep as little as possibe in Monobehaviours. However, I also rarely use OnMouse... handlers, but rather roll my own input with raycasts and state etc., to account for multi-touch etc.

@sschmid
Copy link
Owner Author

sschmid commented Feb 19, 2018

You can definitely have systems for input, no problem. I'm thinking in a very general and very broad context, one in where you can take the game logic as is without any modification and run it everywhere, e.g. server. On the server, probably don't run unity and also you don't have input like mouse. Only commands, e.g. as a result from validated network input. Same is true for views etc. I'm really reducing it down to the core logic. You can still have systems for input and views if you want, but then you'd have to modify the logic in a scenario where code is shared

@ghost
Copy link

ghost commented Feb 19, 2018

What's also really helpful in our project is that we have an Action Context before Command Context. Like in the updated architecture commands are only verified data for the game logic which gets checked in action systems. But actions also help with view systems and the data in view. Actions are allowed to change the view context directly. That's very helpful for menues and other view stuff where you need reactive behaviour, because they are complex (best practice here is, that menues never have state in MonoBehaviour and only react to view components). So you don't have to make commands out of actions that are only interesting for the view and the game logic doesn't need to know about these. Therefore I would create a new rectangle before commands and call it action with a second line to view. (Could be to complex in the MatchOne example or Schmup, but in a bigger project this step is important imo)

@sschmid
Copy link
Owner Author

sschmid commented Feb 20, 2018

There are lots of benefits when you have a defined architecture in your game. One of those benefits is that you can automate and generate code.
Imagine you want to add a new input action, like Shoot. Sticking with the diagram above, you'd have to write and implement the following classes:

  • InputAction.Shoot
  • ShootInputActionService that validates the input and emits the shoot command
  • ShootCommand
  • ShootCommandSystem

You need to update the the CommandSystems list and the InputActions list.

I wrote a custom roslyn code generator that does all that for me. I only need to maintain a static class with fields (basically the input action types)

    public const string START_ROUND = "StartRound";

    [DataField(Data.POSITION_X, typeof(float))]
    [DataField(Data.POSITION_Y, typeof(float))]
    [CommandField("position", typeof(Vector3))]
    [Event]
    public const string AIM = "Aim";

    [DataField(Data.POSITION_X, typeof(float))]
    [DataField(Data.POSITION_Y, typeof(float))]
    [CommandField("position", typeof(Vector3))]
    [CommandField("velocity", typeof(Vector3))]
    [CommandField("ammo", typeof(int))]
    public const string SHOOT = "Shoot";

The code generator will then generate:

  • updates InputActionsServices list
  • generates ShootCommand
  • generates partial ShootCommandSystem
  • adds system to CommandSystems list

All I need to do is to implement the ShootInputService and fill out the rest of the partial ShootCommandSystem (which is only the execute method).

Very nice :) 👍

@mzaks
Copy link
Contributor

mzaks commented Feb 20, 2018

Would be nice to have a link to gist with the result of code generation. Otherwise there are too many questions in my head 😁

@optimisez
Copy link

@sschmid Awesome. I hope I can get that new feature soon.

@optimisez
Copy link

I wrote a custom roslyn code generator that does all that for me. I only need to maintain a static class with fields (basically the input action types)

@sschmid Can you integrate custom roslyn code generator into Entitas? So for those who need it can directly use it.

@sschmid
Copy link
Owner Author

sschmid commented Feb 21, 2018

To write a custom roslyn generator you have to set up a new c# project with .NET 4.6 and install roslyn. If you reference DesperateDevs.Roslyn you can use some utility methods already. I will probably make a video about it.
Maybe a good idea to also ship a working code generator project with Entitas 👍

@optimisez
Copy link

Maybe a good idea to also ship a working code generator project with Entitas 👍

Awesome. It can be a very good starting point to learn how to write custom code gen as until now I still dunno how to do it. lol

@sschmid
Copy link
Owner Author

sschmid commented Feb 21, 2018

I updated the diagram. Feedback very welcome

@FNGgames
Copy link

FNGgames commented Mar 7, 2018

I would also say, the whole "logic talking to view layer == bad" thing is probably overblown. It's a game, it's safe to say that it's being drawn, but less safe to say how it's being drawn.

I don't mind having these systems that talk to views because they're only ever talking to my interfaces. I use events to minimise the amount i have to do this, but i find it really useful to have some references around in case i need to talk to them. That's not to say it's not a bit of a crutch, but i built these before Simon did events and I'm clinging to them for a bit longer :)

@FNGgames
Copy link

FNGgames commented Mar 7, 2018

Does your execute system runs on a fixed update to update the entity's position?

yeah, i use the new non-alloc groups though so i dont notice the cost.

@cruiserkernan
Copy link

@FNGgames Thanks for your help!

@YimingIsCOLD
Copy link

@FNGgames I see. Thanks for the clarification.

@cruiserkernan
Copy link

@FNGgames What is your tag on UnityGameView for?

@FNGgames
Copy link

FNGgames commented Mar 8, 2018

@cruiserkernan it's an enum tag i give to all entities - similar to unity's tag system, so i can find entities with tag or check a tag for something when i have a collision or whatever. It's just a component with an enum field.

@sschmid
Copy link
Owner Author

sschmid commented Mar 8, 2018

@FNGgames

I don't mind having these systems that talk to views because they're only ever talking to my interfaces

That's exactly how I did before, too. If you think about, using the events, that's still exactly the same: a system will call a method that's implemented from an interface. The only difference is, these systems are now automatically generated and you don't have to write them + those systems work on all kinds of things, not only views.

@sschmid
Copy link
Owner Author

sschmid commented Mar 8, 2018

@FNGgames Btw, thank you very much for your post. I love it :) Thanks for taking the time, I think it will help a lot of people who are new to all of this.

@FNGgames
Copy link

FNGgames commented Mar 9, 2018

@sschmid good point RE: the events - there's still something talking to the views, but it's all hidden away.

No problem on the article, tbh i probably wrote it all in chat a couple of times before - it's nice to just be able to give some a link now :).

@sschmid
Copy link
Owner Author

sschmid commented Mar 9, 2018

To answer a question from the chat:

My only question is about the Input -> Command -> Process. Is there an example of this because I have a hard time differentiating it from normal Inputs in the Input context then picked by Systems to be applied to Entities

This is an optional idea that some of use to streamline adding new features and inputs.
Every game has a finite set of inputs like jump, shoot, buyItem, etc. This diagramm is a suggestion how to deal with those inputs. The input action layer is validating them, the command layer is applying them to the game logic.
To be specific (Shoot example):

  • You create a ShootComponent with 2 contexts: InputAction and Command
  • InputController : MonoBehaviour creates new InputEntity with ShootComponent when mouseDown
  • Reactive ShootInputActionSystem is validating the input (e.g. can shoot? hasAmmo? is bullet cooldown complete?) and creates CommandEntitiy with ShootComponent (or not)
  • Reactive ShootCommandSystem does whatever needs to be done, e.g. create a new bullet entity that spawns in the game

@sschmid
Copy link
Owner Author

sschmid commented Mar 9, 2018

the larger the game and the more inputs you have, the more important it becomes to have a defined way how to handle those inputs. This approach helped me managing this even with lots of inputs

@fayte0618
Copy link

I just want to make sure if I understand it correctly. This would mean I have an Input, Command and Game context version of the components? Then, I have an InputSystem to filter the Input entity which creates a Command Entity, then have a CommandSystem to operate on the Command entity which it applies to the Game entity?

@sschmid
Copy link
Owner Author

sschmid commented Mar 9, 2018

see comments above
#610 (comment)

@sschmid
Copy link
Owner Author

sschmid commented Mar 9, 2018

Yes, that's the idea. Most of it can be generated as described in the comment above. So all I implement is just the execute of the input system and the command system. Everything else will be generated

@optimisez
Copy link

I removed InputActionService which is now replaced by individual systems.

@sschmid I wonder how you can like emits InputActionEntity.BuyItem from a UI that has MonoBehaviour since when linking entity to its MonoBehaviour, it will only pass Game Context? Have u use something like EntityService in Match-One?

@OmiCron07
Copy link

What do you think about physics interaction with ECS/Entitas? If we need to interact with the physics, where would you put the "logic"?

Here my example :

I have a plane controlled by physics (Rigidbody.AddForce) and a PID controller to calculate the right amount of force to apply.

  1. Would you make a system to calculate the force and vector to apply to the rigidbody and a PID component to save some values between calculation? You need to know the current velocity too in the calculation... So, with the final force calculated, apply and trigger an event for the view to add that force to itself?

  2. Would you make an event with the view listening the force rate needed to apply and the View/MonoBehaviour do the calculation with its PID controller and apply the calculated force on the rigidbody?

  3. Would you put the rigidbody in a component and add the force directly from the system to that rigidbody, like the endless runner example?

With various Entitas examples, I see that the views listen the position component to update its transform values, because it easy to see the position of the view as a state of the view. But with physics, like the velocity, is it a view state controlled by the rendering engine or is it part of the game logic and it could be unit tested?

@c0ffeeartc
Copy link

@OmiCron07 I'd say it's up to the developer to decide. There is a balance what data to put into ECS layer and what to keep in View layer.
Physics is a special case, cause FixedUpdate runs right before Update [0..N] times (Unity Events Order, Physics.Simulate).

I'd handle most of physics interaction inside View layer(or create FixedSystems to run in FixedUpdate). And occasionally send data through ECS components so that systems and eventSystems could react.

@c0ffeeartc
Copy link

  1. Would you make a system to calculate the force and vector to apply to the rigidbody and a PID component to save some values between calculation? You need to know the current velocity too in the calculation... So, with the final force calculated, apply and trigger an event for the view to add that force to itself?

Sounds ok to create system to calculate something and another system to apply calculations.

  1. Would you make an event with the view listening the force rate needed to apply and the View/MonoBehaviour do the calculation with its PID controller and apply the calculated force on the rigidbody?

It would work, could be tempting and easier to make but sounds more fragile in the long run.

  1. Would you put the rigidbody in a component and add the force directly from the system to that rigidbody, like the endless runner example?

Often ViewComponent is already attached, it's fine to add/access other MonoBehaviours through entity components. Unless you have some architecture restriction on using monoBehaviours in systems

  1. With various Entitas examples, I see that the views listen the position component to update its transform values, because it easy to see the position of the view as a state of the view. But with physics, like the velocity, is it a view state controlled by the rendering engine or is it part of the game logic and it could be unit tested?

Physics changes unity transform values. It's possible to sync changes afterwards by checking Transform.hasChanged or some other trick. Or keep all transform calculations inside unity View layer and use unity values inside systems

@OmiCron07
Copy link

Thank you for the responses, will check it out how it turns out.

Repository owner locked and limited conversation to collaborators Jul 6, 2023
@sschmid sschmid converted this issue into discussion #1068 Jul 6, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
Archived in project
Development

No branches or pull requests