Entity System in 2.0

I’m not exactly sure if spawning entities is thread safe or not. What I’m trying to do is spawn entities with async and Task.Run() but I keep getting errors during runtime. Any help would be appreciated!

public static async Task SpawnEntity()
{
    await Task.Run(() =>
    {
        Entity entity = new Entity();
        SceneSystem.SceneInstance.RootScene.Entities.Add(entity);
    });
}

Errors

[Game]: Error: Unexpected exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.Collections.Generic.HashSet1.Enumerator.MoveNext() at SiliconStudio.Xenko.Engine.Processors.TransformProcessor.Draw(RenderContext context) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\Processors\TransformProcessor.cs:line 132 at SiliconStudio.Xenko.Engine.EntityManager.Draw(RenderContext context) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\EntityManager.cs:line 191 at SiliconStudio.Xenko.Engine.SceneSystem.Draw(GameTime gameTime) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\SceneSystem.cs:line 197 at SiliconStudio.Xenko.Games.GameSystemCollection.Draw(GameTime gameTime) at SiliconStudio.Xenko.Games.GameBase.Draw(GameTime gameTime) at SiliconStudio.Xenko.Games.GameBase.DrawFrame() at SiliconStudio.Xenko.Games.GameBase.TickInternal() System.InvalidOperationException: Collection was modified; enumeration operation may not execute. at System.Collections.Generic.HashSet1.Enumerator.MoveNext()
at SiliconStudio.Xenko.Engine.Processors.TransformProcessor.Draw(RenderContext context) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\Processors\TransformProcessor.cs:line 132
at SiliconStudio.Xenko.Engine.EntityManager.Draw(RenderContext context) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\EntityManager.cs:line 191
at SiliconStudio.Xenko.Engine.SceneSystem.Draw(GameTime gameTime) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\SceneSystem.cs:line 197
at SiliconStudio.Xenko.Games.GameSystemCollection.Draw(GameTime gameTime)
at SiliconStudio.Xenko.Games.GameBase.Draw(GameTime gameTime)
at SiliconStudio.Xenko.Games.GameBase.DrawFrame()
at SiliconStudio.Xenko.Games.GameBase.TickInternal()

Unhandled Exception: System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.Collections.Generic.HashSet`1.Enumerator.MoveNext()
at SiliconStudio.Xenko.Engine.Processors.TransformProcessor.Draw(RenderContext context) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\Processors\TransformProcessor.cs:line 132
at SiliconStudio.Xenko.Engine.EntityManager.Draw(RenderContext context) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\EntityManager.cs:line 191
at SiliconStudio.Xenko.Engine.SceneSystem.Draw(GameTime gameTime) in C:\TeamCity\work\80a49494ba341a6a\sources\engine\SiliconStudio.Xenko.Engine\Engine\SceneSystem.cs:line 197
at SiliconStudio.Xenko.Games.GameSystemCollection.Draw(GameTime gameTime)
at SiliconStudio.Xenko.Games.GameBase.Draw(GameTime gameTime)
at SiliconStudio.Xenko.Games.GameBase.DrawFrame()
at SiliconStudio.Xenko.Games.GameBase.TickInternal()
at SiliconStudio.Xenko.Games.GameBase.Tick()
at SiliconStudio.Xenko.Games.GamePlatform.Tick()
at SiliconStudio.Xenko.Games.GamePlatform.OnRunCallback()
at SiliconStudio.Xenko.Games.GameWindowWinforms.<>c__DisplayClass19_0.b__0()
at SiliconStudio.Xenko.Games.WindowsMessageLoop.Run(Control form, RenderCallback renderCallback, Boolean useApplicationDoEvents)
at SiliconStudio.Xenko.Games.GameWindowWinforms.Run()
at SiliconStudio.Xenko.Games.GamePlatform.Run(GameContext gameContext)
at SiliconStudio.Xenko.Games.GameBase.Run(GameContext gameContext)
at WorldEngine.WorldEngineApp.Main(String[] args) in C:\Users\levig\OneDrive\Workstation\WorldEngine\WorldEngine\WorldEngine.Windows\WorldEngineApp.cs:line 11

I don’t remember it as thread-safe. Though, I, at some point made a multi-threaded system to some extent to spawn cubes, or spheres, or whatever I was wanting to see. :stuck_out_tongue: (like, a lot of them.)

Here is the code if that may be of interest to you, I think that it is quite straightforward. :slight_smile:

public class CubeSpawner : AsyncScript
{

    [DefaultValue(1000)]
    public long cubeNumberToSpawn = 1000;
    [DefaultValue(1000)]
    public int cubeHalfLength = 1000;
    private List<Entity> cubes;
    private Task<Entity>[] runningTasks;
    private Model model;
    public override async Task Execute()
    {
        cubes = new List<Entity>();
        runningTasks = new Task<Entity>[cubeNumberToSpawn];
        model = Content.Load<Model>("Sphere");
        for (int i = 0; i < cubeNumberToSpawn; i++)
        {
            runningTasks[i] = SpawnCube(i);
        }
        await Task.WhenAll(runningTasks);
        await AddEntitiesToScene();
        Log.ActivateLog(LogMessageType.Verbose, true);
    }
    public async Task<Entity> SpawnCube(int iteration)
    {
        Random random = new Random();
        Vector3 position = Vector3.Zero;
        position = new Vector3(random.Next(-cubeHalfLength, cubeHalfLength),
                    random.Next(-cubeHalfLength, cubeHalfLength), random.Next(-cubeHalfLength, cubeHalfLength));
        Entity entity = new Entity(position, "Entity Added by Script" + iteration) { new ModelComponent { Model = model } };
        await Task.Delay(iteration / 10);
        return entity;
    }
    public override void Cancel()
    {
        base.Cancel();
        foreach (Entity sceneEntity in SceneSystem.SceneInstance.Scene.Entities)
        {
            if (sceneEntity.Name.Contains("SpawnedSphere"))
            {
                SceneSystem.SceneInstance.Scene.Entities.Remove(sceneEntity);
            }
        }
    }
    private async Task AddEntitiesToScene()
    {
        foreach (Task<Entity> task in runningTasks)
        {
            SceneSystem.SceneInstance.Scene.Entities.Add(task.Result);
        }
    }
}

You can create entities concurrently, but you will have to add them to the scene collection from a single point. It will work in an async script.

2 Likes

Thank you! Very nice code :slight_smile: I ended up creating my own solution. Posted below. I do want to be able to use tasks. In a few cases it would be useful to use the wait function. I’m going to do some more experimenting to figure out if its possible.

public class SyncActionPool : AsyncScript
    {
        private static readonly object locker = new object();
        public static List<Action> queue = new List<Action>();
        public static void AddAction(Action action)
        {
            lock(locker)
                queue.Add(action);
        }
        public override async Task Execute()
        {
            while(Game.IsRunning)
            {
                if (queue.Count > 0)
                {
                    List<Action> executing = new List<Action>();
                    lock (locker)
                    {
                        executing.AddRange(queue.GetRange(0, queue.Count));
                        queue.RemoveRange(0, executing.Count);
                    }
                    foreach (Action t in executing)
                    {
                        t.Invoke();
                    }
                }
                await Script.NextFrame();
            }
        }
    }

Use case

public static async Task SpawnEntity()
{
	await Task.Run(() =>
	{
		Entity entity = new Entity();
		SyncActionPool.AddAction(new Action(() =>
		{
			SceneSystem.SceneInstance.RootScene.Entities.Add(entity);
		}));
	});
}
1 Like