Hi.
I have some ECS systems manipulating data of entities, but I would like to allow MonoBehaviour logic to also edit values of some specific entities (IComponentData values). Basically, systems would handle global systemic logic, whereas Monobehaviour would be used on a per-case basis (example: when the player is pressing a specific input, change a value on the Player entity).
More generally, I want to take advantage of ECS to store and manipulate my game data, without really using ECS in my scenes (still GameObjects with MonoBehaviours).
Changes from Monobehaviour must have priority over systems changes. So MonoBehaviour changes should never be ignored (because the systems modified the value at the “same time”).
I thought such hybrid approach would have some limitations (related to race conditions, structural changes etc). That it would require solutions like stacking main thread changes on a entity command buffer, that would then be played back once jobs have finished. Or things like that to avoid conflicts between MonoBehaviours and Systems concurrent changes.
But before going that far, I made some basic tests, with MonoBehaviour directly editing entity data, and it seems to work well (surprisingly).
What I tried :
- MySystem : SystemBase incrementing MyFloat value of all entities with MyComponentData and without StructuralChangeTestCompoentData
- MyMonoBehaviour1 : set MyFloat value of a specific entity to 0 every x seconds
- MyMonoBehaviour2 : add/remove StructuralChangeTestCompoentData of a specific entity every x seconds
After observing MyFloat values in the DOTS Hierarchy window and inspector, I’ve not seen any conflicts. Changes from my MonoBehaviour are properly applied.
So is it acceptable to edit entities directly on the main thread? What are the risks or limitations? Any feedback from more experienced ECS users would be welcomed!
Code Snippet
public struct MyComponentData : IComponentData
{
public float MyFloat;
}
public struct StructuralChangeTestComponentData : IComponentData
{
public float AnotherFloat;
}
public partial class MySystem : SystemBase
{
protected override void OnUpdate()
{
Entities
.WithAll<MyComponentData>().WithNone<StructuralChangeTestComponentData>()
.ForEach((ref MyComponentData componentData) =>
{
componentData.MyFloat++;
})
.WithBurst()
.ScheduleParallel();
}
}
// Method called in MonoBehaviour
private void EditValueTest(Entity entity)
{
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
MyComponentData componentData = entityManager.GetComponentData<MyComponentData>(entity);
componentData.MyFloat = 0f;
entityManager.SetComponentData(entity, componentData);
}
// Method called in MonoBehaviour
private void StructuralChangeTest(Entity entity)
{
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
if (entityManager.HasComponent<StructuralChangeTestComponentData>(entity)) entityManager.RemoveComponent<StructuralChangeTestComponentData>(entity);
else entityManager.AddComponent<StructuralChangeTestComponentData>(entity);
}
Note: I’m currently using Entities 0.51.1 (last available version on my current Unity version) but would probably upgrade to the latest version if needed.
It’s fine. MonoBehaviour logic runs on the main thread just like ECS System OnUpdate. It’s more or less logically equivalent to doing main thread updates to component data directly in a System’s OnUpdate method instead of in a job.
The problem would come if you have any long running jobs (that you don’t Complete right away) that also touch that data.
1 Like
EntityManager operations generally complete component dependencies (for safety when reading / writing a specific component type) or complete all the jobs tracked for the world (sync points, basically when structural changes are involved), so it’s generally safe to call these methods, besides when accessing singletons starting in 1.0.0-exp.8, where dependencies aren’t completed for efficiency because of how singletons are usually used. Main thread modification is of course mainly just a matter of losing out on possibly doing something else with that time with your work going on in a worker thread and possibly queueing up commands for later main thread playback through EntityCommandBuffer. Since MonoBehaviour callbacks run out of band with the main 3 system groups (initialization/simulation/presentation), it won’t be sync point madness like when you make structural changes within Entities systems. Focus on trying to write efficient code that does the least archetype changing possible.
1 Like
Yes that’s what I noticed just before reading your answer!
My system was indeed automatically completed when making changes to the entity.
I started implementing an ECB based solution (as mentioned in my initial post), but unfortunately even just reading the component data is triggering the read/write dependency chain.
So it means each time scene logic (MonoBehaviour) is gonna read entity data, it will force related systems to complete. Which will happen constantly, every frame. It means that all my systems/jobs would globally be completed instantly. Schedule early and late complete strategy won’t be possible.
Isn’t there any way to get data of a given entity (read only) without triggering dependency chain? 
I guess another solution would be to maintain two layers of data, entities for ECS side, and C# data for MonoBehaviours, with systems pushing entities data into C# data on completed.
Not sure what would be the best in terms of performance. With this new knowledge, I should probably do some more research and reading about hybrid ECS solutions.
Tried to find a way to read component data value without triggering a write dependency completion.
In my case I just want to retrieve the current component data, I don’t care if it’s gonna be overridden by a system/job in progress. And for cases where I want to write values from MonoBehaviour, I would use an entity command buffer system to apply all changes after jobs completion (thus overriding potential changes from jobs, but that’s what I want).
I found a solution with InternalCompilerInterface.GetComponentData, but there is a clear warning in the documentation:
Do not use the APIs contained in InternalCompilerInterface. They are only in the context of being called from generated code and are likely to change in the future.
I don’t really care if the API changes in the future, especially as we have source access to packages (the method is still available in v1.2.4 by the way). But I feel like I’m on a slope slope, with the risk of not totally knowing the ins and outs of using this internal method. Again, would be very interesting to know what experienced ECS devs think about this solution.
public float GetValue(Entity entity)
{
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.Unmanaged.EntityManager;
// Case 1: failure (triggers CompleteWriteDependency)
return entityManager.GetComponentData<MyComponentData>(entity).MyFloat;
// Case 2: failure (raising safety exception, conflict with scheduled job writing to the same component data)
return World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<MySystem>().GetComponentDataFromEntity<MyComponentData>(isReadOnly: true)[entity].MyFloat;
// Case 3: success! But is it really a good idea???
InternalCompilerInterface.GetComponentData(World.DefaultGameObjectInjectionWorld.EntityManager, character.Entity, ComponentType.ReadOnly<MyComponentData>().TypeIndex, out MyComponentData componentData);
return componentData.MyFloat;
}
// The code of InternalCompilerInterface.GetComponentData
public unsafe static T GetComponentData<T>(EntityManager manager, Entity entity, int typeIndex, out T originalComponent) where T : struct, IComponentData
{
EntityDataAccess* checkedEntityDataAccess = manager.GetCheckedEntityDataAccess();
EntityComponentStore* entityComponentStore = checkedEntityDataAccess->EntityComponentStore;
UnsafeUtility.CopyPtrToStructure<T>(entityComponentStore->GetComponentDataWithTypeRO(entity, typeIndex), out originalComponent);
return originalComponent;
}
What I did before is I have a system that queries for the data that I need and I set these values on the MonoBehaviour objects that need them. This implies that I keep track of these MB objects and they have the entity references. You can simply run a job that collects the data that you need in a NativeHashMap<Entity, YourComponent>. You have to complete this job, of course, so you can get the data. With this, you can just loop through the MB objects and get the data that was for them using the entity that you assigned to them.
1 Like
Thanks for your reply. Unfortunately, that’s assuming you already know what data should be exposed to MonoBehaviours. But in my case in don’t know. I want to allow MonoBehaviours to access potentially any entity value. So I would have to replicate all the entities data on the MonoBehaviour side, not sure if it would be worth it (compared to read/write dependencies being automatically completed when data requested on MonoBehaviour side).
Maybe I could have a system allowing MonoBehaviours to request values, the system would be completed at the end of each frame to process all the requests and send the data to requesters. But that would mean data access is deferred which is not ideal in my case.