Entitas icon indicating copy to clipboard operation
Entitas copied to clipboard

ECS Game Architecture with Unity and Entitas

Open sschmid opened this issue 6 years ago • 82 comments

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 avatar Feb 16 '18 11:02 sschmid

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 avatar Feb 16 '18 12:02 sschmid

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 avatar Feb 16 '18 12:02 sschmid

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).

ghost avatar Feb 16 '18 13:02 ghost

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 :)

sschmid avatar Feb 16 '18 15:02 sschmid

Can you post your experiments into some repository?

RomanZhu avatar Feb 16 '18 15:02 RomanZhu

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 !

chiuan avatar Feb 16 '18 15:02 chiuan

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.

c0ffeeartc avatar Feb 16 '18 20:02 c0ffeeartc

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 avatar Feb 17 '18 21:02 sschmid

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)

sschmid avatar Feb 17 '18 21:02 sschmid

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.

optimisez avatar Feb 18 '18 03:02 optimisez

Yes :)

sschmid avatar Feb 18 '18 08:02 sschmid

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 avatar Feb 18 '18 10:02 sschmid

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 avatar Feb 18 '18 11:02 sschmid

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 avatar Feb 18 '18 15:02 sschmid

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

sschmid avatar Feb 18 '18 15:02 sschmid

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 avatar Feb 19 '18 05:02 koden-km

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?

koden-km avatar Feb 19 '18 09:02 koden-km

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.

thygrrr avatar Feb 19 '18 13:02 thygrrr

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

sschmid avatar Feb 19 '18 14:02 sschmid

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)

ghost avatar Feb 19 '18 14:02 ghost

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 :) 👍

sschmid avatar Feb 20 '18 15:02 sschmid

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

mzaks avatar Feb 20 '18 16:02 mzaks

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

optimisez avatar Feb 20 '18 16:02 optimisez

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.

optimisez avatar Feb 21 '18 16:02 optimisez

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 👍

sschmid avatar Feb 21 '18 16:02 sschmid

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

optimisez avatar Feb 21 '18 16:02 optimisez

I updated the diagram. Feedback very welcome

sschmid avatar Feb 21 '18 17:02 sschmid

repost from @RomanZhu

In that diagram InputService validates input, checks amount of resources and creates command, then that command is executed without validation 150 resources - 100 (command1) = 50 resources; What if we call that InputService twice or more? It will create 2 commands, as we can't change resources from Service. It means there are some validations needed when executing commands? 150 resources - 100 (command1) - 100 (command2) = -50 resources

You are right. In my previous project I had sth similar as @StormRene described, an InputActionSystem in between the action and the command. This way you can do sanity checks like "is there only one command or more". I will probably re-add this to the diagram and my code, as this can be very useful.

sschmid avatar Feb 22 '18 10:02 sschmid

Is a command just an entity with some components?

cruiserkernan avatar Feb 22 '18 15:02 cruiserkernan

@cruiserkernan yes. E.g. entity with ShootCommandComponent added

sschmid avatar Feb 22 '18 18:02 sschmid

Hi, design question, what would you say? (follow up of the problem described by @RomanZhu)

Given as described in the diagram:

  • InputActionService validates input and creates command if valid
  • Commands execute without validation

Problem:

  • more than 1 BuyItem input action. The service checks
if (hasEnoughResources) {
    createBuyItemCommand();
}

The BuyItemCommandSystem.Execute:

resources -= item.cost;

Since we execute the command system later and therefore subtract the resources later, both inputs are valid, even if we don't have enough resources to buy the 2nd item.

2 options I see:

  1. Have a BuyItemInputActionSystem that can do sanity checks like, is there only 1 entity, or, do we have enough resources to buy all items of all input actions and then create the commands.

  2. We change the definition and the command system can also do sanity checks and doesn't apply changes without validation.

With option 1 what we end up doing is basically having almost identical systems:

  1. BuyItem reactive systems that does the validation
  2. BuyItem command system that applies the changes

Seems really redundant somehow. What do you think?

sschmid avatar Feb 23 '18 10:02 sschmid

Option 2 seems more redundant, but is also more flexible, since 1 input action can result in multiple different commands if needed

sschmid avatar Feb 23 '18 10:02 sschmid

I would prefer solution of @StormRene with queue for all external inputs So only one action is served in a single tick

RomanZhu avatar Feb 23 '18 10:02 RomanZhu

@RomanZhu I would say that's a game specific decision and doesn't apply for all scenarios. I'm currently updating my an example code to include the validation layer with reactive InputActionSystems to get a better feel for it.

sschmid avatar Feb 23 '18 11:02 sschmid

I had same problem on start of my project and decided to drop actions at all, all my commands had validation (your option 2). Queue solution must work properly, it's very rare situation when you can get >1 action from user (gui/input) per tick. Option 1 is really redutant because there can be multiple types of command that need some spendable resource. I had 5-6 of such commands ;)

There is no best answer, better do what works for your in current situation, it's not that big of a deal to refactor it and if there are tests, you don't have to worry about it :)

IDNoise avatar Feb 23 '18 11:02 IDNoise

I was thinking about making queue only for actions who are using resources (trading, eating, etc) and serving all other actions without queue (movement, etc)

RomanZhu avatar Feb 23 '18 11:02 RomanZhu

I would say you could do the following: in your InputActionService you could flag all commands that need to be queued with an entity.isQueuedCommand = true flag.

class InputActionService {
   //queued
   public void BuySomething() {
      var entity = commandContext.CreateEntity();
      entity.AddBuyCommand(something);
      entity.isQueuedCommand = true;
   }
   //continuous
   public void MoveSomewhere() {
      var entity = commandContext.CreateEntity();
      entity.AddMoveCommand(somewhere);
      entity.isContinuousCommand  = true;
   }
}

A QueueActiveSystem flags only one command per tick from the queue as active so it can be processed and another deletes active && queued entities after the tick. With that approach you can solve queued and continuous commands easily. For better readability you could also create a entity.isContinuousCommand = true flag and separate in two features: QueuedCommandsFeature and ContinuousCommandsFeature in your CommandsFeature. (Option 2, Commands do sanity checks)

ghost avatar Feb 23 '18 11:02 ghost

I've re-did what we've used in my previous project with some changes. Here's my current setup:

  • InputController (MonoBehaviour) emits InputActionEntity.BuyItem
  • BuyItemSystem (reactive) will do validation and eventually emit CommandEntity.BuyItem
  • BuyItemCommandSystem applies changes

I removed InputActionService which is now replaced by individual systems.

All components, systems and systems wrapper are generated. So to add a new input action I add a field in this class:

public static class InputActions {

    public const string START_ROUND = "StartRound";

    [Event, DataField("lane", typeof(int))]
    public const string JUMP = "Jump";
}

sschmid avatar Feb 23 '18 12:02 sschmid

Thinking about generalizing the input action components to only 1 component

string inputAction;
Dictionary<string, string> data;

to be able to easily serialize and record inputs

sschmid avatar Feb 23 '18 12:02 sschmid

Thinking about generalizing the input action components to only 1 component to be able to easily serialize and record inputs

Ya. It will make it also able to see all the input action with visual debugging.

optimisez avatar Feb 23 '18 12:02 optimisez

That would already be possible now

sschmid avatar Feb 23 '18 12:02 sschmid

Other inputs will have issues with multiple different command types also. For example if there were Move and PickupItem commands both coming from input actions in same frame. If move is done first, then you might be too far away to pickup item. If you pickup item first it might be trapped and paralyse player preventing movement.

I think it might be best to skip early validation. Just create commands (as intents) from any input and validate them on execute. Which ever happens first becomes the new state. The other commands will need to softly fail doing nothing and be skipped or apply slightly differently if possible to the new state.

In my project I had been working with commands late in the game loop, applying after regular game logic, but before render views are updated. This was so AI systems could run early just like player and network input and I could treat their decision as input for creating commands. Movement happens which can cause new views to be streamed in and old view streamed out. Some AI will potentially run from MonoBehaviour assets in that early External Input section and some from Game Logic systems.

koden-km avatar Feb 23 '18 14:02 koden-km

Imo just one thing is missing: You don't want to always create actions and commands for changing the view/view logic systems. With the current diagram you could think it's a good idea to control a full fletched sim city menu via the game logic. I think there should be another part the View Logic as a own box between Game Logic and Observers and another line directly from Input Actions to View Logic. View Logic is not unimportant. When you are creating listeners for directly game logic you are not able to process the data further for the view. Often you want more data that's only for the view. Let's say you have weapons with damage and fire rate, the view could need a damage/min info thats unneeded for game logic. Here's the difference:

class WeaponView : MonoBehaviour, IGameDamageListener, IGameFirerateListener
   
   public float damage;
   public float fireRate;
   public float damagePerMinute;
   void OnEnable() {
      listenerEntity.AddGameDamageListener(this);
      listenerEntity.AddGameFirerateListener(this);
   }
   void GameDamageListener(float damage) {
      this.damage = damage;
      this.damagePerMinute = damage / ... //wtf i don't have firerate now.
   }
   void GameFirerateListener(float fireRate) {
      this.fireRate = fireRate;
      this.damagePerMinute = ... / fireRate //wtf i don't have damage now.
   }

You don't know the order from the view perspective so you beginning to save the state of damage and fireRate and dividing if the divisor is not zero in both. Or start another query against game context. But that shouldn't be happening in the first place, because that's why you are using Events.

With View Logic it's easy. You can react as usual on changes of game data and creating specific view data like a ViewDamagePerMinuteComponent. What you get is super clean MonoBehaviour Views to present.

class WeaponView : MonoBehaviour, IViewDamageListener, IViewFirerateListener, IViewDamagePerMinuteListener
   
   public float damage;
   public float fireRate;
   public float damagePerMinute;
   void OnEnable() {
      listenerEntity.AddViewDamageListener(this);
      listenerEntity.AddViewFirerateListener(this);
      listenerEntity.AddViewDamagePerMinuteListener(this);
   }
   void ViewDamageListener(float damage) {
      this.damage = damage;
   }
   void ViewFirerateListener(float fireRate) {
      this.fireRate = fireRate;
   }
   void ViewDamagePerMinuteListener(float damagePerMinute) {
      this.damagePerMinute= damagePerMinute;
   }

ghost avatar Feb 23 '18 14:02 ghost

Btw, until there's a way to automatically generate this, you can also create custom events that are based on multiple components. In your case sth like IDamageFireRateListener with (GameEntity entity, float damage, float fireRate)

sschmid avatar Feb 23 '18 15:02 sschmid

I use sth like this already

sschmid avatar Feb 23 '18 15:02 sschmid

Btw, until there's a way to automatically generate this, you can also create custom events that are based on multiple components

how?

FNGgames avatar Feb 23 '18 19:02 FNGgames

good old manual coding :) no code generation

sschmid avatar Feb 23 '18 19:02 sschmid

hi everyone, how to avoid this code?

[Buff, Effect, Actor]
public sealed class RemovedComponent : IComponent
{
    
}
public interface IRemoved : IEntity, IRemovedEntity { }
public partial class ActorEntity : IRemoved { }
public partial class EffectEntity : IRemoved { }
public partial class BuffEntity : IRemoved { }

public class RemovedSystem : MultiReactiveSystem<IRemoved, Contexts>, IInitializeSystem
{
    public RemovedSystem(Contexts _contexts) : base(_contexts)
    {

    }

    public void Initialize()
    {
        
    }

    protected override void Execute(List<IRemoved> entities)
    {
        foreach(var e in entities)
        {
            IView view = null;

            ///////////////////////////////////////////////////////////////////
            // because Actor and Effect has view component, but buff is not a viewable entity
           if (e.contextInfo.name == "Actor") 
            {
                var a = e as ActorEntity;
                view = a.view.value;
            }
            else if (e.contextInfo.name == "Effect")
            {
                var eff = e as EffectEntity;
                view = eff.view.value;
            }
            // any way to avoid?
           ///////////////////////////////////////////////////////////////////

            if (view != null)
            {
                view.Unlink();
                view.Hide();
            }
            e.Destroy();
        }
    }

thanks

nomadfighter avatar Feb 24 '18 09:02 nomadfighter

I gutes I would introduce a getView() method on IRemoved interface and implement it in the partial class definitions. BuffEntity would return null other two, this.view.value. Than you can just call IView view = e.getView(); and remove the block you marked.

mzaks avatar Feb 24 '18 09:02 mzaks

@nomadfighter alternatively, you can try a general component check like this:

if (e.HasComponent(ActorComponentsLookup.View)) {
    var view = (ViewComponent)e.GetComponent(ActorComponentsLookup.View);
}

The ids used to line up across contexts, but I'm not sure if that's still the case. You'd have to check if the View index is the same in all the generated lookups

sschmid avatar Feb 24 '18 09:02 sschmid

Btw, with the new events it's simple, the DestroyedComponent would have an [Event(true)] attribute and the views are IDestroyedListeners and can unlink and hide themselves

sschmid avatar Feb 24 '18 09:02 sschmid

@sschmid Would you have time to update some Match-One branch to reflect your experiment? I'm not yet imagining all the details that you are experimenting with. For example you wrote:

Removed all view systems

On the master branch of Match-One there are view systems: https://github.com/sschmid/Match-One/tree/master/Assets/Sources/Systems/View

ethankennerly avatar Feb 26 '18 05:02 ethankennerly

I am waiting for your sample project

qkhuyit avatar Feb 27 '18 08:02 qkhuyit

@sschmid Where is the DestroyEntitiesSystem in your model? Currently I ended up splitting the Eventsystems into two systems, DestroyedEventSystem and the rest. Now my flow is: External Input ---> Input Actions ---> Commands ---> Call Destroy Events ---> Destroy Entities ---> Game Logic ---> Rest of the Events

The reason why I using this is to be able to cleanup everything before the game logic. Simple example: Restart game

  1. User create input with RestartComponent
  2. Input actions triggered by RestartComponent and flag all entity with DestroyComponent
  3. Destroy events has sent to all listeners (including viewservice, so unity objects can go back to pool)
  4. Destroy all entities with DestroyComponent entities
  5. Game Logic triggers RestartComponent and re-initializing all game systems.
  6. New entities are created
  7. Send the other Events (viewservice can render now from pool)

With this flow I can cleanup and re-initialize the whole game in the frame of the input without any leak or garbage.

But maybe I miss something and you have a better implementation. Thanks.

hegi25 avatar Mar 02 '18 15:03 hegi25

Wanted to share the architecture we used in one of our game which is pretty similar to the diagram that Simon shared with us.

As an ‘entry’ point we have the GestureSystem which listens inputs from Unity and translates them meaningful gestures like TouchDown, TouchUp, DragStart, Drag, DragEnd, TouchHold, Pinch etc .. and provides additional data like touch position, hold time etc .. Also any UI input happens at this state.

Then we have ‘Controllers’ that listens to the gestures through an interface or events and they basically validate the inputs based on the state of the game and construct commands. Rarely controllers have a dependency to another controller to coordinate validation.

Commands of different type are sent to a CommandSystem that basically executes them. The command system contains blocks of command executors (one for each type of command) that tries to add entities with specific components to the game (the entity construction happens through a factory service) There might some few simple validation happening here, but most of the commands don’t care about the state of the game. Destruction/cleanup of game entities also happens here by calling specific commands.

When entities are created they enter the ‘Entitas’ loop where Systems only operate on entities with certain components and there is no validation.

There are also ‘Controllers’ that look at the game on each update and change the entities states by executing commands upon them (this is how AI/State machine is handled)

The view is mainly handled by ‘Controllers’ and a Data-binding service but there are cases where view listens to systems.

Services and utilities are injected with a DI framework to controllers/commands/systems.

I think that this is very close to what Simon shared and can be entirely in Entitas and I can say that this has scaled pretty good so far.

adizhavo avatar Mar 06 '18 11:03 adizhavo

@sschmid Do u plan to release custom roslyn code generator soon?

optimisez avatar Mar 06 '18 13:03 optimisez

I shared my interpretation in a wiki post: https://github.com/sschmid/Entitas-CSharp/wiki/How-I-build-games-with-Entitas-%28FNGGames%29

FNGgames avatar Mar 06 '18 15:03 FNGgames

@FNGgames Thanks for wiki post. I really understand more on how important it is to have a good game architecture.

I have a question about combining unity and entitas. How would you make the PhysicsObject (the code can be found in https://unity3d.com/learn/tutorials/topics/2d-game-creation/scripting-collision?playlist=17093) into a PhysicsService and through entitas system update the view of the character?

YimingIsCOLD avatar Mar 07 '18 06:03 YimingIsCOLD

@FNGgames Great post!

I want to get access to the collider bounds of a gameObject. Following your workflow would I create a ColliderComponent and access the value from there or create a service to access the collider bounds? Since I don't have a viewComponent anymore I can't access it through the view.

cruiserkernan avatar Mar 07 '18 12:03 cruiserkernan

@YimingIsCOLD, @cruiserkernan I have kept hold of my ViewComponent for this reason. The IViewController interface acts as the base view, then I have things like IPhysicsController which have fields like mass, drag, and methods like ApplyForce(). In my view service I'm instantiating and then searching for scripts attached to the instantiated gameobject e.g.


var go = Instantiate(...);
var view = go.GetComponent<IViewController>();
if (view == null) return;
view.InitializeView(contexts, entity);
entity.ReplaceView(view);

var physics = go.GetComponent<IPhysicsController>();
if (physics != null) {
    physics.InitializePhysics(contexts, entity);
    entity.ReplacePhysics(physics);
}

You can put whatever you need in those interfaces. You only need a component if you want the ability for a system to be able to pull data from the view. In my case i have an execute system that loops through all the physics components and pulls position and velocity from them onto components - so Unity physics solves for position then i suck it back into entitias.

Every prefab spawned into my game goes through that so if you want your prefab to be a physics object or a collider object or whatever you just hang a script on it with that interface. You dont need special entity configuration code (my physics controller script has public editor variables i can set for mass and drag, these get added as components to the entity during InitializePhysics()). So configuration is just in the editor on the scripts.

So I just have to go _context.CreateEntity().AddAsset("Player") and I get a fully configured player entity with all its necessary components set up according to the values in the scripts on the prefab. My player prefab looks like this:

https://i.imgur.com/i1WHH7g.png

FNGgames avatar Mar 07 '18 12:03 FNGgames

So in my case I could have a IColliderController and have a ColliderComponent?

cruiserkernan avatar Mar 07 '18 13:03 cruiserkernan

Sure - any functionality you need from unity you can get this way.

I tend to separate "ViewController" type things from "Service" type things along the lines of whether the information im looking for is global or local. E.g. Physics Service has raycast methods since these are stateless static methods. It doesn't need to live on a view gameobject to work. Physics Controller on the otherhand has things like mass / drag / apply force which all apply to a particular object instance.

But otherwise its the same - you are saying "what info do i need from unity?" and then writing interfaces that act as a pipeline to get that information into your game code. Thats why i draw the diagram with the interfaces half-in-half-out of the box, they are the information pipes. You get to choose exactly what pipes to build so you dont carry all the extra bloat that comes with the implentation in the game engine or the asset store thing youre using.

FNGgames avatar Mar 07 '18 13:03 FNGgames

You can put whatever you need in those interfaces. You only need a component if you want the ability for a system to be able to pull data from the view. In my case i have an execute system that loops through all the physics components and pulls position and velocity from them onto components - so Unity physics solves for position then i suck it back into entitias.

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

YimingIsCOLD avatar Mar 07 '18 13:03 YimingIsCOLD

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 avatar Mar 07 '18 13:03 FNGgames

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.

FNGgames avatar Mar 07 '18 13:03 FNGgames

@FNGgames Thanks for your help!

cruiserkernan avatar Mar 07 '18 13:03 cruiserkernan

@FNGgames I see. Thanks for the clarification.

YimingIsCOLD avatar Mar 07 '18 13:03 YimingIsCOLD

@FNGgames What is your tag on UnityGameView for?

cruiserkernan avatar Mar 07 '18 17:03 cruiserkernan

@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.

FNGgames avatar Mar 08 '18 10:03 FNGgames

@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 avatar Mar 08 '18 22:03 sschmid

@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.

sschmid avatar Mar 08 '18 22:03 sschmid

@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 :).

FNGgames avatar Mar 09 '18 09:03 FNGgames

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 avatar Mar 09 '18 15:03 sschmid

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

sschmid avatar Mar 09 '18 15:03 sschmid

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?

fayte0618 avatar Mar 09 '18 16:03 fayte0618

see comments above https://github.com/sschmid/Entitas-CSharp/issues/610#issuecomment-367994419

sschmid avatar Mar 09 '18 16:03 sschmid

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

sschmid avatar Mar 09 '18 16:03 sschmid

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?

optimisez avatar Mar 17 '18 05:03 optimisez

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?

OmiCron07 avatar Aug 07 '20 15:08 OmiCron07

@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 avatar Aug 07 '20 20:08 c0ffeeartc

  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

c0ffeeartc avatar Aug 07 '20 20:08 c0ffeeartc

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

OmiCron07 avatar Aug 07 '20 20:08 OmiCron07