Closed HollowGraphic closed 3 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).
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.
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.
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 System
s derive from a class called ScriptableSystem
s. Also deriving from that is a class called ScriptableSystems
(note the s
), which has a list of ScriptableSystem
s that you can nest as deeply as you need. Also, under the hood the ScriptableSystem
s 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.
System
s normally live throughout the game's lifetime. But if you want to mitigate this you can use AssetBundle
s, either directly or with Addressables. In such a case, don't place direct references to assets inside your ScriptableSystem
s; use AssetReference
s (or whatever you do for plain AssetBundle
s).
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.
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 anIInitialize
interface 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 theFeature
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?