This document is about: QUANTUM 1
SWITCH TO

Physics

Introduction

The quantum physics engine is cross-platform deterministic and has full transparent support for Quantums predict rollback model. This document covers all features currently available including API functions, settings and performance characteristics.

This page covers both 2D, 3D and 2.5D Physics documentation.

Important: the 2D and the 3D API are very similar. On this document, you will always see the explanation following a pattern: first the 2D API is explained, then the 3D equivalent comes.

Static Scene Colliders

Adding static colliders to a Scene is straightforward. Just add one of the Quantum Static Collider Scripts to Unity GameObjects and edit its properties to resemble the geometry which you want to be a static obstacle on the scene.

The 2D Physics shapes are: Circle, Box and Polygon. Each of these has a Height field, which is how you create 2.5D shapes. This matter will be explored on the 2.5D Section.

The 3D Physics shapes are: Sphere, Box and Mesh.

You can also attach a specific physics material and a user asset (to be passed on collision callbacks). These two features will be discussed at other section of this document: PhysicsMaterial Data-Asset and Collision Callbacks.

Adding Static Colliders Directly to the Unity Scene

Quantum has a data asset type to represent the Unity Scene in a deterministic format, which is linked to the scene via the MapData Unity script. To export static colliders (and also NavMesh and other things) to this asset, just press the bake map button on the Unity Inspector (or call the corresponding function directly from your custom build process or tool).

Baking the Map Saves the Scene Colliders as a Quantum Asset (Map)

To assign the playable area, fill the desired values for cell size and grid size directly in the Map asset settings. This also doubles as the broadphase collision checks data structure.

Adjust the Grid Size for your Playable Area

The DynamicBody Component

Adding the DynamicBody ECS component to an entity type definition in a Quantum DSL file makes this entity to be taken into account by the physics engine. Notice that the use of DynamicBody implies that Transform2D must also be part of the entity type.

C#

entity Ship[16] {
  use Transform2D;
  use DynamicBody;
}

The same rule is applied to the 3D Physics:

C#

entity Ship[16] {
  use Transform3D;
  use DynamicBody3D;
}

Every entity instance that contains a DynamicBody must be initialized as either dynamic or kinematic after creation. Both take a Quantum.Core.DynamicShape (Circle, Box or Polygon structs), while initializing as a dynamic also requires the mass to be specified:

// instantiating a ship entity from the Frame object
var ship = f.CreateShip();
// initializing it's DynamicBody with shape and mass
ship->DynamicBody.InitDynamic(Core.DynamicShape.CreateCircle(FP_1), FP_1);
// ship->DynamicBody.InitKinematic(Core.DynamicShape.CreateCircle(FP_1));

The 3D DynamicBody supports only Box and Spheres.

var ship = f.CreateShip();
ship->DynamicBody3D.InitDynamic(Core.DynamicShape3D.CreateSphere(FP_1), FP_1);

DynamicShapeConfigs

There's a more convenient, data-driven option to initialize bodies based on the DynamicShapeConfigs types. These structs can be added as a property to any Quantum data-asset, editable from Unity (for shape, size, etc) and used to initialize the DynamicBody with it instead creating a shape directly:

C#

// data asset containing a shape config property
partial class ShipSpec {
  // this will be edited from Unity
  public DynamicShapeConfig Shape2D;
  public DynamicShape3DConfig Shape3D;
  public FP Mass;
}

Now, when initializing the body, we use the shape config instead of the shape directly:

// instantiating a ship entity from the Frame object
var ship = f.CreateShip();
var shipSpec = DB.FindAsset<ShipSpec>("millenium_falcon");
ship->DynamicBody.InitDynamic(shipSpec.Shape2D, shipSpec.Mass);

// or the 3D equivalent:
ship->DynamicBody3D.InitDynamic(shipSpec.Shape3D, shipSpec.Mass);

DynamicBody API

With the pointer to an entity instance, it's possible to add forces, impulses, or even modify the velocity directly.

The 2D Physics API

C#

// considering the ship instance from the examples above, adding a force (optionally pass a relativeTo point)
ship->DynamicBody.AddForce(FPVector2.Up * 10);

// using the relative to point on an impulse (applies to the relative point, so adding torque):
ship->DynamicBody.AddForce(FPVector2.Up * 10, FPVector2.Right);

// applying impulse (not affected by delta time), also possible with relativeTo point:
ship->DynamicBody.AddLinearImpulse(FPVector2.Up * 10);

// doesn't support relativeTo point
ship->DynamicBody.AddAngularImpulse(FPVector2.Up * 10);

// doesn't support relativeTo point
ship->DynamicBody.AddTorque(FP._10);

The 3D Physics API

C#

ship->DynamicBody3D.AddForce(FPVector3.Up * 10);

ship->DynamicBody3D.AddForce(FPVector3.Up * 10, FPVector3.Right);

ship->DynamicBody3D.AddLinearImpulse(FPVector3.Up * 10);

ship->DynamicBody3D.AddAngularImpulse(FPVector3.Up * 10);

ship->DynamicBody3D.AddTorque(FPVector3.Up * 10);

Other things possible to use directly from the DynamicBody API:

  • Enable/Disable body;
  • Set as trigger;
  • Change physics layer;
  • WakeUp body (in case resting optimization is used);
  • Switch PhysicsMaterial data-asset instance (next section);

Kinematic Character Controllers (KCC)

Since SDK version 1.2.4 B1, the KCC was added. It is a very nice option for moving characters based on the physics objects (both static and dynamic) and it is very nice for both prototyping and production grade code. Read more on the KCC Documentation.

PhysicsMaterial Data-Asset

Every DynamicBody needs to reference a PhysicsMaterial (a quantum data-asset), which holds properties that are used by the physics engine to resolve collisions and also integration of forces and velocities (like restitution, friction, drag, etc).

If no specific PhysicsMaterial asset is used, then the default physics material will be assigned (linked from Physics settings in the SimulationConfig data-asset).

Adjusting Properties to Physics Materials

A PhysicsMaterial asset can be assigned to a DynamicBody directly:

C#

// considering a Ship entity instance from the examples above:
var material = DB.FindAsset<PhysicsMaterial>("steel");
ship->DynamicBody.PhysicsMaterial = material;

// the 3D equivalent:
ship->DynamicBody3D.PhysicsMaterial = material;

One important aspect of a PhysicsMaterial is that changing its properties values from the simulation (or from Unity) in runtime leads to non-determinism. The reason is that these properties are not part of the rollbackable game state, but rather part of the quantum asset database.

This follows the same rule as of other data-assets (mentioned on the Data Assets chapter of this manual), so the following rules apply to both 2D and 3D Physics:

C#

// this is NOT safe to do, as it is not rolled-back:
ship->DynamicBody.PhysicsMaterial.Drag = FP._0;

// switching a reference is fine and safe
var newMaterial = DB.FindAsset<PhysicsMaterial>("ice");
ship->DynamicBody.PhysicsMaterial = material;

Exceptions to this rule are: lockstep mode (because it never performs rollbacks) or modifying DB only on verified frames (when combined with the ExposeVerifiedStatus configuration option).

However, even in these conditions. changing DB properties in runtime is considered (very) bad practice and should be avoided whenever possible. Switching materials is a better alternative in most situations.

Collision Callbacks

Collision (and trigger) callbacks in Quantum are handled through the signals feature. The basic ISignalCollisionDynamic and ISignalCollisionStatic interfaces.

C#

namespace Quantum {
  public interface ISignalOnCollisionDynamic {
    Int32 RuntimeIndex { get; }
    void OnCollisionDynamic(Frame f, DynamicCollisionInfo info);
  }

  public interface ISignalOnCollisionStatic {
    Int32 RuntimeIndex { get; }
    void OnCollisionStatic(Frame f, StaticCollisionInfo info);
  }

  public interface ISignalOnTriggerDynamic {
    Int32 RuntimeIndex { get; }
    void OnTriggerDynamic(Frame f, DynamicCollisionInfo info);
  }

  public interface ISignalOnTriggerStatic {
    Int32 RuntimeIndex { get; }
    void OnTriggerStatic(Frame f, StaticCollisionInfo info);
  }

By implementing them in a system, the corresponding callbacks will be executed whenever a collision or trigger will be found during a frame:

C#

public class MySystem : SystemBase, ISignalOnCollisionDynamic {

  void OnCollisionDynamic(Frame f, DynamicCollisionInfo info) {
    // react to the collision here
    // info.EntityA and info.EntityB are the two entities colliding with each other
  }
}

The info field also contains information for both 2D and 3D contact Points and Normals, depending on the type of physics being used.

A dynamic collision (or trigger) happens between two entites (of any defined type in the DSL) containing a DynamicBody (either 2D or 3D) component attached to them. The callback is guaranteed to pass the same entity order (entities A and B on the info parameter) on all client machines to maintain determinism.

Notice static collision callbacks are not raised by default. Check the corresponding configuration option under Physics in the SimulationConfig data asset to enable them (check last section for more details on settings).

Type Safe Collision Callbacks

Besides the generic Entity-Entity and Entity-Static collision callbacks, Quantum offers the possibility to define type-safe (for Entity types) callbacks for the desired collision combinations. The example below defines three type-safe collision callbacks to be code-generated (between two Ship entities, between a Ship and a Projectile, and between a Ship and a Static):

C#

signal collision (Ship* shipA, Ship* shipB);
signal collision (Ship* ship, Projectile* projectile);
signal collision static (Ship* ship);

Important: regardless of the order in which you declare the parameters (entity types), Quantum will re-sort them alphabetically, so the Ship-Projectile example above will actually be generated with this signature (notice the Frame object is added just like any other signal):

C#

void ISignalOnCollisionDynamicProjectileShip(Frame f, Projectile* projectile, Ship* ship, DynamicCollisionInfo info) {
}

Type safe trigger callbacks are part of the collision signal interface, so there's no need to specify them in separate.

Collision Info Data Types

Another important aspect to collision callbacks is having useful data to process. In Quantum, there are two types of data depending on the type of collision being processed: DynamicCollisionInfo and StaticCollisionInfo.

For dynamic collisions between two entities you always receive the pointers to them (type safe optionally as described in the previous section), and Normal/Point/Penetration data (these are not computed for triggers).

Important: it is possible to set a collision to be completely ignored by the physics engine from the callback. Just set info.IgnoreCollision = true;.

Static collisions don't have a second entity (as they mean an entity collided with a static collider from the scene), but it is still possible to have custom data being passed to the collision by dragging any Quantum data-asset directly to the scene static collider script on Unity.

Raycast, Linecast and Shape Overlaps

Raycasting takes into account dynamic, kinematic and static colliders. The API is very similar for rays, lines and also shape overlaps, with the expected common-sense different parameters. The result is always an instance of DynamicHits (or the equivalent DynamicHits3D), and because these are pooled/reusable objects, it's mandatory to restrict scoping with the using keyword:

C#

using (var hits = f.Scene.Linecast(FPVector2.Zero, FPVector2.Up * 10)) {
  for (int i = 0; i < hits.Count; i++) {
    var hit = hits[i];
  }
}

// The 3D equivalent:
using (var hits = f.Scene3D.Linecast(FPVector3.Zero, FPVector3.One)){
  for (int i = 0; i < hits.Count; i++){
    var hit = hits[i];
  }
}

The resulting DynamicHits object, has the following properties:

  • Each DynamicHit individual item holds an Entity pointer (not type safe) or Static collider info (mutually exclusive - one will be valid, the other null);
  • Count should always be used to iterate over the DynamicHit items (because the hits object is pooled and reused between frames, so they array size might be different than the current Count);
  • Hits are not sorted by distance (you can optionally pass a reference FPVector2 to the Sort() function);

Raycasts are syntax-sugar to Linecasts, except they require start, direction and max-distance instead of start and end. There are also optional parameters to restrict the search like LayerMask and options to skip computing normals, skipping statics, etc.

It works the same way for the 3D physics.

Shape Overlaps

Shape overlaps also return an instance of DynamicHits, and the minimum parameters are: a center position (FPVector2 or FPVector3), a rotation (FP or FPQuaternion for the 3D equivalent), and a shape (DynamicShape, either from a DynamicBody or created at the time of calling):

C#

using (var hits = f.Scene.OverlapShape(FPVector2.Zero, FP._0, DynamicShape.CreateCircle(FP._1)))
{
  for (int i = 0; i < hits.Count; i++)
  {
    var hit = hits[i];
  }
}

// The 3D equivalent:
using (var hits = f.Scene3D.OverlapShape(FPVector3.Zero, FPQuaternion.Identity, DynamicShape3D.CreateSphere(1))){
  for (int i = 0; i < hits.Count; i++){
    var hit = hits[i];
  }
}

The 2D Overlap API offers several overloads that take Transform2Ds, Transform2DVertical (covered on the next section), DynamicShapeConfigs, and also optional parameters like LayerMask and OverlapOptions:

C#

public DynamicHits OverlapShape(Transform2D transform, DynamicShapeConfig shapeConfig, Int32 layermask = -1, OverlapOptions options = default(OverlapOptions));

public DynamicHits OverlapShape(Transform2D transform, DynamicShape shape, Int32 layermask = -1, OverlapOptions options = default(OverlapOptions));

public DynamicHits OverlapShape(Transform2D transform, Transform2DVertical verticalTransform, DynamicShapeConfig shapeConfig, Int32 layermask = -1, OverlapOptions options = default(OverlapOptions));

public DynamicHits OverlapShape(Transform2D transform, Transform2DVertical verticalTransform, DynamicShape shape, Int32 layermask = -1, OverlapOptions options = default(OverlapOptions));

public DynamicHits OverlapShape(FPVector2 center, FP rotation, DynamicShapeConfig shapeConfig, Int32 layermask = -1, OverlapOptions options = default(OverlapOptions));

public DynamicHits OverlapShape(FPVector2 center, FP rotation, DynamicShape shape, Int32 layermask = -1, OverlapOptions options = default(OverlapOptions));

public DynamicHits OverlapShape(FPVector2 center, FP rotation, FP verticalOffset, FP height, DynamicShape shape, Int32 layermask = -1, OverlapOptions options = default(OverlapOptions));

The 3D Overlap API offers two options:

C#

public DynamicHits3D OverlapShape(Transform3D transform, DynamicShape3D shape, int layermask = -1, OverlapOptions options = 0);

public DynamicHits3D OverlapShape(FPVector3 center, FPQuaternion rotation, DynamicShape3D shape, int layermask = -1, OverlapOptions options = 0);

Important: on the 2D side, the Scene.OverlapCircle() method is a much faster option to the more generic Scene.OverlapShape. The trade of is that OverlapCircle never computes normals and contact points. Whenever performance is more important, use this option.

2.5D Physics with Vertical Data

When choosing between which type of Physics to use, you might be in which you want your objects to have Height, but you do not want to have full control on all of the three axes. In this case, the 2.5D Physics might be the best choice.

Static colliders and DynamicBodies can have 'thickness' and a 'position' in the 3rd dimension with Quantum's 2.5D physics. For statics, just set the Height and position it directly with the Unity transform handle:

Adding Height to a Static Collider

For Entities, just add the Transform2DVertical component (besides the required Transform2D and DynamicBody) to its definition and set the Height and Position properties to it. On a Quantum XZ-oriented game, this adds height and position on the Y axis, for example.

C#

entity Ship[16] {
  use Transform2D;
  use Transform2DVertical;
  use DynamicBody;
}

When entities or statics have a 3rd dimension, collisions will take that information into consideration, so you can have 'flying' entites going over 'ground-based' ones, etc. This is automatically handled on the collision detection step of Quantum's physics engine.

Important: when a collision is detected, the collision solver doesn't use the extra dimension information, so entity bounce and separation is performed on the basic 2D plane of the physics engine.

It is possible to simulate 3-dimentional gravity by manually integrating speed and forces on the extra axis directly to Transform2DVertical.Position. The physics engine will use that information only for collision detection though.

Implications to Raycast, Linecast and Overlaps: these functions take into consideration the vertical data, so by default they are all flat on the 2D plane, unless you use the overloaded version and pass in height and vertical offset. Here's the shape overlap overload that accepts shapes with vertical data:

C#

public DynamicHits OverlapShape(FPVector2 center, FP rotation, FP verticalOffset, FP height, DynamicShape shape, Int32 layermask = -1, OverlapOptions options = default(OverlapOptions));

Settings and Optimization Tips

The SimulationConfig data asset has an extensive set of setting for the physics engines:

Physics Settings on SimulationConfig

Layers and the corresponding layer collision matrix can be imported from the corresponding Unity ones, or directly edited in the settings. Making sure you enable collisions only between layers that actually are required to be checked against each other is one of the possible optimizations for the physics engine.

Another important setting is enabling Angular Velocity (physics controlled rotation) or not. Disabling this option leads not only to faster but also to more stable physics simulation for games that can use it.

Using kinematic entities whenever possible also helps a lot, as kinematics are never checked for collisions against each other, unless one of them is a trigger kinematic.

In certain games, the extensive use of raycasts can become a bottleneck. One important thing is to use a reasonable distance for rays, as longer ones need to be checked against more colliders, etc. Using a smaller one leads to faster raycasts.

Enabling resting bodies on the settings makes entities that are not moving to not be checked for collisions unless they are awaken again by another moving body, or directly by code. With large number of entities, this helps a lot to reduce the load on the collision detection system.

Using multithreading and collision islands may help in games that have large number of collisions to be checked and solved every frame.

Last, we recommend running the profiler (surrounding your systems as well) before and during performance tweaks. It's important to find where bottlenecks actually are (they may not be related to physics, but actually custom code).

The profiler also helps to check which settings work best under the game specific load. Notice the Unity profiler doesn't work when using Quantum's background thread (this is a Unity profiler limitation), and that its numbers are only meant for comparison between systems, and not representative of release builds (much much faster) in any platform.

Back to top