ecsrx

A reactive take on the ECS pattern for .net game developers

MIT License

Stars
520
Committers
11

Bot releases are visible (Hide)

ecsrx - Minor fixes for Computed Groups Latest Release

Published by grofit 6 months ago

Thanks to @thejayman for adding a fix for ComputedGroups incorrectly raising removal events for entities which were not currently being tracked internally.

ecsrx - Observable Group Changes + Dropping .Net Framework Builds

Published by grofit over 2 years ago

Summary

The high level information here is that we have dropped explicit releases for .Net Framework 4.6 and instead are now just focusing on building netstandard2.0, in the future we would like to move to net7 but until a lot more frameworks have migrated over to it we are just going to stick with netstandard.

We have also changed the way ObservableGroup objects work by separating out the group matching mechanism to a new interface known as an IObservableGroupTracker which will monitor the groups/entities and then relay onto the ObservableGroup when there is a change.

This diagram shows the new process, which historically all used to take place within the ObservableGroup so this makes the object simpler and allows other parts of the system to track group changes.

Other Changes

There are some breaking changes around namespaces for events changing as well as a new wrapper around component interactions on the type called IComponentAccessor, which reduce the amount of type lookups if you are doing a lot of interactions with component types.

ecsrx - Fixed Regression Issue With `IDisposable` Components

Published by grofit almost 3 years ago

Summary

While this isn't a massive release it does have a few small changes in terms of how component removal is handled.

So first of all a HUGE thanks to DoctorDepthum on the discord who noticed the regression of the IDisposable handling of components within entities, this has been rectified now so components with IDisposable are now correctly disposed.

So just to clarify when you remove a component now, the underlying logic that runs in the ComponentPool will check if the pool contains reference types, and if so null out the index, and check if the component implements IDisposable and if so will trigger the disposable logic, then finally release the index for future consumption.

ecsrx - SystemsRx Split From EcsRx

Published by grofit over 3 years ago

Summary

The high level change for this version is that SystemsRx is now its own repository within the EcsRx org, however realistically at a high level you wont notice much difference other than a few package name changes:

  • EcsRx.Infrastructure.Ninject -> SystemsRx.Infrastructure.Ninject
  • EcsRx.ReactiveData -> SystemsRx.ReactiveData
  • EcsRx.MicroRx -> SystemsRx.MicroRx

There is also now a non EcsRx dependent SystemsRx.Plugins.Computeds which has base computed types without being dependent upon IGroup etc. The EcsRx version still exists and builds on top of this allowing you little change in terms of usage.

IEventSystem Changes

The IEventSystem now has 2 publish methods, the default one which is backwards compatible is the default Publish<T> however there is also now a PublishAsync<T> which will use IThreadHandler to Run a new task for it which by default does a Task.Run(...) for the publish.

IThreadHandler Changes

The IThreadHandler now has a Run method which allows you to just do an arbitrary run of an action via the thread handler (defaulted to Task.Run).

ecsrx - Moving more into SystemsRx

Published by grofit over 3 years ago

Summary

Some of these changes actually happened in previous releases but a few things have been moved down into SystemsRx such as the pooling mechanisms, as they are useful regardless of the ECS aspect of things, so now they can be used without.

Also there have been some changes thanks to @Fijo which makes it easier to include IObservableGroup objects on the fly in your systems via attributes by using the GroupBinding plugin.

GroupBinding Plugin

This is optional but if you do want to include multiple IObservableGroup objects within your systems (such as IManualSystem) you can do so like this:

[FromComponents(typeof (LevelComponent))] public IObservableGroup LevelAccessor;     
[FromComponents(typeof(EnemyComponent))]  public IObservableGroup EnemyAccessor;

Which is directly lifted from the Roguelike2d Repository which has some real life usages of this plugin and how it can simplify the system and reduce dev time/boilerplate, there are some other approaches where you can use FromGroup to pass in an explicit group or just use FromGroup without a group on systems which implement IGroupedSystem, and it will take the default group property as its argument.

This also simplifies those usecases for people who were originally using IManualSystems before they got moved to SystemsRx and were left without a simple way to resolve an IObservableGroup when needed.

ecsrx - Splitting Core Systems/Infrastructure (SystemsRx) from EcsRx

Published by grofit over 3 years ago

Summary

Currently a lot of the EcsRx features are not directly tied to the ECS paradigm but as they are part of core they come bundled together.

Part of this change is to decouple some of that so there can be a SystemsRx library which has no notion of EcsRx which can be used by people who want to have nice infrastructure and DI abstractions, as well as nice simple systems to run logic in a simple way, but don't want to have to worry about using entities and components.

SystemsRx

So this split has now made it so the new library acts as a layer underneath EcsRx and provides basic System Execution, Infrastructure, DI and common extensions as well as a couple of simple systems such as IManualSystem and event handling systems, it even supports plugins without needing to worry about the EcsRx layer.

EcsRx

At the high level EcsRx hasn't changed much, its just a lot of the core innards have been moved down a layer (and this is mentioned more on front page readme), there are a couple of breaking changes (mentioned in breaking changes document), but for the most part you continue to use the library as you do right now.

Under the hood to use EcsRx you will be pulling in SystemsRx but this simplifies things a bit and also lets you run systems outside of the ECS paradigm alongside your ECS related ones, so its best of both worlds.

ecsrx - Fixed issue with Struct Batching

Published by grofit almost 4 years ago

Batching Plugin + Structs

As the issue with structs and batches still seemed to have no real avenue to being resolved other than only allowing blittable types, the decision has been made to just make this a requirement.

If you do not use the batching plugin then your structs (if you use them) can be of any type you want, but if you want to get the performance boost of them all being batched together you need to use blittable types otherwise they cannot be pinned.

Also updated group builder

Added a couple of struct methods to the group builder, this should allow you to use the builder to make groups with struct types in too

ecsrx - Persistence Plugin now available, as well as other helpers

Published by grofit over 4 years ago

Persistence Plugin

The major thing to talk about is the new persistence plugin which adds the ability to save your entities to and from files (or anywhere else you want), out the box it supports saving and loading from binary files, but there is an example project for how to change that to JSON if you prefer it to be human readable/editable.

As the persistence functionality is built on top of Persistity you can also make use of pipelines for your own needs, like extracting certain data from your game state and persisting it as a save game file, or even sending off metrics to PlayFab etc.

For more info on this look at the example projects and persistity documentation, or feel free to discuss in the discord server.

DI Improvements

There are some new helpers for DI around custom functions on activation as well as being able to specify that your binding should only be used when injecting into a certain type or set of types.

ecsrx - Streamlining Update/Time Checking

Published by grofit over 4 years ago

As part of the previous update we moved the IObservableScheduler into the core, but on top of this we have streamlined what it does, so its now IUpdateScheduler and also implements ITimeTracker which allows you to get the elapse/total time in a cross platform way.

This would be a minor update but as it breaks the surface API it needs to be a major release.

This isn't a huge update but there are lots of words explaining whats happened :D

IObservableScheduler now in core

This was originally brought in as a way to have a streamlined cross platform "EveryUpdate" style notion, as every engine/framework does their update loop differently, so this exposed a way for those events to be fed through in a sane-ish way. So in Unity it may be from a UniRx Observable.EveryUpdate in MonoGame it may be from a Game.Update while in a console app it may just be a timer ticking at 60fps.

The point is that this allows plugins to have systems which update according to the host platform but be agnostic of it, so plugin systems no longer need to worry about having platform dependent observables, as historically if you wanted to have a cross platform system your only option really was an IManualSystem and make some update loop yourself which is not great.

And off the back of this a new basic system has been introduced which wraps up the underlying update mechanism and exposes a simple processing point.

New IBasicSystem

This is basically a cut down IReactToGroupSystem but the schedule of it is taken care of for you, as in most cases people would just have it default to every update anyway, so in these cases you can just use the normal IBasicSystem and it will work on any platform and doesnt need the ReactiveSystems plugin.

This as mentioned above makes plugin development easier as you can at least have a cross platform system without needing other plugins or writing any of your own boilerplate code.

IGroup and Group are more in line

This isn't a massive update, but the gist is that IGroup and Group have not been on the same page for a while, as IGroup just exposed component lists to match on, whereas the Group implementation had that AND predicates built in.

In most cases predicates are the exception, and while they allow you some great flexibility it needs to be more separated, so there is now a GroupWithPredicate which is basically the same as the old Group class, and the new Group class just contains component lookups.

For the most part this wont effect most people, as the helpers/builders have been updated to automatically create the appropriate class for you, however if you had your own classes inherit off Group and you used predicates in them, you may need to now inherit from GroupWithPredicate.

ecsrx - Reverting to netstandard 2.0 and net 4.6

Published by grofit over 4 years ago

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++;

ecsrx - .Net Standard 2.1, .net 4.7.2 build targets + a small bug fix

Published by grofit over 4 years ago

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.

ecsrx - Some Fixes & Abstract OnUpdate

Published by grofit over 5 years ago

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.

ecsrx - v3.0.0 - Performance And Plugins

Published by grofit almost 6 years ago

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.

ecsrx - Lifecycle changes

Published by grofit about 6 years ago

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.

ecsrx - Another smaller update

Published by grofit about 6 years ago

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
ecsrx - Minor updates and DI changes

Published by grofit about 6 years ago

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).

ecsrx - Really just begrudgingly using semver

Published by grofit about 6 years ago

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!

ecsrx - More Performance Improvements

Published by grofit about 6 years ago

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).

ecsrx - For the power users

Published by grofit about 6 years ago

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.