Simple physics using Unity’s ECS – Part 4: Collision detection and resolution

Unity ECS collision detection

In this part of our tutorial series, we will see how to perform collision detection and prevent colliding objects to move any further. Please be sure to read the previous parts this series:

Before going into the details, I insist on the fact that the physics system we will build here is not physically correct. We will not resolve collision impact or forces in a realistic way, since this is not the goal here. Instead, our aim is to obtain a simple solution focusing on achieving a good gameplay.

Now that this has been made clear, let’s proceed to the details.

Collisions detection

To identify colliders, we will first introduce a new component, the Collider component.

Collider component

The Collider component will also be responsible for storing the size of our object, since all colliders can have different sizes – but no greater than 1 unit:

using Unity.Entities;

public struct Collider : IComponentData
{
    public float Size;
}

We will add this component to our solid blocs and our player right-away. The solid blocs will all have a size of 1 unit, and I chose a size of 0.6 units for the player. This leads to the following modification of our Bootstrap script (see previous part):

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

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

        // 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.SetComponentData(bloc_entity, new Collider { Size = 1f });
                    manager.SetSharedComponentData(bloc_entity, blocRenderer);
                }
            }
        }

        // Create archetype for player
        EntityArchetype player_archetype = manager.CreateArchetype(
            typeof(PlayerInput),
            typeof(PlayerMovement),
            typeof(RigidBody),
            typeof(Position),
            typeof(Unity.Transforms.Position),
            typeof(MeshInstanceRenderer),
            typeof(Collider));

        // Create player entity
        Entity player_entity = manager.CreateEntity(player_archetype);
        manager.SetComponentData(player_entity, new Position { Coords = new float2(0f, 0f) });
        manager.SetComponentData(player_entity, new PlayerMovement { Speed = 4f });
        manager.SetComponentData(player_entity, new Collider { Size = 0.6f });
        manager.SetSharedComponentData(player_entity, playerRenderer);

One of the few restrictions we imposed on our game was that all the colliders of objects must have a square shape. Therefore, detecting collisions mostly means detecting intersection between squares.

Checking if objects overlap

Detecting intersections between non-rotated squares can be easily done by comparing their distance along the X and Y axis with the sum of their half-size. There are 3 different cases, illustrated in the following graphics:

Square intersection detection

We will add the following function to our physics system to check if two squares are intersecting:

    // Checks if the square at position posA and size sizeA overlaps 
    // with the square at position posB and size sizeB
    static bool AreSquaresOverlapping(float2 posA, float sizeA, float2 posB, float sizeB)
    {
        float d = (sizeA / 2) + (sizeB / 2);
        return (math.abs(posA.x - posB.x) < d || math.abs(posA.y - posB.y) < d);
    }

We will also require a function to check if an object is outside our level to prevent it from going outside of our map area.

Checking if objects are outside the level

Similarly to checking if two square objects are overlapping, this can be easily done by comparing the half-size of our object with its distance to the border of the level. We will add the following function to our physics system:

    static bool IsOutSideLevel(float2 pos, float size, int2 level_size)
    {
        float half_size = size / 2;

        return (pos.x - half_size < -0.5f
            || pos.y - half_size < -0.5f || pos.x + half_size > (float)level_size.x - 0.5f
            || pos.y + half_size > (float)level_size.y - 0.5f);
    }

Note: You migh wonder why we substract the value 0.5f to coordinates. This is because we assumed that the first tile of our grid was at coordinates (0,0). Therefore, the first tile extends from (-0.5; -0.5) to (0.5;0.5). The same applies to the other borders of our level. Please also note that we assumed each grid cell to be 1-unit wide.

We our now ready to check if an object is colliding another one, or colliding the border of our level.

Updating our physics system

Before moving an object, our physics sytem will now check if it would result in a colliding position. If so, it will also prevent the colliding object to move.

Component groups

To achieve this goal, the physics system will iterate over all moving objects, and see if they would overlap with any other collider or be outside the level area if moved. As a result, our PhysicsSystem will now require three component groups: that of the moving objects, that of all colliders, including moving objects, and that of the level, in order to retrieve its size:

private ComponentGroup moving_group;
    private ComponentGroup collider_group;
    private ComponentGroup level_group;

    // Here we define the group 
    protected override void OnCreateManager()
    {
        // Get all moving objects
        moving_group = GetComponentGroup(
            typeof(RigidBody),
            ComponentType.ReadOnly<Collider>(),
            typeof(Position)
        );

        // Get all colliders
        collider_group = GetComponentGroup(
            ComponentType.ReadOnly<Collider>(),
            typeof(Position)
        );

        // Get level
        level_group = GetComponentGroup(
            ComponentType.ReadOnly<Level>()
        );
    }

In the update function of our system, we first retrieve all the component arrays in order to easily access them:

    protected override void OnUpdate()
    {
        // Get elapsed time
        float dt = UnityEngine.Time.deltaTime;

        // Get components array
        var moving_positions = moving_group.GetComponentDataArray();
        var moving_colliders = moving_group.GetComponentDataArray();
        var moving_rigidbodies = moving_group.GetComponentDataArray();
        var moving_entities = collider_group.GetEntityArray();

        var colliders_positions = collider_group.GetComponentDataArray();
        var colliders = collider_group.GetComponentDataArray();
        var colliders_entities = collider_group.GetEntityArray();

        var levels = level_group.GetComponentDataArray();

Out of bounds

We then iterate over all moving objects, and for each one, we compute its next position according to the velocity of its rigid body just as before. However, this time we first check if the object is colliding before updating the Position component. Let’s start by checking if the moving objects are going outside the level:

        // Iterate over components
        for (int i = 0; i < moving_rigidbodies.Length; ++i)
        {
            // Retrieve components
            Position position = moving_positions[i];
            Collider collider = moving_colliders[i];
            RigidBody rigid_body = moving_rigidbodies[i];

            // Only update moving objects
            if (rigid_body.Velocity.x != 0f || rigid_body.Velocity.y != 0f)
            {
                // Apply velocity
                position.Coords += rigid_body.Velocity * dt;

                // Define a flag that will contain collision status
                bool collided = false;

                // Check first if we are outside the level
                collided = IsOutSideLevel(position.Coords, collider.Size, levels[0].Size);

                // We will put the rest of our code here
            }
        }

In the case the object is not going outside the level, we then check if it is colliding with another collider.

Collisions!

This can be easily done using our previous AreSquaresOverlapping function. We also stop checking intersection with other colliders as soon as we found one intersecting with the moving object. Doing so allows to save processor cycles in some cases.

                // No need to check collision with other objects if already outside the level
                if (!collided)
                {
                    // Iterate over all other colliders
                    for (int j = 0; j < colliders.Length; j++)
                    {
                        // Don't check an object against itself
                        if (moving_entities[i] != colliders_entities[j])
                        {
                            // Retrieve components
                            Position other_position = colliders_positions[j];
                            Collider other_collider = colliders[j];

                            // Check collision
                            collided = AreSquaresOverlapping(position.Coords, collider.Size, other_position.Coords, other_collider.Size);

                            // No need to check other objects
                            if (collided)
                                break;
                        }
                    }
                }

Note: Do not forget to ensure that you ar not checking your moving object with itself, since it is also in the collider group. This can be done by comparing the Entity value.

Resolution

It the object collided something, we have to prevent it from moving. For this, we do not update its Position component and instead reset the velocity of the RigidBody component to 0. Otherwise, the Position component is updated as usual:

                // A collision occured
                if (collided)
                {
                    // Set velocity to 0
                    rigid_body.Velocity = new float2(0f, 0f);
                    moving_rigidbodies[i] = rigid_body;

                    // And do not store updated position
                }
                else
                {
                    // Store update position
                    moving_positions[i] = position;
                }

That’s it! If you followed these instructions, this is what you should get:

We now have a working solution: collisions are correctly detected and prevent objects from going through each other. However, our solution is not ideal yet

One step at a time

Indeed,in video games, time is not continuous: it is generally simulated in a step-by-step fashion, often following the rate at which images are rendered on the screen. In modern games, the physics simulation rate is often decoupled from rendering but is still performed step-by-step. Actually, because of the way computers work, it would be impractical to do otherwise.

This might rise several issues, especially for fast moving objects:

  • We could miss some collisions because an object’s displacement might be greater than the size of another object it should be colliding with. This would result in the object going through another while it should have stopped.
  • We could prevent some objects from getting closer to each other. Indeed, an object with a high displacement could lead to an early collision detection, preventing the object from moving. We would expect the object to indeed stop, but to stop close to its collider, not miles away!
Collision detection missed
A collision was missed because of the high speed of the object
Early collision detection
The object did not move at all because a collision was detected

Fixing your step

There are several solutions to prevent these situations from occuring:

  • Make your objects bigger or move slower
  • Use raycasting or another continuous collision detection method
  • Iterate your physics system several times in smaller steps
  • Move your objects in smaller steps

Make your objects bigger or move slower

No way! This solution is clearly out of the question as it would mean trading on the gameplay at some point. We cannot accept this, so let’s move forward.

Continuous collision detection

The aim of continuous collision detection is to predict the impact point of an object. The simplest technique uses raycasting: a virtual ray is fired from the object along its moving direction and on a length equals to the displacement of the object for the current time step. If any collision is detected along the ray, the object is moved to that position, the time is advanced to the collision time (which can be predicted using the collision distance), and then the process is iterated.

This is a very simplified explanation, and you can find much more details on the internet. I found this video tutorial to be a very good starting point.

As our goal here is simplicity, we will not implement this technique.

Smaller steps iteration of the whole physics system

This is probably the most common method used. In this method, the physics is not updated once but is instead ran multiple times, in smaller steps. Thus, if we run our physics system N times at each step, will divide the displacement of each object by N. This reduces the risks of having objects with a high displacement. This is often combined with a limited maximum velocity for object, so that the optimal number of steps can be estimated.

While this approach is generally preferred, this is not the one I have chosen for this tutorial series. Indeed, this approach has the drawback of simulating several times even slowly moving objects, and requires to limit the speed of our objects. For a simple grid-based game I prefer the next one.

Move your objects in smaller steps

Hmm, that sounds familiar, doesn’t it? How is that different from the previous solution?

In the previous approach, the physics system is run several times in the same steps. This means that within a step, we perform several substeps, and within each substep, we update all objects.

Here this is kind of the opposite way: within a step, we update all objects, and each object is updated in several substeps. See the difference?

The advantage of this method is that it allows to choose a different number of substeps for each object. We can thus choose a high number of steps for fast objects, and a low number of steps for slow objects.

One major drawback is that objects are updated one by one, resulting in non-realistic collision resolution. Consider two facing objects fastly moving toward each other, with the same speed. With the previous approach, they would collide at the center of their original positions. With this approach, one object will be updated first, and will therefore collide at the original position of the second object, which will not move:

Multiple iterations of whole system
At each substep, all objects are updated
Several steps per object
Each object is updated in several substeps

However, for a simple game that doesn’t require realistic physics, this is more than acceptable and has been working perfectly fine for several of my games, and the previous approach can also be easily implemented.

Updated code

To compute the number of substeps for each moving object, we will divide the length of its displacement by the smallest resolution we wish to have. In this example we will use a step value of 0.05 units. This means that objects should no be smaller than 0.05 units, and that two colliding objects might in fact have a gap of 0.05 units between them after collision resolution (i.e. they will not be perfectly close to each other).

If our game is running at 100 frames-per-second, an object with a relatively low velocity of 10 units per second will be updated in 2 substeps ( (10 / 100) / 0.05 = 2) ; while an object with a high velocity of 200 units per second will be updated in 40 substeps ( (200 / 100) / 0.05 = 40).

Since rounding values could lead to a number of steps equal to 0, we always ensure that at least one substep is performed:

// Compute number of steps and stepwise displacement
float2 displacement = rigid_body.Velocity * dt;
int steps = math.max(1, (int)math.round(math.length(displacement) / 0.05f));
float2 move_step = displacement / steps;

All is left is to keep our old code, but run it in several steps instead of applying the displacement at once:

                // Update object position in substeps
                for (int s = 0; s < steps; s++)
                {
                    // Apply velocity (step-by-step)
                    position.Coords += move_step;

                    // Define a flag that will contain collision status
                    bool collided = false;

                    // Check first if we are outside the level
                    collided = IsOutSideLevel(position.Coords, collider.Size, levels[0].Size);

                    ...

We also now require to stop doing any more substep if a collision occurs:

                    // A collision occured
                    if (collided)
                    {
                        // Set velocity to 0
                        rigid_body.Velocity = new float2(0f, 0f);
                        moving_rigidbodies[i] = rigid_body;

                        // Do not perform any more substeps
                        break;
                    }
                    else
                    {
                        ...

You can now have fast moving objects (at the price of some more computation)!

Source code

As usual the whole Unity project, including all the source code, is freely available for download below:

Click the DOWNLOAD button to download the whole Unity project until this point.

What’s next?

If you played with this sample, you might have noticed that this is not perfect: moving the player arround is hard since it often collides with the corners of the solid blocs. In the next part, we will see how to improve that through sliding collisions!

If you want to be notified of the article publication, feel free to sign to our monthly newsletter:

Do you want to receive cool news about game development?

Subscribe to our newsletter by providing your email address:

* Personal data will be encrypted

Related Posts

Comments (5)

Hi
So in general physics engine doesn’t work with ECS? :/
I mean rigidbody is working but collisions not? This is bad.

Compare all this code to – OnCollisionEnter(other.gameObject).

Is there any way for some kind of hybrid version?
If we have thousands of entities then calculating distance against every other will kill any CPU.

No, that’s not what I mean by doing this series of articles.

The goal here is to show some simple example of gameplay implementation using the new DOTS of Unity.
In many games you will want to have custom physics instead of realistic physics, to give your game a more personal feeling.
Also, some player actions might not be easily achieved using classic physics engine.
That’s why you might require custom physics for gameplay and especially for player control (which you can combine with realistic physics for your world objects).

Now, regarding your question about physics integration.
One direct way of still using the current Unity Physics engine would be to have MonoBehaviour script on your object with OnCollisionEnter method for instance, and in that method create a new entity containing event data so that the ECS can process it.
Anyway, they are also working on a physics implementation for ECS, check this forum discussion for instance: https://forum.unity.com/threads/unity-physics-discussion.646486/

Finally, you don’t have to calculate distance against every other. Here, I chose a simple example to not complicate things too much with this kind of aspect.
You could still implement some octree search using ECS to improve performance in order to avoid checking all entities against each other.

Part 5 please? 😐

This is coming soon, I have been quite busy lately 🙂

Leave a comment