W4 Collision System

From Ciliz|W4

Scope[edit]

The Collision system provides the handling of intersections between objects. For one object, you can set several different volumes (bounding boxes, spheres) at once and subscribe to intersections.

The W4 Engine implements the following types of interactions with colliders:

  • Intersect - an intersection of two colliders.
  • Raycast - collider and ray intersection.
  • Screencast - an intersection of the collider and the ray emitted from the given screen coordinates.

Samples of the Collision system working code provided below are available in the W4 Engine:

  • apps/internal/gist-colliding
  • apps/internal/test-screencast-blocking
  • apps/samples/gist-raycast

Let's take a closer look at the Collision system and review its main elements:

  • BoundingVolume - an interface. This is a visitor whose descendants implement methods for calculating intersections. Also, BoundingVolume contains data about local transformation.
  • CollisionInfo - structure containing an intersection data. The data depends on the type of interaction and the types of interacting volumes.
  • Collider - a class containing BoundingVolume and callback management mechanisms.
  • ColliderEventDispatcher - utility static class containing arrays of colliders, distributed by type of interaction. This class also comprises general onUpdate event handling methods for Intersect and onTouch (Screencast) calculation.
  • Node (see W4_Coordinate_system_and_Node_structure) contains the collider management interface.

Collider management[edit]

Any node has access to the collider management interface, that enables to add, remove, and find colliders by name.

The number of colliders is not limited.

Note. Create a unique name for every collider. When adding a collider with the name coinciding with the existing one, a warning is displayed in the console, and the existing collider is destroyed.

Adding a collider[edit]

1) Adding a collider by passing arguments to the Collider class constructor:

template<typename... Args>
cref<Collider> addCollider(Args&&... args);
template<typename String, typename... Args>
cref<Collider> addCollider(const String& name, Args&&... args);

Code example:

// automatic generation of the collider name
cube->addCollider(make::sptr<core::AABB>(1.f));
// manual setting of the collider name 
cube->addCollider("Intersect", make::sptr<core::AABB>(1.f));

2) Adding a collider by specifying the BoundingVolume type and passing arguments to the constructor of the given type T:

template<typename T, typename... Args>
cref<Collider> addCollider(Args&&... args);
template<typename T, typename String, typename... Args>
cref<Collider> addCollider(const String& name, Args&&... args);

Code example:

// automatic generation of the collider name
cube->addCollider<core::AABB>(1.f);
// manual setting of the collider name  
cube->addCollider<core::AABB>("Intersect", 1.f);

All options give identical results:

  • collider is an Axis-aligned Bounding Box with a side size of 1;
  • collider with an automatically generated name added in the first case, or "Intersect" in the second.

Getting a collider[edit]

Signature:

cref<Collider> getCollider(const std::string& name);

The addCollider method returns a pointer to the created collider:

auto collider = cube->addCollider<core::BoundingBox>("Intersect", 1.f);

If you know the name of the collider, you can get a pointer to it:

auto collider = cube->getCollider("Intersect");

Note. In case the collider's name is specified incorrectly, an error is displayed on the console, and the program terminates.

Removing a collider[edit]

Signature:

void removeCollider(cref<Collider>);
void removeCollider(const std::string& name);

Delete the collider by pointer:

cube->removeCollider(collider);

Delete the collider by name:

cube->removeCollider("Intersect");

The Collider class[edit]

This class implements the control of collider interactions.

Bounding Volume[edit]

The collider cannot be created without a given volume, but you can change the collider's volume using the method:

void setVolume(cref<BoundingVolume>);

Local Transform[edit]

Method signature:

void setLocalTransform(math::Transform::cref);
math::Transform::cref   getLocalTransform() const;

void setLocalTranslation(math::vec3::cref);
math::vec3::cref getLocalTranslation() const;

void setLocalRotation(math::Rotator::cref);
math::Rotator::cref getLocalRotation() const;

void setLocalScale(math::vec3::cref);
math::vec3::cref getLocalScale() const;

Features:

  • BoundingVolume has its own local transform relative to the parent node (its interface is identical to the Node transform management interface).
  • The Collider class proxies the interface to the BoundingVolume.
  • Changes to the node and/or collider transform are calculated via lazy evaluation at the moment of interaction.

Intersect[edit]

Below is the part of the Collider class interface responsible for intersections:

using IntersectCallback = std::function<void(const CollisionInfo&)>;

void setIntersecting(bool flag = true);
bool isIntersecting() const;

void setIntersectCallback(const IntersectCallback&);
void resetIntersectCallback();

Note. By default, the collider does not take part in intersections.

How it works:

  • The intersection is calculated by every frame in the onUpdate() method (see IGame class description).
  • For all intersected colliders, the corresponding IntersectCallback will be called (if set).
  • There can be only one IntersectCallback per collider.
  • When setting IntersectCallback, setIntersecting() is automatically called on the collider.

Enable intersection participation:

collider->setIntersecting();
collider->setIntersecting(true);

Disable intersection participation:

collider->setIntersecting(false);

Find out if the collider is involved in intersections:

auto isIntersecting = collider->isIntersecting();

Set IntersectCallback:

collider->setIntersectCallback([](const CollisionInfo& info)
{
  // ...
});

Reset (i.e. return to default disabled state) IntersectCallback:

collider->resetIntersectCallback();

Screencast[edit]

Below is the part of the Collider class interface that is responsible for Screencast:

enum class ScreencastEvent
{
    Down,
    Move,
    Up
};

using ScreencastCallback = std::function<void(const CollisionInfo&)>;

void setBlockScreencasts(bool flag = true);
bool isBlockScreencasts() const;

void setScreencastCallback(ScreencastEvent, const ScreencastCallback&);
void resetScreencastCallback(ScreencastEvent);

void setScreencastCallback(const ScreencastCallback&);
void resetScreencastCallback();

Features:

  • Input events Touch::Begin, Touch::Move and Touch::End (see W4 HAL and W4 Event System description) are equivalent to ScreencastEvent::Down, ScreencastEvent::Move and ScreencastEvent::Up. A callback can be set for any of these events.
  • The user can generate any of the above Touch-events himself (for example, for the “virtual click”).
  • The collider can block or pass the Screencast through itself. By default, all colliders always block Screencast. If the collider blocks it, then callbacks for colliders farther in the direction of the ray will not be called.

Enable blocking:

collider->setBlockScreencasts();
collider->setBlockScreencasts(true);

Disable blocking:

collider->setBlockScreencasts(false);

Find out if Screencast is being blocked by the collider:

auto isBlockScreencasts = collider->isBlockScreencasts();

Set ScreencastCallback:

// ScreencastEvent::Down
collider->setScreencastCallback(ScreencastEvent::Down, [](const CollisionInfo& info)
{
  // ...
});
// or 
collider->setScreencastCallback([](const CollisionInfo& info)
{
  // ...
});

// ScreencastEvent::Move
collider->setScreencastCallback(ScreencastEvent::Move, [](const CollisionInfo& info)
{
  // ...
});

// ScreencastEvent::Up
collider->setScreencastCallback(ScreencastEvent::Up, [](const CollisionInfo& info)
{
  // ...
});

Reset (i.e. return to default "blocking" state) ScreencastCallback:

// ScreencastEvent::Down
collider->resetScreencastCallback(ScreencastEvent::Down);
// or 
collider->resetScreencastCallback();

// ScreencastEvent::Move
collider->resetScreencastCallback(ScreencastEvent::Move);

// ScreencastEvent::Up
collider->resetScreencastCallback(ScreencastEvent::Up);

Raycast[edit]

Below is the part of the Collider class interface responsible for Raycast:

using RaycastCallback = std::function<void(const CollisionInfo&)>;

void setReceiveRaycasts(bool flag = true);
bool isReceiveRaycasts() const;

void setBlockRaycasts(bool flag = true);
bool isBlockRaycasts() const;

void setRaycastCallback(const RaycastCallback&);
void resetRaycastCallback();

If RaycastCallback is not set for the collider or the isReceiveRaycasts flag is not enabled, the collider does not participate in the raycast calculation. By default, colliders do not block raycast.

How to use:

  • Raycast to nearest collider (the Render class interface):
static core::CollisionInfo Render::raycast(const math::Ray& ray);

The above method returns the closest intersection with the ray. RaycastCallback is called for all colliders, up to the first blocking one. If there is no intersection, then CollisionInfo.target = nullptr.

  • Raycast to nearest blocking collider (the Render class interface):
static std::vector<core::CollisionInfo> Render::raycastAll(const math::Ray& ray);

The above method returns all intersections with the given ray, up to the first blocking collider. RaycastCallback is called for all colliders. The returned array is sorted by distance.

  • Set RaycastCallback:
collider->setRaycastCallback([](const CollisionInfo& info)
{
  // ...
});
  • Reset (i.e. to "do not block raycast" state) RaycastCallback:
collider->resetRaycastCallback();