├── .gitignore ├── LICENSE ├── README.md ├── dist ├── a-game.js ├── cannonWorker.js ├── index.html ├── models │ └── chair.html └── scenes │ ├── _assets.html │ ├── _player.html │ ├── ballmachine.html │ ├── ballmachine.js │ ├── chairs.html │ ├── demo.html │ ├── demo.js │ ├── hello.html │ └── ragdoll.html ├── images ├── grid.png └── grid.xcf ├── package-lock.json ├── package.json └── src ├── a-game.js ├── cannonWorker.js ├── components ├── grabbing.js ├── grabbing.md ├── grabbing │ ├── button.js │ ├── button.md │ ├── climbable.js │ ├── climbable.md │ ├── fingerflex.js │ ├── fingerflex.md │ ├── grabbable.js │ ├── grabbable.md │ ├── receptacle.js │ └── receptacle.md ├── include.js ├── include.md ├── injectglove.js ├── injectplayer.js ├── limit.js ├── limit.md ├── locomotion.js ├── locomotion.md ├── locomotion │ ├── floor.js │ ├── floor.md │ ├── start.js │ ├── start.md │ ├── wall.js │ └── wall.md ├── onevent.js ├── onevent.md ├── onstate.js ├── onstate.md ├── physics.js ├── physics.md ├── physics │ ├── body.js │ ├── body.md │ ├── joint.js │ ├── joint.md │ ├── shape.js │ └── shape.md ├── script.js ├── script.md ├── trigger.js └── trigger.md ├── libs ├── betterRaycaster.js ├── cannon.js ├── cmdCodec.js ├── copyWorldPosRot.js ├── ensureElement.js ├── pools.js └── touchGestures.js └── primitives ├── a-glove.js ├── a-glove.md ├── a-hand.js ├── a-hand.md ├── a-main.js ├── a-main.md ├── a-player.js └── a-player.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 poeticAndroid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A-Game! 2 | 3 | Essential game components for [A-Frame](https://aframe.io/)! 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ``` 22 | 23 | **[Demo!](https://a-game-demo.glitch.me)** 24 | 25 | 26 | ## Primitives 27 | 28 | - [a-hand](./src/primitives/a-hand.md) 29 | - [a-main](./src/primitives/a-main.md) 30 | - [a-player](./src/primitives/a-player.md) 31 | 32 | 33 | ## Components 34 | 35 | - [grabbing](./src/components/grabbing.md) 36 | - [button](./src/components/grabbing/button.md) 37 | - [climbable](./src/components/grabbing/climbable.md) 38 | - [fingerflex](./src/components/grabbing/fingerflex.md) 39 | - [grabbable](./src/components/grabbing/grabbable.md) 40 | - [receptacle](./src/components/grabbing/receptacle.md) 41 | - [include](./src/components/include.md) 42 | - [limit](./src/components/limit.md) 43 | - [locomotion](./src/components/locomotion.md) 44 | - [floor](./src/components/locomotion/floor.md) 45 | - [start](./src/components/locomotion/start.md) 46 | - [wall](./src/components/locomotion/wall.md) 47 | - [onevent](./src/components/onevent.md) 48 | - [onstate](./src/components/onstate.md) 49 | - [physics](./src/components/physics.md) 50 | - [body](./src/components/physics/body.md) 51 | - [joint](./src/components/physics/joint.md) 52 | - [shape](./src/components/physics/shape.md) 53 | - [script](./src/components/script.md) 54 | - [trigger](./src/components/trigger.md) 55 | 56 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 🎮 A-Game! 👍 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /dist/models/chair.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /dist/scenes/_assets.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 11 | 14 | 17 | 20 | 21 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | -------------------------------------------------------------------------------- /dist/scenes/_player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /dist/scenes/ballmachine.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 15 | 17 | 19 | 20 | 22 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /dist/scenes/ballmachine.js: -------------------------------------------------------------------------------- 1 | ({ 2 | init() { 3 | setTimeout(() => { 4 | this.el.removeAttribute("script") 5 | setTimeout(() => { 6 | this.el.setAttribute("script", "") 7 | }, 1000 * 60 * 5) 8 | }, 16000) 9 | }, 10 | 11 | tick() { 12 | let div = document.createElement("div") 13 | div.innerHTML = '' 14 | let ball = div.firstElementChild 15 | ball.setAttribute("radius", Math.random() * 0.125 + 0.125) 16 | ball.setAttribute("position", "" + (0.5 - Math.random()) + " 8 " + (-7.5 - Math.random())) 17 | ball.addEventListener("collision", e => { 18 | if (e.detail.body2.tagName.toLowerCase() === "a-sphere") { 19 | if (e.detail.body2.getAttribute("color") && !ball.getAttribute("color")) { 20 | // ball.setAttribute("color", e.detail.body2.getAttribute("color")) 21 | ball.setAttribute("color", "hsl(" + (360 * Math.random()) + ", 50%, 75%)") 22 | ball.setAttribute("body", "emitsWith", 0) 23 | } 24 | } 25 | // else { 26 | // ball.setAttribute("color", "hsl(" + (360 * Math.random()) + ", 50%, 50%)") 27 | // ball.setAttribute("body", "emitsWith", 0) 28 | // } 29 | }) 30 | let scene = this.el.sceneEl.querySelector("a-main") 31 | scene.appendChild(ball) 32 | } 33 | }) -------------------------------------------------------------------------------- /dist/scenes/chairs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /dist/scenes/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 34 | 36 | 38 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 96 | 98 | 100 | 102 | 104 | -------------------------------------------------------------------------------- /dist/scenes/demo.js: -------------------------------------------------------------------------------- 1 | ({ 2 | init() { 3 | console.log("Hello demo!") 4 | }, 5 | 6 | inside() { 7 | console.log("I just went in!") 8 | }, 9 | outside() { 10 | console.log("I left!") 11 | }, 12 | }) -------------------------------------------------------------------------------- /dist/scenes/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dist/scenes/ragdoll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 14 | 15 | 17 | 18 | 20 | 21 | 23 | 24 | 26 | 27 | 29 | 30 | 32 | 33 | 35 | 36 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /images/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poeticAndroid/a-game/a02bb11b5ab3488c820463cf59d728a13f5c09dc/images/grid.png -------------------------------------------------------------------------------- /images/grid.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poeticAndroid/a-game/a02bb11b5ab3488c820463cf59d728a13f5c09dc/images/grid.xcf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-game", 3 | "title": "A-Game", 4 | "version": "0.48.0", 5 | "description": "game components for A-Frame", 6 | "homepage": "https://github.com/poeticAndroid/a-game/blob/master/README.md", 7 | "main": "index.js", 8 | "scripts": { 9 | "prepare": "npm run build", 10 | "build": "foreach -g src/*.js -x \"browserify #{path} -o dist/#{name}.js\"", 11 | "watch": "foreach -g src/*.js -C -x \"watchify #{path} -d -o dist/#{name}.js\"", 12 | "bump": "npm version minor --no-git-tag-version", 13 | "gitadd": "git add package*.json dist/*.js" 14 | }, 15 | "pre-commit": [ 16 | "bump", 17 | "build", 18 | "gitadd" 19 | ], 20 | "keywords": [ 21 | "aframe", 22 | "aframe-component", 23 | "webvr", 24 | "webxr", 25 | "gamedev" 26 | ], 27 | "author": "poeticAndroid", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "browserify": "^17.0.0", 31 | "foreach-cli": "^1.8.1", 32 | "pre-commit": "^1.2.2", 33 | "watchify": "^4.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/a-game.js: -------------------------------------------------------------------------------- 1 | require("./libs/pools") 2 | require("./libs/copyWorldPosRot") 3 | require("./libs/ensureElement") 4 | require("./libs/touchGestures") 5 | require("./libs/betterRaycaster") 6 | 7 | addEventListener('DOMContentLoaded', e => { 8 | document.body.addEventListener("swipeup", e => { 9 | document.body.requestFullscreen() 10 | }) 11 | }) 12 | 13 | require("./components/grabbing") 14 | require("./components/include") 15 | require("./components/injectglove") 16 | require("./components/injectplayer") 17 | require("./components/limit") 18 | require("./components/locomotion") 19 | require("./components/onevent") 20 | require("./components/onstate") 21 | require("./components/physics") 22 | require("./components/script") 23 | require("./components/trigger") 24 | 25 | require("./primitives/a-glove") 26 | require("./primitives/a-hand") 27 | require("./primitives/a-main") 28 | require("./primitives/a-player") 29 | 30 | const pkg = require("../package") 31 | console.log(`${pkg.title} Version ${pkg.version} by ${pkg.author}\n(${pkg.homepage})`) 32 | -------------------------------------------------------------------------------- /src/cannonWorker.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE, CANNON */ 2 | if (typeof window !== "undefined") 3 | return 4 | 5 | const cmd = require("./libs/cmdCodec") 6 | 7 | global.CANNON = require("./libs/cannon") 8 | global.world = new CANNON.World() 9 | global.bodies = [] 10 | global.movingBodies = [] 11 | global.replacedBodies = [] 12 | global.joints = [] 13 | 14 | let vec = new CANNON.Vec3() 15 | let quat = new CANNON.Quaternion() 16 | let cyloff = new CANNON.Quaternion() 17 | let lastStep = 0 18 | 19 | function init() { 20 | cyloff.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI / 2) 21 | addEventListener("message", onMessage) 22 | } 23 | 24 | function onMessage(e) { 25 | if (typeof e.data === "string") { 26 | let command = cmd.parse(e.data) 27 | switch (command.shift()) { 28 | case "log": 29 | console.log(...command) 30 | break 31 | case "world": 32 | worldCommand(command) 33 | break 34 | } 35 | } 36 | else if (e.data instanceof Float64Array) { 37 | let buffer = e.data 38 | let now = Date.now() 39 | for (let mid = 0; mid < movingBodies.length; mid++) { 40 | let body = movingBodies[mid] 41 | let p = mid * 8 42 | if (!body) continue 43 | if (body.type === CANNON.Body.KINEMATIC) { 44 | vec.set(buffer[p++], buffer[p++], buffer[p++]) 45 | body.position.copy(vec) 46 | buffer[p++] = body.sleepState === CANNON.Body.SLEEPING 47 | quat.set(buffer[p++], buffer[p++], buffer[p++], buffer[p++]) 48 | body.quaternion.copy(quat) 49 | } 50 | } 51 | world.step(Math.min(now - lastStep, 20) / 1000) 52 | while (replacedBodies.length) { 53 | let body = replacedBodies.pop() 54 | body.velocity.set(0, 0, 0) 55 | body.angularVelocity.set(0, 0, 0) 56 | } 57 | for (let mid = 0; mid < movingBodies.length; mid++) { 58 | let body = movingBodies[mid] 59 | let p = mid * 8 60 | if (!body) continue 61 | if (body.type !== CANNON.Body.KINEMATIC) { 62 | buffer[p++] = body.position.x 63 | buffer[p++] = body.position.y 64 | buffer[p++] = body.position.z 65 | buffer[p++] = body.sleepState === CANNON.Body.SLEEPING 66 | buffer[p++] = body.quaternion.x 67 | buffer[p++] = body.quaternion.y 68 | buffer[p++] = body.quaternion.z 69 | buffer[p++] = body.quaternion.w 70 | } 71 | } 72 | postMessage(buffer, [buffer.buffer]) 73 | lastStep = now 74 | } 75 | } 76 | 77 | function worldCommand(params) { 78 | if (typeof params[0] === "number") { 79 | params.shift() 80 | } 81 | switch (params.shift()) { 82 | case "body": 83 | bodyCommand(params) 84 | break 85 | case "joint": 86 | jointCommand(params) 87 | break 88 | case "gravity": 89 | world.gravity.copy(params[0]) 90 | break 91 | case "eval": 92 | eval(params[0]) 93 | break 94 | } 95 | } 96 | 97 | function bodyCommand(params) { 98 | let id = params.shift() 99 | let body = bodies[id] 100 | if (!body && params[0] !== "create") return 101 | switch (params.shift()) { 102 | case "shape": 103 | shapeCommand(body, params) 104 | break 105 | case "create": 106 | if (body) { 107 | world.removeBody(body) 108 | if (body._mid_ !== null) 109 | movingBodies[body._mid_] = null 110 | } 111 | body = new CANNON.Body({ 112 | type: params[0].type === "dynamic" ? CANNON.Body.DYNAMIC : params[0].type === "kinematic" ? CANNON.Body.KINEMATIC : CANNON.Body.STATIC, 113 | sleepSpeedLimit: 1, 114 | position: new CANNON.Vec3().copy(params[0].position), 115 | quaternion: new CANNON.Quaternion().copy(params[0].quaternion), 116 | }) 117 | body.material = new CANNON.Material() 118 | body._id_ = id 119 | body._mid_ = params[0].mid 120 | if (body._mid_ !== null) 121 | movingBodies[body._mid_] = body 122 | body._shapes_ = [] 123 | world.addBody(bodies[id] = body) 124 | break 125 | case "remove": 126 | world.removeBody(body) 127 | bodies[id] = null 128 | if (body._mid_ !== null) 129 | movingBodies[body._mid_] = null 130 | break 131 | case "position": 132 | body.position.copy(params[0]) 133 | replacedBodies.push(body) 134 | break 135 | case "quaternion": 136 | body.quaternion.copy(params[0]) 137 | break 138 | case "type": 139 | body.type = params[0] === "dynamic" ? CANNON.Body.DYNAMIC : params[0] === "kinematic" ? CANNON.Body.KINEMATIC : CANNON.Body.STATIC 140 | break 141 | case "mass": 142 | body.mass = body.type === CANNON.Body.STATIC ? 0 : params[0] 143 | body.updateMassProperties() 144 | break 145 | case "friction": 146 | body.material.friction = params[0] 147 | break 148 | case "restitution": 149 | body.material.restitution = params[0] 150 | break 151 | case "belongsTo": 152 | body.collisionFilterGroup = params[0] 153 | break 154 | case "collidesWith": 155 | body.collisionFilterMask = params[0] 156 | break 157 | case "emitsWith": 158 | if (params[0] && !body._emitsWith_) { 159 | body.addEventListener("collide", onCollide) 160 | } 161 | if (body._emitsWith_ && !params[0]) { 162 | body.removeEventListener("collide", onCollide) 163 | } 164 | body._emitsWith_ = params[0] 165 | break 166 | case "sleeping": 167 | if (params[0]) body.sleep() 168 | else body.wakeUp() 169 | break 170 | case "impulse": 171 | body.applyImpulse(new CANNON.Vec3().copy(params[0]), new CANNON.Vec3().copy(params[1])) 172 | break 173 | case "eval": 174 | eval("const body = bodies[" + id + "];" + params[0]) 175 | break 176 | } 177 | } 178 | 179 | function jointCommand(params) { 180 | let id = params.shift() 181 | let joint = joints[id] 182 | if (!joint && params[0] !== "create") return 183 | switch (params.shift()) { 184 | case "create": 185 | if (joint) { 186 | world.removeConstraint(joint) 187 | } 188 | switch (params[0].type) { 189 | case "hinge": 190 | joint = new CANNON.HingeConstraint( 191 | bodies[params[0].body1], 192 | bodies[params[0].body2], 193 | { 194 | pivotA: new CANNON.Vec3().copy(params[0].pivot1), 195 | pivotB: new CANNON.Vec3().copy(params[0].pivot2), 196 | axisA: new CANNON.Vec3().copy(params[0].axis1), 197 | axisB: new CANNON.Vec3().copy(params[0].axis2) 198 | } 199 | ) 200 | break 201 | case "distance": 202 | joint = new CANNON.DistanceConstraint( 203 | bodies[params[0].body1], 204 | bodies[params[0].body2] 205 | ) 206 | break 207 | case "lock": 208 | joint = new CANNON.LockConstraint( 209 | bodies[params[0].body1], 210 | bodies[params[0].body2] 211 | ) 212 | break 213 | default: 214 | joint = new CANNON.PointToPointConstraint( 215 | bodies[params[0].body1], 216 | new CANNON.Vec3().copy(params[0].pivot1), 217 | bodies[params[0].body2], 218 | new CANNON.Vec3().copy(params[0].pivot2) 219 | ) 220 | break 221 | } 222 | joint.collideConnected = params[0].collision 223 | joint._id_ = id 224 | world.addConstraint(joints[id] = joint) 225 | break 226 | case "remove": 227 | world.removeConstraint(joint) 228 | joints[id] = null 229 | break 230 | case "eval": 231 | eval("const joint = joints[" + id + "];" + params[0]) 232 | break 233 | } 234 | } 235 | 236 | function shapeCommand(body, params) { 237 | if (!body) return 238 | let id = params.shift() 239 | let shape = body._shapes_[id] 240 | if (!shape && params[0] !== "create") return 241 | switch (params.shift()) { 242 | case "create": 243 | if (shape) 244 | body.removeShape(shape) 245 | let quat = (new CANNON.Quaternion()).copy(params[0].quaternion) 246 | switch (params[0].type) { 247 | case "sphere": shape = new CANNON.Sphere(params[0].size.x / 2); break 248 | case "cylinder": shape = new CANNON.Cylinder(params[0].size.x / 2, params[0].size.x / 2, params[0].size.y, 16); quat.mult(cyloff, quat); break 249 | default: shape = new CANNON.Box(new CANNON.Vec3().copy(params[0].size).scale(0.5)) 250 | } 251 | shape.material = body.material 252 | shape._id_ = id 253 | body.addShape(body._shapes_[id] = shape, (new CANNON.Vec3()).copy(params[0].position), quat) 254 | body.updateMassProperties() 255 | break 256 | case "remove": 257 | // body.removeShape(shape) 258 | let i = body.shapes.indexOf(shape) 259 | if (i >= 0) body.shapes.splice(i, 1) 260 | body._shapes_[id] = null 261 | body.updateMassProperties() 262 | break 263 | case "eval": 264 | eval("const body = bodies[" + body._id_ + "],shape = body._shapes_[" + id + "];" + params[0]) 265 | break 266 | } 267 | } 268 | 269 | 270 | function onCollide(e) { 271 | let b1 = e.contact.bi 272 | let b2 = e.contact.bj 273 | if (this === b1 && (b1._emitsWith_ & b2.collisionFilterGroup)) { 274 | postMessage("world body " + b1._id_ + " emits " + cmd.stringifyParam({ 275 | event: "collision", 276 | body1: b1._id_, 277 | body2: b2._id_, 278 | shape1: e.contact.si._id_, 279 | shape2: e.contact.sj._id_ 280 | })) 281 | } 282 | if (this === b2 && (b2._emitsWith_ & b1.collisionFilterGroup)) { 283 | postMessage("world body " + b2._id_ + " emits " + cmd.stringifyParam({ 284 | event: "collision", 285 | body1: b2._id_, 286 | body2: b1._id_, 287 | shape1: e.contact.sj._id_, 288 | shape2: e.contact.si._id_ 289 | })) 290 | } 291 | } 292 | init() -------------------------------------------------------------------------------- /src/components/grabbing.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("grabbing", { 4 | schema: { 5 | hideOnGrab: { type: "boolean", default: false }, 6 | grabDistance: { type: "number", default: 1 }, 7 | attractHand: { type: "boolean", default: true }, 8 | avoidWalls: { type: "boolean", default: true }, 9 | }, 10 | 11 | init() { 12 | this._enableHands = this._enableHands.bind(this) 13 | this._onKeyDown = this._onKeyDown.bind(this) 14 | this._onKeyUp = this._onKeyUp.bind(this) 15 | this._onMouseDown = this._onMouseDown.bind(this) 16 | this._onMouseUp = this._onMouseUp.bind(this) 17 | this._onWheel = this._onWheel.bind(this) 18 | this._onButtonChanged = this._onButtonChanged.bind(this) 19 | this._onTouchTap = this._onTouchTap.bind(this) 20 | this._onTouchHold = this._onTouchHold.bind(this) 21 | 22 | this._btnPress = {} 23 | this._btnFlex = {} 24 | this._keysDown = {} 25 | this._grabCount = 0 26 | this._gamepadBtns = [] 27 | 28 | this._hands = ["head", "left", "right"] 29 | this._head = {} 30 | this._left = {} 31 | this._right = {} 32 | this._head.hand = this.el.querySelector("a-camera") 33 | this._left.hand = this.el.querySelector("a-hand[side=\"left\"]") 34 | this._right.hand = this.el.querySelector("a-hand[side=\"right\"]") 35 | this._head.glove = this._head.hand.ensure(".hitbox", "a-sphere", { class: "hitbox", body: "type:kinematic;", radius: 0.25 }) 36 | this._left.glove = this._left.hand.ensure("a-glove") 37 | this._right.glove = this._right.hand.ensure("a-glove") 38 | 39 | this._left.glove.setAttribute("visible", false) 40 | this._right.glove.setAttribute("visible", false) 41 | for (let hand of this._hands) { 42 | let _hand = "_" + hand 43 | this[_hand].hand.addEventListener("buttonchanged", this._enableHands) 44 | } 45 | 46 | this._head.ray = this._head.hand.ensure(".grabbing-ray", "a-entity", { 47 | class: "grabbing-ray", 48 | raycaster: { 49 | deep: true, 50 | objects: "[wall], [grabbable]", 51 | autoRefresh: false, 52 | // showLine: true, 53 | } 54 | }) 55 | this._head.buttonRay = this._head.hand.ensure(".button.ray", "a-entity", { 56 | class: "button ray", 57 | raycaster: { 58 | deep: true, 59 | objects: "[wall], [button]", 60 | far: 1, 61 | autoRefresh: false, 62 | // showLine: true, 63 | } 64 | }) 65 | this._head.reticle = this._head.hand.ensure(".reticle", "a-plane", { 66 | class: "reticle", 67 | material: "transparent:true; shader:flat;", 68 | position: "0 0 -0.0078125", 69 | scale: "0.00048828125 0.00048828125 0.00048828125" 70 | }) 71 | this._head.anchor = this._head.ray.ensure(".grabbing.anchor", "a-entity", { class: "grabbing anchor", visible: false, body: "type:kinematic;autoShape:false;" }) 72 | }, 73 | 74 | update(oldData) { 75 | for (let hand of this._hands) { 76 | let _hand = "_" + hand 77 | if (this[_hand].ray) 78 | this[_hand].ray.setAttribute("raycaster", "far", this.data.grabDistance + (hand === "head" ? 1 : 0.1875)) 79 | } 80 | }, 81 | 82 | play() { 83 | document.addEventListener("keydown", this._onKeyDown) 84 | document.addEventListener("keyup", this._onKeyUp) 85 | this.el.sceneEl.canvas.addEventListener("mousedown", this._onMouseDown) 86 | this.el.sceneEl.canvas.addEventListener("mouseup", this._onMouseUp) 87 | this.el.sceneEl.canvas.addEventListener("wheel", this._onWheel) 88 | for (let hand of [this._left.hand, this._right.hand]) { 89 | // hand.addEventListener("buttonchanged", this._enableHands) 90 | hand.addEventListener("buttonchanged", this._onButtonChanged) 91 | } 92 | this.el.sceneEl.canvas.addEventListener("tap", this._onTouchTap) 93 | this.el.sceneEl.canvas.addEventListener("hold", this._onTouchHold) 94 | }, 95 | 96 | pause() { 97 | document.removeEventListener("keydown", this._onKeyDown) 98 | document.removeEventListener("keyup", this._onKeyUp) 99 | this.el.sceneEl.canvas.removeEventListener("mousedown", this._onMouseDown) 100 | this.el.sceneEl.canvas.removeEventListener("mouseup", this._onMouseUp) 101 | this.el.sceneEl.canvas.removeEventListener("wheel", this._onWheel) 102 | for (let hand of [this._left.hand, this._right.hand]) { 103 | // hand.removeEventListener("buttonchanged", this._enableHands) 104 | hand.removeEventListener("buttonchanged", this._onButtonChanged) 105 | } 106 | this.el.sceneEl.canvas.removeEventListener("tap", this._onTouchTap) 107 | this.el.sceneEl.canvas.removeEventListener("hold", this._onTouchHold) 108 | }, 109 | 110 | remove() { 111 | for (let hand of this._hands) { 112 | let _hand = "_" + hand 113 | this.drop(hand) 114 | this[_hand].glove.copyWorldPosRot(this[_hand].hand) 115 | let flex = 0.25 116 | for (let finger = 0; finger < 5; finger++) { 117 | this.emit("fingerflex", this[_hand].glove, this[_hand].grabbed, { hand: hand, finger: finger, flex: flex }) 118 | } 119 | } 120 | }, 121 | 122 | tick(time, timeDelta) { 123 | // handle gamepads 124 | for (let i = 0; i < 16; i++) 125 | this._gamepadBtns[i] = 0 126 | for (i = 0, len = navigator.getGamepads().length; i < len; i++) { 127 | gamepad = navigator.getGamepads()[i] 128 | if (gamepad) { 129 | for (let i = 0; i < 16; i++) 130 | this._gamepadBtns[i] += gamepad.buttons[i]?.pressed || 0 131 | } 132 | } 133 | 134 | if ((this._gamepadBtns[4] || this._gamepadBtns[5]) && !this._grabBtn) { 135 | this._setDevice("gamepad") 136 | console.log("grabbing!", this._gamepadBtns[4], this._gamepadBtns[5], this._grabBtn) 137 | this.grab() 138 | } 139 | if (this._grabBtn && !(this._gamepadBtns[4] || this._gamepadBtns[5])) { 140 | console.log("dropping!", this._gamepadBtns[4], this._gamepadBtns[5], this._grabBtn) 141 | this.drop() 142 | } 143 | if ((this._gamepadBtns[6] || this._gamepadBtns[7]) && !this._useBtn0) this.useDown() 144 | if ((this._gamepadBtns[0]) && !this._useBtn1) this.useDown("head", 1) 145 | if ((this._gamepadBtns[1]) && !this._useBtn2) this.useDown("head", 2) 146 | if (this._gamepadBtns[2]) { 147 | if (this._gamepadBtns[12]) this.moveHeadHand(0, -0.03125) 148 | if (this._gamepadBtns[13]) this.moveHeadHand(0, 0.03125) 149 | if (this._gamepadBtns[14]) this.moveHeadHand(0, 0, -0.03125) 150 | if (this._gamepadBtns[15]) this.moveHeadHand(0, 0, 0.03125) 151 | } else { 152 | if (this._gamepadBtns[12]) this.moveHeadHand(-0.03125) 153 | if (this._gamepadBtns[13]) this.moveHeadHand(0.03125) 154 | if (this._gamepadBtns[14]) this.moveHeadHand(0, 0, 0, 0.03125) 155 | if (this._gamepadBtns[15]) this.moveHeadHand(0, 0, 0, -0.03125) 156 | } 157 | this._grabBtn = false 158 | this._useBtn0 = false 159 | this._useBtn1 = false 160 | this._useBtn2 = false 161 | for (i = 0, len = navigator.getGamepads().length; i < len; i++) { 162 | gamepad = navigator.getGamepads()[i] 163 | if (gamepad) { 164 | this._grabBtn = this._grabBtn || this._gamepadBtns[4] || this._gamepadBtns[5] 165 | this._useBtn0 = this._useBtn0 || this._gamepadBtns[6] || this._gamepadBtns[7] 166 | this._useBtn1 = this._useBtn1 || this._gamepadBtns[0] 167 | this._useBtn2 = this._useBtn2 || this._gamepadBtns[1] 168 | } 169 | } 170 | 171 | let headPos = THREE.Vector3.temp() 172 | let delta = THREE.Vector3.temp() 173 | let palmDelta = THREE.Vector3.temp() 174 | headPos.copy(this._head.hand.object3D.position) 175 | this._head.hand.object3D.parent.localToWorld(headPos) 176 | 177 | for (let hand of this._hands) { 178 | let _hand = "_" + hand 179 | 180 | // keep hands out of walls 181 | if (this.data.avoidWalls && this[_hand]._occlusionRay) { 182 | let palm = this[_hand].glove.querySelector(".palm") || this[_hand].glove 183 | this[_hand].glove.copyWorldPosRot(this[_hand].hand) 184 | 185 | this[_hand]._occlusionRay.object3D.position.copy(headPos) 186 | palm.object3D.getWorldPosition(delta) 187 | this[_hand].glove.object3D.getWorldPosition(palmDelta) 188 | palmDelta.sub(delta) 189 | delta.sub(headPos) 190 | let handDist = delta.length() 191 | delta.normalize() 192 | this[_hand]._occlusionRay.setAttribute("raycaster", "direction", `${delta.x} ${delta.y} ${delta.z}`) 193 | this[_hand]._occlusionRay.setAttribute("raycaster", "far", handDist + 0.03125) 194 | 195 | let ray = this[_hand]._occlusionRay.components.raycaster 196 | ray.refreshObjects() 197 | let hit = ray.intersections[0] 198 | if (hit) { 199 | // this[_hand].glove.object3D.position.copy(hit.point) 200 | let dist = delta.copy(hit.point).sub(headPos).length() 201 | this[_hand].glove.object3D.position.copy(headPos).add(palmDelta).add(delta.normalize().multiplyScalar(dist - 0.03125)) 202 | this[_hand].glove.object3D.parent.worldToLocal(this[_hand].glove.object3D.position) 203 | } 204 | } 205 | 206 | // handle grabbables 207 | let reticleMode = "default" 208 | if (this[_hand].grabbed) { 209 | let ray = this[_hand].ray.components.raycaster 210 | ray.refreshObjects() 211 | if (!this[_hand].grabbed.components.grabbable?.data.immovable) { 212 | if (this[_hand].grabbed.components.grabbable?.data.avoidWalls) { 213 | for (let hit of ray.intersections) { 214 | if (hit && hit.el.components.wall && hit.distance < -this[_hand].anchor.object3D.position.z) { 215 | this[_hand].anchor.object3D.position.multiplyScalar(0.5) 216 | } 217 | } 218 | } 219 | let delta = THREE.Vector3.temp().copy(this[_hand].grabbed.object3D.position) 220 | this[_hand].grabbed.copyWorldPosRot(this[_hand].anchor) 221 | delta.sub(this[_hand].grabbed.object3D.position) 222 | if (delta.length() > 1 && !this.ironGrip) this.drop(hand) 223 | this.ironGrip=false 224 | } 225 | if (this[_hand].reticle) this._setReticle(null) 226 | } else { 227 | if (this[_hand].ray) { 228 | let ray = this[_hand].ray.components.raycaster 229 | ray.refreshObjects() 230 | let hit = ray.intersections[0] 231 | if (hit && hit.el.components.grabbable) { 232 | if (this[_hand]._lastHit !== hit.el) { 233 | if (this[_hand]._lastHit) 234 | this.emit("unreachable", this[_hand].glove, this[_hand]._lastHit) 235 | this[_hand]._lastHit = hit.el 236 | this.emit("reachable", this[_hand].glove, this[_hand]._lastHit) 237 | this._flexFinger(hand, 5, -0.125, true) 238 | this._flexFinger(hand, 0, 0, true) 239 | } 240 | if (this[_hand].reticle) { 241 | reticleMode = "grab" 242 | } 243 | } else { 244 | if (this[_hand]._lastHit) { 245 | this.emit("unreachable", this[_hand].glove, this[_hand]._lastHit) 246 | this._restoreUserFlex(hand) 247 | } 248 | this[_hand]._lastHit = null 249 | } 250 | } 251 | 252 | // handle buttons 253 | if (this[_hand].buttonRay) { 254 | let ray = this[_hand].buttonRay.components.raycaster 255 | ray.refreshObjects() 256 | let hit = ray.intersections[0] 257 | if (hit && hit.el.components.button != null) { 258 | if (this[_hand]._lastButton !== hit.el) { 259 | if (this[_hand]._lastButton) 260 | this.emit("unhover", this[_hand].glove, this[_hand]._lastButton) 261 | this[_hand]._lastButton = hit.el 262 | this.emit("hover", this[_hand].glove, this[_hand]._lastButton) 263 | this._flexFinger(hand, 7, 1, true) 264 | this._flexFinger(hand, 1, -0.125, true) 265 | } 266 | if (hit.distance < 0.125) { 267 | if (this[_hand]._lastPress !== hit.el) { 268 | if (this[_hand]._lastPress) { 269 | this.emit("unpress", this[_hand].glove, this[_hand]._lastPress) 270 | this[_hand]._lastPress.removeState("pressed") 271 | } 272 | this[_hand]._lastPress = hit.el 273 | this.emit("press", this[_hand].glove, this[_hand]._lastPress) 274 | this[_hand]._lastPress.addState("pressed") 275 | } 276 | } else { 277 | if (this[_hand]._lastPress) { 278 | this.emit("unpress", this[_hand].glove, this[_hand]._lastPress) 279 | this[_hand]._lastPress.removeState("pressed") 280 | } 281 | this[_hand]._lastPress = null 282 | } 283 | if (this[_hand].reticle) { 284 | reticleMode = "push" 285 | } 286 | } else { 287 | if (this[_hand]._lastPress) { 288 | this.emit("unpress", this[_hand].glove, this[_hand]._lastPress) 289 | this[_hand]._lastPress.removeState("pressed") 290 | } 291 | this[_hand]._lastPress = null 292 | if (this[_hand]._lastButton) { 293 | this.emit("unhover", this[_hand].glove, this[_hand]._lastButton) 294 | this._restoreUserFlex(hand) 295 | } 296 | this[_hand]._lastButton = null 297 | } 298 | } 299 | if (this[_hand].reticle) this._setReticle(reticleMode) 300 | } 301 | 302 | // Track velocity 303 | this[_hand].lastGlovePos = this[_hand].lastGlovePos || new THREE.Vector3() 304 | this[_hand].lastGrabbedPos = this[_hand].lastGrabbedPos || new THREE.Vector3() 305 | this[_hand].gloveVelocity = this[_hand].gloveVelocity || new THREE.Vector3() 306 | this[_hand].grabbedVelocity = this[_hand].grabbedVelocity || new THREE.Vector3() 307 | let pos = THREE.Vector3.temp() 308 | if (this[_hand].glove) { 309 | this[_hand].glove.object3D.localToWorld(pos.set(0, 0, 0)) 310 | this[_hand].gloveVelocity.copy(pos).sub(this[_hand].lastGlovePos).multiplyScalar(500 / timeDelta) 311 | this[_hand].lastGlovePos.copy(pos) 312 | } 313 | if (this[_hand].grabbed) { 314 | this[_hand].grabbed.object3D.localToWorld(pos.set(0, 0, 0)) 315 | this[_hand].grabbedVelocity.copy(pos).sub(this[_hand].lastGrabbedPos).multiplyScalar(500 / timeDelta) 316 | this[_hand].lastGrabbedPos.copy(pos) 317 | } 318 | if (hand === "head") this[_hand].gloveVelocity.copy(this[_hand].grabbedVelocity) 319 | } 320 | 321 | // Update The Matrix! 🐱‍💻 322 | this.el.object3D.updateWorldMatrix(true, true) 323 | }, 324 | 325 | toggleGrab(hand = "head") { 326 | let _hand = "_" + hand 327 | if (this[_hand].grabbed) this.drop(hand) 328 | else this.grab(hand) 329 | }, 330 | grab(hand = "head") { 331 | let _hand = "_" + hand 332 | if (!this[_hand].ray) return 333 | if (this[_hand].grabbed) return 334 | let ray = this[_hand].ray.components.raycaster 335 | ray.refreshObjects() 336 | let hit = ray.intersections[0] 337 | if (hit && hit.el.components.grabbable) { 338 | if (hand === "head" && this.data.attractHand) this[_hand].ray.setAttribute("animation__pos", { 339 | property: "position", 340 | to: { x: 0, y: -0.125, z: 0 }, 341 | dur: 256 342 | }) 343 | this.dropObject(hit.el) 344 | this[_hand].grabbed = hit.el 345 | this[_hand].anchor.copyWorldPosRot(this[_hand].grabbed) 346 | this[_hand].anchor.components.body.commit() 347 | if (this[_hand].grabbed.components.body != null) { 348 | if (!this[_hand].grabbed.components.grabbable?.data.immovable) 349 | this[_hand].anchor.setAttribute("joint__grab", { body2: this[_hand].grabbed, type: "lock" }) 350 | this[_hand].isPhysical = true 351 | } else { 352 | this[_hand].isPhysical = false 353 | } 354 | this[_hand].anchor.removeAttribute("animation__pos") 355 | this[_hand].anchor.removeAttribute("animation__rot") 356 | let delta = hit.distance 357 | if (hand === "head") delta -= 0.5 358 | else delta -= 0.09375 359 | if (this[_hand].grabbed.components.grabbable.data.fixed) { 360 | let pos = THREE.Vector3.temp().copy(this[_hand].grabbed.components.grabbable.data.fixedPosition) 361 | if (hand === "left") pos.x *= -1 362 | if (hand === "head") pos.x = 0 363 | let quat = THREE.Quaternion.temp().copy(this[_hand].ray.object3D.quaternion).conjugate() 364 | pos.applyQuaternion(quat) 365 | pos.z += -0.09375 366 | this[_hand].anchor.setAttribute("animation__pos", { 367 | property: "position", 368 | to: { x: pos.x, y: pos.y, z: pos.z }, 369 | dur: 256 370 | }) 371 | let rot = { x: 0, y: 0, z: 0 } 372 | if (hand === "left") rot.y = 45 373 | if (hand === "right") rot.y = -45 374 | this[_hand].anchor.setAttribute("animation__rot", { 375 | property: "rotation", 376 | to: rot, 377 | dur: 256 378 | }) 379 | } else if (this.data.attractHand && !this[_hand].grabbed.components.grabbable?.data.immovable) { 380 | this[_hand].anchor.setAttribute("animation__pos", { 381 | property: "object3D.position.z", 382 | to: this[_hand].anchor.object3D.position.z + delta, 383 | dur: 256 384 | }) 385 | } 386 | if (this.data.hideOnGrab || this[_hand].grabbed.components.grabbable.data.hideOnGrab) 387 | this[_hand].glove.setAttribute("visible", false) 388 | // if (this[_hand].glove.components.body) 389 | this[_hand].glove.setAttribute("body", "collidesWith", 0) 390 | this.emit("grab", this[_hand].glove, this[_hand].grabbed, { intersection: hit }) 391 | this._grabCount = Math.min(2, this._grabCount + 1) 392 | this.el.addState("grabbing") 393 | this[_hand].grabbed.addState("grabbed") 394 | this.sticky = true 395 | this._flexFinger(hand, 5, 0, true) 396 | setTimeout(() => { 397 | let flexes = this[_hand].grabbed.components.grabbable.data.fingerFlex.map(x => parseFloat(x)) || [0.5] 398 | this._flexFinger(hand, 5, flexes.pop() || 0, true) 399 | let finger = 0 400 | for (let flex of flexes) this._flexFinger(hand, finger++, flex || 0, true) 401 | this.sticky = false 402 | }, 256) 403 | } 404 | }, 405 | drop(hand = "head") { 406 | let _hand = "_" + hand 407 | if (this.sticky) return 408 | this[_hand].anchor.removeAttribute("animation__rot") 409 | this[_hand].anchor.removeAttribute("animation__pos") 410 | this[_hand].glove.setAttribute("visible", true) 411 | this[_hand].anchor.removeAttribute("joint__grab") 412 | this[_hand].anchor.setAttribute("position", "0 0 0") 413 | this[_hand].anchor.setAttribute("rotation", "0 0 0") 414 | setTimeout(() => { 415 | // if (this[_hand].glove.components.body) 416 | this[_hand].glove.setAttribute("body", "collidesWith", 1) 417 | }, 1024) 418 | if (this[_hand].grabbed) { 419 | this.emit("drop", this[_hand].glove, this[_hand].grabbed) 420 | this._grabCount = Math.max(0, this._grabCount - 1) 421 | if (!this._grabCount) 422 | this.el.removeState("grabbing") 423 | this._restoreUserFlex(hand) 424 | this[_hand].grabbed.removeState("grabbed") 425 | if (this[_hand].grabbed.components.grabbable?.data.kinematicGrab && !this[_hand].grabbed.components.grabbable?.data.immovable) { 426 | this[_hand].grabbed.components.body?.applyWorldImpulse(this[_hand].gloveVelocity, this[_hand].lastGlovePos) 427 | this[_hand].grabbed.components.body?.applyWorldImpulse(this[_hand].grabbedVelocity, this[_hand].lastGrabbedPos) 428 | } 429 | this[_hand].grabbed = null 430 | if (hand === "head") { 431 | this[_hand].ray.removeAttribute("animation__pos") 432 | this[_hand].ray.object3D.position.y = 0 433 | } 434 | } 435 | }, 436 | dropObject(el) { 437 | for (let hand of this._hands) { 438 | let _hand = "_" + hand 439 | if (this[_hand].grabbed === el) this.drop(hand) 440 | } 441 | }, 442 | use(hand = "head", button = 0) { 443 | let _hand = "_" + hand 444 | this.useDown(hand, button) 445 | setTimeout(() => { 446 | this.useUp(hand, button) 447 | }, 32) 448 | }, 449 | useDown(hand = "head", button = 0) { 450 | let _hand = "_" + hand 451 | // if (!this[_hand].grabbed) return this.grab(hand) 452 | if (this[_hand].grabbed) { 453 | this._flexFinger(hand, Math.max(0, 1 - button), 0.5, true) 454 | this.emit("usedown", this[_hand].glove, this[_hand].grabbed, { button: button }) 455 | this.emit("use" + (button + 1) + "down", this[_hand].glove, this[_hand].grabbed, { button: button }) 456 | } else if (this[_hand]._lastButton) { 457 | this._flexFinger(hand, 0, 0.5, true) 458 | this[_hand]._lastClick = this[_hand]._lastButton 459 | this.emit("press", this[_hand].glove, this[_hand]._lastClick, { button: button }) 460 | this[_hand]._lastClick.addState("pressed") 461 | } else { 462 | this.emit("usedown", this[_hand].glove, this[_hand].grabbed, { button: button }) 463 | this.emit("use" + (button + 1) + "down", this[_hand].glove, this[_hand].grabbed, { button: button }) 464 | } 465 | }, 466 | useUp(hand = "head", button = 0) { 467 | let _hand = "_" + hand 468 | if (this[_hand].grabbed) { 469 | this._flexFinger(hand, Math.max(0, 1 - button), 0, true) 470 | this.emit("useup", this[_hand].glove, this[_hand].grabbed, { button: button }) 471 | this.emit("use" + (button + 1) + "up", this[_hand].glove, this[_hand].grabbed, { button: button }) 472 | } else if (this[_hand]._lastClick) { 473 | this._flexFinger(hand, 0, 0, true) 474 | this.emit("unpress", this[_hand].glove, this[_hand]._lastClick) 475 | this[_hand]._lastClick.removeState("pressed") 476 | this[_hand]._lastClick = null 477 | } else { 478 | this.emit("useup", this[_hand].glove, this[_hand].grabbed, { button: button }) 479 | this.emit("use" + (button + 1) + "up", this[_hand].glove, this[_hand].grabbed, { button: button }) 480 | } 481 | }, 482 | moveHeadHand(pz = 0, rx = 0, ry = 0, rz = 0) { 483 | this._head.anchor.object3D.position.z = Math.min(Math.max(-1.5, this._head.anchor.object3D.position.z + pz), -0.125) 484 | let quat = THREE.Quaternion.temp().set(rx, ry, rz, 1).normalize() 485 | this._head.anchor.object3D.quaternion.premultiply(quat) 486 | }, 487 | 488 | emit(eventtype, glove, grabbed, e = {}) { 489 | e.grabbing = this.el 490 | e.grabbedElement = grabbed 491 | e.gloveElement = glove 492 | for (let _hand of this._hands) { 493 | if (this["_" + _hand].glove === glove) e.hand = _hand 494 | } 495 | glove.emit(eventtype, e, true) 496 | if (grabbed) grabbed.emit(eventtype, e, true) 497 | }, 498 | 499 | _setReticle(mode) { 500 | if (!this._head.reticle) return 501 | if (this._head._reticleMode === mode) return 502 | let src = "data:image/gif;base64,R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIVlI+py+0PIwQgghDqu9lqCYbiSBoFADs=" 503 | switch (mode) { 504 | case "grab": 505 | src = "data:image/gif;base64,R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAI1lC8AyLkQgloMSotrVHsnhHWXdISS+DzRimIWy3Ii7CU0Tdn3mr93bvDBgMFfozg8OiaTQwEAOw==" 506 | break 507 | case "push": 508 | src = "data:image/gif;base64,R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIylA1wywIRVGMvTgrlRTltl3Wao1RmB0YVxEYqu7ZwGstWbWdcPh94O0rZgjsZEZFIagoAOw==" 509 | break 510 | } 511 | this._head.reticle.setAttribute("src", src) 512 | this._head._reticleMode = mode 513 | }, 514 | 515 | _enableHands() { 516 | this._setDevice("vrcontroller") 517 | for (let hand of this._hands) { 518 | let _hand = "_" + hand 519 | this[_hand].hand.removeEventListener("buttonchanged", this._enableHands) 520 | 521 | let boxsize = 0.0625 522 | this[_hand].glove.ensure(".hitbox", "a-box", { class: "hitbox", visible: false, position: "0 0 0.03125", width: boxsize / 2, height: boxsize, depth: boxsize * 2 }) 523 | this[_hand].glove.setAttribute("body", "type:kinematic;") 524 | 525 | if (hand === "head") continue 526 | this[_hand]._occlusionRay = this.el.sceneEl.ensure(".occlusion-ray." + hand, "a-entity", { 527 | class: "occlusion-ray " + hand, 528 | raycaster: { 529 | deep: true, 530 | objects: "[wall]", 531 | autoRefresh: false 532 | } 533 | }) 534 | 535 | let palm = this[_hand].glove.querySelector(".palm") || this[_hand].glove 536 | this[_hand].ray = palm.ensure(".grabbing.ray", "a-entity", { 537 | class: "grabbing ray", position: hand === "left" ? "-0.0625 0 0.0625" : "0.0625 0 0.0625", rotation: hand === "left" ? "0 -45 0" : "0 45 0", 538 | raycaster: { 539 | deep: true, 540 | objects: "[wall], [grabbable]", 541 | autoRefresh: false, 542 | // showLine: true, 543 | } 544 | }) 545 | this[_hand].buttonRay = palm.ensure(".button.ray", "a-entity", { 546 | class: "button ray", position: "0 0.03125 0", 547 | raycaster: { 548 | deep: true, 549 | objects: "[wall], [button]", 550 | far: 0.5, 551 | autoRefresh: false, 552 | // showLine: true, 553 | } 554 | }) 555 | this[_hand].anchor = this[_hand].ray.ensure(".grabbing.anchor", "a-entity", { class: "grabbing anchor", visible: "false", body: "type:kinematic;autoShape:false;" }) 556 | this[_hand].glove.setAttribute("visible", true) 557 | } 558 | 559 | this._head.ray = null 560 | this._head.buttonRay = null 561 | this._head.reticle.setAttribute("position", "0 0 1") 562 | this._head.reticle.setAttribute("visible", "false") 563 | this._head.reticle = null 564 | this.update() 565 | }, 566 | 567 | _flexFinger(hand, finger, flex, priority = false) { 568 | let _hand = "_" + hand 569 | this[_hand].userFlex = this[_hand].userFlex || [] 570 | if (priority) this[_hand].priorityFlex = true 571 | if (finger < 5) { 572 | if (priority || !this[_hand].priorityFlex) this.emit("fingerflex", this[_hand].glove, this[_hand].grabbed, { hand: hand, finger: finger, flex: flex }) 573 | if (!priority) this[_hand].userFlex[finger] = flex 574 | } else { 575 | for (finger -= 5; finger < 5; finger++) { 576 | if (priority || !this[_hand].priorityFlex) this.emit("fingerflex", this[_hand].glove, this[_hand].grabbed, { hand: hand, finger: finger, flex: flex }) 577 | if (!priority) this[_hand].userFlex[finger] = flex 578 | } 579 | } 580 | }, 581 | _restoreUserFlex(hand) { 582 | let _hand = "_" + hand 583 | this[_hand].userFlex = this[_hand].userFlex || [] 584 | this[_hand].priorityFlex = false 585 | for (let finger = 0; finger < 5; finger++) { 586 | let flex = this[_hand].userFlex[finger] || 0 587 | this.emit("fingerflex", this[_hand].glove, this[_hand].grabbed, { hand: hand, finger: finger, flex: flex }) 588 | } 589 | }, 590 | 591 | _onKeyDown(e) { 592 | if (e.code === "KeyE" && !this._keysDown[e.code]) { 593 | this._setDevice("desktop") 594 | this.grab() 595 | } 596 | this._keysDown[e.code] = true 597 | }, 598 | _onKeyUp(e) { 599 | if (e.code === "KeyE" && this._keysDown[e.code]) { 600 | this.drop() 601 | } 602 | this._keysDown[e.code] = false 603 | }, 604 | _onMouseDown(e) { 605 | this._setDevice("desktop") 606 | let btn = e.button 607 | this.useDown("head", btn ? ((btn % 2) ? btn + 1 : btn - 1) : btn) 608 | }, 609 | _onWheel(e) { 610 | this._setDevice("desktop") 611 | let x = 0, y = 0, z = 0 612 | if (this._keysDown["Digit3"] && e.deltaY > 0) z += -0.125 613 | if (this._keysDown["Digit3"] && e.deltaY < 0) z += 0.125 614 | if (this._keysDown["Digit2"] && e.deltaY > 0) y += -0.125 615 | if (this._keysDown["Digit2"] && e.deltaY < 0) y += 0.125 616 | if (this._keysDown["Digit1"] && e.deltaY > 0) x += 0.125 617 | if (this._keysDown["Digit1"] && e.deltaY < 0) x += -0.125 618 | if (x || y || z) return this.moveHeadHand(0, x, y, z) 619 | if (e.deltaY > 0) return this.moveHeadHand(0.125) 620 | if (e.deltaY < 0) return this.moveHeadHand(-0.125) 621 | }, 622 | _onMouseUp(e) { 623 | let btn = e.button 624 | this.useUp("head", btn ? ((btn % 2) ? btn + 1 : btn - 1) : btn) 625 | }, 626 | _onTouchTap(e) { 627 | this._setDevice("touch") 628 | this.use() 629 | }, 630 | _onTouchHold(e) { 631 | this._setDevice("touch") 632 | this.toggleGrab() 633 | }, 634 | _onButtonChanged(e) { 635 | this._setDevice("vrcontroller") 636 | let hand = e.srcElement.getAttribute("tracked-controls").hand 637 | let _hand = "_" + hand 638 | let finger = -1 639 | let flex = 0 640 | if (e.detail.state.touched) flex = 0.5 641 | if (e.detail.state.pressed) flex = 1 642 | if (e.detail.state.value) flex = 0.5 + e.detail.state.value / 2 643 | this._btnFlex[hand + e.detail.id] = flex 644 | switch (e.detail.id) { 645 | case 0: // Trigger 646 | finger = 1 647 | if (e.detail.state.pressed && !this._btnPress[hand + e.detail.id]) this.useDown(hand) 648 | if (!e.detail.state.pressed && this._btnPress[hand + e.detail.id]) this.useUp(hand) 649 | break 650 | case 1: // Grip 651 | if (flex <= 0 || flex >= 1) { 652 | finger = 7 653 | this._fist = flex > 0.5 654 | } else { 655 | this._flexFinger(hand, 2, this._fist ? 0 : 1) 656 | finger = 3 657 | } 658 | if (e.detail.state.pressed && !this._btnPress[hand + e.detail.id]) this.grab(hand) 659 | if (!e.detail.state.pressed && this._btnPress[hand + e.detail.id]) this.drop(hand) 660 | break 661 | case 3: // Thumbstick 662 | finger = 0 663 | flex = Math.max(this._btnFlex[hand + 3] || 0, this._btnFlex[hand + 4] || 0, this._btnFlex[hand + 5] || 0) 664 | break 665 | case 4: // A/X 666 | finger = 0 667 | flex = Math.max(this._btnFlex[hand + 3] || 0, this._btnFlex[hand + 4] || 0, this._btnFlex[hand + 5] || 0) 668 | if (e.detail.state.pressed && !this._btnPress[hand + e.detail.id]) this.useDown(hand, 1) 669 | if (!e.detail.state.pressed && this._btnPress[hand + e.detail.id]) this.useUp(hand, 1) 670 | break 671 | case 5: // B/Y 672 | finger = 0 673 | flex = Math.max(this._btnFlex[hand + 3] || 0, this._btnFlex[hand + 4] || 0, this._btnFlex[hand + 5] || 0) 674 | if (e.detail.state.pressed && !this._btnPress[hand + e.detail.id]) this.useDown(hand, 2) 675 | if (!e.detail.state.pressed && this._btnPress[hand + e.detail.id]) this.useUp(hand, 2) 676 | break 677 | } 678 | this._btnPress[hand + e.detail.id] = e.detail.state.pressed 679 | this._flexFinger(hand, finger, flex) 680 | }, 681 | 682 | _setDevice(device) { 683 | if (this.device === device) return 684 | this.el.removeState(this.device || "noinput") 685 | this.device = device 686 | this.el.addState(this.device || "noinput") 687 | } 688 | }) 689 | 690 | require("./grabbing/button") 691 | require("./grabbing/climbable") 692 | require("./grabbing/fingerflex") 693 | require("./grabbing/grabbable") 694 | require("./grabbing/receptacle") 695 | -------------------------------------------------------------------------------- /src/components/grabbing.md: -------------------------------------------------------------------------------- 1 | # grabbing 2 | 3 | Components to facilitate grabbable and usable items. 4 | 5 | Add the `grabbing` component to your player rig like so: 6 | 7 | ```html 8 | 9 | ``` 10 | 11 | This makes it possible to grab and use grabbable objects using the following controls. 12 | 13 | | Action | Controller | Desktop | Touch | 14 | | ---------------- | -------------------- | ---------------- | -------- | 15 | | Grab/drop | Grip/shoulder Button | E | Long tap | 16 | | Primary use | Trigger | Left click | Tap | 17 | | Secondary | A | Right click | 18 | | Tertiary | B | Middle click | 19 | | Move/rotate hand | X + D-pad | 1/2/3 + scroll wheel | 20 | 21 | 22 | ## Properties 23 | 24 | | Property | Description | Default | 25 | | ------------ | --------------------------------------------- | ------- | 26 | | hideOnGrab | Hide the glove when grabbing | false | 27 | | grabDistance | Maximum distance to grab object from | 1 | 28 | | attractHand | Make object attract to your hand when grabbed | true | 29 | | avoidWalls | Keep hands from passing through walls | true | 30 | 31 | 32 | ## Methods 33 | 34 | `hand` parameter is either `"head"`(default), `"left"` or `"right"`. 35 | `button` parameter is 0 - 2, where 0 is the primary use button. 36 | 37 | | Method | Description | 38 | | ------------------------- | ----------------------------------------------------- | 39 | | toggleGrab(hand) | Drop if holding something, attempt to grab otherwise. | 40 | | grab(hand) | Attempt to grab something. | 41 | | use(hand, button) | Shortly use grabbable. | 42 | | useDown(hand, button) | Start using grabbable. | 43 | | useUp(hand, button) | Stop using grabbable. | 44 | | drop(hand) | Drop grabbable. | 45 | | dropObject(el) | Drop specified grabbable if held. | 46 | | moveHeadHand(pz,rx,ry,rz) | Move/rotate non-VR hand. | 47 | 48 | 49 | ## Events 50 | 51 | These events are emitted by both the glove and the `grabbable` that it's grabbing, if any. 52 | Events will bubble. 53 | 54 | | Event | Description | 55 | | ----------- | -------------------------- | 56 | | reachable | grabbable is within reach. | 57 | | unreachable | grabbable is out of reach. | 58 | | grab | grabbing. | 59 | | usedown | a use-button is pressed. | 60 | | use1down | primary use-button is pressed. | 61 | | use2down | secondary use-button is pressed. | 62 | | use3down | tertiary use-button is pressed. | 63 | | useup | a use-button is released. | 64 | | use1up | primary use-button is released. | 65 | | use2up | secondary use-button is released. | 66 | | use3up | tertiary use-button is released. | 67 | | drop | dropping. | 68 | | fingerflex | a finger is flexing. | 69 | | hover | button is pointed at | 70 | | press | button got pressed | 71 | | unpress | button no longer pressed | 72 | | unhover | button no longer pointed at | 73 | 74 | 75 | ## States 76 | 77 | | State | Description | 78 | | ------------ | ------------------------------------- | 79 | | grabbing | currently grabbing something | 80 | | noinput | Input method has yet to be determined | 81 | | desktop | Player is using mouse and keyboard | 82 | | touch | Player is using touch screen | 83 | | gamepad | Player is using gamepad | 84 | | vrcontroller | Player is using VR controllers | 85 | 86 | 87 | ## Related components 88 | 89 | - [button](./grabbing/button.md) 90 | - [climbable](./grabbing/climbable.md) 91 | - [fingerflex](./grabbing/fingerflex.md) 92 | - [grabbable](./grabbing/grabbable.md) 93 | - [receptacle](./grabbing/receptacle.md) 94 | -------------------------------------------------------------------------------- /src/components/grabbing/button.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("button", { 4 | schema: { 5 | }, 6 | 7 | }) 8 | -------------------------------------------------------------------------------- /src/components/grabbing/button.md: -------------------------------------------------------------------------------- 1 | # button 2 | 3 | Add the `button` component to any object you want the player to be able to press. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | 10 | ## Events 11 | 12 | | Event | Description | 13 | | ------- | --------------------------- | 14 | | hover | button is pointed at | 15 | | press | button got pressed | 16 | | unpress | button no longer pressed | 17 | | unhover | button no longer pointed at | 18 | 19 | 20 | ## States 21 | 22 | | State | Description | 23 | | ------- | ----------------------- | 24 | | pressed | currently being pressed | 25 | -------------------------------------------------------------------------------- /src/components/grabbing/climbable.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | let currentClimb 4 | 5 | AFRAME.registerComponent("climbable", { 6 | dependencies: ["wall"], 7 | schema: { 8 | }, 9 | 10 | init() { 11 | this.el.setAttribute("grabbable", "physics:false; kinematicGrab:false; immovable:true;") 12 | this._player = this.el.sceneEl.querySelector("[locomotion]") 13 | this._localAnchor = new THREE.Vector3() 14 | 15 | this._onBump = this._onBump.bind(this) 16 | }, 17 | 18 | play() { 19 | this._player.addEventListener("bump", this._onBump) 20 | }, 21 | pause() { 22 | this._player.removeEventListener("bump", this._onBump) 23 | this._climbing = false 24 | }, 25 | 26 | tick() { 27 | if (!this._floating) return 28 | this._player.components.locomotion.stopFall() 29 | if (!this._climbing) return 30 | let worldAnchor = THREE.Vector3.temp().copy(this._localAnchor) 31 | let handPos = THREE.Vector3.temp().set(0, 0, 0) 32 | let delta = THREE.Vector3.temp() 33 | 34 | this.el.object3D.localToWorld(worldAnchor) 35 | this._hand.object3D.localToWorld(handPos) 36 | delta.copy(worldAnchor).sub(handPos).multiplyScalar(0.5) 37 | 38 | this._player.components.locomotion.move(delta) 39 | }, 40 | 41 | events: { 42 | grab(e) { 43 | if (currentClimb && currentClimb !== this.el) this._player.components.grabbing.dropObject(currentClimb) 44 | currentClimb = this.el 45 | this._climbing = true 46 | this._floating = true 47 | this._handName = e.detail.hand 48 | this._hand = e.detail.gloveElement.parentNode 49 | this._localAnchor.copy(e.detail.intersection.point) 50 | this.el.object3D.worldToLocal(this._localAnchor) 51 | if (this._handName === "head") { 52 | this._hand = this._hand.querySelector(".anchor") 53 | this._hand.object3D.position.set(0, 0, -e.detail.intersection.distance) 54 | } 55 | this._player.components.locomotion.jump() 56 | setTimeout(() => { 57 | this.el.sceneEl.querySelector(".legs")?.object3D.position.copy(this._player.components.locomotion.headPos) 58 | }) 59 | clearTimeout(this._autoCrouchTO) 60 | }, 61 | drop(e) { 62 | this._climbing = false 63 | setTimeout(() => { 64 | this.el.sceneEl.querySelector(".legs")?.object3D.position.copy(this._player.components.locomotion.headPos) 65 | }) 66 | clearTimeout(this._autoCrouchTO) 67 | this._autoCrouchTO = setTimeout(() => { 68 | this._floating = false 69 | this._player.components.locomotion.toggleCrouch(true) 70 | }, this._handName === "head" ? 1024 : 256) 71 | currentClimb = null 72 | }, 73 | }, 74 | 75 | _onBump(e) { 76 | this._climbing = false 77 | clearTimeout(this._autoCrouchTO) 78 | this._autoCrouchTO = setTimeout(() => { 79 | this._floating = false 80 | }, 1024) 81 | this._player.components.grabbing.dropObject(this.el) 82 | }, 83 | }) 84 | -------------------------------------------------------------------------------- /src/components/grabbing/climbable.md: -------------------------------------------------------------------------------- 1 | # climbable 2 | 3 | Add the `climbable` component to any object you want the player to be able to climb. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | ## Events and states 10 | 11 | Same as [grabbable](grabbable.md). 12 | -------------------------------------------------------------------------------- /src/components/grabbing/fingerflex.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("fingerflex", { 4 | schema: { 5 | min: { type: "number", default: 10 }, 6 | max: { type: "number", default: 90 }, 7 | }, 8 | 9 | init() { 10 | this._fingers = ["thumb", "index", "middle", "ring", "little"] 11 | this._currentFlex = [0, 0, 0, 0, 0] 12 | this._targetFlex = [0, 0, 0, 0, 0] 13 | }, 14 | 15 | tick(time, timeDelta) { 16 | for (let finger = 0; finger < 5; finger++) { 17 | let name = this._fingers[finger] 18 | let current = this._currentFlex[finger] 19 | let target = this._targetFlex[finger] 20 | 21 | current = current + Math.random() * Math.random() * (target - current) 22 | let degrees = this.data.min + current * (this.data.max - this.data.min) 23 | let bend = this.el.querySelector(".bend." + name) 24 | while (bend) { 25 | let rot = bend.getAttribute("rotation") 26 | rot.y = degrees 27 | bend.setAttribute("rotation", rot) 28 | bend = bend.querySelector(".bend") 29 | } 30 | 31 | this._currentFlex[finger] = current 32 | } 33 | }, 34 | 35 | events: { 36 | fingerflex(e) { 37 | this._targetFlex[e.detail.finger] = e.detail.flex 38 | } 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/components/grabbing/fingerflex.md: -------------------------------------------------------------------------------- 1 | # fingerflex 2 | 3 | The `fingerflex` component "bends" fingers on a glove object according to `fingerflex` events. 4 | 5 | E.g. if the glove recieves an event with `event.detail.finger == 1` (index finger), then the component will find the entity matching `.bend.index` selector, rotate it around the Y-axis between `min` and `max` property values according to `event.detail.flex`.. It will then do the same with any descendant `.bend` entity.. 6 | 7 | ```html 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ``` 22 | 23 | 24 | ## Properties 25 | 26 | | Property | Description | 27 | | -------- | -------------------------------------- | 28 | | min | Angle for when the finger is straight. | 29 | | max | Angle for when the finger is bent. | 30 | -------------------------------------------------------------------------------- /src/components/grabbing/grabbable.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("grabbable", { 4 | schema: { 5 | physics: { type: "boolean", default: true }, 6 | kinematicGrab: { type: "boolean", default: true }, 7 | hideOnGrab: { type: "boolean", default: false }, 8 | fixed: { type: "boolean", default: false }, 9 | fixedPosition: { type: "vec3", default: { x: 0, y: 0, z: 0 } }, 10 | fingerFlex: { type: "array", default: [0.5] }, 11 | immovable: { type: "boolean", default: false }, 12 | avoidWalls: { type: "boolean", default: true }, 13 | }, 14 | 15 | init() { 16 | if (this.data.physics && !this.el.hasAttribute("body")) this.el.setAttribute("body", "type:dynamic;") 17 | }, 18 | 19 | events: { 20 | grab(e) { 21 | if (e.detail.hand !== "head") { 22 | this._grabbed = e.detail 23 | this._glove = e.detail.gloveElement 24 | this._anchor = this._glove.querySelector(".anchor") 25 | } 26 | if (this.data.kinematicGrab) this.el.setAttribute("body", "type", "kinematic") 27 | }, 28 | drop(e) { 29 | this._grabbed = false 30 | if (this.data.physics) this.el.setAttribute("body", "type", "dynamic") 31 | }, 32 | limited(e) { 33 | if (this._grabbed) { 34 | let delta = THREE.Vector3.temp() 35 | let quat = THREE.Quaternion.temp() 36 | this._glove.copyWorldPosRot(this.el) 37 | let el = this._anchor 38 | while (el !== this._glove) { 39 | quat.copy(el.object3D.quaternion).conjugate() 40 | this._glove.object3D.quaternion.multiply(quat) 41 | el = el.parentNode 42 | } 43 | this._glove.object3D.updateWorldMatrix(true, true) 44 | delta.copy(this._anchor.object3D.position) 45 | this._anchor.object3D.parent.localToWorld(delta) 46 | this._glove.object3D.worldToLocal(delta) 47 | this._glove.object3D.position.sub(delta) 48 | } 49 | }, 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /src/components/grabbing/grabbable.md: -------------------------------------------------------------------------------- 1 | # grabbable 2 | 3 | Add the `grabbable` component to any object you want the player to be able to pick up. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | 10 | ## Properties 11 | 12 | | Property | Description | Default | 13 | | ------------- | -------------------------------------------------------------------------- | ------- | 14 | | physics | Whether or not to add physics body automatically. | true | 15 | | kinematicGrab | Whether or not to make physics kinematic during grab. | true | 16 | | hideOnGrab | Hide the glove when grabbing | false | 17 | | fixed | If `true` the object will have a fixed position and rotation when grabbed. | false | 18 | | fixedPosition | Relative position in hand, if `fixed` is `true`. | 0 0 0 | 19 | | fingerFlex | How much to flex each finger when grabbed. | 0.5, 0.5, 0.5, 0.5, 0.5 | 20 | | immovable | Make object immovable. | false | 21 | | avoidWalls | Keep object from passing through walls when grabbed. | true | 22 | 23 | 24 | ## Events 25 | 26 | | Event | Description | 27 | | ----------- | -------------------------- | 28 | | reachable | grabbable is within reach. | 29 | | unreachable | grabbable is out of reach. | 30 | | grab | grabbing. | 31 | | usedown | a use-button is pressed. | 32 | | use1down | primary use-button is pressed. | 33 | | use2down | secondary use-button is pressed. | 34 | | use3down | tertiary use-button is pressed. | 35 | | useup | a use-button is released. | 36 | | use1up | primary use-button is released. | 37 | | use2up | secondary use-button is released. | 38 | | use3up | tertiary use-button is released. | 39 | | drop | dropping. | 40 | | fingerflex | a finger is flexing. | 41 | 42 | 43 | ## States 44 | 45 | | State | Description | 46 | | ------- | --------------------------- | 47 | | grabbed | currently being grabbed | 48 | | put | currently in a `receptacle` | 49 | -------------------------------------------------------------------------------- /src/components/grabbing/receptacle.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("receptacle", { 4 | schema: { 5 | objects: { type: "string", default: "[grabbable]" }, 6 | radius: { type: "number", default: 0.125 }, 7 | onlyGrabbed: { type: "boolean", default: false }, 8 | autoDrop: { type: "boolean", default: false }, 9 | }, 10 | 11 | init() { 12 | this._anchor = this.el.ensure(".receptacle.anchor", "a-entity", { 13 | class: "receptacle anchor", 14 | body: "type:kinematic;autoShape:false;" 15 | }) 16 | this._refreshTO = setInterval(this.refreshObjects.bind(this), 1024) 17 | }, 18 | 19 | remove() { 20 | clearInterval(this._refreshTO) 21 | }, 22 | 23 | tick() { 24 | if (!this.nearest) return this.refreshObjects() 25 | let thisPos = THREE.Vector3.temp() 26 | let delta = THREE.Vector3.temp() 27 | this.el.object3D.localToWorld(thisPos.set(0, 0, 0)) 28 | this.nearest.object3D.localToWorld(delta.set(0, 0, 0)) 29 | delta.sub(thisPos) 30 | if (this._lastNearest && this._lastNearest !== this.nearest) { 31 | if (this.el.is("occupied")) { 32 | this._anchor.removeAttribute("joint__put") 33 | this._anchor.removeAttribute("animation__pos") 34 | this._anchor.removeAttribute("animation__rot") 35 | this.el.removeState("occupied") 36 | this._lastNearest.removeState("put") 37 | this.el.emit("take", { 38 | grabbable: this._lastNearest 39 | }) 40 | this._lastNearest.emit("take", { 41 | receptacle: this.el 42 | }) 43 | } 44 | if (this._hover) { 45 | this.el.emit("unhover", { 46 | grabbable: this._lastNearest 47 | }) 48 | this._lastNearest.emit("unhover", { 49 | receptacle: this.el 50 | }) 51 | } 52 | this._hover = false 53 | this._grabbed = false 54 | } else if (delta.length() > this.data.radius) { 55 | if (this.el.is("occupied")) { 56 | this._anchor.removeAttribute("joint__put") 57 | this._anchor.removeAttribute("animation__pos") 58 | this._anchor.removeAttribute("animation__rot") 59 | this.el.removeState("occupied") 60 | this.nearest.removeState("put") 61 | this.el.emit("take", { 62 | grabbable: this.nearest 63 | }) 64 | this.nearest.emit("take", { 65 | receptacle: this.el 66 | }) 67 | } 68 | if (this._hover) { 69 | this.el.emit("unhover", { 70 | grabbable: this.nearest 71 | }) 72 | this.nearest.emit("unhover", { 73 | receptacle: this.el 74 | }) 75 | } 76 | this._hover = false 77 | this._grabbed = false 78 | } else if (this.nearest.is("grabbed") || !this._hover) { 79 | if (!this._hover) { 80 | this.el.emit("hover", { 81 | grabbable: this.nearest 82 | }) 83 | this.nearest.emit("hover", { 84 | receptacle: this.el 85 | }) 86 | if (this.data.autoDrop && this._grabber) this._grabber.dropObject(this.nearest) 87 | } 88 | this._anchor.removeAttribute("animation__pos") 89 | this._anchor.removeAttribute("animation__rot") 90 | this._anchor.copyWorldPosRot(this.nearest) 91 | this._hover = true 92 | if (this.nearest.is("grabbed")) 93 | this._grabbed = true 94 | } else if (this._grabbed || !this.data.onlyGrabbed) { 95 | if (!this.el.is("occupied")) { 96 | this._anchor.copyWorldPosRot(this.nearest) 97 | this._anchor.components.body.commit() 98 | if (this.nearest.components.body) 99 | this._anchor.setAttribute("joint__put", { body2: this.nearest, type: "lock" }) 100 | this.el.addState("occupied") 101 | this.nearest.addState("put") 102 | this.el.emit("put", { 103 | grabbable: this.nearest 104 | }) 105 | this.nearest.emit("put", { 106 | receptacle: this.el 107 | }) 108 | } 109 | if (!this._anchor.getAttribute("animation__pos")) { 110 | this._anchor.setAttribute("animation__pos", { 111 | property: "position", 112 | to: { x: 0, y: 0, z: 0 }, 113 | dur: 256 114 | }) 115 | this._anchor.setAttribute("animation__rot", { 116 | property: "rotation", 117 | to: { x: 0, y: 0, z: 0 }, 118 | dur: 256 119 | }) 120 | } 121 | this.nearest.copyWorldPosRot(this._anchor) 122 | this._hover = true 123 | } 124 | this._lastNearest = this.nearest 125 | }, 126 | 127 | refreshObjects() { 128 | let shortest = Infinity 129 | let thisPos = THREE.Vector3.temp() 130 | let thatPos = THREE.Vector3.temp() 131 | let delta = THREE.Vector3.temp() 132 | let els = this.el.sceneEl.querySelectorAll(this.data.objects) 133 | this.nearest = null 134 | if (!els) return 135 | this.el.object3D.localToWorld(thisPos.set(0, 0, 0)) 136 | els.forEach(el => { 137 | el.object3D.localToWorld(thatPos.set(0, 0, 0)) 138 | delta.copy(thatPos).sub(thisPos) 139 | if (shortest > delta.length()) { 140 | shortest = delta.length() 141 | this.nearest = el 142 | } 143 | }) 144 | 145 | this._grabber = this.el.sceneEl.querySelector("[grabbing]")?.components.grabbing 146 | }, 147 | 148 | 149 | }) 150 | -------------------------------------------------------------------------------- /src/components/grabbing/receptacle.md: -------------------------------------------------------------------------------- 1 | # receptacle 2 | 3 | Add the `receptacle` component to any object you want to attract and hold another object within a given radius. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | 10 | ## Properties 11 | 12 | | Property | Description | Default | 13 | | ----------- | --------------------------------------------------------- | ------------- | 14 | | objects | Selector for the type of objects this receptacle attracts | `[grabbable]` | 15 | | radius | Radius of attraction | 0.125 | 16 | | onlyGrabbed | Only accept grabbed objects | false | 17 | | autoDrop | Automatically drop in object when within radius | false | 18 | 19 | 20 | ## Events 21 | 22 | These event are emitted on the receptacle as well as the object it attracts. 23 | 24 | | Event | Description | 25 | | ------- | -------------------------------------------------------------- | 26 | | put | object is placed in the receptacle | 27 | | take | object is taken out of the receptacle | 28 | | hover | attractive object is within radius | 29 | | unhover | attractive object is out of radius | 30 | 31 | 32 | ## States 33 | 34 | | Event | Description | 35 | | ------ | --------------------------- | 36 | | occupied | currently holding something | 37 | -------------------------------------------------------------------------------- /src/components/include.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("include", { 4 | schema: { type: "string" }, 5 | 6 | async init() { 7 | if (this.data && !this.el.sceneEl._including_) { 8 | this.el.sceneEl._including_ = true 9 | let b4Content = this.el.outerHTML 10 | 11 | let p1 = b4Content.indexOf(" ") 12 | let p2 = b4Content.indexOf(" include=") 13 | let attrs = b4Content.substr(p1, p2 - p1) 14 | 15 | p1 = b4Content.indexOf("\"", p2 + 10) + 1 16 | p2 = b4Content.indexOf(">") 17 | attrs += b4Content.substr(p1, p2 - p1) 18 | 19 | let response = await fetch(this.data) 20 | if (response.status >= 200 && response.status < 300) { 21 | this.el.outerHTML = await (await (response).text()).replace(">", " >").replace(" ", " " + attrs + " ") 22 | } else { 23 | this.el.removeAttribute("include") 24 | } 25 | this.el.sceneEl._including_ = false 26 | let next = this.el.sceneEl.querySelector("[include]") 27 | if (next && next.components && next.components.include) next.components.include.init() 28 | } 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/components/include.md: -------------------------------------------------------------------------------- 1 | # include 2 | 3 | Component for including external files into the scene. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | Any attributes except the `include` attribute will be added to the root of the included content. 10 | 11 | ```html 12 | 13 | 14 | 15 | ``` 16 | -------------------------------------------------------------------------------- /src/components/injectglove.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("injectglove", { 4 | init() { 5 | if (!this.el.innerHTML.trim()) this.defaultGlove() 6 | let hand = this.el.getAttribute("side") || this.el.parentNode.getAttribute("side") 7 | this.el.ensure(".palm", "a-entity", { 8 | class: "palm", 9 | position: `${hand === "left" ? -0.01 : 0.01} -0.03 0.08`, 10 | rotation: "-35 0 0" 11 | }) 12 | this.el.ensure("a-hand[side=\"right\"]", "a-hand", { side: "right" }) 13 | }, 14 | 15 | defaultGlove() { 16 | let hand = this.el.getAttribute("side") || this.el.parentNode.getAttribute("side") 17 | let color = this.el.getAttribute("color") || this.el.parentNode.getAttribute("color") || "lightblue" 18 | if (!this.el.getAttribute("fingerflex")) this.el.setAttribute("fingerflex", { 19 | min: hand === "left" ? -10 : 10, 20 | max: hand === "left" ? -90 : 90, 21 | }) 22 | this.el.innerHTML = ` 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ` 67 | }, 68 | }) 69 | -------------------------------------------------------------------------------- /src/components/injectplayer.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("injectplayer", { 4 | 5 | init() { 6 | this.el.addState("noinput") 7 | this.el.ensure("a-camera", "a-camera", { 8 | "look-controls": { pointerLockEnabled: true, touchEnabled: false }, 9 | "wasd-controls": { enabled: false } 10 | }) 11 | this.el.ensure("a-hand[side=\"left\"]", "a-hand", { side: "left" }) 12 | this.el.ensure("a-hand[side=\"right\"]", "a-hand", { side: "right" }) 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /src/components/limit.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("limit", { 4 | schema: { 5 | minPos: { type: "vec3" }, 6 | maxPos: { type: "vec3" }, 7 | rotationRange: { type: "vec3", default: { x: 1, y: 1, z: 1 } }, 8 | }, 9 | 10 | tick() { 11 | let delta = THREE.Vector3.temp() 12 | let pos = this.el.object3D.position 13 | let minPos = this.data.minPos 14 | let maxPos = this.data.maxPos 15 | let quat = this.el.object3D.quaternion 16 | let minQuat = THREE.Quaternion.temp().set( 17 | -Math.abs(this.data.rotationRange.x), 18 | -Math.abs(this.data.rotationRange.y), 19 | -Math.abs(this.data.rotationRange.z), 20 | -1) 21 | let maxQuat = THREE.Quaternion.temp().set( 22 | Math.abs(this.data.rotationRange.x), 23 | Math.abs(this.data.rotationRange.y), 24 | Math.abs(this.data.rotationRange.z), 25 | 1) 26 | delta.copy(pos) 27 | pos.set( 28 | Math.min(Math.max(minPos.x, pos.x), maxPos.x), 29 | Math.min(Math.max(minPos.y, pos.y), maxPos.y), 30 | Math.min(Math.max(minPos.z, pos.z), maxPos.z) 31 | ) 32 | quat.set( 33 | Math.min(Math.max(minQuat.x, quat.x), maxQuat.x), 34 | Math.min(Math.max(minQuat.y, quat.y), maxQuat.y), 35 | Math.min(Math.max(minQuat.z, quat.z), maxQuat.z), 36 | Math.min(Math.max(minQuat.w, quat.w), maxQuat.w) 37 | ).normalize() 38 | delta.sub(pos) 39 | if (delta.length() > 0) { 40 | setTimeout(() => { 41 | this.el.components.body?.commit() 42 | }) 43 | this.el.object3D.updateWorldMatrix(true, true) 44 | this.el.emit("limited") 45 | } 46 | }, 47 | 48 | events: { 49 | drop(e) { 50 | 51 | } 52 | } 53 | 54 | }) 55 | -------------------------------------------------------------------------------- /src/components/limit.md: -------------------------------------------------------------------------------- 1 | # limit 2 | 3 | Constrain an entity's position to a given range. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | 10 | ## Properties 11 | 12 | | Property | Description | Default | 13 | | ------------ | --------------------------- | --------- | 14 | | minPos | Lowest possible position coordinates | 0 0 0 15 | | maxPos | Highest possible position coordinates | 0 0 0 16 | | rotationRange | Maximum range of rotation | 0 0 0 17 | 18 | 19 | ## Events 20 | 21 | | Event | Description | 22 | | ------- | ------------------ | 23 | | limited | object hit the limit | 24 | -------------------------------------------------------------------------------- /src/components/locomotion.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("locomotion", { 4 | dependencies: ["position", "injectplayer"], 5 | schema: { 6 | speed: { type: "number", default: 4 }, 7 | stepLength: { type: "number", default: 1 }, 8 | rotationSpeed: { type: "number", default: 1 }, 9 | teleportDistance: { type: "number", default: 5 }, 10 | jumpForce: { type: "number", default: 4 }, 11 | gravity: { type: "number", default: 10 }, 12 | godMode: { type: "boolean", default: false } 13 | }, 14 | 15 | init() { 16 | this._onKeyDown = this._onKeyDown.bind(this) 17 | this._onKeyUp = this._onKeyUp.bind(this) 18 | this._onAxisMove = this._onAxisMove.bind(this) 19 | this._onButtonChanged = this._onButtonChanged.bind(this) 20 | this._onTouchStart = this._onTouchStart.bind(this) 21 | this._onTouchMove = this._onTouchMove.bind(this) 22 | this._onTouchEnd = this._onTouchEnd.bind(this) 23 | this._onEnterVR = this._onEnterVR.bind(this) 24 | this._onExitVR = this._onExitVR.bind(this) 25 | 26 | this._keysDown = {} 27 | this._kbStick = new THREE.Vector2() 28 | this._axes = [0, 0, 0, 0] 29 | this._leftTouchCenter = new THREE.Vector2() 30 | this._leftTouchDir = new THREE.Vector2() 31 | this._rightTouchCenter = new THREE.Vector2() 32 | this._rightTouchDir = new THREE.Vector2() 33 | this._teleporting = true 34 | this._bumpOverload = 0 35 | this._vertVelocity = 1 36 | this.currentFloorPosition = new THREE.Vector3() 37 | this.centerPos = new THREE.Vector3() 38 | this.headPos = new THREE.Vector3() 39 | this.headDir = new THREE.Vector3() 40 | this.feetPos = new THREE.Vector3() 41 | this.lastStep = new THREE.Vector3() 42 | 43 | this._config = { 44 | quantizeMovement: false, 45 | quantizeRotation: false, 46 | quantizeMovementVR: !!(this.el.sceneEl.isMobile), 47 | quantizeRotationVR: true 48 | } 49 | if (this.el.sceneEl.is('vr-mode')) this._onEnterVR() 50 | else this._onExitVR() 51 | 52 | this._camera = this.el.querySelector("a-camera") 53 | this._leftHand = this.el.querySelector("a-hand[side=\"left\"]") 54 | this._rightHand = this.el.querySelector("a-hand[side=\"right\"]") 55 | this._legs = this.el.sceneEl.ensure(".legs", "a-entity", { 56 | class: "legs", position: "0 0.5 0", // radius: 0.125, color: "blue", 57 | raycaster: { 58 | deep: true, 59 | autoRefresh: false, 60 | objects: "[floor]", 61 | direction: "0 -1 0", 62 | far: 0.625, 63 | // showLine: true 64 | } 65 | }) 66 | this._legBumper = this.el.sceneEl.ensure(".leg-bumper", "a-entity", { 67 | class: "leg-bumper", position: "0 0.5 0", // radius: 0.125, color: "red", 68 | raycaster: { 69 | deep: true, 70 | autoRefresh: false, 71 | objects: "[wall]", 72 | // showLine: true 73 | } 74 | }) 75 | this._headBumper = this.el.sceneEl.ensure(".head-bumper", "a-entity", { 76 | class: "head-bumper", position: "0 0.5 0", // radius: 0.125, color: "green", 77 | raycaster: { 78 | deep: true, 79 | autoRefresh: false, 80 | objects: "[wall]", 81 | // showLine: true 82 | } 83 | }) 84 | this._teleportBeam = this._camera.ensure(".teleport-ray", "a-entity", { 85 | class: "teleport-ray", 86 | raycaster: { 87 | deep: true, 88 | autoRefresh: false, 89 | objects: "[wall]", 90 | // showLine: true 91 | } 92 | }) 93 | this._teleportCursor = this.el.ensure(".teleport-cursor", "a-cylinder", { 94 | class: "teleport-cursor", radius: 0.5, height: 0.0625, material: "opacity:0.5;" 95 | }) 96 | this._teleportCursor.setAttribute("visible", false) 97 | }, 98 | 99 | update(oldData) { 100 | // if (this.data.jumpForce) this.data.teleportDistance = 0 101 | this._godMode = this.data.godMode 102 | }, 103 | 104 | play() { 105 | document.addEventListener("keydown", this._onKeyDown) 106 | document.addEventListener("keyup", this._onKeyUp) 107 | this._leftHand.addEventListener("axismove", this._onAxisMove) 108 | this._rightHand.addEventListener("axismove", this._onAxisMove) 109 | this._leftHand.addEventListener("buttonchanged", this._onButtonChanged) 110 | this._rightHand.addEventListener("buttonchanged", this._onButtonChanged) 111 | this.el.sceneEl.canvas.addEventListener("touchstart", this._onTouchStart) 112 | this.el.sceneEl.canvas.addEventListener("touchmove", this._onTouchMove) 113 | this.el.sceneEl.canvas.addEventListener("touchend", this._onTouchEnd) 114 | this.el.sceneEl.addEventListener("enter-vr", this._onEnterVR) 115 | this.el.sceneEl.addEventListener("exit-vr", this._onExitVR) 116 | }, 117 | 118 | pause() { 119 | document.removeEventListener("keydown", this._onKeyDown) 120 | document.removeEventListener("keyup", this._onKeyUp) 121 | this._leftHand.removeEventListener("axismove", this._onAxisMove) 122 | this._rightHand.removeEventListener("axismove", this._onAxisMove) 123 | this._leftHand.removeEventListener("buttonchanged", this._onButtonChanged) 124 | this._rightHand.removeEventListener("buttonchanged", this._onButtonChanged) 125 | this.el.sceneEl.canvas.removeEventListener("touchstart", this._onTouchStart) 126 | this.el.sceneEl.canvas.removeEventListener("touchmove", this._onTouchMove) 127 | this.el.sceneEl.canvas.removeEventListener("touchend", this._onTouchEnd) 128 | this.el.sceneEl.removeEventListener("enter-vr", this._onEnterVR) 129 | this.el.sceneEl.removeEventListener("exit-vr", this._onExitVR) 130 | }, 131 | 132 | remove() { 133 | this.el.sceneEl.removeChild(this._legs) 134 | this.el.sceneEl.removeChild(this._legBumper) 135 | this.el.sceneEl.removeChild(this._headBumper) 136 | }, 137 | 138 | tick(time, timeDelta) { 139 | timeDelta /= 1000 140 | this.el.object3D.localToWorld(this.centerPos.set(0, 0, 0)) 141 | this.headPos.copy(this._camera.object3D.position) 142 | this._camera.object3D.parent.localToWorld(this.headPos) 143 | this.headDir.set(0, 0, -1) 144 | .applyQuaternion(this._camera.object3D.quaternion) 145 | .applyQuaternion(this.el.object3D.getWorldQuaternion(THREE.Quaternion.temp())) 146 | this._legs.object3D.localToWorld(this.feetPos.set(0, 0, 0)) 147 | this.feetPos.y -= 0.5 148 | 149 | this._applyButtons(timeDelta) 150 | this._applyMoveStick(timeDelta) 151 | this._applyAuxStick(timeDelta) 152 | 153 | // drag feet 154 | let head2toe = THREE.Vector3.temp() 155 | .copy(this.headPos).sub(this.feetPos) 156 | head2toe.y = 0 157 | if (head2toe.length() > 0.5 || !this.currentFloor) { 158 | if (this.currentFloor) 159 | head2toe.multiplyScalar(0.1) 160 | this._legs.object3D.position.add(head2toe) 161 | this.feetPos.add(head2toe) 162 | } 163 | 164 | // fall 165 | if (!this._godMode && !this._caution) { 166 | let ray = this._legs.components.raycaster 167 | ray.refreshObjects() 168 | let hit = ray.intersections[0] 169 | if (hit && this._vertVelocity <= 0) { 170 | this._vertVelocity = 0 171 | if (this.currentFloor === hit.el) { 172 | let delta = THREE.Vector3.temp() 173 | delta.copy(this.currentFloor.object3D.position).sub(this.currentFloorPosition) 174 | this.move(delta) 175 | this.lastStep.add(delta) 176 | delta.y = 0 177 | this._legs.object3D.position.add(delta) 178 | } else { 179 | if (this.currentFloor) this.currentFloor.emit("leave") 180 | hit.el.emit("enter") 181 | } 182 | this.move(THREE.Vector3.temp().set(0, 0.5 - hit.distance, 0)) 183 | this.currentFloor = hit.el 184 | this.currentFloorPosition.copy(this.currentFloor.object3D.position) 185 | } else { 186 | if (this.currentFloor) this.currentFloor.emit("leave") 187 | this._vertVelocity -= this.data.gravity * timeDelta 188 | this.move(THREE.Vector3.temp().set(0, Math.max(-0.5, this._vertVelocity * timeDelta), 0)) 189 | this.currentFloor = null 190 | } 191 | } 192 | 193 | // bump walls 194 | if (this._godMode) { 195 | this._legBumper.object3D.position.copy(this._legs.object3D.position) 196 | this._headBumper.object3D.position.copy(this._legs.object3D.position) 197 | } else if (this._bumpOverload > 4 || Math.abs(this.headPos.y - this.feetPos.y) > 3) { 198 | this.feetPos.y = this.centerPos.y 199 | this._legs.object3D.position.y = this.feetPos.y + 0.5 200 | this.teleport(this._legBumper.object3D.position, true) 201 | if (this._bumpOverload) this._bumpOverload-- 202 | } else { 203 | let pos = THREE.Vector3.temp() 204 | pos.copy(this.feetPos).y += 0.5 205 | this._bump(pos, this._legBumper) 206 | pos.copy(this.headPos) 207 | this._bump(pos, this._headBumper) 208 | } 209 | 210 | // take step 211 | let delta = THREE.Vector3.temp() 212 | delta.copy(this.feetPos).sub(this.lastStep) 213 | if (delta.length() > this.data.stepLength) { 214 | if (this.currentFloor) { 215 | this.el.emit("step") 216 | this.currentFloor.emit("step") 217 | } 218 | while (delta.length() > this.data.stepLength) { 219 | delta.multiplyScalar(this.data.stepLength / delta.length()) 220 | this.lastStep.add(delta) 221 | delta.copy(this.feetPos).sub(this.lastStep) 222 | } 223 | } 224 | 225 | // Update The Matrix! 🐱‍💻 226 | this.el.object3D.updateWorldMatrix(true, true) 227 | }, 228 | 229 | teleport(pos, force) { 230 | let delta = THREE.Vector3.temp() 231 | delta.copy(pos).sub(this.feetPos) 232 | this.move(delta) 233 | this._legs.object3D.position.x = this.feetPos.x = this.headPos.x 234 | this._legs.object3D.position.z = this.feetPos.z = this.headPos.z 235 | this._caution = 8 236 | if (force) { 237 | this._legBumper.object3D.position.copy(this._legs.object3D.position) 238 | this._headBumper.object3D.position.copy(this._legs.object3D.position) 239 | } 240 | if (this.el.components?.grabbing) 241 | this.el.components.grabbing.ironGrip = true 242 | }, 243 | 244 | jump() { 245 | // jump! 246 | if (this.currentFloor) { 247 | this._vertVelocity = this.data.jumpForce 248 | } 249 | }, 250 | stopFall() { 251 | this._legs.object3D.position.x = this.feetPos.x = this.headPos.x 252 | this._legs.object3D.position.z = this.feetPos.z = this.headPos.z 253 | this._vertVelocity = 0 254 | }, 255 | 256 | toggleCrouch(reset) { 257 | if (!this.currentFloor) return setTimeout(() => { 258 | this.toggleCrouch(reset) 259 | }, 256) 260 | let head2toe = this.headPos.y - this.feetPos.y 261 | let delta 262 | clearTimeout(this._crouchResetTO) 263 | this._crouchResetTO = null 264 | if (Math.abs(this.centerPos.y - this.feetPos.y) > 0.03125) { 265 | delta = this.feetPos.y - this.centerPos.y 266 | } else if (!reset) { 267 | if (head2toe > 1) { 268 | delta = -1 269 | } else { 270 | delta = 1 271 | } 272 | } 273 | this.el.removeAttribute("animation") 274 | if (delta) { 275 | this.el.setAttribute("animation", { 276 | property: "object3D.position.y", 277 | to: this.el.object3D.position.y + delta, 278 | dur: 256, 279 | // easing: "easeInOutSine" 280 | }) 281 | } 282 | }, 283 | 284 | move(delta) { 285 | this.el.object3D.position.add(delta) 286 | this.centerPos.add(delta) 287 | this.headPos.add(delta) 288 | this._legs.object3D.position.y += delta.y 289 | this.feetPos.y += delta.y 290 | }, 291 | 292 | _bump(pos, bumper) { 293 | let matrix = THREE.Matrix3.temp() 294 | let delta = THREE.Vector3.temp() 295 | delta.copy(pos) 296 | delta.sub(bumper.object3D.position) 297 | let dist = delta.length() 298 | if (dist) { 299 | bumper.setAttribute("raycaster", "far", dist + 0.125) 300 | bumper.setAttribute("raycaster", "direction", `${delta.x} ${delta.y} ${delta.z}`) 301 | // bumper.setAttribute("raycaster", "origin", delta.multiplyScalar(-0.25)) 302 | let ray = bumper.components.raycaster 303 | ray.refreshObjects() 304 | let hit = ray.intersections[0] 305 | if (hit) { 306 | this.el.removeAttribute("animation") 307 | matrix.getNormalMatrix(hit.el.object3D.matrixWorld) 308 | delta 309 | .copy(hit.face.normal) 310 | .applyMatrix3(matrix) 311 | .normalize() 312 | .multiplyScalar(dist + 0.125) 313 | let feety = this._legs.object3D.position.y 314 | this.move(delta) 315 | bumper.object3D.position.add(delta) 316 | if (this._legs.object3D.position.y !== feety) { 317 | if (bumper === this._headBumper) this._headBumper.object3D.position.copy(this._legBumper.object3D.position) 318 | clearTimeout(this._crouchResetTO) 319 | this._crouchResetTO = setTimeout(() => { 320 | this.toggleCrouch(true) 321 | }, 4096) 322 | } 323 | this._legs.object3D.position.add(delta) 324 | this._legs.object3D.position.y = Math.max(feety, this.headPos.y - 1.5) 325 | this._caution = 4 326 | this._bumpOverload++ 327 | this._vertVelocity = Math.min(0, this._vertVelocity) 328 | let detail = { 329 | player: this.el, 330 | object: hit.el 331 | } 332 | this.el.emit("bump", detail) 333 | hit.el.emit("bump", detail) 334 | } else if (this._caution) { 335 | this._caution-- 336 | } else { 337 | if (this._bumpOverload) this._bumpOverload-- 338 | bumper.object3D.position.lerp(pos, 0.25) 339 | } 340 | } 341 | }, 342 | 343 | _callMoveStick() { 344 | let bestStick = THREE.Vector2.temp().set(0, 0) 345 | let stick = THREE.Vector2.temp() 346 | 347 | stick.set(0, 0) 348 | if (this._keysDown["KeyA"]) stick.x-- 349 | if (this._keysDown["KeyD"]) stick.x++ 350 | if (this._keysDown["KeyW"] || this._keysDown["ArrowUp"]) stick.y-- 351 | if (this._keysDown["KeyS"] || this._keysDown["ArrowDown"]) stick.y++ 352 | if (this._kbStick.length() > 0.1) this._kbStick.multiplyScalar((this._kbStick.length() - 0.1) / this._kbStick.length()) 353 | else (this._kbStick.set(0, 0)) 354 | this._kbStick.add(stick.multiplyScalar(0.2)) 355 | if (this._kbStick.length() > 1) this._kbStick.normalize() 356 | if (this._kbStick.length() > bestStick.length()) bestStick.copy(this._kbStick) 357 | 358 | this._deadZone(stick.set(this._axes[0], this._axes[1])) 359 | if (stick.length() > bestStick.length()) bestStick.copy(stick) 360 | 361 | stick.copy(this._leftTouchDir) 362 | if (stick.length() > bestStick.length()) bestStick.copy(stick) 363 | 364 | for (i = 0, len = navigator.getGamepads().length; i < len; i++) { 365 | gamepad = navigator.getGamepads()[i] 366 | if (gamepad) { 367 | this._deadZone(stick.set(gamepad.axes[0], gamepad.axes[1])) 368 | if (stick.length() > bestStick.length()) { 369 | this._setDevice("gamepad") 370 | bestStick.copy(stick) 371 | } 372 | } 373 | } 374 | 375 | if (bestStick.length() > 1) bestStick.normalize() 376 | if (this._keysDown["ShiftLeft"] || this._keysDown["ShiftRight"]) bestStick.multiplyScalar(0.25) 377 | return bestStick 378 | }, 379 | _applyMoveStick(seconds) { 380 | let stick = this._callMoveStick() 381 | stick.multiplyScalar(this.data.speed) 382 | stick.multiplyScalar(seconds) 383 | let heading = THREE.Vector2.temp().set(this.headDir.z, -this.headDir.x).angle() - Math.PI 384 | let x2 = Math.cos(heading) * stick.x - Math.sin(heading) * stick.y 385 | let y2 = Math.sin(heading) * stick.x + Math.cos(heading) * stick.y 386 | let delta = THREE.Vector3.temp().set(x2, 0, y2) 387 | if (this.quantizeMovement) { 388 | this._quantTime = this._quantTime || 0 389 | this._quantDelta = this._quantDelta || new THREE.Vector3() 390 | this._quantTime += seconds 391 | this._quantDelta.add(delta) 392 | if (this._quantTime > 0.25) { 393 | this._quantTime -= 0.25 394 | delta.copy(this._quantDelta) 395 | this._quantDelta.set(0, 0, 0) 396 | } else { 397 | delta.set(0, 0, 0) 398 | } 399 | } 400 | this.move(delta) 401 | }, 402 | 403 | _callAuxStick() { 404 | let bestStick = THREE.Vector2.temp().set(0, 0) 405 | let stick = THREE.Vector2.temp() 406 | 407 | stick.set(0, 0) 408 | if (this._keysDown["ArrowLeft"]) stick.x-- 409 | if (this._keysDown["ArrowRight"]) stick.x++ 410 | if (this._keysDown["KeyQ"]) stick.y-- 411 | if (this._keysDown["KeyC"]) stick.y++ 412 | if (stick.length() > bestStick.length()) bestStick.copy(stick) 413 | 414 | this._fourWay(this._deadZone(stick.set(this._axes[2], this._axes[3]))) 415 | if (stick.length() > bestStick.length()) bestStick.copy(stick) 416 | 417 | this._fourWay(stick.copy(this._rightTouchDir)) 418 | if (stick.length() > bestStick.length()) bestStick.copy(stick) 419 | 420 | for (i = 0, len = navigator.getGamepads().length; i < len; i++) { 421 | gamepad = navigator.getGamepads()[i] 422 | if (gamepad) { 423 | this._fourWay(this._deadZone(stick.set(gamepad.axes[2], gamepad.axes[3]))) 424 | if (stick.length() > bestStick.length()) { 425 | this._setDevice("gamepad") 426 | bestStick.copy(stick) 427 | } 428 | } 429 | } 430 | 431 | if (bestStick.length() > 1) bestStick.normalize() 432 | if (this._keysDown["ShiftLeft"] || this._keysDown["ShiftRight"]) bestStick.multiplyScalar(0.25) 433 | return bestStick 434 | }, 435 | _applyAuxStick(seconds) { 436 | let stick = this._callAuxStick() 437 | let rotation = 0 438 | 439 | // Rotation 440 | if (this.quantizeRotation) { 441 | if (Math.round(stick.x)) { 442 | if (!this._rotating) { 443 | this._rotating = true 444 | rotation = -Math.round(stick.x) * Math.PI / 4 445 | } 446 | } else { 447 | this._rotating = false 448 | } 449 | } else { 450 | rotation = -stick.x * this.data.rotationSpeed * seconds 451 | } 452 | if (rotation) { 453 | let pos = THREE.Vector2.temp() 454 | let pivot = THREE.Vector2.temp() 455 | let delta = THREE.Vector3.temp() 456 | pos.set(this.feetPos.x, this.feetPos.z) 457 | pivot.set(this.centerPos.x, this.centerPos.z) 458 | pos.rotateAround(pivot, -rotation) 459 | delta.set(this.feetPos.x - pos.x, 0, this.feetPos.z - pos.y) 460 | 461 | this.el.object3D.rotateY(rotation) 462 | this.el.object3D.position.add(delta) 463 | this.centerPos.add(delta) 464 | } 465 | 466 | // Levitating 467 | if (this._godMode) { 468 | this.el.object3D.position.y += -stick.y * this.data.speed * seconds 469 | this._legs.object3D.position.y += -stick.y * this.data.speed * seconds 470 | } else { 471 | // Crouching 472 | if (Math.round(stick.y) > 0) { 473 | if (!this._crouching) { 474 | this._crouching = true 475 | this.toggleCrouch() 476 | } 477 | } else { 478 | this._crouching = false 479 | } 480 | 481 | // Teleportation and jumping 482 | if (Math.round(stick.y) < 0) { 483 | if (!this._teleporting && this.data.teleportDistance) { 484 | this._teleportCursor.setAttribute("visible", true) 485 | this._teleporting = true 486 | } 487 | let quat = THREE.Quaternion.temp() 488 | this._teleportCursor.object3D.getWorldQuaternion(quat) 489 | this._teleportCursor.object3D.quaternion.multiply(quat.conjugate().normalize()).multiply(quat.copy(this.el.object3D.quaternion).multiply(this._camera.object3D.quaternion)) 490 | this._teleportCursor.object3D.quaternion.x = 0 491 | this._teleportCursor.object3D.quaternion.z = 0 492 | this._teleportCursor.object3D.quaternion.normalize() 493 | 494 | ray = this._teleportBeam.components.raycaster 495 | ray.refreshObjects() 496 | hit = ray.intersections[0] 497 | if (hit && hit.el.components.floor) { 498 | let straight = THREE.Vector3.temp() 499 | let delta = THREE.Vector3.temp() 500 | let matrix = THREE.Matrix3.temp() 501 | delta.copy(hit.point).sub(this.feetPos) 502 | if (delta.y > 1.5) delta.multiplyScalar(0) 503 | if (delta.length() > this.data.teleportDistance) delta.normalize().multiplyScalar(this.data.teleportDistance) 504 | delta.add(this.feetPos) 505 | this._teleportCursor.object3D.position.copy(delta) 506 | this._teleportCursor.object3D.parent.worldToLocal(this._teleportCursor.object3D.position) 507 | 508 | matrix.getNormalMatrix(hit.el.object3D.matrixWorld) 509 | delta 510 | .copy(hit.face.normal) 511 | .applyMatrix3(matrix) 512 | .normalize() 513 | delta.applyQuaternion(quat.copy(this.el.object3D.quaternion).conjugate()) 514 | straight.set(0, 1, 0) 515 | quat.setFromUnitVectors(straight, delta) 516 | this._teleportCursor.object3D.quaternion.premultiply(quat) 517 | } else { 518 | this._teleportCursor.object3D.position.copy(this.feetPos) 519 | this._teleportCursor.object3D.parent.worldToLocal(this._teleportCursor.object3D.position) 520 | } 521 | } else if (this._teleporting) { 522 | let pos = THREE.Vector3.temp() 523 | this._teleportCursor.object3D.localToWorld(pos.set(0, 0, 0)) 524 | this.teleport(pos) 525 | this._teleportCursor.setAttribute("visible", false) 526 | this._teleportCursor.setAttribute("position", "0 0 0") 527 | this._teleporting = false 528 | } 529 | } 530 | }, 531 | 532 | _callButtons() { 533 | let buttons = 0 534 | 535 | if (this._keysDown["Space"]) buttons = buttons | 1 536 | if (this._keysDown["KeyG"]) buttons = buttons | 2 537 | if (this._vrRightClick) buttons = buttons | 1 538 | if (this._vrLeftClick) buttons = buttons | 2 539 | 540 | for (i = 0, len = navigator.getGamepads().length; i < len; i++) { 541 | gamepad = navigator.getGamepads()[i] 542 | if (gamepad) { 543 | if (gamepad.buttons[3]?.pressed) { 544 | this._setDevice("gamepad") 545 | buttons = buttons | 1 546 | } 547 | if (gamepad.buttons[11]?.pressed) { 548 | this._setDevice("gamepad") 549 | buttons = buttons | 1 550 | } 551 | if (gamepad.buttons[10]?.pressed) { 552 | this._setDevice("gamepad") 553 | buttons = buttons | 2 554 | } 555 | } 556 | } 557 | 558 | return buttons 559 | }, 560 | _applyButtons() { 561 | let buttons = this._callButtons() 562 | if (buttons) { 563 | if (!this._toggling) { 564 | if (buttons & 1) this.jump() 565 | if (this.data.godMode && buttons & 2) this._godMode = !this._godMode 566 | if (this._godMode) this._vertVelocity = 0 567 | } 568 | this._toggling = true 569 | } else { 570 | this._toggling = false 571 | } 572 | }, 573 | 574 | _deadZone(vec, limit = 0.25) { 575 | if (vec.length() > limit) { 576 | vec.multiplyScalar(((vec.length() - limit) / (1 - limit)) / vec.length()) 577 | } else { 578 | vec.set(0, 0) 579 | } 580 | return vec 581 | }, 582 | _fourWay(vec) { 583 | let len = vec.length() 584 | if (Math.abs(vec.x) > Math.abs(vec.y)) { 585 | vec.y = 0 586 | } else { 587 | vec.x = 0 588 | } 589 | vec.multiplyScalar(len / vec.length()) 590 | return vec 591 | }, 592 | 593 | _onKeyDown(e) { 594 | this._setDevice("desktop") 595 | this._keysDown[e.code] = true 596 | }, 597 | _onKeyUp(e) { this._keysDown[e.code] = false }, 598 | _onAxisMove(e) { 599 | this._setDevice("vrcontroller") 600 | if (e.srcElement.getAttribute("tracked-controls").hand === "left") { 601 | this._axes[0] = e.detail.axis[2] 602 | this._axes[1] = e.detail.axis[3] 603 | } else { 604 | this._axes[2] = e.detail.axis[2] 605 | this._axes[3] = e.detail.axis[3] 606 | } 607 | if (!this._handEnabled) { 608 | this._teleportBeam.parentElement.removeChild(this._teleportBeam) 609 | this._teleportBeam = this._rightHand.ensure(".teleportBeam", "a-entity", { 610 | class: "teleportBeam", rotation: "-45 0 0", 611 | raycaster: { 612 | deep: true, 613 | autoRefresh: false, 614 | objects: "[wall]", 615 | } 616 | }) 617 | this._handEnabled = true 618 | } 619 | }, 620 | _onButtonChanged(e) { 621 | this._setDevice("vrcontroller") 622 | if (e.srcElement.getAttribute("tracked-controls").hand === "left") { 623 | if (e.detail.id == 3) this._vrLeftClick = e.detail.state.pressed 624 | } else { 625 | if (e.detail.id == 3) this._vrRightClick = e.detail.state.pressed 626 | } 627 | }, 628 | 629 | _onTouchStart(e) { 630 | this._setDevice("touch") 631 | let vw = this.el.sceneEl.canvas.clientWidth 632 | for (let j = 0; j < e.changedTouches.length; j++) { 633 | let touchEvent = e.changedTouches[j] 634 | if (touchEvent.clientX < vw / 2) { 635 | this._leftTouchId = touchEvent.identifier 636 | this._leftTouchCenter.set(touchEvent.clientX, touchEvent.clientY) 637 | } 638 | if (touchEvent.clientX > vw / 2) { 639 | this._rightTouchId = touchEvent.identifier 640 | this._rightTouchCenter.set(touchEvent.clientX, touchEvent.clientY) 641 | } 642 | } 643 | e.preventDefault() 644 | }, 645 | _onTouchMove(e) { 646 | let stickRadius = 32 647 | for (let j = 0; j < e.changedTouches.length; j++) { 648 | let touchEvent = e.changedTouches[j] 649 | let touchCenter = null 650 | let touchDir = null 651 | if (this._leftTouchId === touchEvent.identifier) { 652 | touchCenter = this._leftTouchCenter 653 | touchDir = this._leftTouchDir 654 | } 655 | if (this._rightTouchId === touchEvent.identifier) { 656 | touchCenter = this._rightTouchCenter 657 | touchDir = this._rightTouchDir 658 | } 659 | if (touchDir) { 660 | touchDir.set(touchEvent.clientX, touchEvent.clientY) 661 | touchDir.sub(touchCenter) 662 | if (touchDir.length() > stickRadius) { 663 | touchDir.multiplyScalar((touchDir.length() - stickRadius) / touchDir.length()) 664 | touchCenter.add(touchDir) 665 | touchDir.multiplyScalar(stickRadius / touchDir.length()) 666 | } 667 | touchDir.divideScalar(stickRadius) 668 | } 669 | } 670 | e.preventDefault() 671 | }, 672 | _onTouchEnd(e) { 673 | for (let j = 0; j < e.changedTouches.length; j++) { 674 | let touchEvent = e.changedTouches[j]; 675 | if (this._leftTouchId === touchEvent.identifier) { 676 | this._leftTouchId = null 677 | this._leftTouchDir.set(0, 0) 678 | } 679 | if (this._rightTouchId === touchEvent.identifier) { 680 | this._rightTouchId = null 681 | this._rightTouchDir.set(0, 0) 682 | } 683 | } 684 | }, 685 | 686 | _onEnterVR(e) { 687 | this.isVR = true 688 | this.quantizeMovement = this._config.quantizeMovementVR 689 | this.quantizeRotation = this._config.quantizeRotationVR 690 | }, 691 | _onExitVR(e) { 692 | this.isVR = false 693 | this.quantizeMovement = this._config.quantizeMovement 694 | this.quantizeRotation = this._config.quantizeRotation 695 | }, 696 | 697 | _setDevice(device) { 698 | if (this.device === device) return 699 | this.el.removeState(this.device || "noinput") 700 | this.device = device 701 | this.el.addState(this.device || "noinput") 702 | } 703 | }) 704 | 705 | require("./locomotion/floor") 706 | require("./locomotion/wall") 707 | require("./locomotion/start") 708 | -------------------------------------------------------------------------------- /src/components/locomotion.md: -------------------------------------------------------------------------------- 1 | # locomotion 2 | 3 | Component to facilitate moving about and stuff. 4 | 5 | Add the `locomotion` component to your player rig like so: 6 | 7 | ```html 8 | 9 | ``` 10 | 11 | This makes it possible to move around the using the following controls. 12 | 13 | | Action | Controller | Desktop | Touch | 14 | | --------------------------------------- | ---------------------- | ------- | --------------------------- | 15 | | Move | Left stick | WASD | Left side swipe | 16 | | Rotate | Right stick left/right | Arrows | Right side swipe left/right | 17 | | Teleport/Move up | Right stick up | Q | Right side swipe up | 18 | | Jump | Y or Click right stick | Space | 19 | | Crouch/Move down | Right stick down | C | Right side swipe down | 20 | | Toggle god mode (if enabled) | Click left stick | G | 21 | 22 | 23 | ## Properties 24 | 25 | | Property | Description | Default | 26 | | ---------------- | ---------------------------------------------------------------- | ------- | 27 | | speed | Speed of movement | 4 | 28 | | stepLength | Distance between `step` events | 1 | 29 | | teleportDistance | Maximum teleportation distance | 5 | 30 | | jumpForce | Amount of force to jump | 4 | 31 | | gravity | Amount of gravity when jumping and falling | 10 | 32 | | godMode | Enable ability to fly through walls and floors in any direction | false | 33 | 34 | 35 | ## Methods 36 | 37 | | Method | Description | 38 | | -------------------- | ------------------------------------------------------------------------------------------------------ | 39 | | teleport(pos, force) | Teleport to given position. if `force` is `true`, player will pass through walls/floors along the way. | 40 | | move(delta) | Move by given vector. | 41 | | toggleCrouch(reset) | Toggle crouch mode. if `reset` is `true`, player height will be reset to default. | 42 | | jump() | Make the player jump if possible. | 43 | | stopFall() | Stop the fall momentarily. | 44 | 45 | 46 | ## Events 47 | 48 | | Event | Description | 49 | | ----- | -------------------------------------------- | 50 | | step | Every time the player takes a simulated step | 51 | | bump | Player bumps into wall | 52 | 53 | 54 | ## States 55 | 56 | | State | Description | 57 | | ------------ | ------------------------------------- | 58 | | noinput | Input method has yet to be determined | 59 | | desktop | Player is using mouse and keyboard | 60 | | touch | Player is using touch screen | 61 | | gamepad | Player is using gamepad | 62 | | vrcontroller | Player is using VR controllers | 63 | 64 | 65 | ## Related components 66 | 67 | - [floor](./locomotion/floor.md) 68 | - [wall](./locomotion/wall.md) 69 | - [start](./locomotion/start.md) 70 | -------------------------------------------------------------------------------- /src/components/locomotion/floor.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("floor", { 4 | schema: { 5 | physics: { type: "boolean", default: true } 6 | }, 7 | 8 | update() { 9 | this.el.setAttribute("wall", this.data) 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/locomotion/floor.md: -------------------------------------------------------------------------------- 1 | # floor 2 | 3 | Add the `floor` component to any object you want the player to be able to walk on. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | 10 | ## Properties 11 | 12 | | Property | Description | Default | 13 | | ---------- | ------------------------------------------------------------ | ------- | 14 | | physics | Whether or not to add physics components automatically. | true | 15 | 16 | 17 | ## Events 18 | 19 | | Event | Description | 20 | | ----- | ----------------------------- | 21 | | enter | Player enters the floor | 22 | | leave | Player leaves the floor | 23 | | step | Player takes a simulated step | 24 | -------------------------------------------------------------------------------- /src/components/locomotion/start.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("start", { 4 | 5 | init() { 6 | let loco = this.el.sceneEl.querySelector("[locomotion]").components.locomotion 7 | if (!loco) return setTimeout(() => { this.init() }, 256) 8 | let pos = new THREE.Vector3() 9 | // console.log("starting at", pos) 10 | 11 | setTimeout(() => { 12 | this.el.object3D.localToWorld(pos.set(0, 0, 0)) 13 | loco.teleport(pos, true) 14 | setTimeout(() => { 15 | loco.toggleCrouch(true) 16 | }, 256) 17 | }, 256) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/components/locomotion/start.md: -------------------------------------------------------------------------------- 1 | # start 2 | 3 | Add the `start` component to the entity you want the player to start on top of. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | -------------------------------------------------------------------------------- /src/components/locomotion/wall.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("wall", { 4 | schema: { 5 | physics: { type: "boolean", default: true } 6 | }, 7 | 8 | update() { 9 | if (this.data.physics && !this.el.hasAttribute("body")) this.el.setAttribute("body", "type:static") 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/locomotion/wall.md: -------------------------------------------------------------------------------- 1 | # wall 2 | 3 | Add the `wall` component to any object you want the player not to be able to walk through. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | 10 | ## Properties 11 | 12 | | Property | Description | Default | 13 | | ---------- | ------------------------------------------------------------ | ------- | 14 | | physics | Whether or not to add physics components automatically. | true | 15 | 16 | 17 | ## Events 18 | 19 | | Event | Description | 20 | | ----- | ---------------------- | 21 | | bump | Player bumps into wall | 22 | -------------------------------------------------------------------------------- /src/components/onevent.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("onevent", { 4 | multiple: true, 5 | schema: { 6 | event: { type: "string" }, 7 | entity: { type: "selector" }, 8 | property: { type: "string" }, 9 | value: { type: "string" }, 10 | }, 11 | 12 | init() { 13 | this.trigger = this.trigger.bind(this) 14 | }, 15 | 16 | update(oldData) { 17 | this.pause() 18 | this._event = this.data.event || this.id || "" 19 | this._entity = this.data.entity || this.el 20 | this._property = this.data.property || "" 21 | this._value = this.data.value || "" 22 | if (this.el.isPlaying) 23 | this.play() 24 | }, 25 | 26 | play() { 27 | if (!this._event) return 28 | this.el.addEventListener(this._event, this.trigger) 29 | }, 30 | 31 | pause() { 32 | if (!this._event) return 33 | this.el.removeEventListener(this._event, this.trigger) 34 | }, 35 | 36 | trigger(e) { 37 | let args = this._property.split(".") 38 | args.push(this._value) 39 | this._entity.setAttribute(...args) 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /src/components/onevent.md: -------------------------------------------------------------------------------- 1 | # onevent 2 | 3 | Change a property when an event happens. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | 10 | ## Properties 11 | 12 | | Property | Description | Default | 13 | | ------------ | --------------------------- | --------- | 14 | | event | event to listen for | `this.id` | 15 | | entity | entity to affect | `this.el` | 16 | | property | property to change on event | 17 | | value | value to set property to | 18 | -------------------------------------------------------------------------------- /src/components/onstate.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("onstate", { 4 | multiple: true, 5 | schema: { 6 | state: { type: "string" }, 7 | entity: { type: "selector" }, 8 | property: { type: "string" }, 9 | on: { type: "string" }, 10 | off: { type: "string" }, 11 | }, 12 | 13 | init() { 14 | this.trigger = this.trigger.bind(this) 15 | }, 16 | 17 | update(oldData) { 18 | this._state = this.data.state || this.id || "" 19 | this._entity = this.data.entity || this.el 20 | this._property = this.data.property || "" 21 | this._on = this.data.on || "" 22 | this._off = this.data.off || "" 23 | }, 24 | 25 | play() { 26 | this.trigger() 27 | this.el.addEventListener("stateadded", this.trigger) 28 | this.el.addEventListener("stateremoved", this.trigger) 29 | }, 30 | 31 | pause() { 32 | this.el.removeEventListener("stateadded", this.trigger) 33 | this.el.removeEventListener("stateremoved", this.trigger) 34 | }, 35 | 36 | trigger(e) { 37 | if (e && e.detail !== this._state) return 38 | let args = this._property.split(".") 39 | args.push(this.el.is(this._state) ? this._on : this._off) 40 | this._entity.setAttribute(...args) 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /src/components/onstate.md: -------------------------------------------------------------------------------- 1 | # onstate 2 | 3 | Change a property when an state is added or removed. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | 10 | ## Properties 11 | 12 | | Property | Description | Default | 13 | | ------------ | ---------------------------------------------- | --------- | 14 | | state | state to listen for | `this.id` | 15 | | entity | entity to affect | `this.el` | 16 | | property | property to change on state change | 17 | | on | value to set property to when state is added | 18 | | off | value to set property to when state is removed | 19 | -------------------------------------------------------------------------------- /src/components/physics.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | const cmd = require("../libs/cmdCodec") 4 | const pkg = require("../../package") 5 | 6 | 7 | AFRAME.registerSystem("physics", { 8 | schema: { 9 | workerUrl: { type: "string", default: `https://cdn.jsdelivr.net/npm/a-game@${pkg.version}/dist/cannonWorker.min.js` }, 10 | gravity: { type: "vec3", default: { x: 0, y: -10, z: 0 } }, 11 | debug: { type: "boolean", default: false } 12 | }, 13 | 14 | update() { 15 | if (this.data.workerUrl) { 16 | if (!this.worker) { 17 | if (this.data.workerUrl.includes("//")) { 18 | let script = `importScripts(${JSON.stringify(this.data.workerUrl)})` 19 | this.worker = new Worker(`data:text/javascript;base64,${btoa(script)}`) 20 | } else { 21 | this.worker = new Worker(this.data.workerUrl) 22 | } 23 | this.worker.postMessage("log " + cmd.stringifyParam("Physics worker ready!")) 24 | this.worker.addEventListener("message", this.onMessage.bind(this)) 25 | } 26 | this.bodies = this.bodies || [] 27 | this.movingBodies = this.movingBodies || [] 28 | this.joints = this.joints || [] 29 | this.buffers = [new Float64Array(8), new Float64Array(8)] 30 | this.worker.postMessage("world gravity = " + cmd.stringifyParam(this.data.gravity)) 31 | this._debug = this.data.debug 32 | } else { 33 | this.remove() 34 | } 35 | }, 36 | 37 | remove() { 38 | this.worker && this.worker.terminate() 39 | this.worker = null 40 | this.bodies = [] 41 | this.movingBodies = [] 42 | }, 43 | 44 | tick(time, timeDelta) { 45 | if (!this.worker) return 46 | if (this.buffers.length < 2) return 47 | let buffer = this.buffers.shift() 48 | if (buffer.length < 8 * this.movingBodies.length) { 49 | let len = buffer.length 50 | while (len < 8 * this.movingBodies.length) { 51 | len *= 2 52 | } 53 | let bods = this.movingBodies 54 | buffer = new Float64Array(len) 55 | buffer.fill(NaN) 56 | let vec = THREE.Vector3.temp() 57 | let quat = THREE.Quaternion.temp() 58 | for (let i = 0; i < bods.length; i++) { 59 | let p = i * 8 60 | if (bods[i]) { 61 | bods[i].object3D.localToWorld(vec.set(0, 0, 0)) 62 | buffer[p++] = vec.x 63 | buffer[p++] = vec.y 64 | buffer[p++] = vec.z 65 | p++ 66 | bods[i].object3D.getWorldQuaternion(quat) 67 | buffer[p++] = quat.x 68 | buffer[p++] = quat.y 69 | buffer[p++] = quat.z 70 | buffer[p++] = quat.w 71 | } 72 | } 73 | } 74 | this.worker.postMessage(buffer, [buffer.buffer]) 75 | }, 76 | 77 | onMessage(e) { 78 | if (typeof e.data === "string") { 79 | let command = cmd.parse(e.data) 80 | switch (command.shift()) { 81 | case "world": 82 | this.command(command) 83 | break 84 | } 85 | } 86 | else if (e.data instanceof Float64Array) { 87 | this.buffers.push(e.data) 88 | while (this.buffers.length > 2) 89 | this.buffers.shift() 90 | } 91 | }, 92 | 93 | command(params) { 94 | if (typeof params[0] === "number") { 95 | params.shift() 96 | } 97 | switch (params.shift()) { 98 | case "body": 99 | let id = params.shift() 100 | let body = this.bodies[id] 101 | if (body) 102 | body.components.body.command(params) 103 | break 104 | } 105 | }, 106 | eval(expr) { 107 | this.worker.postMessage("world eval " + cmd.stringifyParam(expr)) 108 | } 109 | }) 110 | 111 | require("./physics/body") 112 | require("./physics/shape") 113 | require("./physics/joint") 114 | -------------------------------------------------------------------------------- /src/components/physics.md: -------------------------------------------------------------------------------- 1 | # physics 2 | 3 | A WebWorker based physics system.. 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ``` 15 | 16 | 17 | ## Properties 18 | 19 | | Property | Description | Default | 20 | | --------- | -------------------- | -------- | 21 | | workerUrl | URL of worker script | | 22 | | gravity | Gravity vector | 0 -10 0 | 23 | 24 | 25 | ## Methods 26 | 27 | | Method | Description | 28 | | ---------------- | -------------------------------------------------------------------------------- | 29 | | eval(expression) | Evaluate an expression in the worker, where `world` refers to the physics world. | 30 | 31 | Note: the `eval` method depends on the specific physics engine that the worker is based on.. 32 | 33 | 34 | ## Related components 35 | 36 | - [body](./physics/body.md) 37 | - [shape](./physics/shape.md) 38 | - [joint](./physics/joint.md) 39 | -------------------------------------------------------------------------------- /src/components/physics/body.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | const cmd = require("../../libs/cmdCodec") 4 | 5 | AFRAME.registerComponent("body", { 6 | dependencies: ["position", "rotation", "scale"], 7 | 8 | schema: { 9 | type: { type: "string", default: "dynamic" }, 10 | mass: { type: "number", default: 1 }, 11 | friction: { type: "number", default: 0.3 }, 12 | restitution: { type: "number", default: 0.3 }, 13 | belongsTo: { type: "int", default: 1 }, 14 | collidesWith: { type: "int", default: 1 }, 15 | emitsWith: { type: "int", default: 0 }, 16 | sleeping: { type: "boolean", default: false }, 17 | autoShape: { type: "boolean", default: true }, 18 | }, 19 | 20 | init() { 21 | let worker = this.el.sceneEl.systems.physics.worker 22 | let bodies = this.el.sceneEl.systems.physics.bodies 23 | let movingBodies = this.el.sceneEl.systems.physics.movingBodies 24 | let buffer = this.el.sceneEl.systems.physics.buffers[0] 25 | if (!worker) return 26 | this.id = bodies.indexOf(null) 27 | if (this.id < 0) this.id = bodies.length 28 | bodies[this.id] = this.el 29 | if (this.data.type !== "static") { 30 | this.mid = movingBodies.indexOf(null) 31 | if (this.mid < 0) this.mid = movingBodies.length 32 | movingBodies[this.mid] = this.el 33 | } else { 34 | this.mid = null 35 | } 36 | let body = { mid: this.mid } 37 | body.type = this.data.type 38 | body.position = this.el.object3D.localToWorld(THREE.Vector3.temp().set(0, 0, 0)) 39 | body.quaternion = this.el.object3D.getWorldQuaternion(THREE.Quaternion.temp()) 40 | if (this.mid !== null) { 41 | let p = this.mid * 8 42 | buffer[p++] = body.position.x 43 | buffer[p++] = body.position.y 44 | buffer[p++] = body.position.z 45 | buffer[p++] = this.data.sleeping 46 | buffer[p++] = body.quaternion.x 47 | buffer[p++] = body.quaternion.y 48 | buffer[p++] = body.quaternion.z 49 | buffer[p++] = body.quaternion.w 50 | } 51 | this.shapes = [] 52 | this.sleeping = true 53 | worker.postMessage("world body " + this.id + " create " + cmd.stringifyParam(body)) 54 | // if (body.type === "static") 55 | setTimeout(() => { 56 | body.position = this.el.object3D.localToWorld(THREE.Vector3.temp().set(0, 0, 0)) 57 | body.quaternion = this.el.object3D.getWorldQuaternion(THREE.Quaternion.temp()) 58 | worker.postMessage("world body " + this.id + " position = " + cmd.stringifyParam(body.position)) 59 | worker.postMessage("world body " + this.id + " quaternion = " + cmd.stringifyParam(body.quaternion)) 60 | 61 | if (this.el.components.shape) this.el.components.shape.play() 62 | let els = this.el.querySelectorAll("[shape]") 63 | if (els) els.forEach(el => { 64 | if (el.components.shape) el.components.shape.play() 65 | }) 66 | if (this.el.components.joint) this.el.components.joint.play() 67 | for (let comp in this.el.components) { 68 | if (comp.substr(0, 7) === "joint__") this.el.components[comp].play() 69 | } 70 | }) 71 | 72 | if (this.data.autoShape) { 73 | if (!this.el.components.shape) { 74 | if (this.el.firstElementChild) { 75 | let els = this.el.querySelectorAll("a-box, a-sphere, a-cylinder") 76 | if (els) els.forEach(el => { 77 | if (!el.components.shape) el.setAttribute("shape", true) 78 | }) 79 | } else { 80 | this.el.setAttribute("shape", true) 81 | } 82 | } 83 | } 84 | this._initiated = true 85 | }, 86 | 87 | play() { 88 | if (!this._initiated) { 89 | this.init() 90 | this.update({}) 91 | } 92 | }, 93 | 94 | update(oldData) { 95 | let worker = this.el.sceneEl.systems.physics.worker 96 | if (!worker) return 97 | if (this.data.type !== oldData.type) 98 | worker.postMessage("world body " + this.id + " type = " + cmd.stringifyParam(this.data.type)) 99 | if (this.data.mass !== oldData.mass) 100 | worker.postMessage("world body " + this.id + " mass = " + cmd.stringifyParam(this.data.mass)) 101 | if (this.data.friction !== oldData.friction) 102 | worker.postMessage("world body " + this.id + " friction = " + cmd.stringifyParam(this.data.friction)) 103 | if (this.data.restitution !== oldData.restitution) 104 | worker.postMessage("world body " + this.id + " restitution = " + cmd.stringifyParam(this.data.restitution)) 105 | if (this.data.belongsTo !== oldData.belongsTo) 106 | worker.postMessage("world body " + this.id + " belongsTo = " + cmd.stringifyParam(this.data.belongsTo)) 107 | if (this.data.collidesWith !== oldData.collidesWith) 108 | worker.postMessage("world body " + this.id + " collidesWith = " + cmd.stringifyParam(this.data.collidesWith)) 109 | if (this.data.emitsWith !== oldData.emitsWith) 110 | worker.postMessage("world body " + this.id + " emitsWith = " + cmd.stringifyParam(this.data.emitsWith)) 111 | // if (this.data.sleeping !== oldData.sleeping) 112 | setTimeout(() => { 113 | worker.postMessage("world body " + this.id + " sleeping = " + !!(this.data.sleeping)) 114 | }) 115 | }, 116 | 117 | sleep() { 118 | let worker = this.el.sceneEl.systems.physics.worker 119 | if (!worker) return 120 | worker.postMessage("world body " + this.id + " sleeping = true") 121 | this.sleeping = true 122 | }, 123 | 124 | applyWorldImpulse(force, point) { 125 | let worker = this.el.sceneEl.systems.physics.worker 126 | if (!worker) return 127 | worker.postMessage("world body " + this.id + " impulse " + cmd.stringifyParam(force) + " " + cmd.stringifyParam(point)) 128 | }, 129 | applyLocalImpulse(force, point) { 130 | let _point = this.el.object3D.localToWorld(THREE.Vector3.temp().copy(point)) 131 | let _force = this.el.object3D.localToWorld(THREE.Vector3.temp().copy(force)).sub(this.el.object3D.localToWorld(THREE.Vector3.temp().set(0, 0, 0))) 132 | this.applyWorldImpulse(_force, _point) 133 | }, 134 | 135 | pause() { 136 | let worker = this.el.sceneEl.systems.physics.worker 137 | let bodies = this.el.sceneEl.systems.physics.bodies 138 | let movingBodies = this.el.sceneEl.systems.physics.movingBodies 139 | if (!worker) return 140 | 141 | if (this.el.components.joint) this.el.components.joint.pause() 142 | for (let comp in this.el.components) { 143 | if (comp.substr(0, 7) === "joint__") this.el.components[comp].pause() 144 | } 145 | let els = this.el.querySelectorAll("[shape]") 146 | if (els) els.forEach(el => { 147 | if (el.components.shape) el.components.shape.pause() 148 | }) 149 | if (this.el.components.shape) this.el.components.shape.pause() 150 | 151 | bodies[this.id] = null 152 | if (this.mid !== null) 153 | movingBodies[this.mid] = null 154 | worker.postMessage("world body " + this.id + " remove") 155 | this._initiated = false 156 | }, 157 | 158 | tick() { 159 | let worker = this.el.sceneEl.systems.physics.worker 160 | let buffer = this.el.sceneEl.systems.physics.buffers[0] 161 | if (!worker) return 162 | if (this.mid !== null) { 163 | let p = this.mid * 8 164 | if (buffer.length <= p) return 165 | if (this.data.type === "kinematic") { 166 | let vec = this.el.object3D.localToWorld(THREE.Vector3.temp().set(0, 0, 0)) 167 | buffer[p++] = vec.x 168 | buffer[p++] = vec.y 169 | buffer[p++] = vec.z 170 | this.sleeping = !!(buffer[p++]) 171 | let quat = this.el.object3D.getWorldQuaternion(THREE.Quaternion.temp()) 172 | buffer[p++] = quat.x 173 | buffer[p++] = quat.y 174 | buffer[p++] = quat.z 175 | buffer[p++] = quat.w 176 | } else if (buffer[p + 1]) { 177 | let quat = THREE.Quaternion.temp() 178 | 179 | this.el.object3D.position.set(buffer[p++], buffer[p++], buffer[p++]) 180 | this.el.object3D.parent.worldToLocal(this.el.object3D.position) 181 | this.sleeping = !!(buffer[p++]) 182 | 183 | this.el.object3D.getWorldQuaternion(quat) 184 | this.el.object3D.quaternion.multiply(quat.conjugate().normalize()) 185 | quat.set(buffer[p++], buffer[p++], buffer[p++], buffer[p++]) 186 | this.el.object3D.quaternion.multiply(quat.normalize()) 187 | } 188 | } 189 | }, 190 | 191 | command(params) { 192 | switch (params.shift()) { 193 | case "emits": 194 | let e = params.shift() 195 | switch (e.event) { 196 | case "collision": 197 | let bodies = this.el.sceneEl.systems.physics.bodies 198 | e.body1 = bodies[e.body1] 199 | e.body2 = bodies[e.body2] 200 | if (!e.body1 || !e.body2) return 201 | e.shape1 = e.body1.components.body.shapes[e.shape1] 202 | e.shape2 = e.body2.components.body.shapes[e.shape2] 203 | break 204 | } 205 | this.el.emit(e.event, e) 206 | break 207 | } 208 | }, 209 | eval(expr) { 210 | let worker = this.el.sceneEl.systems.physics.worker 211 | worker.postMessage("world body " + this.id + " eval " + cmd.stringifyParam(expr)) 212 | }, 213 | 214 | commit() { 215 | let worker = this.el.sceneEl.systems.physics.worker 216 | let pos = THREE.Vector3.temp() 217 | let quat = THREE.Quaternion.temp() 218 | this.el.object3D.localToWorld(pos.set(0, 0, 0)) 219 | worker.postMessage("world body " + this.id + " position " + cmd.stringifyParam(pos)) 220 | this.el.object3D.getWorldQuaternion(quat) 221 | worker.postMessage("world body " + this.id + " quaternion " + cmd.stringifyParam(quat)) 222 | } 223 | }) 224 | 225 | -------------------------------------------------------------------------------- /src/components/physics/body.md: -------------------------------------------------------------------------------- 1 | # body 2 | 3 | A physical body.. 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ``` 15 | 16 | 17 | ## Properties 18 | 19 | | Property | Description | Default | 20 | | ------------ | ------------------------------------------------------ | ---------- | 21 | | type | `static`, `dynamic` or `kinematic` | dynamic | 22 | | mass | Mass of body | 1 | 23 | | friction | Friction of body | 0.3 | 24 | | restitution | Bounciness of body | 0.3 | 25 | | belongsTo | Bitmask of groups body belongs to | 1 | 26 | | collidesWith | Bitmask of groups body collides with | 1 | 27 | | emitsWith | Bitmask of groups body emits event when colliding with | 0 | 28 | | sleeping | Whether or not to start sleeping | false | 29 | | autoShape | Automatically add `shape` components | true | 30 | 31 | If `autoShape` is `true`, `shape` components will be added to all applicable descendants (`a-box`, `a-sphere` and `a-cylinder`).. If the body entity has no child nodes, `shape` component will be added to the body entity itself.. 32 | 33 | 34 | ## Methods 35 | 36 | | Method | Description | 37 | | ------------------------------- | ----------------------------------------------------------------------------------------------- | 38 | | eval(expression) | Evaluate an expression in the worker, where `world` and `body` refer to their native instances. | 39 | | commit() | Commit current position and orientation to the physics world. | 40 | | applyWorldImpulse(force, point) | Apply impulse to the body in world vectors. | 41 | | applyLocalImpulse(force, point) | Apply impulse to the body in local vectors. | 42 | 43 | Note: the `eval` method depends on the specific physics engine that the worker is based on.. 44 | -------------------------------------------------------------------------------- /src/components/physics/joint.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | const cmd = require("../../libs/cmdCodec") 4 | 5 | AFRAME.registerComponent("joint", { 6 | // dependencies: ["body", "shape"], 7 | multiple: true, 8 | 9 | schema: { 10 | type: { type: "string", default: "ball" }, 11 | body1: { type: "selector" }, 12 | body2: { type: "selector" }, 13 | pivot1: { type: "vec3", default: { x: 0, y: 0, z: 0 } }, 14 | pivot2: { type: "vec3", default: { x: 0, y: 0, z: 0 } }, 15 | axis1: { type: "vec3", default: { x: 0, y: 1, z: 0 } }, 16 | axis2: { type: "vec3", default: { x: 0, y: 1, z: 0 } }, 17 | min: { type: "number", default: 0 }, 18 | max: { type: "number", default: 1 }, 19 | collision: { type: "boolean", default: true }, 20 | // limit: { type: "array" }, 21 | // motor: { type: "array" }, 22 | // spring: { type: "array" }, 23 | }, 24 | 25 | play() { 26 | if (this._id != null) return 27 | let worker = this.el.sceneEl.systems.physics.worker 28 | let joints = this.el.sceneEl.systems.physics.joints 29 | if (!worker) return 30 | if (!this.data.body1.components.body) return this._retry = setTimeout(() => { 31 | this.play() 32 | }, 256) 33 | if (!this.data.body2.components.body) return this._retry = setTimeout(() => { 34 | this.play() 35 | }, 256) 36 | this._id = joints.indexOf(null) 37 | if (this._id < 0) this._id = joints.length 38 | joints[this._id] = this.el 39 | 40 | // setTimeout(() => { 41 | let joint = {} 42 | joint.type = this.data.type 43 | joint.body1 = this.data.body1 ? this.data.body1.components.body.id : this.el.components.body.id 44 | joint.body2 = this.data.body2.components.body.id 45 | joint.pivot1 = THREE.Vector3.temp().copy(this.data.pivot1) 46 | joint.pivot2 = THREE.Vector3.temp().copy(this.data.pivot2) 47 | joint.axis1 = this.data.axis1 48 | joint.axis2 = this.data.axis2 49 | joint.min = this.data.min 50 | joint.max = this.data.max 51 | joint.collision = this.data.collision 52 | let scale = this.el.object3D.getWorldScale(THREE.Vector3.temp()) 53 | joint.pivot1.multiply(scale) 54 | joint.pivot2.multiply(scale) 55 | worker.postMessage("world joint " + this._id + " create " + cmd.stringifyParam(joint)) 56 | // }) 57 | }, 58 | 59 | update(oldData) { 60 | let worker = this.el.sceneEl.systems.physics.worker 61 | if (!worker) return 62 | this.data.body1 = this.data.body1 || this.el 63 | // if (this.data.type !== oldData.type) 64 | // worker.postMessage("world joint " + this._id + " type = " + cmd.stringifyParam(this.data.type)) 65 | }, 66 | 67 | pause() { 68 | clearTimeout(this._retry) 69 | let worker = this.el.sceneEl.systems.physics.worker 70 | let joints = this.el.sceneEl.systems.physics.joints 71 | if (!worker) return 72 | joints[this._id] = null 73 | worker.postMessage("world joint " + this._id + " remove") 74 | this._id = null 75 | }, 76 | eval(expr) { 77 | let worker = this.el.sceneEl.systems.physics.worker 78 | worker.postMessage("world joint " + this._id + " eval " + cmd.stringifyParam(expr)) 79 | } 80 | 81 | }) 82 | 83 | -------------------------------------------------------------------------------- /src/components/physics/joint.md: -------------------------------------------------------------------------------- 1 | # joint 2 | 3 | A way to connect bodies together.. 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ``` 20 | 21 | 22 | ## Properties 23 | 24 | | Property | Description | Default | 25 | | --------- | -------------------------------------- | -------- | 26 | | type | `distance`, `hinge`, `lock` or `point` | point | 27 | | body1 | first body to join | this one | 28 | | body2 | second body to join | null | 29 | | pivot1 | pivot point of first body | 0 0 0 | 30 | | pivot2 | pivot point of second body | 0 0 0 | 31 | | axis1 | axis of first body | 0 1 0 | 32 | | axis2 | axis of second body | 0 1 0 | 33 | | min | minimum distance between bodies | 0 | 34 | | max | maximum distance between bodies | 1 | 35 | | collision | connected bodies allowed to collide | true | 36 | 37 | 38 | ## Methods 39 | 40 | | Method | Description | 41 | | ---------------- | ------------------------------------------------------------------------------------------------ | 42 | | eval(expression) | Evaluate an expression in the worker, where `world` and `joint` refer to their native instances. | 43 | 44 | Note: the `eval` method depends on the specific physics engine that the worker is based on.. 45 | -------------------------------------------------------------------------------- /src/components/physics/shape.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | const cmd = require("../../libs/cmdCodec") 4 | 5 | AFRAME.registerComponent("shape", { 6 | // dependencies: ["body"], 7 | schema: { 8 | }, 9 | 10 | play() { 11 | if (this.id != null) return 12 | let worker = this.el.sceneEl.systems.physics.worker 13 | if (!worker) return 14 | 15 | this.body = this.el 16 | while (this.body && !this.body.matches("[body]")) this.body = this.body.parentElement 17 | if (!this.body) return this._retry = setTimeout(() => { 18 | this.play() 19 | }, 256) 20 | this.bodyId = this.body.components.body.id 21 | 22 | let shapes = this.body.components.body.shapes 23 | this.id = shapes.indexOf(null) 24 | if (this.id < 0) this.id = shapes.length 25 | shapes[this.id] = this.el 26 | 27 | let shape = {} 28 | shape.position = this.el.object3D.getWorldPosition(THREE.Vector3.temp()) 29 | this.body.object3D.worldToLocal(shape.position) 30 | shape.quaternion = this.el.object3D.getWorldQuaternion(THREE.Quaternion.temp()) 31 | let bodyquat = this.body.object3D.getWorldQuaternion(THREE.Quaternion.temp()) 32 | shape.quaternion.multiply(bodyquat.conjugate().normalize()).normalize() 33 | shape.size = THREE.Vector3.temp().set(1, 1, 1) 34 | 35 | switch (this.el.tagName.toLowerCase()) { 36 | case "a-sphere": 37 | shape.type = "sphere" 38 | shape.size.multiplyScalar(this.el.components.geometry.data.radius * 2) 39 | break 40 | case "a-cylinder": 41 | shape.type = "cylinder" 42 | shape.size.multiplyScalar(this.el.components.geometry.data.radius * 2).y = this.el.components.geometry.data.height 43 | break 44 | case "a-box": 45 | shape.type = "box" 46 | shape.size.set( 47 | this.el.components.geometry.data.width, 48 | this.el.components.geometry.data.height, 49 | this.el.components.geometry.data.depth 50 | ) 51 | break 52 | // case "a-plane": 53 | // shape.type = "plane" 54 | // break 55 | } 56 | let scale = this.el.object3D.getWorldScale(THREE.Vector3.temp()) 57 | shape.size.multiply(scale) 58 | shape.position.multiply(scale) 59 | 60 | worker.postMessage("world body " + this.bodyId + " shape " + this.id + " create " + cmd.stringifyParam(shape)) 61 | }, 62 | 63 | pause() { 64 | clearTimeout(this._retry) 65 | if (!this.body) return 66 | let worker = this.el.sceneEl.systems.physics.worker 67 | if (!worker) return 68 | let shapes = this.body.components.body.shapes 69 | worker.postMessage("world body " + this.bodyId + " shape " + this.id + " remove") 70 | shapes[this.id] = null 71 | this.id = null 72 | }, 73 | 74 | eval(expr) { 75 | let worker = this.el.sceneEl.systems.physics.worker 76 | worker.postMessage("world body " + this.bodyId + " shape " + this.id + " eval " + cmd.stringifyParam(expr)) 77 | } 78 | }) 79 | 80 | -------------------------------------------------------------------------------- /src/components/physics/shape.md: -------------------------------------------------------------------------------- 1 | # shape 2 | 3 | A physical shape for a physical body.. The `shape` component are added automatically by default.. 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ``` 16 | 17 | 18 | ## Methods 19 | 20 | | Method | Description | 21 | | ---------------- | -------------------------------------------------------------------------------------------------------- | 22 | | eval(expression) | Evaluate an expression in the worker, where `world`, `body` and `shape` refer to their native instances. | 23 | 24 | Note: the `eval` method depends on the specific physics engine that the worker is based on.. 25 | -------------------------------------------------------------------------------- /src/components/script.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("script", { 4 | schema: { 5 | src: { type: "string" }, 6 | call: { type: "string" }, 7 | args: { type: "array" }, 8 | }, 9 | 10 | async update(oldData) { 11 | if (this.data.src !== oldData.src) { 12 | if (this.script) { 13 | if (this.el.isPlaying) 14 | this.script.pause?.() 15 | this.script.remove?.() 16 | } 17 | this.script = null 18 | 19 | let response = await fetch(this.data.src) 20 | if (response.status >= 200 && response.status < 300) { 21 | this.script = eval(await (await (response).text())) 22 | this.script.el = this.el 23 | if (this.script.events) { 24 | for (let event in this.script.events) { 25 | this.script.events[event] = this.script.events[event].bind(this.script) 26 | } 27 | } 28 | } else { 29 | console.error("Could not load", this.data.src) 30 | } 31 | this.script.init?.() 32 | if (this.el.isPlaying) 33 | this.script.play?.() 34 | } 35 | if (this.script && this.data.call?.trim()) { 36 | this.script[this.data.call.trim()](...this.data.args) 37 | this.el.setAttribute("script", "call", "") 38 | } 39 | }, 40 | 41 | remove() { 42 | if (!this.script) return 43 | this.script.remove?.(...arguments) 44 | }, 45 | tick() { 46 | if (!this.script) return 47 | this.script.tick?.(...arguments) 48 | }, 49 | tock() { 50 | if (!this.script) return 51 | this.script.tock?.(...arguments) 52 | }, 53 | play() { 54 | if (!this.script) return 55 | if (this.script.events) { 56 | for (let event in this.script.events) { 57 | this.el.addEventListener(event, this.script.events[event]) 58 | } 59 | } 60 | this.script.play?.(...arguments) 61 | }, 62 | pause() { 63 | if (!this.script) return 64 | if (this.script.events) { 65 | for (let event in this.script.events) { 66 | this.el.removeEventListener(event, this.script.events[event]) 67 | } 68 | } 69 | this.script.pause?.(...arguments) 70 | }, 71 | }) 72 | -------------------------------------------------------------------------------- /src/components/script.md: -------------------------------------------------------------------------------- 1 | # script 2 | 3 | Component for including external scripts into the scene. 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | ``` 11 | 12 | The script file is expected to be an object of named functions that can be called by setting the `call` property. This can work well in combination with the [onevent](./onevent.md), [onstate](./onstate.md) and [trigger](./trigger.md) components. 13 | 14 | It can even act like its own component, with most of the same methods regular components have, except for `schema` and `update`. 15 | 16 | ```js 17 | ({ 18 | init() { 19 | console.log("Hello demo!") 20 | }, 21 | 22 | inside() { 23 | console.log("I just went in!") 24 | }, 25 | outside() { 26 | console.log("I left!") 27 | }, 28 | }) 29 | ``` 30 | 31 | 32 | ## Properties 33 | 34 | | Property | Description | 35 | | -------- | --------------------------------------------------------- | 36 | | src | url to the script file | 37 | | call | name of a function to call | 38 | | args | comma-separated list of arguments to pass to the function | 39 | -------------------------------------------------------------------------------- /src/components/trigger.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerComponent("trigger", { 4 | schema: { 5 | objects: { type: "string", default: ".head-bumper" }, 6 | }, 7 | 8 | init() { 9 | this._refreshTO = setInterval(this.refreshObjects.bind(this), 1024) 10 | }, 11 | 12 | remove() { 13 | clearInterval(this._refreshTO) 14 | }, 15 | 16 | tick() { 17 | if (!this.objects) return this.refreshObjects() 18 | let local = THREE.Vector3.temp() 19 | let width = this.el.components.geometry.data.width 20 | let height = this.el.components.geometry.data.height 21 | let depth = this.el.components.geometry.data.depth 22 | let radius = this.el.components.geometry.data.radius 23 | let inside 24 | for (let obj of this.objects) { 25 | obj.object3D.localToWorld(local.set(0, 0, 0)) 26 | this.el.object3D.worldToLocal(local) 27 | switch (this.el.tagName.toLowerCase()) { 28 | case "a-sphere": 29 | inside = local.length() < radius 30 | break 31 | case "a-box": 32 | inside = Math.abs(local.x) < width / 2 33 | && Math.abs(local.y) < height / 2 34 | && Math.abs(local.z) < depth / 2 35 | break 36 | case "a-cylinder": 37 | inside = Math.abs(local.y) < height / 2 38 | local.y = 0 39 | inside = inside && local.length() < radius 40 | break 41 | } 42 | if (inside && this.triggered.indexOf(obj) < 0) { 43 | let d = { 44 | trigger: this.el, 45 | object: obj, 46 | } 47 | this.el.addState("triggered") 48 | this.el.emit("trigger", d) 49 | obj.emit("trigger", d) 50 | this.triggered.push(obj) 51 | } 52 | if (!inside && this.triggered.indexOf(obj) >= 0) { 53 | let d = { 54 | trigger: this.el, 55 | object: obj, 56 | } 57 | this.el.emit("untrigger", d) 58 | obj.emit("untrigger", d) 59 | this.triggered.splice(this.triggered.indexOf(obj), 1) 60 | if (!this.triggered.length) 61 | this.el.removeState("triggered") 62 | } 63 | } 64 | }, 65 | 66 | refreshObjects() { 67 | this.objects = this.objects || [] 68 | this.triggered = this.triggered || [] 69 | this.objects.splice(0, this.objects.length) 70 | let els = this.el.sceneEl.querySelectorAll(this.data.objects) 71 | if (!els) return 72 | els.forEach(el => { 73 | this.objects.push(el) 74 | }) 75 | for (let i = 0; i < this.triggered.length; i++) { 76 | let obj = this.triggered[i] 77 | if (this.objects.indexOf(obj) < 0) { 78 | this.triggered.splice(i, 1) 79 | i-- 80 | } 81 | } 82 | }, 83 | 84 | 85 | }) 86 | -------------------------------------------------------------------------------- /src/components/trigger.md: -------------------------------------------------------------------------------- 1 | # trigger 2 | 3 | Trigger an event when certain objects enter or leave this object. 4 | Only works with `a-box`, `a-sphere` and `a-cylinder` entities. 5 | 6 | ```html 7 | 8 | ``` 9 | 10 | 11 | ## Properties 12 | 13 | | Property | Description | Default | 14 | | -------- | ---------------------------------------------------- | ---------- | 15 | | objects | selector for the type of objects to get triggered by | `.head-bumper` | 16 | 17 | 18 | ## Events 19 | 20 | These events are emitted by both the triggered and the triggering object. 21 | 22 | | Event | Description | 23 | | --------- | ------------------ | 24 | | trigger | object has entered | 25 | | untrigger | object has left | 26 | 27 | 28 | ## States 29 | 30 | | State | Description | 31 | | --------- | ----------------------------------------- | 32 | | triggered | at least one object is inside the trigger | 33 | -------------------------------------------------------------------------------- /src/libs/betterRaycaster.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | const _update = AFRAME.components.raycaster.Component.prototype.update 4 | const _refreshObjects = AFRAME.components.raycaster.Component.prototype.refreshObjects 5 | AFRAME.components.raycaster.schema.deep = AFRAME.components.raycaster.schema.showLine 6 | 7 | AFRAME.components.raycaster.Component.prototype.update = function (oldData) { 8 | if (this.data.deep && oldData.objects !== this.data.objects) { 9 | this._matchSelector = this.data.objects 10 | this.data.objects = deepMatch(this.data.objects) 11 | } 12 | return _update.apply(this, arguments) 13 | } 14 | 15 | AFRAME.components.raycaster.Component.prototype.refreshObjects = function () { 16 | let result = _refreshObjects.apply(this, arguments) 17 | if (this.data.deep) { 18 | let hits = this.intersections 19 | for (let hit of hits) { 20 | hit.el = hit.object.el 21 | while (hit.el && !hit.el.matches(this._matchSelector)) hit.el = hit.el.parentNode 22 | } 23 | } 24 | return result 25 | } 26 | 27 | 28 | function deepMatch(selector) { 29 | if (selector.indexOf("*") >= 0) return selector 30 | let deep = (selector + ", ").replaceAll(",", " *,") 31 | return deep + selector 32 | } -------------------------------------------------------------------------------- /src/libs/cmdCodec.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parse(cmd) { 3 | let words = cmd.split(" ") 4 | let args = [] 5 | for (let word of words) { 6 | if (word) { 7 | try { 8 | args.push(JSON.parse(word)) 9 | } catch (error) { 10 | if (word !== "=") 11 | args.push(word) 12 | } 13 | } 14 | } 15 | return args 16 | }, 17 | stringifyParam(val) { 18 | return JSON.stringify(val).replaceAll(" ", "\\u0020").replaceAll("\"_", "\"") 19 | } 20 | } -------------------------------------------------------------------------------- /src/libs/copyWorldPosRot.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.AEntity.prototype.copyWorldPosRot = function (srcEl) { 4 | let quat = THREE.Quaternion.temp() 5 | let src = srcEl.object3D 6 | let dest = this.object3D 7 | if (!src) return 8 | if (!dest) return 9 | if (!dest.parent) return 10 | src.localToWorld(dest.position.set(0, 0, 0)) 11 | dest.parent.worldToLocal(dest.position) 12 | 13 | dest.getWorldQuaternion(quat) 14 | dest.quaternion.multiply(quat.conjugate().normalize()) 15 | src.getWorldQuaternion(quat) 16 | dest.quaternion.multiply(quat.normalize()) 17 | dest.updateWorldMatrix(true, true) 18 | } -------------------------------------------------------------------------------- /src/libs/ensureElement.js: -------------------------------------------------------------------------------- 1 | Element.prototype.ensure = function (selector, name = selector, attrs = {}, innerHTML = "") { 2 | let _childEl, attr, val 3 | _childEl = this.querySelector(selector) 4 | if (!_childEl) { 5 | _childEl = document.createElement(name) 6 | this.appendChild(_childEl) 7 | for (attr in attrs) { 8 | val = attrs[attr] 9 | _childEl.setAttribute(attr, val) 10 | } 11 | _childEl.innerHTML = innerHTML 12 | } 13 | return _childEl 14 | } -------------------------------------------------------------------------------- /src/libs/pools.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | function makePool(Class) { 4 | Class._pool = [] 5 | Class._inUse = [] 6 | Class.temp = function () { 7 | let v = Class._pool.pop() || new Class() 8 | Class._inUse.push(v) 9 | if (!Class._gc) 10 | Class._gc = setTimeout(Class._recycle) 11 | return v 12 | } 13 | Class._recycle = function () { 14 | while (Class._inUse.length) 15 | Class._pool.push(Class._inUse.pop()) 16 | Class._gc = false 17 | } 18 | } 19 | 20 | makePool(THREE.Vector2) 21 | makePool(THREE.Vector3) 22 | makePool(THREE.Quaternion) 23 | makePool(THREE.Matrix3) 24 | makePool(THREE.Matrix4) 25 | -------------------------------------------------------------------------------- /src/libs/touchGestures.js: -------------------------------------------------------------------------------- 1 | let _addEventListener = Element.prototype.addEventListener 2 | let _removeEventListener = Element.prototype.removeEventListener 3 | let init = el => { 4 | if (el._tgest) return el._tgest 5 | el._tgest = { 6 | handlers: { 7 | swipeup: [], 8 | swipedown: [], 9 | swipeleft: [], 10 | swiperight: [], 11 | tap: [], 12 | hold: [] 13 | } 14 | } 15 | let cx, cy, to, held 16 | let emit = (type, e) => { 17 | if (el._tgest.handlers[type]) { 18 | for (let handler of el._tgest.handlers[type]) { 19 | handler(e) 20 | } 21 | } else console.log(type, el._tgest.handlers[type]) 22 | } 23 | el.addEventListener("touchstart", e => { 24 | cx = e.changedTouches[0].screenX 25 | cy = e.changedTouches[0].screenY 26 | held = false 27 | to = setTimeout(() => { 28 | held = true 29 | emit("hold", e) 30 | }, 512) 31 | }) 32 | el.addEventListener("touchmove", e => { 33 | let x = e.changedTouches[0].screenX, 34 | y = e.changedTouches[0].screenY, 35 | l = Math.sqrt(Math.pow(cx - x, 2) + Math.pow(cy - y, 2)) 36 | if (l > 32) { 37 | clearTimeout(to) 38 | if (held) return 39 | if (Math.abs(cx - x) > Math.abs(cy - y)) { 40 | if (x < cx) emit("swipeleft", e) 41 | else emit("swiperight", e) 42 | } else { 43 | if (y < cy) emit("swipeup", e) 44 | else emit("swipedown", e) 45 | } 46 | held = true 47 | } 48 | }) 49 | el.addEventListener("touchend", e => { 50 | clearTimeout(to) 51 | let x = e.changedTouches[0].screenX, 52 | y = e.changedTouches[0].screenY, 53 | l = Math.sqrt(Math.pow(cx - x, 2) + Math.pow(cy - y, 2)) 54 | if (l < 32) { 55 | if (held) return 56 | emit("tap", e) 57 | } 58 | }) 59 | 60 | return el._tgest 61 | } 62 | Element.prototype.addEventListener = function (eventtype, handler) { 63 | switch (eventtype) { 64 | case "swipeup": 65 | case "swipedown": 66 | case "swipeleft": 67 | case "swiperight": 68 | case "tap": 69 | case "hold": 70 | let tg = init(this) 71 | tg.handlers[eventtype].push(handler) 72 | break 73 | default: 74 | return _addEventListener.call(this, eventtype, handler) 75 | } 76 | } 77 | Element.prototype.removeEventListener = function (eventtype, handler) { 78 | switch (eventtype) { 79 | case "swipeup": 80 | case "swipedown": 81 | case "swipeleft": 82 | case "swiperight": 83 | case "tap": 84 | case "hold": 85 | let tg = init(this) 86 | let i = tg.handlers[eventtype].indexOf(handler) 87 | if (i >= 0) tg.handlers[eventtype].splice(i, 1) 88 | break 89 | default: 90 | return _removeEventListener.call(this, eventtype, handler) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/primitives/a-glove.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerPrimitive("a-glove", { 4 | defaultComponents: { 5 | injectglove: {} 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /src/primitives/a-glove.md: -------------------------------------------------------------------------------- 1 | # a-hand 2 | 3 | Primitive entity for player's virtual hands. This will auto-populate with a default glove, if empty. 4 | 5 | ```html 6 | 7 | 8 | 9 | ``` 10 | -------------------------------------------------------------------------------- /src/primitives/a-hand.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerPrimitive("a-hand", { 4 | mappings: { 5 | side: "tracked-controls.hand" 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /src/primitives/a-hand.md: -------------------------------------------------------------------------------- 1 | # a-hand 2 | 3 | Primitive entity for player hands. 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | ``` 11 | -------------------------------------------------------------------------------- /src/primitives/a-main.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerPrimitive("a-main", {}) -------------------------------------------------------------------------------- /src/primitives/a-main.md: -------------------------------------------------------------------------------- 1 | # a-main 2 | 3 | Just a semantic primitive to wrap all the main content of the scene, that is not assets, scene setup or other boilerplate.. 4 | Useful for loading scenes and levels using the `include` component and have a place to dynamically add new entities without cluttering up the root of the `a-scene`.. 5 | 6 | ```html 7 | 8 | 9 | 10 | 11 | 12 | ``` 13 | -------------------------------------------------------------------------------- /src/primitives/a-player.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | AFRAME.registerPrimitive("a-player", { 4 | defaultComponents: { 5 | injectplayer: {} 6 | } 7 | }) -------------------------------------------------------------------------------- /src/primitives/a-player.md: -------------------------------------------------------------------------------- 1 | # a-player 2 | 3 | Primitive entity for the player space.. 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | `a-player` will ensure that it's populated with at least these entities: 10 | 11 | ```html 12 | 13 | 14 | 15 | 16 | 17 | ``` 18 | 19 | If `grabbing` component is added, `a-hand` entities will be populated with [`a-glove`](a-glove.md) entities.. 20 | 21 | Any of these entities can be overruled with custom properties and additional entities can be added.. --------------------------------------------------------------------------------