jeffcampbellmakesgames / Entitas-Redux

An entity-component framework for Unity with code generation and visual debugging
MIT License
103 stars 13 forks source link

[FEATURE REQUEST] Configure Systems and Features through the inspector #12

Closed HollowGraphic closed 3 years ago

HollowGraphic commented 4 years ago

Is your feature request related to a problem? Please describe. Setting up systems through code is all fine and dandy, but being able to set them up through the Unity inspector would be handy. This can be done with most systems (IUpdate, ILateUpdate, etc) by using an IInitializeinterface to set up the system. Reactive systems are a different story. One of the main driving forces behind a request like this is being able to set system fields through the inspector. You would either have to pass what you need through constructors, or create singleton entities that you can access. But this could leave you with a MonoBehavior that is loaded with fields that you can set in the inspector, and then when you create you systems, you pass them in. This can confuse the user as to what system needed that data. Having the ability to see at a glance that your RayCastToWorld system requires a camera to function would reduce the complexity. In my opinion. This could also allow the user to set up system configurations as prefabs. That way you can have a prefab of systems that you know work well, then set up a new one (or even a variant) for testing some newfangled feature.

Describe the solution you'd like I would be nice if we could add systems to a list/array through the inspector.

Describe alternatives you've considered I've tried wrapping systems in a constructorless class with the relevant fields, implement IInitialize and setup the system that way. It feels like too much indirection however. I've also thought about writing my own version of all the systems, but I lose the ability to Debug them through the Feature class (mainly because I don't fully understand the ins and outs of all the systems).

Additional context One problem you could run into with this approach is that it could be hard to manage if you end up with a lot of systems in the inspector. Some way to organize them should probably be considered (system groups?). If the idea of system group sounds appealing, then my next request would be to be able to automate the process with an Attribute. Maybe something like GroupIn("Group Name") and then codegen up a SystemGroup that contains the systems. This could get tricky because everytime you codegen, you will lose any modifications made the the SystemGroup file. Would partial classes solve this?

jeffcampbellmakesgames commented 4 years ago

I've also been thinking about easier ways for setting up systems without manually requiring a developer to explicitly add them in code. Part of this comes from the fact that much of the code-generated framework for EntitasRedux and Entitas is seamless for entities, components in a way that does away much of the boilerplate work a developer would normally need to do by hand. Systems in general are a bit of a missing puzzle piece in that there isn't a lot of code gen for them and setting them up manually has been the standard.

Part of the reason why this has been up until this point is largely because there are not many restrictions or constraints on what defines a system and when a Systems or Features instance is enabled/disabled is largely an app-specific topic. All it currently takes to implement a bare-bones system is to add the interface to a new or existing class, add an instance of that system to your controller-type class (also manually defined), and off you go. Systems also typically need access to certain external data like a context (even if only briefly to get access to a group or in your example a camera that is largely left up to the developer to decide how to do that (no requirement to use an injection/IOC framework to assign dependencies). Because of these loose restrictions, a developer is free to implement a system as a plain-old CSharp class (POCO), Monobehaviour, or ScriptableObject.

I've tried wrapping systems in a constructorless class with the relevant fields, implement IInitialize and setup the system that way.

One of the better ways I've heard of someone making it easy to configure systems both in the order they execute and configuring fields on them is by way of implementing their systems as a ScriptableObject. In this way, they are essentially able to define a single instance of that system in the Unity Editor at design time, expose any serialized fields they want to configure in the Inspector, and then assign it where desired in a a list of other systems on a prefab or scene (or even on another ScriptableObject). This might look something like this:

Pseudo Code Example of Scriptable System Setup

public abstract class BaseScriptableSystem : ScriptableObject
{

}

public sealed class ExampleInitializeSystem : BaseScriptableSystem, IInitialize
{
    public void Initialize()
    {
        // Do init work here
    }
}

public class AppSpecificController : MonoBehaviour
{
        [SerializeField]
        private List<BaseScriptableSystem> _scriptableSystems;

        private Systems _systems;

        private void Awake()
        {
            _systems = new Systems();
            for(var i = 0; i < _scriptableSystems.Count; ++i)
            {
                // If needed, you could add your own interface for configuring them with other types of resources, like the context or others, before adding them to _systems
                _systems.Add(_scriptableSystems[i]);
            }
            _systems.Initialize();
        }

        private void OnDestroy()
        {
            _systems.TearDown();
        }

        private void FixedUpdate()
        {
            _systems.FixedUpdate();
        }

        private void Update()
        {
            _systems.Update();
            _systems.Execute();
        }

        private void LateUpdate()
        {
            _systems.LateUpdate();
            _systems.Cleanup();
        }
}

With a little more work, you could also define a ScriptableObject based Feature, where all it ends up being is a list of scriptable systems. With that in mind, you could then have a collection of these scriptable features that each end up being their own Systems group (and if nesting is needed, each scriptable feature could reference other instances that would be childed to them).

WeslomPo commented 3 years ago

ScriptableObjects tends to hook all data, callbacks and so on to himself, and can cause a memory leak because they will live all time that game is live. Even if you know this, even if you aware of problem of this, you can easily shoot your leg. So I recommend to avoid that approach.

If project have some DI container, this problem easily solvable by generating a container file with all systems in neat order. I write myself a code generator that find all attributes, and generate an CS installer for the Extenject.

jeffcampbellmakesgames commented 3 years ago

ScriptableObjects tends to hook all data, callbacks and so on to himself, and can cause a memory leak because they will live all time that game is live. Even if you know this, even if you aware of problem of this, you can easily shoot your leg. So I recommend to avoid that approach.

There is definitely the potential for a ScriptableObject class to be considered a memory leak in that it can contain a lot of references to resources that would not be necessarily de-referenced or unloaded if the ScriptableObject were still in-memory. For the purpose of using it as the basis of an ECS system that should largely be stateless I wouldn't think this would be a concern in this way, particularly if you take advantage of IInitializeSystem and ITeardownSystem to retain and release resources.

It's worth noting as well that the way I would see something like this being implemented is that it would be an option for a developer to create systems in this way, not necessarily a requirement. Ideally the outcome of an official scriptable systems/feature would be to make it easier for developers to make them in some way.

If project have some DI container, this problem easily solvable by generating a container file with all systems in neat order. I write myself a code generator that find all attributes, and generate an CS installer for the Extenject.

This is done currently for the cleanup systems per context where they are gathered up into both a unique Systems and Feature instance. What the scriptable system and feature approach has over auto-generating a Systems or Feature is that it enables the developer to compose any number of systems into features and restructure/reorder them as needed from the Unity Editor without needing to write any code to setup how they're ordered or grouped.

JesseTG commented 3 years ago

I'm doing this for my game. So far it's working nicely; once my code is battle-tested I'll publish it. Not sure how yet; it might be as a PR for this repo, a standalone package, or a Gist for someone else to polish up. It might even be as a customizable codegen. Dunno, I need to make sure my code solves my problems first. Here are some other thoughts:

One problem you could run into with this approach is that it could be hard to manage if you end up with a lot of systems in the inspector. Some way to organize them should probably be considered (system groups?).

All of my Systems derive from a class called ScriptableSystems. Also deriving from that is a class called ScriptableSystems (note the s), which has a list of ScriptableSystems that you can nest as deeply as you need. Also, under the hood the ScriptableSystems are all placed inside a Feature, so I still get to use the visual debugger. (The similar names aren't actually confusing in practice because you deal with them in the editor more than in the code.)

ScriptableObjects tends to hook all data, callbacks and so on to himself, and can cause a memory leak because they will live all time that game is live. Even if you know this, even if you aware of problem of this, you can easily shoot your leg. So I recommend to avoid that approach.

Systems normally live throughout the game's lifetime. But if you want to mitigate this you can use AssetBundles, either directly or with Addressables. In such a case, don't place direct references to assets inside your ScriptableSystems; use AssetReferences (or whatever you do for plain AssetBundles).

jeffcampbellmakesgames commented 3 years ago

Closing this issue in favor of https://github.com/jeffcampbellmakesgames/Entitas-Redux/issues/35 to better handle describing implementation and tasks for scriptable systems and features.