The input system is based on virtual controllers and actions, actions can be either binary or analog. Even though the initial implementations of Decentraland have converged into a specific keymapping configuration, the best input mapping is left up to the implementation and available HID.
In a first person camera engine, there will be only one controller with a ray from the camera position pointing to the forward vector when the mouse is locked. If the mouse is unlocked, the vector will have its origin again in the position of the camera, but the target will be now projected using the mouse position.
The input system can be described in a sentence as:
For each interactable entity (PointerEvents
component), all input commands are
sent back to the scene for processing by the rendering engine (PointerEventsResult
component).
PointerEvents adds configurable pointer-based interactions to the attached Entity.
Events that match the criteria defined in the PointerEvents structure are reported back to the entity via the PointerEventsResult component.
Some examples of events that can be detected:
It also supports simple visual feedback when interactions occur, by showing floating text. More sophisticated feedback requires the use of other components.
parameters:
COMPONENT_ID: 1062
COMPONENT_NAME: core::PointerEvents
CRDT_TYPE: LastWriteWin-Element-Set
parameters:
COMPONENT_ID: 1063
COMPONENT_NAME: core::PointerEventsResult
CRDT_TYPE: GrowOnly-Value-Set
message PBPointerEvents {
message Info {
// key/button in use (default IA_ANY)
optional InputAction button = 1;
// feedback on hover (default 'Interact')
optional string hover_text = 2;
// range of interaction (default 10)
optional float max_distance = 3;
// enable or disable hover text (default true)
optional bool show_feedback = 4;
}
message Entry {
// the kind of interaction to detect
PointerEventType event_type = 1;
// additional configuration for this detection
Info event_info = 2;
}
// the list of relevant events to detect
repeated Entry pointer_events = 1;
}
// since PointerEventsResult is an APPEND type of component, we can send many PBPointerEventsResult to the scene per frame.
message PBPointerEventsResult {
// identifier of the input
InputAction button = 1;
// described in ADR-200
raycast.RaycastHit hit = 2;
// kind of interaction that was detected
PointerEventType event_type = 4;
// could be a Lamport timestamp or a global monotonic counter
uint32 timestamp = 5;
// if the input is analog then we store it here
optional float analog = 6;
// number of tick in which the event was produced, equals to EngineInfo.tick_number (ADR-148) + (ADR-219)
uint32 tick_number = 7;
}
enum InputAction {
IA_POINTER = 0;
IA_PRIMARY = 1;
IA_SECONDARY = 2;
IA_ANY = 3;
IA_FORWARD = 4;
IA_BACKWARD = 5;
IA_RIGHT = 6;
IA_LEFT = 7;
IA_JUMP = 8;
IA_WALK = 9;
IA_ACTION_3 = 10;
IA_ACTION_4 = 11;
IA_ACTION_5 = 12;
IA_ACTION_6 = 13;
}
// PointerEventType is a kind of interaction that can be detected.
enum PointerEventType {
PET_UP = 0;
PET_DOWN = 1;
PET_HOVER_ENTER = 2;
PET_HOVER_LEAVE = 3;
}
A component called PointerEvents
is designed to signal the Renderer which
entities are interactable. It is also used to provide information about "call to action
placeholders" which are recommended to appear as visual cues in the screen. These cues
are specialized per input action, enabling the renderer to select a special texture for each
action key-mapping.
Based on the PointerEvents
component, at the "executeRaycast" stage of
the tick (ADR-148), a continuous raycast (ADR-200) is added to process the events to be sent to the scene.
The eligible meshes for raycast are the ones that meet any of this criteria:
Examples, whith an ✅ we mark the meshes that are eligible. With an ❌ the ones that are not.
In this scenario, the entity A has both PointerEvents and a MeshCollider
components. The raycasts against the MeshCollider will be eligible for
PointerEventResults
ROOT_ENTITY
└── A ✅ (PointerEvents, MeshCollider)
In this scenario, the entity B has a MeshCollider, but since the entity
does not have one, it will not intercept any event.
ROOT_ENTITY
└── A ❌ (PointerEvents)
└── B ❌ (MeshCollider)
In this scenario, the entity A has a GltfContainer and PointerEvents.
Like in the SDK entities, the interal collider meshes of the
GltfContainer will be used to filter which meshes are eligible for raycasts.
ROOT_ENTITY
└── A ✅ (GltfContainner, PointerEvents) <─┐
├── GLTF_ROOT │
│ └── ... │
│ └── ✅ internal gltf node (MeshCollider)
└── B ❌ (MeshCollider)
Any entity with UiTransform
(ADR-124) can be eligible
for pointer events if they have a PointerEvents
component, all other UI entities
will not be eligible and pointer events will be bubbled up until finding an entity that
matches the criteria. A rectangular mesh occupying the whole area designated by the
UiTransform
MUST be used.
In UI entities, contrary to the render order (ADR-151), the events
MUST bubble up from the leaves of the tree up to the root until finding an entity that matches
the pointer event. In that case, the propagation of the event MUST stop, and the event MUST be
added to the PointerEventsResult
.
Like in Raycasts ADR-200, the selection of the meshes for the
pointer events matches any mesh including at least the CL_POINTER
bit in its
collision layers.
For the ECS to work, "events" and "state" must be separated into two different categories. A challenge is faced here, since "events" or "interrupts" are not compatible with data representations at one moment in time (like the state of the ECS). To overcome this, all the input events of the frame must be queued and processed at the "executeRaycast" stage of the tick for all the scenes that are running. Naturally, this compute MUST count towards the quota/limit suggested by ADR-148.
Then, in the "executeRaycast" step of the scene tick (on the renderer side), that queue will be consumed and "InputCommands" will be produced. An InputCommand is a record in a PointerEventResult component listing all the pertinent input actions for that entity. This is done to process when the pointer is "hovering" an entity, when it leaves, when the key down and key up in input. All of that can happen in the same frame. For example, a cube can be aimed at, clicked, and released the click on the same frame.
This design enables rapid games with low input lag and without losing information regardless of the framerate of the scene and renderer.
It also enables high resolution events for cases of moving entities, e.g. assuming one is pointing still, with one finger on the trigger, if the engine puts an entity in front of someone for one frame, the scene will receive the "HoverEnter" event for that unique frame, and if one is fast enough, it may also be clicked.
In summary, the input system does not send the last snapshot of every action to the scene like other engines. Instead, a list of commands for each entity is sent. The SDK takes care of making sense of these commands with input helpers.
Before jumping into the implementation, a new CRDT operation called append
must
be introduced, together with its own kind of CRDT store called
GrowOnly-Value-Set
(ADR-117). It is used by the
PointerEventResult
component, and instead of being a
Last-Write-Win Element set
, it is an ordered set of events. Since it is a grow
only set, the only possible operation over this set is "append". More details about
this CRDT data structure are available at (ADR-117).
Eligible meshes including hover_text
and with show_feedback
enabled
should show a tooltip with a visual queue or indication about the button to be pressed to
interact.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.