Simple physics using Unity’s ECS – Part 2: Building the level

Building our level using Pure ECS

Here we are for the next part of our simple physics story. Be sure to read the previous part before starting with this one: Simple physics using Unity’s ECS – Part 1: Introduction.

In this part, we will introduce our first ECS components and systems, then build a sample level that we will use for testing.

PROJECT SETUP

Unity’s Entity-Component-System comes as a preview package which is not enabled by default since it is still under development. In order to use it, you have to take the following steps:

  • Open the Unity Editor (I am currently using the Unity 2018.2.12f1 64-bit version)
  • Create a new project
  • Open the package manager (Window > Package Manager)
  • Make sure to select the “ALL” tab to display preview packages
  • Add the Entities package to your project (I am currently using version 0.0.12-preview.21)
  • Go to the Player settings (Edit > Project Settings > Player)
  • Change the Scripting Runtime Version to .NET 4.x Equivalent
  • Restart Unity Editor

Our first components

In order to build our level, we will need 2 custom components:

  • The Position component will be used to store and modify the position of objects in our game scene
  • The Level component will store the size of our level so that we can prevent our player from going outside the level

We will also use the built-in MeshInstanceRenderer (from Unity.Rendering) and the built-in Position (from Unity.Transforms, which is slightly different than our Position component) component to display all our objects as simple cubes.

World Position

The Position component represents the position of an object in the scene. Since all our objects are on the same height, it will only contains a 2D float position:

using Unity.Entities;
using Unity.Mathematics;

public struct Position : IComponentData
{
    public float2 Coords; // Our 2D coordinates
}

Level

The Level component contains any useful information about our level. For now, it will thus only contain our level size. The size of our level, in number of grid cells, will be useful to prevent our player from going outside the level.

In all the following articles, we will assume that each cell of our grid level is 1-unit wide. We will also assume that the bottom-left cell of our grid is at world coordinates (0;0) so that we have a straight-forward conversion from integer grid coordinates to real world coordinates.

using Unity.Entities;
using Unity.Mathematics;

public struct Level : IComponentData
{
    public int2 Size; // The size of our level, in number of grid cells
}

Our first system

Our first system will be used to update the world transform used by the mesh renderer (represented by the built-in Unity.Transforms.Position component) from the position of the object in our game (represented by our Position component). We could have used the built-in Unity.Transforms.Position component to also represent the position of our game objects by just looking at the X and Z coordinates. However, I always prefer to separate game logical from rendering as much as possible, so that both are not tightly coupled.

This means that all objects in our game that will need to be rendered will require both a Position and a Unity.Transforms.Position components.

Bridging game logic with rendering

Our first system will be called PositionToWorldTransformSystem since, well, it converts our object’s logical position to a world transform for rendering.

This system will operate on all entities having both the Position and Unity.Transforms.Position components. It copies the X and Y coordinates of the Position component to the X and Z coordinates of the Unity.Transforms.Position component, respectively. Our logical Y coordinate is mapped to the Z coordinate, since Y is generally used as the “height” coordinate for rendering.

using Unity.Mathematics;
using Unity.Entities;

class PositionToWorldTransformSystem : ComponentSystem
{
    private ComponentGroup group;

    // Here we define the group 
    protected override void OnCreateManager()
    {
        // Get all entities with both Position and Transform
        group = GetComponentGroup(
            ComponentType.ReadOnly<Position>(), // Since we don't modify it, using ReadOnly is faster
            typeof(Unity.Transforms.Position) // This one is actually modified so we don't use read-only
        );
    }

    protected override void OnUpdate()
    {
        // Get components array
        var positions = group.GetComponentDataArray<Position>();
        var transforms = group.GetComponentDataArray<Unity.Transforms.Position>();

        // Iterate over components
        for (int i = 0; i < positions.Length; ++i)
        {
            // we need to take a copy since components are value-type
            Unity.Transforms.Position transform = transforms[i]; 

            // we modify the copy
            transform.Value.x = positions[i].Coords.x;
            transform.Value.y = 0f;
            transform.Value.z = positions[i].Coords.y;

            // then assign the modified component
            transforms[i] = transform; 
        }
    }
}

Note: You can now use the new ForEach syntax instead of chunk iteration which is simpler. However, this is only available since the version 25 of the preview package, while I used the version 21 for this tutorial. The same could also be achieved using Injection but was recently flagged as deprecated so I chose not to use it for this tutorial.

Building our level

For the sake of this tutorial, we will use a hard-coded level. Thus we will manually create all the objects that will be present in our level inside our code.

What should our level look like?

Our sample level will be a 13-by-9 grid. Each cell of our grid will contain a ground object. For now, the ground will be purely visual, but later on each ground cell could have a different friction, for instance to simulate ice. We will also add a solid bloc one-out-of-two cells.

This is the end result we are trying to achieve:

Our first ECS level

An ECS entry point

We need an entry point so that we can safely use the ECS entity manager to create entities and assign components. For this purpose, we will attach a script to a game object in our scene that will call a static initialization function after the scene has been loaded. This function will be responsible for creating our level.

This script will be called Bootstrap and will require to be attached to a game object also called “Bootstrap”. We will also use this script to specify which mesh and material we will be using for the solid blocs and ground:

using UnityEngine;
using Unity.Entities;
using Unity.Rendering;
using UnityEngine.Rendering;
using Unity.Mathematics;

public class Bootstrap : MonoBehaviour
{
    public Mesh BlocMesh;
    public Material BlocMaterial;

    public Mesh GroundMesh;
    public Material GroundMaterial;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    public static void AfterSceneLoad()
    {
        // NOTE: The object this script is attached to should be named &quot;Bootstrap&quot;
        Bootstrap bootstrap = GameObject.Find("Bootstrap").GetComponent();

        // Create or get EntityManager
        EntityManager manager = World.Active.GetOrCreateManager();

        /* 
        WE WILL PUT OUR LEVEL CREATION CODE HERE
        */
    }
}

Shared Mesh Instance Renderer

In order to make our objects visible, we need to attach a MeshInstanceRenderer component to their corresponding entities. For efficiency reasons, the MeshInstanceRenderer component is a shared component, meaning that the same data will be shared accross multiple entities. Thus, modifying this component once will actually impact all entities sharing this same component.

In our case, we will display all grounds identically as well as all solid blocs identically. Therefore we require two MeshInstanceRenderer components, one for the solid blocs and one for the ground:

        // Create the shared mesh renderer for solid blocs
        MeshInstanceRenderer blocRenderer = new MeshInstanceRenderer
        {
            mesh = bootstrap.BlocMesh,
            material = bootstrap.BlocMaterial,
            subMesh = 0,
            castShadows = ShadowCastingMode.On,
            receiveShadows = true
        };

        // Create the shared mesh renderer for ground
        MeshInstanceRenderer groundRenderer = new MeshInstanceRenderer
        {
            mesh = bootstrap.GroundMesh,
            material = bootstrap.GroundMaterial,
            subMesh = 0,
            castShadows = ShadowCastingMode.Off, // the ground does not cast any shadow
            receiveShadows = true
        };

Creating our first entities

Then, we need to create a new entity with a Level component attached to it in order to store the size of our level:

        // First, create the level entity
        int width = 13, height = 9;
        Entity level_entity = manager.CreateEntity(typeof(Level));
        manager.SetComponentData(level_entity, new Level { Size = new int2(width, height) });

Finally, we create entities and components for all the objects of our level.

Rather than adding all components to a newly created entity one-by-one, it is actually faster to specify first which component an entity will have. This is achieved by first defining an EntityArchetype that will list all the components an entity will have on creation. In order to be rendered, all our entities should have both a Position and a Unity.Transform.Position components, as well as a MeshInstanceRenderer component. Let’s define our archetypes for solics blocs and grounds:

        // Create archetype for blocs
        EntityArchetype bloc_archetype = manager.CreateArchetype(
            typeof(Position), 
            typeof(Unity.Transforms.Position), 
            typeof(MeshInstanceRenderer));

        // Create archetype for grounds
        EntityArchetype ground_archetype = manager.CreateArchetype(
            typeof(Position), 
            typeof(Unity.Transforms.Position), 
            typeof(MeshInstanceRenderer));

The entity can then be created with all these components already assigned so that we just require to modify their data if needed:

        // Fill our level
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                // We create our ground for each cell
                Entity ground_entity = manager.CreateEntity(ground_archetype);
                manager.SetComponentData(ground_entity, new Position { Coords = new float2(x, y) });
                manager.SetSharedComponentData(ground_entity, groundRenderer);

                // We create a solid bloc one-out-of-two cells
                if (x % 2 == 1 && y % 2 == 1)
                {
                    Entity bloc_entity = manager.CreateEntity(bloc_archetype);
                    manager.SetComponentData(bloc_entity, new Position { Coords = new float2(x, y) });
                    manager.SetSharedComponentData(bloc_entity, blocRenderer);
                }
            }
        }

Note: We don’t actually require to set the initial data of the Unity.Transform.Position component, since our PositionToWorldTransform system will be in charge of this.

Before running our scene, do not forget to actually fill the Material and Mesh fields of our Bootstrap script instance, both for the solid blocs and grounds. For the solid blocs, I used a cube mesh, and for the grounds a plane mesh. I didn’t use built-in unity meshes since their size and position didn’t fit my needs. You can find the meshes I used in the downloadable project file at the end of this article. This is what your Bootstrap script component should look like:

ECS Bootstrap script

You should also move and setup the camera manually to ensure our whole level is centered and visible. That’s it! Here is what you should see, after tweaking some material, light and shadow settings. Of course the choice of colors is up to you:

Our first ECS level

The source code

You can download the whole unity project below, including source code. This project will cover eveything until this part.

Click the DOWNLOAD button to download the whole Unity project until this point, including source code.

What’s next?

In the next part of this article series, we will see how to implement basic player movements. The next article is already available: Simple physics using Unity’s ECS – Part 3: Basic movement.

Related Posts

Comments (2)

[…] Part 2 – Building the level: First ECS components, system and our test level […]

Leave a comment