Skip to content

Instantly share code, notes, and snippets.

@nicloay
Created May 4, 2023 09:57
Show Gist options
  • Save nicloay/d66f19306ea1458256735d1af5f79c53 to your computer and use it in GitHub Desktop.
Save nicloay/d66f19306ea1458256735d1af5f79c53 to your computer and use it in GitHub Desktop.
Waiting ecs singleton in the monobehaviour component.

When using ECS singletons in a MonoBehaviour, it's important to wait until the World is ready before accessing the component data through World.EntityManager. Attempting to access the data before the World is ready can result in errors or unexpected behavior.

In the editor, this may still work if the entity scene is loaded but in the build it's completely not a reliable approach.

// Dangerous to use approach
public static T GetSingleton<T>() where T : unmanaged, IComponentData
{
    var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    var query = entityManager.CreateEntityQuery(ComponentType.ReadOnly<T>());
    var entity = query.GetSingletonEntity();
    return entityManager.GetComponentData<T>(entity);
}

One way to solve this problem is to use a dependency injection (DI) framework such as VContainer and a task management library like UniTask. Here's how it works:

  1. Define the singleton component as usual.
  2. Create a specific system that waits for the singleton component to become available.
  3. Once the singleton component is ready, the system completes a task with the component data.
  4. The consumer class waits for the task to complete and retrieves the component result.

p.s. let me know if there are any mistakes in the code or there is a better approach with the single scene.

code is distributed under WTFPL license

// Finally, we need to connect all of this together with VContainer.
// We create and inject the taskCompletionSource and respective task.
// We register our system.
// Finally, we register our consumer component in the scene.
public class GamePlayScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
var singletonDataSource = new UniTaskCompletionSource<SingletonData>();
builder.RegisterInstance(singletonDataSource);
builder.RegisterInstance(singletonDataSource.Task);
builder.RegisterSystemFromDefaultWorld<SingletonsReadySystem>();
builder.RegisterComponentInHierarchy<MonoBehaviourExample>();
}
}
// First, we inject the UniTask with our SingletonData - be careful that the type is UniTask, not the UniTaskCompletionSource as in the ECS system.
// Then, we simply wait for the task to complete.
public class MonoBehaviourExample : MonoBehaviour
{
[UsedImplicitly] [Inject] private UniTask<SingletonData> _singletonDataTask;
private async void Start()
{
var data = await _singletonDataTask;
Debug.Log("entity = "+data.IntExample);
}
}
// Define any values that need to be accessible in both ECS and MonoBehaviour here.
// Also create the Authoring component as usual.
public struct SingletonData : IComponentData
{
public int IntExample;
...
}
// This system receives a TaskCompletionSource through DI.
// The Create() method ensures that the Update() method is only called when the LevelSpawnerData is loaded.
// In the Update() method, we set the result and deactivate the system since it's no longer needed.
public partial class SingletonsReadySystem : SystemBase
{
private UniTaskCompletionSource<SingletonData> _singletonDataSource;
[Inject]
private void Inject(UniTaskCompletionSource<SingletonData> singletonDataSource)
{
_singletonDataSource = singletonDataSource;
}
protected override void OnCreate()
{
RequireForUpdate<SingletonData>();
}
protected override void OnUpdate()
{
_levelSpawnerData.TrySetResult(SystemAPI.GetSingleton<SingletonData>());
Enabled = false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment