Button events
Decentraland accepts events from pointer clicks, a primary button and a secondary button.
Clicks can be done either with a mouse, a touch screen, a VR controller or some other device, these all generate the same type of event.
The primary and secondary buttons map respectively to the E and F key on a keyboard.
📔 Note: Entities that don’t have a shape component, or that have their shape’s visible
field set to false don’t respond to pointer events.
Pointer event components #
OnPointerDown #
The best way to handle pointer and button down events is to add an OnPointerDown
component to an entity.
The component requires that you pass it a function as a main argument. This function declares what to do in the event of a button down event while the player points at the entity.
const myEntity = new Entity()
myEntity.addComponent(new BoxShape())
myEntity.addComponent(
new OnPointerDown((e) => {
log("myEntity was clicked", e)
})
)
💡 Tip: To keep your code easier to read, the function in the OnPointerDown
can consist of just a call to a separate function that contains all of the logic.
The OnPointerDown
component has a second optional parameter, this parameter is an object that can include multiple properties about the event. These properties are explained in greater detail in the next few sub-sections.
button
: Which key to listen for, from theActionButton
enum:ActionButton.POINTER
(left mouse click on PC)ActionButton.PRIMARY
(E on PC)ActionButton.SECONDARY
(F on PC)
hoverText
: Hint text to display on the UI when pointing at the entity.distance
: Maximum click distance.
OnPointerUp #
Add an OnPointerUp
component to track when a player releases the mouse button, the primary or the secondary button while pointing at the entity.
Like the OnPointerDown
, the OnPointerUp
component requires a callback function that declares what to do in the event of a button up event while pointing at the entity.
This component also takes a second argument that supports the same additional fields as teh OnPointerDown
component.
const myEntity = new Entity()
myEntity.addComponent(new BoxShape())
myEntity.addComponent(
new OnPointerUp((e) => {
log("pointer up", e)
})
)
Specific button events #
The OnPointerDown
and OnPointerUp
components can respond to the following different buttons:
POINTER
(left mouse click on PC)PRIMARY
(E on PC)SECONDARY
(F on PC)
You can configure the components by setting the button
field in the second argument of the component initializer.
If no button is specified, ActionButton.ANY
is used as the default. This detects events from any of the available buttons on these components.
const myEntity = new Entity()
myEntity.addComponent(new BoxShape())
myEntity.addComponent(
new OnPointerDown(
(e) => {
log("myEntity was clicked", e)
},
{ button: ActionButton.POINTER }
)
)
Hint messages #
When a player hovers the cursor over an item with an OnPointerDown
or OnPointerUp
component, the cursor changes shape to hint to the player that the entity is interactive.
You can also display a toast message in the UI that lets the player know what happens when interacting with the entity.
myEntity.addComponent(
new OnPointerDown(
(e) => {
log("myEntity clicked", e)
},
{
button: ActionButton.PRIMARY,
showFeedback: true,
hoverText: "open",
}
)
)
In the example above, the second argument of the OnPointerDown
component has an object with the following arguments:
button
: What button to respond toshowFeedback
: Boolean to turn the feedback on or off. It’s true by default.hoverText
: String to display in the UI while the player points at the entity. By default, this string spells Interact, unlessshowFeedback
is false.
💡 Tip: ThehoverText
string should describe the action that happens when interacting. For exampleOpen
,Activate
,Grab
,Select
. These strings should be as short as possible, to avoid stealing too much focus from the player.
The hoverText
of an OnPointerUp
component is only displayed while the player is already holding down the corresponding key and pointing at the entity.
If an entity has both an OnPointerDown
and an OnPointerUp
component, the hint for the OnPointerDown
is shown while the button is not being pressed. The hint switches to the one from the OnPointerUp
only when the button is pressed and remains pressed.
myEntity.addComponent(
new OnPointerDown(
(e) => {
log("myEntity clicked", e)
},
{ button: ActionButton.PRIMARY, showFeedback: true, hoverText: "Drag" }
)
)
myEntity.addComponent(
new OnPointerUp(
(e) => {
log("myEntity released", e)
},
{ button: ActionButton.PRIMARY, showFeedback: true, hoverText: "Drop" }
)
)
Max click distance #
By default, entities are only clickable when the player is within a close range of the entity, at a maximum distance of 10 meters. You can optionally configure the maximum distance through the distance
parameter of the OnPointerDown
and OnPointerUp
components.
myEntity.addComponent(
new OnPointerDown(
(e) => {
log("myEntity clicked", e)
},
{
button: ActionButton.PRIMARY,
showFeedback: true,
hoverText: "Activate",
distance: 8,
}
)
)
The example above sets the maximum distance to 8 meters.
Event arguments #
The pointer down event and the pointer up event objects are implicitly passed as parameters of the functions in the OnPointerDown
and OnPointerUp
components, respectively. This event object contains various properties that might be useful for the function. See Properties of button events for more details.
const myEntity = new Entity()
myEntity.addComponent(new BoxShape())
myEntity.addComponent(
new OnPointerDown(
(e) => {
log("Click distance: " + e.length)
},
{ button: ActionButton.PRIMARY }
)
)
Multiple buttons on an entity #
You may want to make an entity respond to different buttons in different ways. Each entity can only have one OnPointerDown
component, and one OnPointerUp
component, but you can use ActionButton.ANY
and then tell them apart within the function.
Check the buttonId
field from the event data. The value of this field returns a number, which maps to the values in the ActionButton
array, for example by POINTER
maps to 0, PRIMARY
to 1, SECONDARY
to 2, etc.
const myEntity = new Entity()
myEntity.addComponent(new BoxShape())
myEntity.addComponent(
new OnPointerDown(
(e) => {
if (e.buttonId == 0) {
log("Clicked pointer")
} else if (e.buttonId == 1) {
log("Pressed primary button")
} else if (e.buttonId == 2) {
log("Pressed secondary button")
}
},
{ button: ActionButton.ANY }
)
)
Players will see a single label when hovering over the entity, so make sure it’s clear that there are multiple ways to interact with it.
Properties of button events #
The events from OnPointerDown
and OnPointerUp
components, as well as all the global button event objects, contain the following parameters:
-
origin
: Origin point of the ray, as a Vector3 -
direction
: Direction vector of the ray, as a normalized Vector3 that points in the same direction. -
buttonId
: ID of the button that triggered the event (POINTER, PRIMARY or SECONDARY) -
hit
: (Optional) Object describing the entity that was clicked on. If the click didn’t hit any specific entity, this field isn’t present. Thehit
object contains the following parameters:length
: Length of the ray in meters, as a numberhitPoint
: The intersection point between the ray and the entity’s mesh, as a Vector3meshName
: The name of the mesh, if applicable, as a stringworldNormal
: The normal of the hit, in world space, as a Vector3entityId
: The ID of the entity, if applicable, as a string
Differentiate meshes inside a model #
Often, .glTF 3D models are made up of multiple meshes, that each have an individual internal name. All button events events include the information of what specific mesh was clicked, so you can use this information to trigger different click behaviors in each case.
To see how the meshes inside the model are named, you must open the 3D model with an editing tool, like Blender for example.
💡 Tip: You can also learn the name of the clicked mesh by logging it and reading it off console.
You access the meshName
property as part of the hit
object, that’s returned by the click event.
In the example below we have a house model that includes a mesh named firePlace
. We want to turn on the fireplace only when its corresponding mesh is clicked.
houseEntity.addComponent(
new OnPointerDown(
(e) => {
log("button A Down", e.hit.meshName)
if (e.hit.meshName === "firePlace") {
// light fire
fireAnimation.play()
}
},
{ button: ActionButton.POINTER, showFeeback: false }
)
)
📔 Note: Since theOnPointerDown
component belongs to the whole entity, the on-hover feedback would be seen when hovering over any part of the entity. In this case, any part of the house, not just the fireplace. For that reason, we set theshowFeedback
argument of theOnPointerDown
component to false, so that no on-hover feedback is shown. For a better player experience, it’s recommended to instead have the fireplace as a separate entity and maintain the on-hover feedback.
Global button events #
The BUTTON_DOWN and BUTTON_UP events are fired whenever the player presses or releases an input controller button.
These events are triggered every time that the buttons are pressed or released, regardless of where the player’s pointer is pointing at, as long as the player is standing inside the scene’s boundaries.
It also tracks keys used for basic avatar movement whilst in the scene.
Instance an Input
object and use its subscribe()
method to initiate a listener that’s subscribed to one of the button events. Whenever the event is caught, it executes a provided function.
The subscribe()
method takes four arguments:
-
eventName
: The type of action, this can be either"BUTTON_DOWN"
or"BUTTON_UP"
-
buttonId
: Which button to listen for. The following buttons can be tracked for both BUTTON_DOWN and BUTTON_UP events:- `POINTER` (left mouse click on PC) - `PRIMARY` (_E_ on PC) - `SECONDARY`(_F_ on PC) - `JUMP`(_space bar_ on PC) - `FORWARD`(_W_ on PC) - `LEFT`(_A_ on PC) - `RIGHT`(_D_ on PC) - `BACK`(_S_ on PC) - `WALK`(_Shift_ on PC) - `ACTION_3`(_1_ on PC) - `ACTION_4`(_2_ on PC) - `ACTION_5`(_3_ on PC) - `ACTION_6`(_4_ on PC)
-
useRaycast
: Boolean to define if raycasting will be used. Iffalse
, the button event will not contain information about anyhit
objects that align with the pointer at the time of the event. Avoid setting this field totrue
when information about hit objects is not required, as it involves extra calculations. -
fn
: The function to execute each time the event occurs.
📔 Note: Other keys on the PC keyboard aren’t tracked for future cross-platform compatibility, as this limited set of keys can be mapped to a joystick. For detecting key-strokes when writing text, check the UIInputBox.
// Instance the input object
const input = Input.instance
// button down event
input.subscribe("BUTTON_DOWN", ActionButton.POINTER, false, (e) => {
log("pointer Down", e)
})
// button up event
input.subscribe("BUTTON_UP", ActionButton.POINTER, false, (e) => {
log("pointer Up", e)
})
The example above logs messages and the contents of the event object every time the pointer button is pushed down or released.
The event objects of both the BUTTON_DOWN
and the BUTTON_UP
contain various useful properties. See Properties of button events for more details.
📔 Note: The code for subscribing an input event only needs to be executed once, thesubscribe()
method keeps polling for the event. Don’t add this into a system’supdate()
function, as that would register a new listener on every frame.
Detect hit entities #
If the third argument of the subscribe()
function (useRaycast
)is true, and the player is pointing at an entity that has a collider, the event object includes a nested hit
object. The hit
object includes information about the collision and the entity that was hit.
Raycasting is not available when detecting basic movement keys. It’s only available when tracking the following buttons:
POINTER
PRIMARY
SECONDARY
ACTION_3
ACTION_4
ACTION_5
ACTION_6
The ray of a global button event only detects entities that have a collider mesh. Primitive shapes have a collider mesh on by default, 3D models need to have one built into them.
💡 Tip: See Colliders for details on how to add collider meshes to a 3D model.
input.subscribe("BUTTON_DOWN", ActionButton.POINTER, true, (e) => {
if (e.hit) {
let hitEntity = engine.entities[e.hit.entityId]
hitEntity.addComponent(greenMaterial)
}
})
The example above checks if any entities were hit, and if so it fetches the entity and applies a material component to it.
The event data returns a string for the entityId
. If you want to reference the actual entity by that ID to affect it in some way, use engine.entities[e.hit.entityId]
.
📔 Note: We recommend that when possible you use the approach of adding anOnPointerDown
component to each entity you want to make interactive, instead of using a global button event. The scene’s code isn’t able to hint to a player that an entity is interactive when hovering on it unless the entity has anOnPointerDown
,OnPointerUp
, orOnClick
component.
Tracking player movements #
In real-time multiplayer games where the timing of player movements is critical, you may want to keep track of each player’s position using a 3rd party server as the source of truth. You can improve response time by listening to the button in advance and predict their effects in your server before the avatar has shifted position.
This approach helps compensate for network delays, but is sure to result in discrepancies, so you should also regularly poll the player’s current position to make corrections. Balancing these predictions and corrections may require plenty of fine-tuning.
Ray Obstacles #
Button events cast rays that only interact with the first entity on their path, as long as the entity is closer than its distance limit.
For an entity to be intercepted by the ray of a button event, the entity’s shape must either have a collider mesh, or the entity must have a component related to button events (OnPointerDown
, OnPointerUp
or OnClick
).
If another entity’s collider is standing on the way of the entity that the player wants to click, the player won’t be able to click the entity that’s behind, unless the entity that’s in-front has a shape with its isPointerBlocker
property set to false.
let myEntity = new Entity()
let box = new BoxShape()
box.isPointerBlocker = false
box.visible = false
myEntity.addComponent(box)
engine.addEntity(myEntity)
OnHover Component #
Add OnPointerHoverEnter
and OnPointerHoverExit
components to an entity to run a callback function every time that the player’s cursor starts or stops pointing at the entity.
You can use this to hint that something is interactable in some custom way, like showing a glowing highlight around the entity, playing a subtle sound, etc. It could also be used for specific gameplay mechanics.
myEntity.addComponent(
new OnPointerHoverEnter((e) => {
log("Started Pointing at entity")
})
)
myEntity.addComponent(
new OnPointerHoverExit((e) => {
log("Stopped Pointing at entity")
})
)
On the OnPointerHoverEnter
component, you can set a maximum distance, to only trigger the callback when the player is near the entity.
myEntity.addComponent(
new OnPointerHoverEnter(
(e) => {
log("Started Pointing at entity")
},
{
distance: 8,
}
)
)
💡 Tip: Note that all entities with anOnPointerDown
component by default show a UI hint when hovered over. You can disable this UI hint by setting theshowFeeback
property on theOnPointerDown
component to false.
Button state #
You can check for the button’s current state (up or down) using the Input object.
let buttonState = Input.instance.isButtonPressed(ActionButton.POINTER)
if(buttonState.BUTTON_DOWN){
log("button is being held down")
} else {
log("button is up
}
You can use the .isButtonPressed
function to check for the states of any of the globally tracked buttons. The function returns an object that contains a BUTTON_DOWN
boolean field, with the current state of the button.
As an example, you can implement this in a system’s update()
function to check a button’s state regularly.
class ButtonChecker implements ISystem {
update() {
if (Input.instance.isButtonPressed(ActionButton.FORWARD).BUTTON_DOWN) {
log("player walking forward")
} else {
log("player not walking forward")
}
}
}
engine.addSystem(new ButtonChecker())
OnClick Component - DEPRECATED #
As an alternative to OnPointerDown
, you can use the OnClick
component. This component only tracks button events from the POINTER
, not from the primary or secondary buttons.
You declare what to do in the event of a click by writing a function in the OnClick
component.
const myEntity = new Entity()
myEntity.addComponent(new BoxShape())
myEntity.addComponent(
new OnClick((e) => {
log("myEntity clicked")
})
)
The OnClick
component passes less event information than the OnPointerDown
component, it lacks the click distance or the mesh name, for example.
📔 Note: Entities that don’t have a shape component, or that have their shape’s visible
field set to false can’t be clicked.