10 - Collision Detection
Collision System
Quantum is ECS based, so collision events are not per entity based but instead global signals that systems can listen to. A common pattern for performance and convenience is to have a single system that receives all collision and then filters them by type and invokes other signals accordingly.
Before implementing the collision system, create a new AsteroidsAsteroid.qtn
and add the following code to it:
C#
component AsteroidsAsteroid
{
}
Add this empty tag component to the AsteroidsLarge
prefab.
Next, create a new c# script and name it AsteroidsCollisionSystem
.
Add the following code to it:
C#
using UnityEngine.Scripting;
namespace Quantum.Asteroids
{
[Preserve]
public unsafe class AsteroidsCollisionsSystem : SystemSignalsOnly, ISignalOnCollisionEnter2D
{
public void OnCollisionEnter2D(Frame f, CollisionInfo2D info)
{
// Projectile is colliding with something
if (f.Unsafe.TryGetPointer<AsteroidsProjectile>(info.Entity, out var projectile))
{
if (f.Unsafe.TryGetPointer<AsteroidsShip>(info.Other, out var ship))
{
// Projectile Hit Ship
}
else if (f.Unsafe.TryGetPointer<AsteroidsAsteroid>(info.Other, out var asteroid))
{
// projectile Hit Asteroid
}
}
// Ship is colliding with something
else if (f.Unsafe.TryGetPointer<AsteroidsShip>(info.Entity, out var ship))
{
if (f.Unsafe.TryGetPointer<AsteroidsAsteroid>(info.Other, out var asteroid))
{
// Asteroid Hit Ship
}
}
}
}
}
This code listens to the global collision signal and then filters it by entity type.
Next create a AsteroidsCollisionSignals.qtn
and add the following signals to it:
C#
signal OnCollisionProjectileHitShip(CollisionInfo2D info, AsteroidsProjectile* projectile, AsteroidsShip* ship);
signal OnCollisionProjectileHitAsteroid(CollisionInfo2D info, AsteroidsProjectile* projectile, AsteroidsAsteroid* asteroid);
signal OnCollisionAsteroidHitShip(CollisionInfo2D info, AsteroidsShip* ship, AsteroidsAsteroid* asteroid);
Projectiles colliding with Ships
The projectile based interactions will be implemented in the AsteroidsProjectileSystem
. Open the system and add the following code:
C#
public void OnCollisionProjectileHitShip(Frame f, CollisionInfo2D info, AsteroidsProjectile* projectile, AsteroidsShip* ship)
{
if (projectile->Owner == info.Other)
{
info.IgnoreCollision = true;
return;
}
f.Destroy(info.Entity);
}
The first part of the code ignores collisions when a projectile hits the own ship. The second part otherwise destroys the projectile. Additional logic could be added to destroy enemy ships on hit.
Projectiles colliding with Asteroids
When Projectiles collide with asteroids, the asteroid should be split into multiple smaller asteroids.
There is already a function for spawning asteroids in the AsteroidsWaveSpawnerSystem
that can be repurposed to also spawn child asteroids by turning it into a signal.
Add the following signal to the AsteroidsAsteroid.qtn
. Also add a ChildAsteroid
file to the component.
C#
component AsteroidsAsteroid
{
asset_ref<EntityPrototype> ChildAsteroid;
}
signal SpawnAsteroid(AssetRef<EntityPrototype> childPrototype, EntityRef parent);
The ChildAsteroid
field won't be modified at runtime, so technically it would be better fit in a config file. However, creating and linking a config file to a component that only contains a single field is overkill and only results in worse performance and increased code complexity.
Open the AsteroidsWaveSpawnerSystem
and have it inherit from ISignalSpawnAsteroid
.
Add EntityRef parent
parameter to the SpawnAsteroid
function so that implements the signal correctly. In the line that calls SpawnAsteroid()
in the SpawnAsteroidWave
function pass EntityRef.None
as the parent entity into the function.
Finally, replace the following line
C#
asteroidTransform->Position = GetRandomEdgePointOnCircle(f, config.AsteroidSpawnDistanceToCenter);
with:
C#
if (parent == EntityRef.None)
{
asteroidTransform->Position = GetRandomEdgePointOnCircle(f, config.AsteroidSpawnDistanceToCenter);
}
else
{
asteroidTransform->Position = f.Get<Transform2D>(parent).Position;
}
Now, when a parent entity is provided the asteroid will be spawned at the position of the parent instead of a random position.
Return to the AsteroidsProjectileSystem
and add the following function to it:
C#
public void OnCollisionProjectileHitAsteroid(Frame f, CollisionInfo2D info, AsteroidsProjectile* projectile, AsteroidsAsteroid* asteroid)
{
if (asteroid->ChildAsteroid != null)
{
f.Signals.SpawnAsteroid(asteroid->ChildAsteroid, info.Other);
f.Signals.SpawnAsteroid(asteroid->ChildAsteroid, info.Other);
}
f.Destroy(info.Entity);
f.Destroy(info.Other);
}
Finally, for the two signals in the AsteroidsProjectileSystem
to work it has to implement ISignalOnCollisionProjectileHitShip
and ISignalOnCollisionProjectileHitAsteroid
so add the two interfaces to it.
C#
public unsafe class AsteroidsProjectileSystem : SystemMainThreadFilter<AsteroidsProjectileSystem.Filter>, ISignalAsteroidsShipShoot, ISignalOnCollisionProjectileHitShip, ISignalOnCollisionProjectileHitAsteroid
Ships colliding with Asteroids
Open the AsteroidsShipSystem
and have it implement the ISignalOnCollisionAsteroidHitShip
interface.
Add the corresponding function with the following code to it:
C#
public void OnCollisionAsteroidHitShip(Frame f, CollisionInfo2D info, AsteroidsShip* ship, AsteroidsAsteroid* asteroid)
{
f.Destroy(info.Entity);
}
For now, when a ship gets hit by an asteroid it is simply destroyed. In a full game loop there would be additional logic here to respawn the ship, adjust the score and end the game once all ships are destroyed.
Return to the AsteroidsCollisionsSystem
and hook the signals up by replacing
C#
// Projectile Hit Ship
with
C#
f.Signals.OnCollisionProjectileHitShip(info, projectile, ship);
and,
C#
// Projectile Hit Asteroid
with
C#
f.Signals.OnCollisionProjectileHitAsteroid(info, projectile, asteroid);
and finally,
C#
// Asteroid Hit Ship
with
C#
f.Signals.OnCollisionAsteroidHitShip(info, ship, asteroid);
Return to Unity and add the AsteroidsCollisionSystem
to the AsteroidsSystemConfig
.
Child Asteroid Prefab
Drag the AsteroidLarge
prefab into the scene. Adjust the Circle Radius
to 0.5
in the EntityPrototype
. Also scale the child model down to (0.6, 0.6, 0.6)
. Rename it to AsteroidSmall
. Drag the AsteroidSmall
object back into the Resources
folder and select Prefab Variant
in the popup to create a smaller prefab variant for the asteroid. Then delete it from the scene.
Drag the AsteroidSmall VariantEntityPrototype
into the Child Asteroid
field of the AsteroidLarge
prefab's EntityPrototype
.
Enabling Collision Callbacks
Code-wise collisions are now fully setup. However, by default Quantum does not invoke collision callbacks between entities for performance reasons. They can be enabled by modifying the Callback Flags
on the PhysicsCollider2D
part of the EntityPrototype
.
Enable the OnDynamicCollisionEnter
callback flag on the AsteroidLarge
, AsteroidsProjectile
and AsteroidsShip
prefab. (there is no need to adjust the AsteroidSmall
as it is a prefab variant of AsteroidLarge
and thus is adjusted automatically).
Enter play mode and test the multiple collision scenarios. The following should occur:
- When a projectile hits an asteroid the asteroid is destroyed and split into two asteroids. The projectile is destroyed as well.
- When the ship collides with an asteroid the ship is destroyed.
- When shooting another ship the projectile is destroyed.
Fixing Wave Spawning
Currently, only the first wave of asteroids is being spawned. To fix this add the following function to the AsteroidWaveSpawnerSystem
:
C#
public void OnRemoved(Frame f, EntityRef entity, AsteroidsAsteroid* component)
{
if (f.ComponentCount<AsteroidsAsteroid>() < 1)
{
SpawnAsteroidWave(f);
}
}
And have it implement the ISignalOnComponentRemoved<AsteroidsAsteroid>
interface. This signal gets called whenever a component is removed from an entity (when an entity is destroyed all components are removed from it as well).
With that the core gameplay mechanics of asteroids are implemented.
![gameplay](/docs/img/quantum/v3/tutorials/asteroids/10-multiplayer-gameplay.gif)