Queries
Introduction
Queries may take into account dynamic entities and static colliders. The API for rays, lines and shape overlaps is very similar in that is always results a collection of hits (with the same kind of data in their fields).
Queries
Linecast and Raycast
C#
// For 2D
var hits = f.Physics2D.LinecastAll(FPVector2.Zero, FPVector2.One);
for (int i = 0; i < hits.Count; i++) {
var hit = hits[i];
}
// For 3D
var hits = f.Physics3D.LinecastAll(FPVector3.Zero, FPVector3.One);
for (int i = 0; i < hits.Count; i++){
var hit = hits[i];
}
The resulting HitCollection object, contains the following properties:
- Each item in the HitCollection holds an EntityRef or Static collider info. They mutually exclusive - one will be valid, and the other null;
- Count should always be used to iterate over the HitCollection; and,
- Hits are not sorted. You can sort them by calling
Sort()
and passing in a FPVector2, this will result in the hits being sorted according to their distance to the reference point provided to the function.
Raycasts are syntax-sugar for Linecasts. They work the same and simply require a start, direction and max-distance instead of start and end. Additionally, you may pass these optional parameters to the linecast and raycast:
- LayerMask, to specify which physics layers to perform the cast against; and,
- QueryOptions, to specify the type of collider to consider in the cast.
Shape Queries
Quantum supports two different types of shape queries:
- ShapeOverlap; and,
- ShapeCasts (since v2.1).
These can be used with all dynamic shapes supported in Quantum.
Note: CompoundShapes
can be used for performing shape queries. For more information, please read the Shape Config page.
ShapeOverlaps
OverlapShape()
returns a HitCollection. The required parameters are:
- a center position (FPVector2 or FPVector3);
- a rotation (FP or FPQuaternion for the 3D equivalent); and,
- a shape (Shape2D or Shape3D - either from a PhysicsCollider, or created at the time of calling).
C#
// For 2D
var hits = f.Physics2D.OverlapShape(FPVector2.Zero, FP._0, Shape2D.CreateCircle(FP._1))
for (int i = 0; i < hits.Count; i++){
var hit = hits[i];
}
// For 3D
var hits = f.Physics3D.OverlapShape(FPVector3.Zero, FPQuaternion.Identity, Shape3D.CreateSphere(1));
for (int i = 0; i < hits.Count; i++){
var hit = hits[i];
}
ShapeCasts
ShapeCasts (2D & 3D) are available since Quantum 2.1 .
ShapeCastAll()
returns a HitCollection. The required parameters are:
- the center position ( FPVector2 or FPVector3);
- the rotation of the shape ( FP or FPQuaternion for the 3D equivalent);
- the shape pointer ( _Shape2D* _ or Shape3D* - either from a PhysicsCollider, or created at the time of calling); and,
- the distance and direction expressed as a vector ( FPVector2 or FPVector3 ).
C#
// For 2D
var shape = Shape2D.CreateCircle(FP._1);
var hits = f.Physics2D.ShapeCastAll(FPVector2.Zero, FP._0, &shape, FPVector2.One);
for (int i = 0; i < hits.Count; i++){
var hit = hits[i];
}
// For 3D
var shape = Shape3D.CreateSphere(1);
var hits = f.Physics3D.ShapeCastAll(FPVector3.Zero, FPQuaternion.Identity, &shape, FPVector3.One);
for (int i = 0; i < hits.Count; i++){
var hit = hits[i];
}
It uses a custom GJK-based algorithm. The GJKConfig settings are available in the SimulationConfig
asset's Physics > GJKConfig
section. The settings allow to balance accuracy and performance as both come with their trade-offs. The default values are balanced to compromise for regular sized shapes.
Simplex Min/Max Bit Shift
: Allows better precision for points in the Voronoy Simplex by progressively shifting their raw values, avoiding degenerate cases without compromising the valid range of positions in the Physics space. Consider increasing the values if the scale of the shapes involved and/or the distance between them is very small.Shape Cast Max Iterations
: The max number of iterations performed by the algorithm while searching for a solution below the hard tolerance. Increasing it might result in more accurate results, at the cost of performance in worst-case scenarios, and vice-versa.Shape Cast Hard Tolerance
: An iteration result (closest distance between the shapes) below this threshold is acceptable as a finishing condition. Decreasing it might result in more accurate results, at the cost of more iterations, and vice-versa.Shape Cast Soft Tolerance
: A shape cast resolution that fails to find an acceptable result below the defined Hard Tolerance within the Max Iterations allowed will still return positive if the best result found so far is below this soft threshold. In these cases, increasing this threshold enhances the probability of false-positives, while decreasing it enhances false-negatives.
Sorting Hits
All queries returning a HitCollection
can be sorted.
Sort()
: takes a FPVector2 in 2D and a FPVector3 in 3D and sorts the collection according to the hits' respective distance to the point provided.SortCastDistance()
: used for sorting the results ofShapeCast
query. It takes no arguments and orders the hits based on the cast distance.
Options
All queries, including their broadphase version, can use QueryOptions
to customize the operation and its results.
QueryOptions
create a mask that filters which types of objects are taken into account and what information will be computed. You can combined these by using the binary |
operator.
Hit Normals
To offer the most performant query, all default queries only check whether the two shapes are overlapping.
In order to receive additional information, more computation will be needed which in turn creates additional overhead; it is therefore necessary you explicitly specify it by passing ComputeDetailedInfo
as the QueryOptions parameter. This will enable the computation of the hit's:
- point
- normal
- penetration
For ray-triangle checks the normal is always the triangle's normal. Since this is cached in the triangle data, there is no additional computation in this case.
Filtering Hits
The following QueryOptions
allow you to define the mask used by the query. If an object does not match the QueryOptions
specified as the parameter, it will be skipped; only objects matching the QueryOptions
will be evaluated and returned in the result.
HitStatics : will only hit static colliders
HitKinematics : will hit entities who meet any of the following conditions:
- entities with a PhysicsCollider and no PhysicsBody
- entities with a PhysicsCollider and a disabled PhysicsBody
- entities with a PhysicsCollider and a kinematic PhysicsBody
HitDynamics : will only hit entities with an enabled and non-kinematic PhysicsBody
HitTriggers : has to be used in combination with other flags to hit trigger colliders.
HitAll : will hit all entities that have a PhysicsCollider
By default, a query will use the HitAll
option. Choosing any other option will save computation.
Broadphase Queries
Quantum comes with an option for injecting physics queries (ray-casts and overlaps) to be resolved during the physics systems. For this you need to:
- Create a system.
- Insert it before
Core.PhysicsSystem
when adding it toSystemSetup.cs
. - Retrieve the information in any system running after
Core.PhysicsSystem
.
This setup benefits from the parallel resolution on physics steps which makes it significantly faster than normal querying after physics.
C#
public static class SystemSetup {
public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig) {
return new SystemBase[] {
// pre-defined core systems
new Core.CullingSystem2D(),
new Core.CullingSystem3D(),
// Placing systems scheduling Broadphase queries here
// allows them to benefit from the CullingSystems on predicted frames.
new ProjectileHitQueryInjectionSystem(),
new Core.PhysicsSystem2D(),
new Core.PhysicsSystem3D(),
new Core.NavigationSystem(),
new Core.EntityPrototypeSystem(),
// user systems go here
// This is also where systems retrieving the results of broadphase queries go
new ProjectileHitRetrievalSystem(),
};
}
}
Note: Sometimes broadphase queries are also referred to as injected queries or scheduled queries, because they are scheduled/injected into the physics engine before the solver runs.
Injecting Queries
You can inject a query from any main thread system running before physics. The injected query will return a 0-based index, using this same index you will be able to retrieve the results after the physics system ran. The query's index is meant to be generated and consumed within the frame, therefore it can be stored anywhere - including outside the rollback-able frame data.
C#
namespace Quantum
{
public unsafe struct ProjectileFilter
{
public EntityRef EntityRef;
public Transform3D* Transform;
public Projectile* Component;
}
public unsafe class ProjectileHitQueryInjectionSystem : SystemMainThread
{
public override void Update(Frame f)
{
var projectileFilter = f.Unsafe.FilterStruct<ProjectileFilter>();
var projectile = default(ProjectileFilter);
while (projectileFilter.Next(&projectile))
{
projectile.Component->PathQueryIndex = f.Physics3D.AddRaycastQuery(
projectile.Transform->Position,
projectile.Transform->Forward,
projectile.Component->Speed * f.DeltaTime);
var spec = f.FindAsset<WeaponSpec>(projectile.Component->WeaponSpec.Id);
projectile.Component->DamageZoneQueryIndex = f.Physics3D.AddOverlapShapeQuery(
projectile.Transform->Position,
projectile.Transform->Rotation,
spec.AttackShape.CreateShape(f),
spec.AttackLayers);
}
}
}
}
IMPORTANT: The query indices returned by AddXXXQuery
are absolutely necessary to retrieve the results of the queries later on. It is thus advisable to save in a component attached to the entity who will need to process the hits down the line.
Retrieving Query Results
The query results can be retrieved from any system that runs after the physics. To retrieve the results (HitCollection*) you need the pass the index previous saved into Frame.Physics.GetQueryHits()
.
C#
using Photon.Deterministic;
namespace Quantum
{
public unsafe class ProjectileHitRetrievalSystem : SystemMainThread
{
public override void Update(Frame f)
{
var projectileFilter = f.Unsafe.FilterStruct<ProjectileFilter>();
var projectile = default(ProjectileFilter);
while (projectileFilter.Next(&projectile))
{
var hitsOnTrajectory = f.Physics3D.GetQueryHits(projectile.Component->PathQueryIndex);
if (hitsOnTrajectory.Count <= FP._0)
{
projectile.Transform->Position =
projectile.Transform->Rotation *
projectile.Transform->Forward *
projectile.Component->Speed * f.DeltaTime;
continue;
}
var damageZoneHits = f.Physics3D.GetQueryHits(projectile.Component->DamageZoneQueryIndex);
for (int i = 0; i < damageZoneHits.Count; i++)
{
// Apply damage logic
}
}
}
}
}
In addition to that, you can grab all broadphase results via the public bool GetAllQueriesHits(out HitCollection* queriesHits, out int queriesCount)
call which is also available via Frame.Physics
.
Note
A few important points to keep in mind when using broadphase queries:
- The performance is around 20x better for large numbers (e.g. projectiles).
- They are based on the frame state before the Physics system kicks in.
- Broadphase queries do not carry over between frames; i.e. they need to be injected at the start of a frame before the Physics. A broadphase query injected after the Physics has run will never return a result. This is because Quantum's Physics are stateless.
Emulating CCD
Quantum's physics engine is stateless. Continuous collision detection would be excessively expensive in such a system. The solutions to emulate the CCD behaviour in stateless physics engines normally involve raycasts or shape overlaps that extend to the expected movement for one frame.
This topic usually comes up in combination with fast moving entities such as projectiles. Depending on the size of your fast moving object, we recommend using one of the following approaches:
- a short ray in the movement direction with length being
velocity * deltaTime
; or, - a single overlap; note that a shape overlap can also be done with compound shapes.
Either of these solutions would replicate a 100% accurate CCD and result in a much better performance overall. To further improve performance, you could combine this with broadphase queries.
Back to top