Handling GearVR Controller events in AFrame - aframe

So, I should start out with the warning that I'm new to AFrame, (though not to programming, game programming, or Javascript). I'm currently trying to put together a simple scene with basic GearVR controller interaction.
When I say "basic interaction", I mean I have the little starter scene that you get when you follow the official tutorial, and I'm just trying to expand that out, such that when you press the trigger, (and specifically the trigger, not just any button), the little text component has it's text value changed.
To make a long story short -- my code isn't working. Despite the fact that I add the event listener in the component init function, (and I've confirmed that init is being called), the listener callback never seems to be invoked. If I had to guess, I'd say that the entity I've attached 'laser-controls' to isn't emitting the 'triggerdown' event, but I'm not sure how to confirm that. Here are the relevant pieces:
AFRAME.registerComponent('gearvr-trigger', {
schema: {
textValue: {
default: "Hello WebVR!"
}
},
init: function() {
var message_box = document.querySelector('#message');
var el = this.el;
el.addEventListener('triggerdown', function(evt) {
message_box.setAttribute('value', this.data.textValue);
});
}
});
<a-text id='message' value="Hello, A-Frame!" color="#BBB"
position="-0.9 0.2 -3" scale="1.5 1.5 1.5">
</a-text>
<a-box gearvr-trigger='textValue: "New Text Value";' src='#boxTexture' position='0 2 -5' rotation='0 45 45' scale='2 2 2'>
<a-animation attribute='position' to='0 2.5 -5' direction='alternate' dur='2000' repeat='indefinite'></a-animation>
<a-animation attribute="scale" begin="mouseenter" dur="300" to="2.3 2.3 2.3"></a-animation>
<a-animation attribute="scale" begin="mouseleave" dur="300" to="2 2 2"></a-animation>
<a-animation attribute="rotation" begin="click" dur="2000" to="360 405 45"></a-animation>
</a-box>
<a-entity raycaster='far: 100; showLine: true;' line='color: red;' laser-controls></a-entity>
If someone with more experience than me sees what I'm doing wrong, that would be great, but I'd also be happy if you could just point me at some decent code examples of responding to GearVR controller events.

As others have noted, the reason you're not picking up the 'triggerdown' events in your custom component is that these events are emitted by the controller entity (a-entity[laser-controls] in this case), and they bubble just as in the standard DOM, (which would be next to a-scene in this case), so a-box is not seeing them.
When you combine information about controller buttons and the controller's relation to other entities in the scene (e.g. pointing with a laser or collisions), I like to call that a gesture. The laser-controls component provides basic gesture interpretation, but I have created the super-hands package for rich gesture interpretation including what you require: button discrimination.
<html>
<head>
<script src="https://aframe.io/releases/0.7.1/aframe.min.js"></script>
<script src="https://unpkg.com/super-hands/dist/super-hands.min.js"></script>
<script>
AFRAME.registerComponent('gearvr-trigger', {
schema: {
textValue: {
default: "Hello WebVR!"
}
},
init: function() {
var message_box = document.querySelector('#message');
var el = this.el;
var triggerResponse = function (evt) {
if (evt.detail.state === 'clicked') {
message_box.setAttribute('value', this.data.textValue);
}
}
el.addEventListener('stateadded', triggerResponse.bind(this));
}
});
</script>
</head>
<body>
<a-scene>
<a-assets>
<a-mixin id="point-laser" raycaster='far: 100; showLine: true;' line='color: red;'></a-mixin>
</a-assets>
<a-text id='message' value="Hello, A-Frame!" color="#BBB"
position="-0.9 0.2 -3" scale="1.5 1.5 1.5">
</a-text>
<a-box clickable="startButtons: triggerdown, mousedown; endButtons: triggerup, mouseup" gearvr-trigger='textValue: "New Text Value";' src='#boxTexture' position='0 2 -5' rotation='0 45 45' scale='2 2 2'>
<a-animation attribute='position' to='0 2.5 -5' direction='alternate' dur='2000' repeat='indefinite'></a-animation>
<a-animation attribute="scale" begin="hover-start" dur="300" to="2.3 2.3 2.3"></a-animation>
<a-animation attribute="scale" begin="hover-end" dur="300" to="2 2 2"></a-animation>
<a-animation attribute="rotation" begin="grab-end" dur="2000" to="360 405 45"></a-animation>
</a-box>
<a-entity progressive-controls="maxLevel: point; pointMixin: point-laser" ></a-entity>
</a-scene>
</body>
</html>
For the controller, progressive-controls auto-detects the controller and sets up the laser pointer like laser-controls, but using super-hands gesture interpretation (it also provides cursor-like input for desktop/cardboard users).
<a-entity progressive-controls="maxLevel: point; pointMixin: point-laser"></a-entity>
For the target entity, clickable activates it as a gesture receiving component and defines which button events it will accept (you can remove the mouse ones if you don't want cross-platform support). The animation trigger events were also updatedaa to work with super-hands.
<a-box clickable="startButtons: triggerdown, mousedown; endButtons: triggerup, mouseup" gearvr-trigger='textValue: "New Text Value";' ...>
...
<a-animation attribute="scale" begin="hover-start" dur="300" to="2.3 2.3 2.3"></a-animation>
<a-animation attribute="scale" begin="hover-end" dur="300" to="2 2 2"></a-animation>
<a-animation attribute="rotation" begin="grab-end" dur="2000" to="360 405 45"></a-animation>
For your custom reaction component, I changed it to react to state changes, which is how clickable communicates that is has received an acceptable gesture. I also resolved a scoping issue that you would have caught once you had the events triggering this.
var triggerResponse = function (evt) {
if (evt.detail.state === 'clicked') {
message_box.setAttribute('value', this.data.textValue);
}
}
el.addEventListener('stateadded', triggerResponse.bind(this));

Try setAttribute('text', 'value, 'whatever you wanted') ?

In case it's not the trigger code, it could be a scope issue:
'this' is referring to the element in the handler as opposed to the component, so this.data is undefined during the event
A potential fix:
AFRAME.registerComponent('gearvr-trigger', {
schema: {
textValue: {
default: "Hello WebVR!"
}
},
init: function() {
var message_box = document.querySelector('#message');
var el = this.el;
var self = this;
el.addEventListener('triggerdown', function(evt) {
message_box.setAttribute('value', self.data.textValue);
});
}
});
or using an arrow function
AFRAME.registerComponent('gearvr-trigger', {
schema: {
textValue: {
default: "Hello WebVR!"
}
},
init: function() {
var message_box = document.querySelector('#message');
var el = this.el;
el.addEventListener('triggerdown', (evt) => {
message_box.setAttribute('value', this.data.textValue);
});
}
});

The triggerdown event fires on the entity with the laser-controls attribute.
My code uses the click event and responds to either button press (unlike what you want) but implementing this pattern may nudge you forward. (It's also more on the path to cross-device operation.) The click event may contain which button was pressed.
In the scene, I have:
<a-entity laser-controls="hand: right"></a-entity>
In a script, I register
AFRAME.registerComponent('cursor-listener', {
init: function () {
var lastIndex = -1;
var COLORS = ['purple', 'orange', 'white'];
this.el.addEventListener('click', function (evt) {
lastIndex = (lastIndex + 1) % COLORS.length;
this.setAttribute('material', 'color', COLORS[lastIndex]);
console.log('I was clicked at: ', evt.detail.intersection.point);
});
}
});
Then I set the cursor-listener attribute on the target boxes:
boxEl.setAttribute('cursor-listener', true);

Related

A-Frame: parent's raycaster-intersected-cleared triggered if no intersection with children

I'm implementing a feature to get coordinate of an a-sky while moving the VR controller.
<a-scene scenelistener>
<a-sky
id="map-sky"
color="#222"
radius="700"
rotation="0 45 0"
rotation-control
raycaster-listen
class="ray-castable"
>
<a-entity
id="country-tiles"
country-tiles
scale="1 1 1"
rotation="0 90 0"
></a-entity>
</a-sky>
... // Oculus entities
</a-scene>
a-sky and its children is listening for raycasting, and print out the console logging
AFRAME.registerComponent("raycaster-listen", {
init: function() {
this.el.addEventListener("raycaster-intersected", evt => {
console.log(`raycaster ${this.el.id} intersected!!!`);
});
this.el.addEventListener("raycaster-intersected-cleared", evt => {
console.log(`raycaster ${this.el.id} cleared!!!`);
});
}
});
RESULT: while moving in and out the children country-tiles, raycaster-intersected-cleared event is triggered from the children and also its parent map-sky, mean A-Frame cannot get the intersections between a-sky and the raycasting VR controller.
raycaster intersected!!!
raycaster map-sky intersected!!!
// Moving out the tile
raycaster cleared!!!
raycaster map-sky cleared!!!
You can check on this Glitch
NOTE: since I am developing for VR controller, therefore please use the WebXR emulator extension for interaction
It turns out that raycaster-intersected-cleared event from parent element is fired but it does not mean that it is not intersected anymore. To confirm if it has been still intersected I have to check if getIntersection result is NULL.
AFRAME.registerComponent("raycaster-listen", {
init: function() {
this.el.addEventListener("raycaster-intersected", evt => {
console.log(`raycaster ${this.el.id} intersected!!!`);
this.raycaster = evt.detail.el;
});
this.el.addEventListener("raycaster-intersected-cleared", evt => {
if (this.raycaster) {
const intersection = this.raycaster.components.raycaster.getIntersection(this.el);
if (!intersection) {
console.log(`raycaster ${this.el.id} cleared!!!`);
}
}
});
}
});

aframe - How to get what element collided with another element

I'm developing a simple VR game using A-Frame, and I'm struggling with collisions.
Specifically, I'm using aframe-physics-extras.min.js for the collision-filter, and aframe-extras.min.js for "hit" (and "hitend", in case) event handling.
In my game there are many bullets and many targets. I can get a "hit" event when a target is hit, but I can't find a way to get what bullet hit that target.
When a target is hit and I use the "hit" event, I can then refer to that specific target using "this.el", so that for example I can remove it from the scene with this.el.sceneEl.removeChild(this.el).
Is there a way to get the element that collided with the target? For example something like this.el.collidingEntity ?
This is the relevant part of the code:
// collision-filter : Requires aframe-physics-extras
// hit (and hitend, if used) : Requires aframe-extras
AFRAME.registerComponent('hit_target', {
init: function() {
this.el.addEventListener('hit', (e) => {
this.el.sceneEl.removeChild(this.el); // This is the Target
// this.el.collidingEntity.sceneEl.removeChild(this.el.collidingEntity); // THIS is what I'd need, to know what hit the Target
})
}
});
// Bullet
var elbullet = document.createElement('a-sphere');
elbullet.setAttribute('class', 'bullet');
elbullet.setAttribute('scale', '0.05 0.05 0.05');
elbullet.setAttribute('opacity', '1');
elbullet.setAttribute('color', '#ff3333');
elbullet.setAttribute('position', point);
elbullet.setAttribute('collision-filter', 'group: bullet; collidesWith: target');
elbullet.setAttribute('dynamic-body', 'shape: sphere; sphereRadius:0.05;');
elbullet.setAttribute('sphere-collider','');
document.querySelector('a-scene').appendChild(elbullet);
// Target
var eltarget = document.createElement('a-gltf-model');
eltarget.setAttribute('src', '#target');
eltarget.setAttribute('class', 'target');
eltarget.setAttribute('scale', '1 1 1');
eltarget.setAttribute('opacity', '1');
eltarget.setAttribute('position', (rnd(-8,8,0))+' '+(rnd(-8,8,0))+' '+(rnd(-20,-6,0)));
eltarget.setAttribute('collision-filter', 'group: target; collidesWith: bullet');
eltarget.setAttribute('hit_target','');
document.querySelector('a-scene').appendChild(eltarget);
A-Frame's aabb-collider component can help you print out the following:
bullet collided with ["plate_big", "plate_small"]
When the bullet hits the target, hitstart event will fire.
You can use event.target.id to get the bullet id.
You can use event.target.components["aabb-collider"]["intersectedEls"] to get the targets.
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("a-entity").forEach(function(entity) {
entity.addEventListener("hitstart", function(event) {
console.log(
event.target.id,
"collided with",
event.target.components["aabb-collider"]["intersectedEls"].map(x => x.id)
);
});
});
});
<script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-aabb-collider-component#3.2.0/dist/aframe-aabb-collider-component.min.js"></script>
<a-scene>
<a-entity id="bullet" geometry="primitive: cone; radiusBottom:0.5; height:2;" material="color: red" position="0 1 -5" aabb-collider="objects: a-entity"></a-entity>
<a-entity id="plate_big" geometry="primitive: cylinder; height:0.2;" material="color: blue" position="0 0.8 -5" aabb-collider="objects: a-entity"></a-entity>
<a-entity id="plate_small" geometry="primitive: cylinder; height:0.2; radius:0.6;" material="color: blue" position="0 1.4 -5" aabb-collider="objects: a-entity"></a-entity>
</a-scene>

Double click listener requires three taps on iOS 13

I'm trying to listen for double clicks on the full screen in iOS.
A minimal non-aframe example works when double tapping on a simple div:
https://fluoridated-nebulous-zebu.glitch.me/
But when attached to aframe's canvas it takes three taps on iOS to trigger:
https://classic-infrequent-pram.glitch.me/
<script>
function doubleClick(fn, timeout = 500) {
let last = Date.now();
return function(e) {
const now = Date.now();
const diff = now - last;
console.log('single');
if (diff < timeout) {
fn(e);
console.log('double');
}
last = now;
}
};
AFRAME.registerComponent('double-click', {
init: function() {
this.el.sceneEl.canvas.addEventListener('click', doubleClick(this.onDoubleClick.bind(this)));
},
onDoubleClick: function() {
this.el.setAttribute('color', '#ff0000');
}
});
</script>
<a-scene background="color: #FAFAFA">
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow double-click></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane>
</a-scene>
It seems the second click is getting throttled, but I'm not sure by whom. I wasn't able to find any code that would do this in aframe's canvas setup, and even manually removing any listeners that were not my own in Safari's inspector did not make a difference.
Edit: It also works if you tap just slow enough to avoid being throttled, and just fast enough that it's under the timeout.
Edit 2: Also seems like it works on iOS 12, but not 13.
Found a solution from a Github thread: https://github.com/Leaflet/Leaflet/issues/6817#issuecomment-535919797
came across this thread while looking into a similar issue. I found that calling preventDefault on touchstart will allow you to receive touch events as usual and thus check for double tap as before.
I believe this comes down to the fact that mobile Safari changed the way it is simulating hover events (to make websites work that solely rely on hover events to open menus and such)
I'm now listening for touchstart instead, and calling e.preventDefault() on the first event:
function doubleClick (fn, timeout = 500) {
let last = Date.now();
return function (e) {
const now = Date.now();
const diff = now - last;
console.log('single');
if (diff < timeout) {
fn(e);
console.log('double');
} else {
e.preventDefault();
}
last = now;
}
};
AFRAME.registerComponent('double-click', {
init: function () {
this.el.sceneEl.canvas.addEventListener('touchstart', doubleClick(this.onDoubleClick.bind(this)));
},
onDoubleClick: function () {
this.el.setAttribute('color', '#ff0000');
}
});
Of course, preventing default might cause issues in your situation, so YMMV. Also still should handle click as fallback for normally working devices.

A-frame score counter with multiple objects

I've looked at this answer: Implementing a score counter in A-Frame
... but cannot figure out how to write a similar function that would work for multiple objects, since querySelector only finds the first element.
I'm trying to create a scene with 5-8 objects, and each one can only be clicked once. When an object is clicked, the counter will increment by 1.
Either use querySelectorAll:
var targets = querySelectorAll('a-box')`
for (var i = 0; i < targets.length; i++) { // Add event listeners... }
But better, use a container entity that listens to the click events that bubble up.
<a-entity listener>
<a-box data-raycastable></a-box>
<a-box data-raycastable></a-box>
</a-entity>
<a-entity cursor="rayOrigin: mouse" raycaster="objects: [data-raycastable]"></a-entity>
Component:
AFRAME.registerComponent('listener', {
init: function () {
this.el.addEventListener('click', evt => {
if (evt.target.dataset.wasClicked) { return; }
// evt.target tells you which was clicked.
evt.target.dataset.wasClicked = true;
// Score up.
});
}
});
As a side note, state component is a great place to store your score. https://www.npmjs.com/package/aframe-state-component

A-Frame: I am trying to add a click function that will change the color of a box. Why isn't it working?

I am trying to start by adding a simple click function to a box that will change its color to red. Can you please have a look at my script and html and please tell me what I am doing wrong?
HTML(only the element meant to change color):
<a-box id='soundbox' position='0 2 -5' color="#6173F4" rotation="0 45 45 opacity=" 0.8" depth="1" alongpath="path:2,2,-5 -2,1,-2.5 0,1,-1; closed:false; dur:5000; delay:4000; inspect:false;" change-colors></a-box>
Script:
var soundbox = document.querySelector('#soundbox');
AFRAME.registerComponent('change-color', {
init: function(){
this.soundbox = soundbox;
this.el.addEventListener('click', this.onClick.bind(this));
},
onClick: function(){
soundbox.setAttribute('color', 'red');
}
});
There is a very good description of how to create a component in A-Frame in the Building a Basic Scene Tutorial on the A-Frame website.
Your component would look like this:
AFRAME.registerComponent('change-color', {
schema: {
color: {default: '#666'}
},
init: function(){
var data = this.data;
this.el.addEventListener('click', function(){
this.setAttribute('color', data.color);
})
}
});
And your a-box:
<a-box
position="0 2 -5"
color="#6173F4"
rotation="0 45 45"
opacity=" 0.8"
depth="1"
change-color="color: #f00"
>
</a-box>
You also need to add a cursor to the scene in order to click on entities. You do this by attaching it to the camera entity:
<a-camera position="0 -0.5 0">
<a-cursor scale="0.5 0.5" color="#fff"></a-cursor>
</a-camera>
The whole code is here: https://glitch.com/edit/#!/enshrined-energy
And the working example: https://enshrined-energy.glitch.me/

Resources