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:
- Instantiate an Entity Manager.
- Instantiate and define an Entity Archetype and fill it with components.
- Create a Native Array (always use native arrays when multithreading in Unity).
- Instantiate your Entities.
- Add components to those Entities using SetSharedComponentData().
- Modify members of those components to your liking.
- Dispose of any Native Arrays you created.
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.
Here is a fractal tree with 5 branches (children) per generation (parent). Num Entities = 5^2 = 25.
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
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
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.
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