Skip to content
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

Managing systems executed at Update, FixedUpdate and LateUpdate #434

Closed
christianvoigt opened this issue Jun 21, 2017 · 11 comments
Closed

Comments

@christianvoigt
Copy link

christianvoigt commented Jun 21, 2017

Hi everyone, I was putting some thought into how to manage systems that have to be updated at Update, FixedUpdate or LateUpdate (see #402). I think it is the right decision to not distinguish between systems that execute at these different points in the Unity lifecycle. The IExecuteSystem API should be decoupled from Unity. On the other hand, the Feature class is specifically designed to make it easier to use and debug systems in Unity, so I think this class actually should distinguish between systems that are executed during Update, FixedUpdate and LateUpdate. This would make it much easier to manage systems in a complex game.

For example, I have an In-Game-Editor in which I want to enable the user to move the camera with the mouse. This MoveCameraSystem should be executed at LateUpdate. I would like to add this system to my InGameEditorFeature. But as this feature also contains an InputSystem that has to be executed at Update, this is currently impossible.

This gets even more inconvenient because I want to switch between different "modules" in the game by deactivating the reactive systems in the old module (e.g. the game), resetting contexts and activating the reactive systems in the new module (e.g. the in-game-editor).

So I decided to add this feature by creating a "UnitySystems" class that inherits from Systems and has OnUpdate, OnFixedUpdate and OnLateUpdate methods that can be called in the GameController. ISystems can be added to a UnitySystems instance by specifying an update type (Update, FixedUpdate or LateUpdate). If no update type is specified and the system is an IExecuteSystem, the IExecuteSystem will be executed when calling OnUpdate. Adding a UnitySystems object to another UnitySystems object is also supported. I have not tested it extensively, but I think it works quite well.

The question I would like to discuss now is how to support visual debugging. Wouldn't it make sense to change DebugSystems so that it inherits from a class like UnitySystems? Or maybe there is a way to make the visual debugging system more adaptable so that different kinds of Feature implementations can be used?

Here is what I have so far:

using Entitas;
using System.Collections.Generic;
using System;

public enum UpdateTypes{
        UPDATE,
        FIXED_UPDATE,
        LATE_UPDATE
}

public class DelegatedExecutionSystem:IExecuteSystem{
    readonly Action _executeAction;
    public DelegatedExecutionSystem(Action action){
        _executeAction = action;
    }

    public void Execute()
    {
        if(_executeAction != null){
            _executeAction();
        }
    }
}

public class UnitySystems : Systems{
        protected readonly List<IExecuteSystem> _executeOnUpdateSystems;
        protected readonly List<IExecuteSystem> _executeOnFixedUpdateSystems;
        protected readonly List<IExecuteSystem> _executeOnLateUpdateSystems;

        public IExecuteSystem updateExecution;
        public IExecuteSystem fixedUpdateExecution;
        public IExecuteSystem lateUpdateExecution;

        public UnitySystems():base(){
                _executeOnUpdateSystems = new List<IExecuteSystem>();
                _executeOnFixedUpdateSystems = new List<IExecuteSystem>();
                _executeOnLateUpdateSystems = new List<IExecuteSystem>();
                updateExecution = new DelegatedExecutionSystem(OnUpdate);
                fixedUpdateExecution = new DelegatedExecutionSystem(OnFixedUpdate);
                lateUpdateExecution = new DelegatedExecutionSystem(OnLateUpdate);
        }
        public override Systems Add(ISystem system){
            return Add(system, UpdateTypes.UPDATE);
        }
        public UnitySystems Add(ISystem system, UpdateTypes updateType){            
            var initializeSystem = system as IInitializeSystem;
            if (initializeSystem != null) {
                _initializeSystems.Add(initializeSystem);
            }

            var unitySystems = system as UnitySystems;
            var executeSystem = system as IExecuteSystem;
            if (executeSystem != null) {
                 _executeSystems.Add(executeSystem);
                if(unitySystems != null){
                        _executeOnFixedUpdateSystems.Add(unitySystems.fixedUpdateExecution);
                        _executeOnLateUpdateSystems.Add(unitySystems.lateUpdateExecution);
                        _executeOnUpdateSystems.Add(unitySystems.updateExecution);
                }else if(updateType == UpdateTypes.FIXED_UPDATE){
                        _executeOnFixedUpdateSystems.Add(executeSystem);
                }else if(updateType == UpdateTypes.LATE_UPDATE){
                        _executeOnLateUpdateSystems.Add(executeSystem);
                }else{
                        _executeOnUpdateSystems.Add(executeSystem);
                }
            }

            var cleanupSystem = system as ICleanupSystem;
            if (cleanupSystem != null) {
                _cleanupSystems.Add(cleanupSystem);
            }

            var tearDownSystem = system as ITearDownSystem;
            if (tearDownSystem != null) {
                _tearDownSystems.Add(tearDownSystem);
            }

            return this;         
        }


        public void OnFixedUpdate(){
            for (int i = 0; i < _executeOnFixedUpdateSystems.Count; i++) {
                _executeOnFixedUpdateSystems[i].Execute();
            }
        }
        public void OnUpdate(){
            for (int i = 0; i < _executeOnUpdateSystems.Count; i++) {
                _executeOnUpdateSystems[i].Execute();
            }                
        }
        public void OnLateUpdate(){
            for (int i = 0; i < _executeOnLateUpdateSystems.Count; i++) {
                _executeOnLateUpdateSystems[i].Execute();
            }                                
        }
        public override void Execute(){
                OnUpdate();
        }
}
@heynemann
Copy link

heynemann commented Oct 12, 2017

I did my own version of this a little differently.

In order to use LateUpdate or FixedUpdate, all my systems must do is implement IFixedUpdateSystem or ILateUpdateSystem.

One drawback is that they still must implement IExecuteSystem (even if I don't need anything to be executed in update - meaning empty Execute method).

Another drawback is that I can't use ReactiveSystems if I need either Late or Fixed Updates. I can live with that, though.

If anyone cleverer than me thinks of a better way of doing this, I'm all for contributing back. Pardon me for the code below, c# is not my most fluent language.

using Entitas;
using System.Collections.Generic;
using System;

public interface IFixedUpdateSystem
{
    void FixedUpdate();
}

public interface ILateUpdateSystem
{
    void LateUpdate();
}

public static class UnityEntitasExtensions
{
    public static void FixedUpdate(List<IExecuteSystem> executeSystems)
    {
        foreach (var system in executeSystems)
        {
            var s = system as UnityFeature;
            if (s != null)
            {
                s.FixedUpdate();
                continue;
            }

            var f = system as IFixedUpdateSystem;
            if (f != null)
            {
                f.FixedUpdate();
            }
        }
    }

    public static void LateUpdate(List<IExecuteSystem> executeSystems)
    {
        foreach (var system in executeSystems)
        {
            var s = system as UnityFeature;
            if (s != null)
            {
                s.LateUpdate();
                continue;
            }

            var f = system as ILateUpdateSystem;
            if (f != null)
            {
                f.LateUpdate();
            }
        }
    }
}

public class UnitySystems : Systems
{
    public void FixedUpdate()
    {
        UnityEntitasExtensions.FixedUpdate(_executeSystems);
    }

    public void LateUpdate()
    {
        UnityEntitasExtensions.LateUpdate(_executeSystems);
    }
}

public class UnityFeature : Feature {
    public UnityFeature(string name) : base(name) {
    }

    public UnityFeature() {
    }

    public void FixedUpdate()
    {
        UnityEntitasExtensions.FixedUpdate(_executeSystems);
    }

    public void LateUpdate()
    {
        UnityEntitasExtensions.LateUpdate(_executeSystems);
    }
}

A sample system that implements IFixedUpdate:

using Entitas;
using System.Linq;

namespace Sample {
    public class RenderPositionSystem : IInitializeSystem, IExecuteSystem, IFixedUpdateSystem
    {
        readonly GameContext _context;
        IGroup<GameEntity> _movables;

        public RenderPositionSystem(Contexts contexts) {
            _context = contexts.game;
        }

        public void Initialize()
        {
            _movables = _context.GetGroup(GameMatcher.Position);
        }

        private GameEntity[] Filter(GameEntity[] entities)
        {
            return entities.Where(e => e.hasView).ToArray();
        }

        public void FixedUpdate()
        {
            var entities = Filter(_movables.GetEntities());
            foreach (var e in entities)
            {
                var go = e.view.gameObject;
                go.transform.position = e.position.value;
            }
        }

        public void Execute()
        {
            //As I said, I have to leave this empty.
        }
    }
}

When I create my features it's pretty much the same:

namespace Sample {
	public class GameplaySystems : UnityFeature {
		public GameplaySystems(Contexts contexts) : base("Gameplay Systems") {
	        	Add(new AddViewSystem(contexts));
			Add(new RenderPositionSystem(contexts));
			Add(new RenderDirectionSystem(contexts));
		}
	}
}

@heynemann
Copy link

@sschmid any thoughts on how to proceed here? I understand you said I could have a completely different set of Systems for the specific purpose of Fixed and Late Updates, but it strikes me as odd that I divide my Systems by when they are called, instead of by cohesion.

Imagine a scenario where my system must rotate the character. I must rotate the main model in the FixedUpdate method, but the character's spine in the LateUpdate method (after animation has already finished). This is just an example.

What do you think?

@sschmid
Copy link
Owner

sschmid commented Nov 7, 2017

Hi, sorry for the late reply. So basically you'd like to have multiple different update steps. This kind of reminds me of IExecuteSystem and ICleanupSystem where you actually have one system that contains the code of both phases. So what we would need is adding ILateExecute and IFixedExecute and update the Systems and DebugSystems class. We also would need that for ReactiveSystems and MultiReactiveSystems, but that could be solved simpler with a Execute enum.
Yeah, probably that makes sense. Will add this to my todo

@heynemann
Copy link

Not to put pressure here @sschmid but do you see this being implemented soon? I can try to get a PR done if you think it's useful. I can try to start with what you described above.

@heynemann
Copy link

Also, do you have any instructions on how to develop? I'm trying to run the tests in VS 2017 and it can't find any tests... (sorry for the newbie questions)

@heynemann
Copy link

I managed to start a PR. I haven't created it as a PR because I'm fairly certain I'm still missing stuff.

https://github.com/heynemann/Entitas-CSharp/tree/feature/%23434-Unity-Events

Can you guys help me in the right direction? I can now run all the tests.

What I have so far:

  • LateExecute in the Systems (with tests);
  • FixedExecute in the Systems (with tests).

I'm still missing, that I know of:

  • Integrating with ReactiveSystems;
  • Unity Editor integration (to show ILateExecuteSystems and IFixedExecuteSystems).

Any help would be very appreciated. Sorry if this is not what you meant.

@heynemann
Copy link

heynemann commented Dec 4, 2017

Btw, sorry for the identation diffs. I just managed to get the project to compile and its 2am. I will configure MonoDevelop properly later. And will squash everything to 1 commit in the end, before the PR.

@sschmid sschmid closed this as completed Jan 12, 2018
@heynemann
Copy link

@sschmid has this been fixed in the framework? Why was this closed?

@sschmid
Copy link
Owner

sschmid commented Jan 13, 2018

@heynemann Oh, how could this happen, I somehow totally wasn't aware of your comments, that should usually not happen. I'm sorry about not answering.
There's a CONTRIBUTING.md document with instructions. I need to update this now that I released 0.47.x.
After cloning Entitas, simply open Entitas.sln, it contains all projects and Addons. To test, run the Test project as a cli program or navigate with the terminal to the Entitas root and call ./Scripts/bee tests. This will compile and run the tests.

About the feature request itself: you already can group and nest systems and call them when necessary. So you can group fixed systems, late systems, or others like every-other-frame-systems, basically anything you need. But of course you can still implement sth like suggested yourself. I suggest looking at IExecuteSystem and the other interfaces and how it's done there. See those classes to fully integrate it:

  • Systems
  • DebugSystems
  • DebugSystemsInspector
  • SystemInfo

Again, sorry for not answering, I can't explain why I wasn't aware of additional comments in this issue...

@heynemann
Copy link

Thanks!

@christianvoigt
Copy link
Author

Hi @sschmid,

my original issue was not so much about how to implement this (as I had already found a solution that works for me). It was more about how to add VisualDebugging support for my UnitySystems class. I could somehow not use DebugSystems for my own class and did not want to reinvent the wheel. To be honest I don't remember the exact issue right now, but I will look into this again if I find some time, and describe the issues I had in more detail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

No branches or pull requests

3 participants