
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:
- Part 1 – Introduction: Information about what we will be building and what we will not.
- Part 2 – Building the level: First ECS components, system and our test level
- Part 3 – Basic movement: Moving the player arround
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:
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!
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:
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:
[…] Collision detection: this is covered in Part 4 […]
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 🙂