Skip to content

Releases: EcsRx/ecsrx

Reverting to netstandard 2.0 and net 4.6

03 Mar 13:18
Compare
Choose a tag to compare

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

03 Mar 13:00
Compare
Choose a tag to compare

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

03 Apr 07:55
Compare
Choose a tag to compare

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

18 Jan 14:02
Compare
Choose a tag to compare

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

25 Sep 08:11
Compare
Choose a tag to compare

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 app
  • ApplicationStarted - 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

17 Sep 19:26
Compare
Choose a tag to compare

Fixes!

  • This update contains a fix for the dependency binder which was incorrectly using TFrom instead of TTo 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 on ISystemExecutor
  • ISystemExecutor will now throw an exception if you try to add the same system twice

Minor updates and DI changes

04 Sep 21:13
Compare
Choose a tag to compare

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

23 Aug 21:25
Compare
Choose a tag to compare

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

30 Jul 12:32
Compare
Choose a tag to compare

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

27 Jul 14:42
Compare
Choose a tag to compare

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.