├── .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..
--------------------------------------------------------------------------------