Releases: EcsRx/ecsrx
Reverting to netstandard 2.0 and net 4.6
Didn't realise but some people are still stuck on 4.6 and netstandard 2.1 wont support .net framework :(.
Anyway happy days a new release++;
.Net Standard 2.1, .net 4.7.2 build targets + a small bug fix
This update fixes an issue which caused some components to not get registered correctly from plugins depending upon how they were called in the codebase. This fix should hopefully ensure that all plugins can be resolved and loaded before component types are mapped. This being said you are still free to provide your own IComponentTypeLookup (those users of custom ones would have been unaffected by this bug).
There has also been a jump to .net standard 2.1 and .net 4.7.2 as build targets for the libs.
Some Fixes & Abstract OnUpdate
As there havent been many descriptions on the updates in the 3.x.x cycle I just wanted to dump a few of them in here:
Abstract OnUpdate
One of the problems with supporting multiple platforms and frameworks is being able to depend on native code but without having a hard dependency on it, so as part of this there is now an IObservableScheduler
which exposes an OnUpdate
observable. This is an opt-in thing and you can still keep using your UniRx EveryUpdate
or your GameScheduler.OnUpdate
on monogame, but under the hood the OnUpdate
in this new object will implement the platform specific update loop notifier so plugins can be made cross platform while still depending upon the same update loop as everything else.
ReactToGroupExSystem
This was added to the EcsRx.Plugins.ReactiveSystems
plugin which acts the same as the normal ReactToGroupSystem
but also provides the ability to run code BeforeProcessing
and AfterProcessing
which can help with scenario where you want to setup something before processing everything and cleaning up afterwards.
Batched Systems Plugin Changes
There have been a few minor improvements to the batched systems so you can now override the behaviour of group change notifications, i.e rather than reacting to each one, throttle the changes for a period. There is also a fix for entities changing while rebuilding where it will break out of the rebuild and attempt to rebuild on the next cycle.
General Fixes
Thanks to @floatW0lf there have been a some great fixes added which caused some pretty rubbish problems around component notifications and group changing, these were in previous releases but still wanted to say thanks to the community for assisting with notifying us of issues and assisting with PRs.
Namespace Changes
Some of the previous releases in this cycle have moved some namespaces around, but hopefully this is all internal classes and wont effect many people.
v3.0.0 - Performance And Plugins
Performance Changes - Structs, Refs, Batched Systems
Under the hood a lot of the layering and architecture from IEntity
down to IComponentDatabase
has been changed to use less memory, be more efficient and support structs. There is a lot of other changes but lets drill down into them a bit more.
struct
support for components
Historically a component always used to be a class, which is fine for most people, and you can continue to use components this way without issue. However this comes with a performance overhead in terms of how its accessed and allocated.
Now that structs can be used it allows them to be allocated far quicker and accessed quicker, this also can provide MASSIVE performance benefits if you combine it with batching where you are trying to make better use of the CPU prefetch and cache to only access the bits you care about and just iterate over them.
public struct PositionComponent : IComponent
{
public Vector3 Position;
}
More docs on this subject will be added shortly which will go into far more depth on the subject.
Component Database Changes
Without droning on too much, the database used to historically bulk allocate component memory based on the entity size, whereas now it will instead allocate what it needs and try re-using until it needs to expand, this will reduce memory usage and also makes it more efficient.
Building off the back of those changes we also now are able to expose the underlying component pools and arrays easier, which means you can actually grab them and manually iterate through them yourself if you want to get faster performance than querying each entity individually.
Getting components by ref
If you mainly use class based components this wont be of any interest to you, but to those who use struct based components you can get the components by ref which allows you to update it in place, and ripple those changes down into the underlying component. This can make things slightly quicker and more succinct without having to replace the component every time you change something.
// Additions to IEntity
ref T GetComponent<T>(int componentTypeId) where T : IComponent;
ref T AddComponent<T>(int componentTypeId) where T : IComponent, new();
// Example use case
ref var positionComponent = ref entity.AddComponent<PositionComponent>(PositionComponentTypeId);
Batched Systems
Until now when you were dealing with groups and systems you would generally get given a load of entities and you would loop through them getting the components for each one and then doing your calculations on them. While this is fine for simple things, when have lots of entities and more components it can become a chore to get each component, and it is also slower to do as it needs to keep retrieving random bits of memory all over the place.
With the new BatchedSystem
and ReferenceBatchedSystem
types you are able to stipulate ahead of time what components you require for this system to operate and pre-fetch them all in one big chunk of memory. This makes performance FAAAAR quicker and in most cases makes it slightly easier to do your work, as you can just loop through each Batch
(contains entity id and the components you need) and already have the components in memory ready to go, meaning less effort for the computer to resolve all your guff.
// example of typical component access
public void Process(IEntity entity)
{
var basicComponent = entity.GetComponent<SomeComponent1>();
basicComponent.Position += Vector3.One;
basicComponent.Something += 10;
var basicComponent2 = entity.GetComponent<SomeComponent2>();
basicComponent2.Value += 10;
basicComponent2.IsTrue = true;
}
// example of batched component access (showing structs, but reference types same without ref
)
public void Process(int entityId, ref SomeComponent1 basicComponent, ref SomeComponent2 basicComponent2)
{
basicComponent.Position += Vector3.One;
basicComponent.Something += 10;
basicComponent2.Value += 10;
basicComponent2.IsTrue = true;
}
As an example if you were to compare the 2 approaches you would get a drastic difference in time taken to process, there is an example bare bones scenario which does this (abiet simpler form) in the project it makes 200,000 entities with 2 components, and has some logic to mimic the system process, then loops 100 times.
- Looping each entity and getting each component individually || 13s to complete
- Looping through the batched version || 600ms to complete
This is on a potato laptop and goes to show that the batched approach is roughly 20x faster, and its very little extra effort, it also provides large performance boosts for class based components, just not as fast as structs.
One other benefit is behind the scenes the batches are managed for you, much like IObservableGroup
instances, so if you have 5 systems sharing the same batches, they will all be using the same underlying batch behind the scenes which means each system isn't having to keep its own copy and maintain it.
Plugins
So plugins have existed for quite a while in EcsRx but all changes to EcsRx framework have happened within the core part. Going forward we have tried to split out more of the optional parts of the system into plugins. This allows you to decide if you want to use reactive systems, and this has also allowed the new batching process to be developed without impacting the core framework (as it requires unsafe
code).
The first parts to be split into plugins are:
EcsRx.Views
->EcsRx.Plugins.Views
EcsRx.Systems
->EcsRx.Plugins.ReactiveSystems
EcsRx
(computeds) ->EcsRx.Plugins.Computeds
EcsRx.Plugins.Batches
(new)
WARNINGS!!!
This latest version makes use of the latest and greatest C# 7.3 language features, this is going to be a problem for some people, and for those people who are not able to adopt the latest C# version I would suggest sticking with the previous version until you can update.
Closing Blurbs
As part of these changes it paves the way to potentially have more performance increases going forward as well as improve the eco system in a simpler more isolated way using plugins. There is a lot of work that is still proposed for interacting with entities and observable groups (as this is still a slow part of the system), but this hopefully will give people more freedom and more flexibility to do what they want with the system.
These changes will hopefully be rolled into the Unity and Monogame versions shortly, and if anyone wants to help out we could really do with assistance with docs/example maintenance and creation.
Lifecycle changes
Lifecycle changes in Application
Historically you had 2 methods that were mainly used for setting up your application:
ApplicationStarting
- Modules are loaded, go prep your appApplicationStarted
- Everything is loaded, go use your app
This was fine to begin with but does not scale well when you have more complex scenarios or plugins that augment the internal parts of the framework.
v2 Lifecycle changes
So as part of trying to improve this workflow the lifecycle has now been changed to have several virtual methods which you can opt in to plug in your own logic, here is a list of the methods and the order they run in.
1. 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.
2. 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.
One major change in plugin loading is that it now happens before internal dependencies are resolved, as historically this was run AFTER certain dependencies were resolved such as ISystemExecutor
and IEventSystem
so if you had a plugin which removed base bindings and put its own in, you would be unable to consume them as the application had already resolved the things it was changing, so this now allows plugins to augment the framework and application dependencies before they are resolved, which makes everything more flexible.
3. ResolveApplicationDependencies
This is where the 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 EcsRxApplication dependencies so you should call this then resolve anything specific you need after this point.
4. 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.
5. 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.
6. ApplicationStarted
Much like the old world, this is where you should start using everything, by this point:
- All the dependencies you require for everything should be bound
- All the plugins you require should be loaded and initialized
- All the systems you require should be in the systems executor
So from here you can just get on with making your entities and starting your game, but as you can see you now have far more flexibility and structure to how you compose your application and in default scenarios you will generally get everything loaded for you ready to go assuming default conventions.
Another smaller update
Fixes!
- This update contains a fix for the dependency binder which was incorrectly using
TFrom
instead ofTTo
in the Ninject wrapper.
Additions
- There is now a
HasBinding(name?)
method which lets you check if a binding exists in the DI container. - There is now a
HasSystem
method onISystemExecutor
ISystemExecutor
will now throw an exception if you try to add the same system twice
Minor updates and DI changes
This release includes some more extension methods and has added to the DI part of the framework, there has also been some improvements to DI support adding support for:
- Resolving observable groups directly from DI
- Allowing typed constructor args
- Allowing binding via a method
There has also been a change in the order that components are added in batches, which now makes it add left to right rather than right to left like it was doing before (due to optimization on iterating towards zero).
Really just begrudgingly using semver
This release requires little fanfare really, it is just a version bump to align with semver (which I very much like, but wish breaking changes could be represented by 0.THISBIT.0.0
).
That aside the polyfills inside EcsRx have now been moved to a new MicroRx
project, and the Ninject
dependency wrapper has been released as a separate package, finally the EcsRx.ReactiveData
package is now out there which contains ReactiveDictionary
, ReactiveCollection
and ReactiveProperty
incase you are not using UniRx (its all unirx code which has been slightly altered to work with rx.net anyway).
Other than that nothing to report!
More Performance Improvements
Summary
This release is generally a performance tweak, but it changes surface API of events from entities all the way up to collections. For most users this wont change anything, but really its 0.3.1 but due to the change in surface API I have bumped the version (its not 100% semver as I am using minor as the major in this instance).
For the power users
Summary
This contains some breaking changes hence the version bump, but this is mainly around slimming down IEntity
and moving a lot of helper calls to extension methods (this way they can be applied to any implementation).
Fixes
Big thanks to @JayPavlina for helping with testing and bugs around component removal and duplicate events in Observable Groups have now been fixed and test cases updated accordingly. So now your ITeardownSystem
implementations should only trigger once per entity, and your entity remove calls will verify the components exist and raise events accordingly.
Application Module Override
This release exposes the underlying application paradigm further so by default it will install all needed framework components, however if you are a power user you may want to add your own implementations for different things, such as adding your own component type lookups, your own entity implementations, collection factories etc.
Historically it was a pain to unbind everything and re-bind your own stuff, but now there is a GetFrameworkModule
virtual method in the application which lets you inject your own bootstrap module, there is an example of this in the optimized performance test examples.
DONT WORRY you dont need to use any of this stuff, and by default you will be fine, but for those who want to build upon this framework further with their own conventions and implementations this goes a long way to helping them.
Optimizations
So as part of the previous performance improvements there was underlying potential to bypass entity interactions by type and provide the raw component type id. This has now been exposed further so IEntity
implementations how allow you to Get/Has/Remove by both Type
and int (componentTypeId)
.
Now in most common scenarios component types are cached ahead of time and automatically assigned ids, so when you call GetComponent
with a Type
it actually looks up the id of the component and then passes that to the underlying database to resolve it. However now you are able to bypass this type lookup if you need to and use the ids directly but to do this you need to explicitly set your component type ids and manage that yourself within the project.
You can potentially add your own codegen to do some of this for you or hand roll your own approach, but if you look at the example performance tests which are optimized you will see that they are overriding the default framework module and providing a hardcoded version of the component lookups.
There are a few different ways to approach the management of ids, you could set them as an enum, or a static class full of properties, or even extension methods which have hardcoded ids like GetHealthComponent()
which internally knows HealthComponent
is id 54 etc.