├── .gitignore ├── .jshintrc ├── README.md ├── .editorconfig ├── test.html ├── test ├── assert.js ├── TestSlideMove.js └── TestTrace.js ├── LICENSE ├── index.html └── src ├── Collisions.js ├── vendor └── fpsmeter.min.js ├── Game.js └── Player.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.debug.js -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bunnyhop-webgl 2 | ============== 3 | 4 | Super work-in-progress. Eventually might become a bunnyhopping tutorial based on [Fortress Forever](http://www.fortress-forever.com/)'s training mode. 5 | 6 | Try it (`WASD` to move, `space` to jump): https://squeek502.github.io/bunnyhop-webgl/ 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = null 10 | 11 | [*.html] 12 | indent_style = space 13 | indent_size = 2 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | max_line_length = null 18 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | Bunnyhop Test Runner 11 | 12 | 13 | 14 | 15 | 16 |
tests running or failed, see console
17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/assert.js: -------------------------------------------------------------------------------- 1 | export function equal(firstValue, secondValue) { 2 | if (firstValue != secondValue) 3 | throw new Error('Assert failed, ' + firstValue + ' is not equal to ' + secondValue + '.'); 4 | } 5 | 6 | export function notEqual(firstValue, secondValue) { 7 | if (firstValue == secondValue) 8 | throw new Error('Assert failed, ' + firstValue + ' is equal to ' + secondValue + '.'); 9 | } 10 | 11 | export function closeTo(firstValue, secondValue) { 12 | let delta = Math.abs(firstValue - secondValue); 13 | if (delta > 0.01) 14 | throw new Error('Assert failed, ' + firstValue + ' is not close to ' + secondValue + '.'); 15 | } 16 | 17 | export function vecsEqual(firstValue, secondValue) { 18 | if (!firstValue.equalsWithEpsilon(secondValue, 0.01)) 19 | throw new Error('Assert failed, ' + firstValue + ' is not equal to ' + secondValue + '.'); 20 | } 21 | -------------------------------------------------------------------------------- /test/TestSlideMove.js: -------------------------------------------------------------------------------- 1 | import * as Collisions from '../src/Collisions.js'; 2 | import Player from '../src/Player.js'; 3 | import * as assert from './assert.js'; 4 | 5 | // This tests an edge case of slideMove where the collision is so close to not being 6 | // a collision that things have a chance of failing, i.e. clipVelocity will not alter velocity 7 | // and slideMove will set velocity to zero even though it shouldn't 8 | // 9 | // It was fixed by adding COLLIDE_EPSILON in Collisions.js 10 | var scene = { 11 | meshes: [ 12 | [ 13 | { 14 | dist: -118.28204018567811, 15 | normal: new BABYLON.Vector3(0, 0.9510565161477913, -0.3090169948284817) 16 | } 17 | ] 18 | ] 19 | }; 20 | var pos = new BABYLON.Vector3(229.83204725586555, 62.68445117563382, 559.5904979793653); 21 | var vel = new BABYLON.Vector3(-138.86182667422605, 275.297184958244, 847.277612601874); 22 | var player = new Player(scene, pos); 23 | player.velocity = vel; 24 | player.onGround = false; 25 | 26 | player.slideMove(0.033); 27 | 28 | if (player.velocity.equals(BABYLON.Vector3.Zero())) { 29 | throw new Error("player velocity set to zero when it shouldn't be"); 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /test/TestTrace.js: -------------------------------------------------------------------------------- 1 | import * as Collisions from '../src/Collisions.js'; 2 | import * as assert from './assert.js'; 3 | 4 | var runTest = function(input, expected) { 5 | var trace = Collisions.ClipBoxToPlanes(input.mins, input.maxs, input.start, input.end, [input.plane]); 6 | 7 | assert.equal(trace.startsolid, expected.startsolid); 8 | assert.equal(trace.allsolid, expected.allsolid); 9 | assert.closeTo(trace.fraction, expected.fraction); 10 | if (expected.normal !== undefined) 11 | assert.vecsEqual(trace.plane.normal, expected.normal); 12 | else 13 | assert.equal(trace.plane, undefined); 14 | }; 15 | 16 | // start out -> collide 17 | runTest({ 18 | mins: new BABYLON.Vector3(-16,-16,-32), 19 | maxs: new BABYLON.Vector3(16,16,32), 20 | start: new BABYLON.Vector3(0,0,128), 21 | end: new BABYLON.Vector3(0,0,0), 22 | plane:{ 23 | dist: 0, 24 | normal: new BABYLON.Vector3(0,0,1) 25 | }, 26 | },{ 27 | startsolid: 0, 28 | allsolid: 0, 29 | fraction: 0.75, 30 | normal: new BABYLON.Vector3(0,0,1) 31 | }); 32 | 33 | // start out -> stay out 34 | runTest({ 35 | mins: new BABYLON.Vector3(-16,-16,-32), 36 | maxs: new BABYLON.Vector3(16,16,32), 37 | start: new BABYLON.Vector3(0,0,128), 38 | end: new BABYLON.Vector3(0,0,128), 39 | plane:{ 40 | dist: 0, 41 | normal: new BABYLON.Vector3(0,0,1) 42 | }, 43 | },{ 44 | startsolid: 0, 45 | allsolid: 0, 46 | fraction: 1, 47 | normal: undefined 48 | }); 49 | 50 | // start in -> stay in 51 | runTest({ 52 | mins: new BABYLON.Vector3(-16,-16,-32), 53 | maxs: new BABYLON.Vector3(16,16,32), 54 | start: new BABYLON.Vector3(0,0,0), 55 | end: new BABYLON.Vector3(0,0,0), 56 | plane:{ 57 | dist: 0, 58 | normal: new BABYLON.Vector3(0,0,1) 59 | }, 60 | },{ 61 | startsolid: 1, 62 | allsolid: 1, 63 | fraction: 1, 64 | normal: undefined 65 | }); 66 | 67 | // start in -> leave 68 | runTest({ 69 | mins: new BABYLON.Vector3(-16,-16,-32), 70 | maxs: new BABYLON.Vector3(16,16,32), 71 | start: new BABYLON.Vector3(0,0,0), 72 | end: new BABYLON.Vector3(0,0,128), 73 | plane:{ 74 | dist: 0, 75 | normal: new BABYLON.Vector3(0,0,1) 76 | }, 77 | },{ 78 | startsolid: 1, 79 | allsolid: 0, 80 | fraction: 1, 81 | normal: undefined 82 | }); 83 | 84 | // start in -> collide with angled vector 85 | runTest({ 86 | mins: new BABYLON.Vector3(-16,-16,-32), 87 | maxs: new BABYLON.Vector3(16,16,32), 88 | start: new BABYLON.Vector3(0,0,128), 89 | end: new BABYLON.Vector3(0,0,0), 90 | plane:{ 91 | dist: 0, 92 | normal: new BABYLON.Vector3(0.57735,0.57735,0.57735) 93 | }, 94 | },{ 95 | startsolid: 0, 96 | allsolid: 0, 97 | fraction: 0.5, 98 | normal: new BABYLON.Vector3(0.57735,0.57735,0.57735) 99 | }); 100 | 101 | // start exactly ovelapping -> don't move 102 | runTest({ 103 | mins: new BABYLON.Vector3(-16,-16,-32), 104 | maxs: new BABYLON.Vector3(16,16,32), 105 | start: new BABYLON.Vector3(0,0,32), 106 | end: new BABYLON.Vector3(0,0,32), 107 | plane:{ 108 | dist: 0, 109 | normal: new BABYLON.Vector3(0,0,1) 110 | }, 111 | },{ 112 | startsolid: 1, 113 | allsolid: 1, 114 | fraction: 1, 115 | normal: undefined 116 | }); 117 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bunnyhop 7 | 8 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |

Speed

0
67 |
68 |
69 |

Controls

70 |
Move: WASD
71 |
Jump: Space
72 |
Conc: Q
73 | Pause/Unpause 74 |
75 |
76 | 77 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/Collisions.js: -------------------------------------------------------------------------------- 1 | export class Trace { 2 | constructor() { 3 | this.fraction = 1; 4 | this.allsolid = false; 5 | this.startsolid = false; 6 | this.plane = undefined; 7 | } 8 | } 9 | 10 | export function PlayerTrace(meshes, start, end, mins, maxs, predicate) { 11 | var trace = BoxTrace(meshes, start, end, mins, maxs, predicate); 12 | if (trace.allsolid) { 13 | trace.startsolid = true; 14 | } 15 | if (trace.startsolid) { 16 | trace.fraction = 0; 17 | } 18 | return trace; 19 | } 20 | 21 | // Returns false if the given player position is not valid (in solid) 22 | export function PlayerTestPosition(meshes, pos, mins, maxs, predicate) { 23 | // TODO: make this more effecient, the full trace code doesn't need to be run here 24 | var trace = PlayerTrace(meshes, pos, pos, mins, maxs, predicate); 25 | return !trace.startsolid && !trace.allsolid; 26 | } 27 | 28 | // TODO: Should this be related to Number.EPSILON in any way? 29 | var DIST_EPSILON = 0.03125; 30 | // COLLIDE_EPSILON is necessary to avoid instances where ClipBoxToPlanes 31 | // thinks a collision happened but its so close to not colliding that future 32 | // movement will fail (i.e. clipVelocity will not alter velocity since 33 | // Dot(velocity, normal) will return exactly 0) 34 | var COLLIDE_EPSILON = DIST_EPSILON/1024; 35 | 36 | export function ClipBoxToPlanes(mins, maxs, start, end, planes, lastTrace) { 37 | var trace = lastTrace ? lastTrace : new Trace(); 38 | var enterfrac = -1; 39 | var leavefrac = 1; 40 | var clipplane; 41 | var getout = false; 42 | var startout = false; 43 | 44 | for (let i=0; i 0) 58 | getout = true; 59 | if (d1 > 0) 60 | startout = true; 61 | 62 | if (d1 > 0 && d2 >= (d1-COLLIDE_EPSILON)) 63 | return trace; 64 | 65 | if (d1 <= 0 && d2 <= 0) 66 | continue; 67 | 68 | // crosses face 69 | if (d1 > d2) { 70 | // entering plane 71 | let f = (d1 - DIST_EPSILON) / (d1 - d2); 72 | if (f > enterfrac) { 73 | enterfrac = f; 74 | clipplane = plane; 75 | } 76 | } 77 | else { 78 | // leaving plane 79 | let f = (d1 + DIST_EPSILON) / (d1 - d2); 80 | if (f < leavefrac) { 81 | leavefrac = f; 82 | } 83 | } 84 | } 85 | 86 | if (!startout) { 87 | trace.startsolid = true; 88 | if (!getout) { 89 | trace.allsolid = true; 90 | } 91 | return trace; 92 | } 93 | if (enterfrac < leavefrac) { 94 | if (enterfrac > -1 && enterfrac < trace.fraction) { 95 | if (enterfrac < 0) { 96 | enterfrac = 0; 97 | } 98 | trace.fraction = enterfrac; 99 | trace.plane = clipplane; 100 | } 101 | } 102 | 103 | return trace; 104 | } 105 | 106 | // sometimes verts/normals of meshes will have very slightly different values 107 | // even though they are acfually on the same plane 108 | const PLANE_EPSILON = Math.pow(10, -8); 109 | const PLANE_CACHE = {}; 110 | 111 | export function MeshToPlanes(object) { 112 | // if object is already an array of planes, then return it 113 | if (Array.isArray(object)) { 114 | return object; 115 | } 116 | if (object.bspPlanes) { 117 | return object.bspPlanes; 118 | } 119 | var cached = PLANE_CACHE[object.uniqueId]; 120 | if (cached) { 121 | return cached; 122 | } 123 | 124 | var rawVerts = object.getVerticesData ? object.getVerticesData(BABYLON.VertexBuffer.PositionKind) : []; 125 | //var rawFaces = object.getIndices ? object.getIndices() : []; 126 | var rawNormals = object.getVerticesData ? object.getVerticesData(BABYLON.VertexBuffer.NormalKind) : []; 127 | 128 | var planes = []; 129 | var seenPlane = function(dist, normal) { 130 | for (let i=0; i 0) { 175 | trace = ClipBoxToPlanes(mins, maxs, start, end, planes, trace); 176 | if (trace.fraction === 0) { 177 | break; 178 | } 179 | } 180 | } 181 | 182 | if (trace.fraction == 1) { 183 | trace.endpos = end.clone(); 184 | } 185 | else { 186 | trace.endpos = new BABYLON.Vector3( 187 | start.x + trace.fraction * (end.x - start.x), 188 | start.y + trace.fraction * (end.y - start.y), 189 | start.z + trace.fraction * (end.z - start.z) 190 | ); 191 | } 192 | return trace; 193 | } 194 | 195 | export function AABBsIntersect(aMins, aMaxs, bMins, bMaxs) { 196 | return (aMins.x <= bMaxs.x && aMaxs.x >= bMins.x) && 197 | (aMins.y <= bMaxs.y && aMaxs.y >= bMins.y) && 198 | (aMins.z <= bMaxs.z && aMaxs.z >= bMins.z); 199 | } 200 | 201 | -------------------------------------------------------------------------------- /src/vendor/fpsmeter.min.js: -------------------------------------------------------------------------------- 1 | /*! FPSMeter 0.3.1 - 9th May 2013 | https://github.com/Darsain/fpsmeter */ 2 | (function(m,j){function s(a,e){for(var g in e)try{a.style[g]=e[g]}catch(j){}return a}function H(a){return null==a?String(a):"object"===typeof a||"function"===typeof a?Object.prototype.toString.call(a).match(/\s([a-z]+)/i)[1].toLowerCase()||"object":typeof a}function R(a,e){if("array"!==H(e))return-1;if(e.indexOf)return e.indexOf(a);for(var g=0,j=e.length;gd.interval?(x=M(k),m()):(x=setTimeout(k,d.interval),P=M(m))}function G(a){a=a||window.event;a.preventDefault?(a.preventDefault(),a.stopPropagation()):(a.returnValue= 6 | !1,a.cancelBubble=!0);b.toggle()}function U(){d.toggleOn&&S(f.container,d.toggleOn,G,1);a.removeChild(f.container)}function V(){f.container&&U();h=D.theme[d.theme];y=h.compiledHeatmaps||[];if(!y.length&&h.heatmaps.length){for(p=0;p=m?m*(1+j):m+j-m*j;0===l?g="#000":(t=2*m-l,k=(l-t)/l,g*=6,n=Math.floor(g), 7 | v=g-n,v*=l*k,0===n||6===n?(n=l,k=t+v,l=t):1===n?(n=l-v,k=l,l=t):2===n?(n=t,k=l,l=t+v):3===n?(n=t,k=l-v):4===n?(n=t+v,k=t):(n=l,k=t,l-=v),g="#"+N(n)+N(k)+N(l));b[e]=g}}h.compiledHeatmaps=y}f.container=s(document.createElement("div"),h.container);f.count=f.container.appendChild(s(document.createElement("div"),h.count));f.legend=f.container.appendChild(s(document.createElement("div"),h.legend));f.graph=d.graph?f.container.appendChild(s(document.createElement("div"),h.graph)):0;w.length=0;for(var q in f)f[q]&& 8 | h[q].heatOn&&w.push({name:q,el:f[q]});u.length=0;if(f.graph){f.graph.style.width=d.history*h.column.width+(d.history-1)*h.column.spacing+"px";for(c=0;c{ 109 | let deltaTime = this.engine.getDeltaTime(); 110 | let dt = deltaTime / 1000; 111 | this.update(dt); 112 | }); 113 | 114 | scene.camera = camera; 115 | this._initPointerLock(); 116 | 117 | return scene; 118 | } 119 | 120 | update(dt) { 121 | // HACK: this update can be called before the scene is fully ready, 122 | // so just check for dt == 0 and return early in that case 123 | // TODO: onBeforeRenderObservable is probably not the best way 124 | // of implementing the game loop? 125 | if (dt == 0) return; 126 | 127 | this.player.update(dt, this.inputMap); 128 | 129 | let player_mins = this.player.position.add(this.player.mins); 130 | let player_maxs = this.player.position.add(this.player.maxs); 131 | this.scene.triggers.forEach((trigger) => { 132 | let trigger_mins = trigger.getBoundingInfo().boundingBox.minimumWorld; 133 | let trigger_maxs = trigger.getBoundingInfo().boundingBox.maximumWorld; 134 | if (Collisions.AABBsIntersect(trigger_mins, trigger_maxs, player_mins, player_maxs)) { 135 | if (!this.scene.triggersBeingTouched.has(trigger.name)) { 136 | this.scene.triggersBeingTouched.add(trigger.name); 137 | if (this.scene.triggerCallbacks[trigger.name]) { 138 | this.scene.triggerCallbacks[trigger.name](this); 139 | } 140 | } 141 | } else { 142 | this.scene.triggersBeingTouched.delete(trigger.name); 143 | } 144 | }); 145 | 146 | this.scene.camera.position = new BABYLON.Vector3(this.player.position.x, this.player.position.y + this.player.eyeHeight, this.player.position.z); 147 | 148 | if (performance.now() >= this.nextHUDUpdate) { 149 | this.updateHUD(dt); 150 | this.nextHUDUpdate = performance.now() + 100; 151 | } 152 | } 153 | 154 | updateHUD(dt) { 155 | this.speedometerElement.innerHTML = Math.round(this.player.getHorizSpeed()); 156 | this.debugElement.innerHTML = `
pos: ${this.player.position.x.toFixed(2)},${this.player.position.y.toFixed(2)},${this.player.position.z.toFixed(2)}
`; 157 | this.debugElement.innerHTML += `
vel: ${this.player.velocity.x.toFixed(2)},${this.player.velocity.y.toFixed(2)},${this.player.velocity.z.toFixed(2)}
`; 158 | this.debugElement.innerHTML += `
onGround: ${this.player.onGround}
`; 159 | if (this.scene.debugTrace) { 160 | this.debugElement.innerHTML += `
trace: fraction: ${this.scene.debugTrace.fraction} startsolid: ${this.scene.debugTrace.startsolid} allsolid: ${this.scene.debugTrace.allsolid} plane: ${this.scene.debugTrace.plane}
`; 161 | } 162 | } 163 | 164 | _initPointerLock() { 165 | var _this = this; 166 | // Request pointer lock 167 | var canvas = this.canvas; 168 | canvas.addEventListener("click", function(evt) { 169 | canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock; 170 | if (canvas.requestPointerLock) { 171 | canvas.requestPointerLock(); 172 | } 173 | }, false); 174 | 175 | // Event listener when the pointerlock is updated. 176 | var pointerlockchange = function (event) { 177 | _this.controlEnabled = (document.mozPointerLockElement === canvas || document.webkitPointerLockElement === canvas || document.msPointerLockElement === canvas || document.pointerLockElement === canvas); 178 | if (!_this.controlEnabled) { 179 | _this.scene.camera.detachControl(canvas); 180 | } else { 181 | _this.scene.camera.attachControl(canvas); 182 | } 183 | }; 184 | document.addEventListener("pointerlockchange", pointerlockchange, false); 185 | document.addEventListener("mspointerlockchange", pointerlockchange, false); 186 | document.addEventListener("mozpointerlockchange", pointerlockchange, false); 187 | document.addEventListener("webkitpointerlockchange", pointerlockchange, false); 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/Player.js: -------------------------------------------------------------------------------- 1 | import * as Collisions from './Collisions.js'; 2 | 3 | export default class Player { 4 | 5 | constructor(scene, position) { 6 | this.scene = scene; 7 | this.position = position; 8 | this.velocity = BABYLON.Vector3.Zero(); 9 | this.onGround = true; 10 | this.moveSpeed = 400; 11 | this.height = 72; 12 | this.eyeHeight = this.height - 8; 13 | this.duckHeight = 36; 14 | this.duckEyeHeight = this.duckHeight - 6; 15 | this.jumpVelocity = 295; 16 | this.mins = new BABYLON.Vector3(-16, 0, -16); 17 | this.maxs = new BABYLON.Vector3(16, this.height, 16); 18 | // TODO this seems like the wrong place for this 19 | this.useStayOnGround = true; 20 | } 21 | 22 | update(dt, inputMap) { 23 | this.playerMove(dt, inputMap); 24 | } 25 | 26 | playerMove(dt, inputMap) { 27 | if (this.checkStuck()) { 28 | // this is kind of an unrecoverable state as of now, so just throw 29 | throw new Error("stuck and unable to get unstuck"); 30 | } 31 | this.categorizePosition(); 32 | this.addHalfGravity(dt); 33 | 34 | if(this.onGround && inputMap[" "]) { 35 | this.jump(); 36 | // this is done in jump normally, extracted out to here instead 37 | // from HL: "Decay it for simulation" 38 | this.addHalfGravity(dt); 39 | } 40 | 41 | if (this.onGround) { 42 | this.velocity.y = 0; 43 | this.applyFriction(dt, this.scene.friction); 44 | } 45 | 46 | var wishDir = this.getWishDirection(inputMap); 47 | if (this.onGround) { 48 | this.groundMove(dt, wishDir); 49 | } else { 50 | this.airMove(dt, wishDir); 51 | } 52 | 53 | this.categorizePosition(); 54 | this.addHalfGravity(dt); 55 | 56 | if (this.onGround) { 57 | this.velocity.y = 0; 58 | } 59 | } 60 | 61 | // this is Quake 1's version of handling stuckness; it's very simple compared to HL1's 62 | // This function assumes that we are currently stuck, 63 | // Returns true if able to get unstuck, false otherwise. 64 | nudgePosition() { 65 | var testPosition = new BABYLON.Vector3(); 66 | var nudge = [0, -1/8, 1/8]; 67 | var x, y, z; 68 | // try nudging in every possible combination of directions 69 | for (z=0; z<3; z++) { 70 | for (x=0; x<3; x++) { 71 | for (y=0; y<3; y++) { 72 | testPosition.x = this.position.x + nudge[x]; 73 | testPosition.y = this.position.y + nudge[y]; 74 | testPosition.z = this.position.z + nudge[z]; 75 | // if we are not stuck anymore, we're done 76 | if (Collisions.PlayerTestPosition(this.scene.meshes, testPosition, this.mins, this.maxs)) { 77 | this.position = testPosition; 78 | return true; 79 | } 80 | } 81 | } 82 | } 83 | return false; 84 | } 85 | 86 | // Returns true if we are stuck and we weren't able to get unstuck 87 | checkStuck() { 88 | var wasStuck = !Collisions.PlayerTestPosition(this.scene.meshes, this.position, this.mins, this.maxs); 89 | var didUnstuck = false; 90 | if (wasStuck) { 91 | // TODO: more robust stuckness handling if nudgePosition on its own isn't adequate 92 | didUnstuck = this.nudgePosition(); 93 | } 94 | return wasStuck && !didUnstuck; 95 | } 96 | 97 | groundMove(dt, wishDir) { 98 | this.accelerate(dt, wishDir, this.moveSpeed, this.scene.accel); 99 | var wasOnGround = this.onGround; 100 | var dest = new BABYLON.Vector3( 101 | this.position.x + this.velocity.x * dt, 102 | this.position.y, 103 | this.position.z + this.velocity.z * dt 104 | ); 105 | var trace = Collisions.PlayerTrace(this.scene.meshes, this.position, dest, this.mins, this.maxs); 106 | if (trace.fraction == 1) { 107 | this.position = dest; 108 | } else { 109 | this.stairMove(dt); 110 | } 111 | if (this.useStayOnGround) { 112 | this.stayOnGround(); 113 | } 114 | } 115 | 116 | // This is a Source-engine-specific function that allows players to 117 | // walk down slopes and stairs without leaving the ground. 118 | stayOnGround() { 119 | var start = new BABYLON.Vector3( 120 | this.position.x, 121 | this.position.y + 2, 122 | this.position.z 123 | ); 124 | var end = new BABYLON.Vector3( 125 | this.position.x, 126 | this.position.y - this.scene.stepsize, 127 | this.position.z 128 | ); 129 | 130 | // See how far up we can go without getting stuck 131 | var trace = Collisions.PlayerTrace(this.scene.meshes, this.position, start, this.mins, this.maxs); 132 | start = trace.endpos; 133 | 134 | // Now trace down from a known safe position 135 | trace = Collisions.PlayerTrace(this.scene.meshes, start, end, this.mins, this.maxs); 136 | if (trace.fraction > 0 && // must go somewhere 137 | trace.fraction < 1 && // must hit something 138 | !trace.startsolid && // can't be embedded in a solid 139 | trace.plane.normal.y >= 0.7) // can't hit a too-steep slope 140 | { 141 | this.position = trace.endpos; 142 | } 143 | } 144 | 145 | stairMove(dt) { 146 | var stepsize = this.scene.stepsize; 147 | var originalPosition = this.position.clone(); 148 | var originalVelocity = this.velocity.clone(); 149 | 150 | var clip = this.slideMove(dt); 151 | 152 | var downPosition = this.position.clone(); 153 | var downVelocity = this.velocity.clone(); 154 | 155 | this.position = originalPosition.clone(); 156 | this.velocity = originalVelocity.clone(); 157 | 158 | var dest = this.position.clone(); 159 | dest.y += stepsize; 160 | 161 | var trace = Collisions.PlayerTrace(this.scene.meshes, this.position, dest, this.mins, this.maxs); 162 | if (!trace.startsolid && !trace.allsolid) { 163 | this.position = trace.endpos.clone(); 164 | } 165 | 166 | clip = this.slideMove(dt); 167 | 168 | dest = this.position.clone(); 169 | dest.y -= stepsize; 170 | 171 | trace = Collisions.PlayerTrace(this.scene.meshes, this.position, dest, this.mins, this.maxs); 172 | if (!trace.plane || trace.plane.normal.y < 0.7) { 173 | this.position = downPosition; 174 | this.velocity = downVelocity; 175 | return; 176 | } 177 | 178 | if (!trace.startsolid && !trace.allsolid) { 179 | this.position = trace.endpos; 180 | } 181 | 182 | var upPosition = this.position.clone(); 183 | 184 | var downdist = (downPosition.x-originalPosition.x)*(downPosition.x-originalPosition.x) + 185 | (downPosition.z-originalPosition.z)*(downPosition.z-originalPosition.z); 186 | var updist = (upPosition.x-originalPosition.x)*(upPosition.x-originalPosition.x) + 187 | (upPosition.z-originalPosition.z)*(upPosition.z-originalPosition.z); 188 | 189 | if (downdist > updist) { 190 | this.position = downPosition; 191 | this.velocity = downVelocity; 192 | } else { 193 | this.velocity.y = downVelocity.y; 194 | } 195 | } 196 | 197 | airMove(dt, wishDir) { 198 | this.airAccelerate(dt, wishDir, this.moveSpeed, this.scene.airAccel); 199 | this.slideMove(dt); 200 | } 201 | 202 | clipVelocity(velocity, normal, overbounce) { 203 | if (!normal) { normal = BABYLON.Vector3.Zero(); } 204 | const STOP_EPSILON = 0.1; 205 | var angle = normal.y; 206 | var blocked = 0; 207 | if (angle > 0) 208 | blocked |= 1; 209 | if (angle === 0) 210 | blocked |= 2; 211 | 212 | var backoff = BABYLON.Vector3.Dot(velocity, normal) * overbounce; 213 | 214 | var apply = function(compVel, compNorm) { 215 | var change = compNorm*backoff; 216 | var compOut = compVel - change; 217 | if (compOut > -STOP_EPSILON && compOut < STOP_EPSILON) 218 | compOut = 0; 219 | return compOut; 220 | }; 221 | var newVelocity = new BABYLON.Vector3( 222 | apply(velocity.x, normal.x), 223 | apply(velocity.y, normal.y), 224 | apply(velocity.z, normal.z) 225 | ); 226 | 227 | // this was a new addition in the source engine 228 | var adjust = BABYLON.Vector3.Dot(newVelocity, normal); 229 | if( adjust < 0.0 ) { 230 | newVelocity.x -= normal.x * adjust; 231 | newVelocity.y -= normal.y * adjust; 232 | newVelocity.z -= normal.z * adjust; 233 | } 234 | 235 | return { 236 | blocked: blocked, 237 | velocity: newVelocity 238 | }; 239 | } 240 | 241 | // called PM_FlyMove in HL: applies velocity while sliding along touched planes 242 | slideMove(dt) { 243 | const MAX_CLIP_PLANES = 5; 244 | var numbumps = 4; 245 | var blocked = 0; 246 | var numplanes = 0; 247 | var originalVelocity = this.velocity.clone(); 248 | var primalVelocity = this.velocity.clone(); 249 | var planes = []; 250 | 251 | var allFraction = 0; 252 | var timeLeft = dt; 253 | 254 | for (var bumpcount=0; bumpcount 0) { 274 | this.position = trace.endpos.clone(); 275 | originalVelocity = this.velocity.clone(); 276 | numplanes = 0; 277 | } 278 | 279 | if (trace.fraction == 1) { 280 | break; 281 | } 282 | 283 | //PM_AddToTouched(trace, pmove->velocity); 284 | 285 | if (trace.plane.normal.y > 0.7) { 286 | blocked |= 1; 287 | } 288 | 289 | if (trace.plane.normal.y === 0) { 290 | blocked |= 2; 291 | } 292 | 293 | timeLeft -= timeLeft * trace.fraction; 294 | 295 | if (numplanes >= MAX_CLIP_PLANES) { 296 | // this shouldn't happen 297 | this.velocity = BABYLON.Vector3.Zero(); 298 | break; 299 | } 300 | 301 | planes[numplanes] = trace.plane.normal; 302 | numplanes++; 303 | 304 | if (!this.onGround) { 305 | var newVelocity; 306 | for (let i=0; i 0.7) { 308 | let clipped = this.clipVelocity(originalVelocity, planes[i], 1); 309 | newVelocity = clipped.velocity; 310 | } 311 | else { 312 | let clipped = this.clipVelocity(originalVelocity, planes[i], 1.0 /*+ pmove->movevars->bounce * (1-pmove->friction)*/); 313 | newVelocity = clipped.velocity; 314 | } 315 | } 316 | this.velocity = newVelocity.clone(); 317 | originalVelocity = newVelocity.clone(); 318 | } 319 | else { 320 | var i; 321 | for (i=0; i 180) { 419 | this.onGround = false; 420 | } 421 | else { 422 | var tr = Collisions.PlayerTrace(this.scene.meshes, this.position, point, this.mins, this.maxs); 423 | if (!tr.plane || tr.plane.normal.y < 0.7) { 424 | this.onGround = false; 425 | } 426 | else { 427 | this.onGround = true; 428 | // move the player based on the trace so they don't float slightly above the ground 429 | if (!this.useStayOnGround && !tr.startsolid && !tr.allsolid) { 430 | this.position = tr.endpos; 431 | } 432 | } 433 | } 434 | } 435 | 436 | jump() { 437 | this.onGround = false; 438 | this.velocity.y += this.jumpVelocity; 439 | } 440 | 441 | accelerate(dt, wishDir, wishSpeed, accel) { 442 | var currentSpeed = BABYLON.Vector3.Dot(this.velocity, wishDir); 443 | var addSpeed = wishSpeed - currentSpeed; 444 | if (addSpeed <= 0) 445 | return; 446 | 447 | var accelSpeed = accel * wishSpeed * dt; // * friction (personal friction?) 448 | 449 | if (accelSpeed > addSpeed) { 450 | accelSpeed = addSpeed; 451 | } 452 | 453 | this.velocity.x += accelSpeed * wishDir.x; 454 | this.velocity.y += accelSpeed * wishDir.y; 455 | this.velocity.z += accelSpeed * wishDir.z; 456 | } 457 | 458 | airAccelerate(dt, wishDir, wishSpeed, accel) { 459 | var wishSpd = wishSpeed; 460 | if (wishSpd > 30) { 461 | wishSpd = 30; 462 | } 463 | var currentSpeed = BABYLON.Vector3.Dot(this.velocity, wishDir); 464 | var addSpeed = wishSpd - currentSpeed; 465 | if (addSpeed <= 0) 466 | return; 467 | 468 | var accelSpeed = accel * wishSpeed * dt; 469 | if (accelSpeed > addSpeed) { 470 | accelSpeed = addSpeed; 471 | } 472 | 473 | this.velocity.x += accelSpeed * wishDir.x; 474 | this.velocity.y += accelSpeed * wishDir.y; 475 | this.velocity.z += accelSpeed * wishDir.z; 476 | } 477 | 478 | addHalfGravity(dt) { 479 | var gravity = this.scene.gravity; 480 | this.velocity.y -= gravity * 0.5 * dt; 481 | } 482 | 483 | getHorizSpeed() { 484 | return Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.z * this.velocity.z); 485 | } 486 | 487 | conc() { 488 | const LATERAL_POWER = 2.74; 489 | const VERTICAL_POWER = 4.10; 490 | const GROUND_UP_PUSH = 90; 491 | 492 | if (this.onGround) { 493 | this.velocity = new BABYLON.Vector3( 494 | this.velocity.x * LATERAL_POWER * 0.95, 495 | (this.velocity.y + GROUND_UP_PUSH) * VERTICAL_POWER, 496 | this.velocity.z * LATERAL_POWER * 0.95 497 | ); 498 | } 499 | else { 500 | this.velocity.multiplyInPlace(new BABYLON.Vector3(LATERAL_POWER, VERTICAL_POWER, LATERAL_POWER)); 501 | } 502 | } 503 | } 504 | --------------------------------------------------------------------------------