David Nichol
ECS Fractal Code Examples
Back to Works
What you see above is the generation of 100,000 cubes in Unity. Had those been GameObjects, it wouldn't have been possible to smoothly render. The graphics buffer would need to deal with 100,000 mesh renderers, with 100,000 meshes, 100,000 transforms etc. in an order that is dependent of one another. But with ECS pipeline, these flyweight entities are computed and rendered in an order that is convinient for the machine. This results in vastly higher performance!
This is all the code you need to generate ECS entities. The process goes like this:
  1. Instantiate an Entity Manager.
  2. Instantiate and define an Entity Archetype and fill it with components.
  3. Create a Native Array (always use native arrays when multithreading in Unity).
  4. Instantiate your Entities.
  5. Add components to those Entities using SetSharedComponentData().
  6. Modify members of those components to your liking.
  7. Dispose of any Native Arrays you created.
Multithreading with Jobs
Unity's Job system is simple. At it's core, you have a struct that inherits from with an execute method. Any logic you want to be multithreaded goes in that method. Then, .Schedule() the struct and specify arrayLength and innerloopBatchCount.
  1. Declare a struct that inherits from IJobParallelFor with public void Execute(int index)
  2. Fill that method with logic that will be computed asynch.
  3. Instantiate that struct and any members it has.
  4. Schedule the Job.
  5. Don't forget to use NativeArrays. If you do, dispose of them when you're done.
Creating Fractals with ECS and Jobs
To start, we define a series of variables that our entities are going to use to define themselves. To define a fractal, we need to define a number of 'parents' and a number of 'child' entities per 'parent.' I put parent and child in quotes because in Unity we refer to an owning object as a parent, but there is no objecet hierarchy in ECS. In this context, every nth entity we mark as a parent, where n = number of children per generation. and we build the child entity's orientation around that of it's parents'. Once we have determined a target number of generations and To visualize this, see below.
Fractal Tree Generator by Andrew Herman ashmystic.com
Here is a fractal tree with 5 branches (children) per generation (parent). Num Entities = 5^2 = 25.
Fractal Tree Generator by Andrew Herman ashmystic.com
Here is a fractal tree with 7 branches (children) per generation (parent) at 5 generations. Num Entities = 5^7 = 78125. That is the number of entities we will be creating for this demonstration. Without ECS and Jobs, the GPU would surely have a meltdown!
To start setting up our fractal, we declare a total amount of enteties, which is equal to the number of generations and children per generation. Then we set up a native array to store our enteties. We instantiate our EntityManager and EntityArchetype to create components that will modify the behaviors of the entity. Then we set the materials, set a starting location, and call our method that will spawn entities. The breakdown for that is shown below.
We start by setting a default scale for our entities, then use the standard entityManager.CreateEntity() method to instantiate an entity. The overload that I used here takes in an EntityArchetype and NativeArray of type Entity. There is an overload that takes just an EntityArchetype and one that takes none. Again, EntityArchetype refers to what I would describe as a container of behavior modifications ("Components") that make up each entity. I define those "Components" later in the code. Because I passed in a nativearray, I only need to call CreateEntity once because it will repeat until the array is at it's end. next, I instantiate a variable I use for indexing child orientation directions.
Next I create an entity to use as a reference and set it to the existing entity in the array that was created by CreateEntity(). Below that I determine the child scale and a "parent" variable I use to get the previous entity's properites. What I did was create a parent every 5th index at the start of each 'generation.' Below that i set a scale reference that is equal to the parent's scale * the child scale, which in this case is 0.5. Lastly, I call InterpolateColor(), shown below.
What this method does is iterate a depth value which is used to modify the color of the material in the method shown below:
This is where we have a nice color transition from white, to yellow, to cyan, to red and then magenta in a random order. This randomness is just an interesting way for the fractals to be unique and have some variation. we set our lerp t value to increase as maxDepth increases, and then square it so we get a nice "ease in ease out" parabolic wave of color.
Next, we start by setting up our components for our entities. The first SetComponentData() method sets data for a custom class I created called FractalComponent, which you'll see is used for rotation later. We declare our required LocalToWorld value and set it to loc, which contains the location that we want our fractals to start spawning. This next part is where things get interesting. We fill a variable called 'newLoc' which is being set to the parent's position.
Then we add to that based on depth and a variable called offset, which slowly increases. Thus, the position of the child fractals increase more and more outward per generation. This is where the heart of the fractal algorithm lies. Just by modifying newLoc slightly, you can get wildly different fractals, which you'll see below.
After that, we set the rotation, scale, and mesh components, and OffsetEntitySpawn is another optional variation which makes the fractal stretch backward on the z axis by X units. Then we iterate direction so the next fractal spawns in a different orientation. Next, we need to rotate these entities. We don't need to, but why not squeeze every bit of performance we can? Let's multithread that with Jobs.
Enter: asynchronous rotation. It's actually more simple than you would expect. This implementation is different from the example I showed above however. It uses IJobForEach, which takes in a number of components. Specifically Rotation, Fractal Component (custom class), and LocalToWorld. Within our required Execute() method, we change the value of the reference to rotation's component.
We get that by multiplying the current normalized rotation by FractalComponent.radiansPerSecond * deltaTime, which is set in FractalECS.cs on line 133. Vector3.right means the entity rotates on the X axis. After that, we setup our required protected override JobHandle OnUpdate() method. There we set up our FractalJobSystem that contains our rotation logic that we wrote above, and gets our deltaTime value.
The reason why we have to obtain this in OnUpdate when we declare our Job is simply because anything that lives inside that FractalJobSystem struct will be computed asynchronously, which would make Time.deltaTime innacurate due to the logic not needing a specific execution order. I made a demonstration of this below.
That completes this walkthrough of ECS! Fractals are so fascinating, and with ECS and Jobs it's possible to visualize them in 3D! I think that's amazing. It's very fun to experiment with different meshes, colors and positions. Thank you for making it this far and checking out my work. If you have the time, please check out my other works. Also, feel free to reach out to me at any time if you have any questions about this project! I'll leave you with some examples of fractals I have created with my code.
The three arches