Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Before we begin there should be some existing knowledge in place, if you don't know the stuff below then just go read up on it (each heading a link you can click on to learn about it). You don't NEED to know all of this, but it will be VERY beneficial.
This is about how your objects (generally classes) get their dependencies, this approach is used heavily throughout the codebase and means most of what the class needs is passed in via its constructor.
This is related to IoC and is basically a industry standard way of resolving dependencies for objects in a way that is flexible and highly configurable. If you have ever seen any code with bits like Bind<ISomething>().To<Something>()
then that is DI configuration.
This library tries to make use of the common ECS paradigm while making reactivity one of its big bonuses. If you are unsure about what rx is or how it can benefit your project then have a read into it, simply speaking its a push approach to data changing rather than a polling approach.
If you have not done any unit testing, then its not the end of the world, you don't NEED to suddenly become a testing rockstar but part of the benefit of using this framework is it tries to keep your code highly testable, so if you want to make use of that benefit its worth knowing how.
Related to unit testing, it covers how you can provide mocked/fake implementations of dependencies to force your code to act a certain way and then confirm that it behaves as you expect.
If you want to know more on the above topics then feel free to drop into our Discord Channel to discuss further or ask any questions.
As part of SystemsRx there is some basic infrastructure provided for you (if you choose to use SystemsRx.Infrastructure
), this contains:
A dependency injection abstraction system (So you can consume DI on any platform with any DI framework)
An ISystemsRxApplication
interface as well as a default implementation SystemsRxApplication
(So you can start your app in a consistent way)
A plugin framework via ISystemsRxPlugin
(so you can write your own plugins which can be re-used across many projects and shared with others)
A default EventSystem
(So you can send events around your application, which implements IEventSystem
)
All of this combined basically provides you an entry point to start creating your applications.
To have some sort of consistency and contract in place for extensibility, for example by adding the infrastructure you can out the box consume any SystemsRx plugins (assuming they dont contain any native platform code), you can also make use of specific lifetime methods and conventions.
If you have a specific scenario and dont want to use the built in infrastructure then can easily just ignore this and put your own stuff in place, but this would then mean you are then incompatible with a lot of good stuff that comes with the consistency and the community all adhering to those contracts.
Welcome etc!
If you are viewing this in github you can view the book version of it HERE
This is an attempt to document most of the important stuff around how to use the library, it is recommended that you follow the order of the TOC, however if you are not using the gitbook version then just look at the summary.md file and follow that order.
The flow is generally:
Stuff you should ideally know before using the framework
About the framework and how to use it
The bits which make up the framework and how to use them
Making larger applications with the framework
How the underlying architecture fits together
How you can extend the framework and use plugins
How to use the framework in a more performance oriented manner
Other guff
Feel free to add to the docs or come on the Discord Channel if you need help!
Systems are where all the logic lives.
The way systems are designed there is an orchestration layer which wraps all systems and handles the communication between the pools and the execution/reaction/setup methods known as ISystemExecutor
(Which can be read about on other pages).
You just express how you want to trigger your systems and let the SystemExecutor
handle the heavy lifting and trigger the relevant method in the system when its time. This can easily be seen when you look at all the available system interfaces which all process individual entities not groups of them.
This only documents the SystemsRx available systems but EcsRx builds on top of this and provides many other system types and an ECS paradigm.
This is where it gets interesting, so we have multiple flavours of systems depending on how you want to trigger them, by default there is IManualSystem
which acts as a simple setup/teardown style system. You can also mix them up so you could have a single system implement IManualSystem
, IBasicSystem
and IReactToEventSystem
which would trigger all the required methods when system sets up/tears down, when an update happens and when an event comes in, but ultimately you can mix and match the interfaces however you want.
IManualSystem
This is a niche system for when you want to carry out some logic outside the scope of entities, or want to have more fine grained control over how you deal with the entities matched.
Rather than the SystemExecutor
doing most of the work for you and managing the subscriptions it leaves it up to you to manage everything how you want once the system has been started.
The StartSystem
method will be triggered when the system has been added to the executor, and the StopSystem
will be triggered when the system is removed.
IBasicSystem
This is a basic system that is triggered every update (based on scheduler update frequency) and lets you do anything you want per update.
IReactToEventSystem
This allows you to react to any event of that type which is published over the IEventSystem
.
So by default (with the default implementation of ISystemExecutor
) systems will load in the order you add them, however you can add a [Priority]
attribute to indicate an explicit order for running.
The infrastructure aspect of the library provides a default application class that should be inherited from and overidden as needed. This provides a way to keep applications consistent and to make sure that things load when you expect. Here is the default lifecycle methods and when they will run in the process of an application starting.
LoadModules
This is where you should load your own modules, the base.LoadModules() will load the default framework so if you do not want this and want to load your own optimized framework components just dont call the base version. An example of this is shown in the optimized performance tests where we are manually assigning the component type ids so we do not want the default loader.
LoadPlugins
This is where you should load any plugins you want to use, if you have no plugins to use then dont bother overriding it.
BindSystems
This is where all systems are BOUND (which means they are in the DI container but not resolved), by default it will auto bind all systems within application scope (using BindAllSystemsWithinApplicationScope), however you can override and remove the base call if you do not want this behaviour, or if you want to manually register other systems you can let it auto register systems within application scope and then manually bind any other systems you require.
ResolveApplicationDependencies
This is where the dependency resolver is generated and dependencies of the application are manually resolved from the DI Container, so the ISystemExecutor and IEventSystem etc are all resolved at this point, once all plugins and modules are run. The base.ResolveApplicationDependencies() will setup the core SystemsRxApplication dependencies so you should call this then resolve anything specific you need after this point.
StartSystems
This is where all systems that are bound should be started (they are added to the ISystemExecutor), by default this stage will add all bound systems to the active system executor (using StartAllBoundSystems), however you can override this behaviour to manually control what systems are to be started, but in most cases this default behaviour will satisfy what you want.
ApplicationStarted
At this point everything you need should be setup ready for you to start creating collections, entities and starting your game.
You can use SystemsRx with an infrastructure layer or just by itself, but it is recommended to use the infrastructure if possible.
SystemsRx is more of a bare bones library which other libraries such as EcsRx builds on top of so while you can use this by itself you may want to look at the libraries that build on top of this layer.
Out the box SystemsRx comes with a load of infrastructure classes which simplify setup, if you are happy to use that as a basis for setup then your job becomes a bit simpler.
There is also a whole section in these docs around the infrastructure and how to use the application and other classes within there in common scenarios.
A wise choice so to start with its advised that you take:
SystemsRx
SystemsRx.Infrastructure
This will provide the basic classes for you to extend, however one fundamental piece of the puzzle is the DI abstraction layer. It doesn't really care which DI framework as it provides an interface for you to implement and then consume that in your own EcsRxApplication
implementation.
So here are the main bits you need:
Download a premade provider or implement IDependencyContainer
for your DI framework of choice.
Extend SystemsRxApplication
implementation, providing it the DI container provider you wish to use
There are pre-made DI implementations for Ninject and Zenject so if you can use one of those on your platform GREAT! if not then just pick a DI framework of choice and implement your own handler for it (using the ninject one as an example to base it off).
So if you dont know what DI (Dependency Injection) is I recommend you go read this and this which will give you a quick overview on what IoC (Inversion of Control) and DI is and how you use it.
It is worth noting here that this is EXACTLY how the examples work in this project so its worth cracking them open to see how its all done, but the same principals can be applied to your own applications.
Ok captain, if you just want to get things going with minimum effort then I would just get the core lib and manually instantiate everything that is needed.
This is like the most bare bones setup I would advise:
Then all you need to do is go:
HUZZAH! you are now up and running and you can make your own conventions or design patterns around this.
Both rx.net and unirx have problems on different engines/platforms so SystemsRx core cannot depend on either, so it depends on this TINY rx implementation so it can work with either.
So in .net currently there are 2 main rx implementations:
System.Reactive
(aka dotnet reactive
or rx.net
)
UniRx
(a unity specific rx implementation)
Herein lies the problem, so this library was first created as a unity project, then it was split off into a generic .net framework agnostic of engines and frameworks, with a unity layer (and other engine specific layers) which would sit on top of this.
Unity and a lot of other engines/frameworks (Monogame, Godot, Xenko etc) support modern .net syntax and libraries, and they allow you to release to different platforms, and this is where the problem rears its head.
As certain platforms don't support JIT and require AOT compilation, which the normal System.Reactive
framework doesn't support. There were however some discussions on the work needed to allow it to work in AOT scenarios, but no solid work seems to have resulted from that yet.
This then means that if you want to target iOS for example you cannot use System.Reactive
out the box, so for unity its not really possible to use it due to IL2CPP and certain platform constraints.
If you are in Unity then yes, UniRx will work in all of those scenarios (with some tweaks). HOWEVER, UniRx is not really maintained that well these days as the maintainer drops in every now and again and then vanishes and no one else has been able to assist in maintaining the project.
This also means that if you are not using Unity you cannot really use UniRx. While there is a nuget package for it, it has not been updated in years and given the lack of communication on the main releases of unirx to the asset store and github I would not hold my breath for a new version of that any time soon (Believe me, I wont stop pestering the maintainer of it).
Kinda, so SystemsRx originally made use of fancy rx linq stuff and some other fancy rx bits, but we couldn't have a dependency on rx.net
as then the unity layer would fail, and I couldn't have a dependency on UniRx
as its nuget package is wildly outdated and the other part cannot be used outside of unity.
So because of these problems we decided to make a TINY rx implementation, which is pretty much a rip off and simplification of parts of UniRx
. This allows EcsRx to internally be able to make use of basic rx paradigms like Subject
and CompositeDisposable
(and a few other bits) but has very little else, i.e no Linq stuff, no scheduler, no async stuffs.
This makes it a very small library and very specific in its usecase, so as EcsRx uses IObservable
as its contract which is a part of .net it means the stuff inside MicroRx
should be compatible with unity and/or any other framework/engine, and place nicely with other more feature rich rx implementations, so you get to decide if you want to use rx.net
or unirx
(or any other rx implementation) in your consuming project, while the EcsRx core is blissfully unaware of what you are using.
We don't really want to have a micro rx implementation, we would much rather depend on a better, more maintained version of rx, but until rx.net
works in unity on all platforms, or UniRx
starts releasing maintained nuget packages which can be consumed on any platform I am going to need to rely upon this to polyfill those needs and stay agnostic of implementation.
Really you dont need to worry about this and dont even really need to care about MicroRx
's existence, although I would say DONT USE IT FOR ANYTHING MAJOR, its only really there to polyfill the internal EcsRx libs.
7.0 Splits IDependencyContainer
into IDependencyRegistry
and IDependencyResolver
as well as removing some niche DI methods, see DI docs for more info.
As this framework is built to enable plugin development and work on any platform/framework, there has been some efforts to streamline how DI is handled within the framework by creating an abstraction over the underlying DI system.
So lets say you wanted to create an RPG plugin where it contained components, events, systems etc for handling buffs, items, inventory etc. Now none of this logic really is dependent on Unity, Monogame etc... its all just raw .net, but you will want to setup the DI concerns for this plugin so it can be loaded into any of those platforms and just work.
To be able to do that we need to have the notion of DI in the framework without having an ACTUAL DI container available. As there would be no point having a hard dependency on Zenject for your plugin if you wanted to consume it in the Monogame world etc.
You may be thinking "why have your own abstraction when Microsoft has now got its own DI abstractions?" and thats a really good point, there are 2 reasons. First being that not all DI frameworks (especially more game dev specific ones) adhere to the MS one so we cant rely upon them conforming to that interface.
Second there is some implicit behaviour in the MS DI framework that is not entirely consistent, such as how keyed/named and non named services are segregated, different DI frameworks do this differently, so we wrap some underlying logic to give the same behaviour across all implementations.
IDependencyRegistry
and IDependencyResolver
There are 2 main interfaces that are used to make DI possible, one is the IDependencyRegistry
which is responisbile for the binding/registering of dependencies and is used in the first stage of setting up dependency trees.
Then there is the latter IDependencyResolver
which is responsible for resolving the registered dependencies. For the most part you wont need to worry about how these get handled internally, you just need to be aware of which one you want to use.
IDependencyRegistry
FeaturesBind<From, To>
, Bind<T>
, Unbind<T>
This lets you bind a given type to another type, so it could be Bind<ISomething, Something>()
or if you want to self bind the concrete type just do Bind<Something>()
. You can also unbind a type by using Unbind<Something>()
. You can also add named bindings and other configuration by passing in a configuration object (discussed further on).
LoadModule<T>
, LoadModule(IDependencyModule)
There is also support for creating modules that setup your DI configuration, this requires you to implement IDependencyModule
and has a Setup
method which provides you the container to setup your bindings on.
If you want to configure how a binding should work, you can pass into the Bind
methods an optional configuration object which exposes the current properties.
bool
Setting this to true
will mean that only one instance of this binding should exist, so if you were to do:
Then resolve IEventSystem
in multiple places you will always get back the same instance, which is extremely handy for infrastructure style objects which should act as singletons. If you provide false
as the value it will return a new instance for every resolve request.
string
This will allow you to give the binding a name for resolving via name.
object
This allows you to bind to an actual instance of an object rather than a type, which is useful if you need to manually setup something yourself.
Func<IDependencyContainer, object>
This allows you to lazy bind something to a method rather than an instance/type, which is useful if you want to setup something in a custom way once all DI configuration has been processed.
There is a slightly nicer way to set this up using a builder pattern extension shown further on.
IDependencyResolver
FeaturesResolve<T>
, ResolveAll<T>
This lets you get an instance of something from the DI container, if you want a single instance do Resolve<T>()
or if you want all instances matching that type do ResolveAll<T>()
which returns an enumerable of the given type. You can also request an instance of a type with a given name providing you have bound the type with a name, by doing Resolve<ISomething>("something-1")
which would return the implementation of Something
named "something-1".
The configuration object is simple but can be unsightly for larger configurations, to assist in this we have added a couple of extension methods which can be used to make your binding config a little more succinct.
There is a builder pattern helper which lets you setup your binding config via a builder rather than an instance of BindingConfiguration
, this can be used by just creating a lambda within the bind method like so:
This lets you setup the configuration in a nice way, it also has type safety so you can setup instances and methods using it like so:
The following old DI methods have been removed as not all DI frameworks supported them (mainly Microsoft DI sigh).
OnActivation
WhenInjectedInto
WithNamedConstructorArgs
WithTypedConstructorArgs
These old methods used to exist and provide a cross platform way of being able to express these concerns, however most of this can still be handled by using the method based resolution and adding a custom activation function in as a side effect.
The only one which doesnt have a way to resolve really is the WhenInjectedInto
scenario but that is extremely niche and can be handed via facade interfaces or some other approach that just proxies the type and is used for the various classes which need specific implementations.
You still have access to the underlying
NativeRegistry
andNativeResolver
so you can still use the native equivalents to express these concerns if you need them, just be aware in cross platform plugins any native code will limit the platforms your plugin can be used in.
There are a few plugins provided with SystemsRx that you can opt in to use by just referencing the dll and loading the plugin from your application class in the LoadPlugins
phase. Anyone in the community can make their own plugins and hopefully going forward there will be more plugins available.
You just need to reference the dll in your project and then in your LoadPlugins
phase you just load the desired plugin entry point. Here is an example of loading some of the official SystemsRx plugins:
Once they are registered there the application will setup any dependencies for the plugin and kick start anything that needs to be running.
Plugins are pretty simple, they just require you to implement the ISystemsRxPlugin
interface and that's it.
Here is an example of the reactive systems plugin, which binds some conventional system handlers for the SystemExecutor
to make use of, then we output any systems we need to register (in this case none).
There are plenty of example plugins in the core repo and there is also a buff one available on github too which was designed for unity originally but same concept applies.
Sharing your own custom conventions (much like reactive systems and batched systems plugins)
Sharing pre-made game logic, i.e a buffs, stealth, inventory etc
Optimized implementations for specific platforms
You can wrap up almost anything into a shareable plugin, and if you are doing architectural plugins you can replace almost any part of EcsRx from within a plugin, or if you are making game logic you can use events/components as your contracts and just register your systems on setup so everything "just works" out the box on any platform (assuming you have no platform specific code in there).
Computed values are basically read only values which are updated on changes, much like IObservable
instances which notify you on data changing, computed objects also let you see what the value of the object is as well.
There are 3 computed types available within the system:
IComputed<T>
(For computed single values)Simplest computed and provides a current value and allows subscription to when the value changes, this can be very useful for precomputing things based off other data, i.e calculating MaxHp once all buffs have been taken into account.
IComputedCollection<T>
(For computed collections of data)A reactive collection which provides an up to date collection of values and allows you to subscribe to when it changes, this could be useful for tracking all beneficial buffs on a player where the source data is just ALL buffs/debuffs on the entity.
EcsRx adds on top of this and provides
IComputedGroup
and other related functionality
So there are a few different ways to use them and most of that is based upon your use cases, and you can make your own implementations if you want to wrap up your own scenarios.
All of these classes are provided as abstract
classes so you should inherit from them if you wish to build off them.
ComputedFromData
This is a versatile computed generator where you can basically create a pre computed variable based upon anything. So you pass in any object you require which represents the state, then you calculate what the output value should be internally.
You may never need this functionality, but in some cases you may want to share pre-computed data around your application without having to constantly re-compute it everywhere. This can make your code more simplistic and easier to maintain while also providing performance benefits were you do not need to keep doing live queries for data.
So to look at some real world scenarios, lets pretend we have a game with the following components:
CanAttack
HasHealth
HasLevel
HasGroup
We now have a few requirements:
We need to show on the HUD all the people within OUR group
We need to show an effect on someone in the group when their HP < 20%
We need to show a value as to how hard the current area is
Now I appreciate this is all a bit whimsical but stay with me, now we can easily constrain on groups based on the components, so we can find all entities which are in a group and can attack etc, but thats where our current observations stop.
IComputedCollection
scenario)We now have a computed group of party members, but now we want to be able to know who in that group has low health, so we can create an IComputedCollection
which is already constrained to the party (so we dont need to worry about working out that bit again), then we can check if their health is < 20% and if so put them in the list with their HP value, this way we can just bind our whimscial PartyMembersWithLowHealthComputedCollection
which would implement IComputedCollection<PartyMemberWithLowHealth>
(verbose I know) in the DI config then inject it into a system and boom you now have a system which can just look at this one object to find out whos got low health and be notified when anything changes.
IComputed
scenario)So with the other bits out the way we basically want a way to quickly identify how hard the current area is, lets just assume this is based on what level all the enemies within a 30 unit radius is.
So we know how to make computed groups, so we can make one of them to wrap up all entities which are not within our group and are within 30 units of the player. Once we have that we can then create a computed variable (which just exposes a singular value) to loop through all the enemies within 30 units (which we now have from the computed group) and get all the enemies levels and average then, returning that as the result.
This way you can inject this into various other places, so if you need to show some colour indicator on current difficulty or warn the player you can use this value.
Big thanks to all the community who help out with SystemsRx/EcsRx.
Great stuff buddy, all you need to do is add it here in a PR with a link to the plugin (with optional profile link) and a blurb about what the plugin is/does, or if you don't fancy that just raise an issue detailing the above and we can add it for you.
A SystemsRx Plugin that provides a base notion for implementing a plugin to control when a system is started and stopped with ease.
A simple web based auto battler made using SystemRx, OpenRpg and Blazor. It was mainly done as a live stream but the repo can be used to see how to run SystemsRx/EcsRx in the browser.