UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

264 lines (237 loc) 7.85 kB
import {CapsuleSopOperation} from './../../engine/operations/sop/Capsule'; import {Object3D} from 'three'; import {Vector3} from 'three'; import {Box3} from 'three'; import {Line3} from 'three'; import {Matrix4} from 'three'; import {MeshWithBVH, ExtendedTriangle} from '../../core/geometry/bvh/three-mesh-bvh'; import {CapsuleOptions} from './CapsuleGeometry'; import {MathUtils} from 'three'; export interface CorePlayerOptions { object: Object3D; collider: MeshWithBVH; // meshName?: string; } type ResetRequiredCallback = () => boolean; const tmpGravity = new Vector3(0, 0, 0); const upVector = new Vector3(0, 1, 0); const tempVector1 = new Vector3(); const tempVector2 = new Vector3(); const tempVector3 = new Vector3(); const tempVector4 = new Vector3(); const tempVector5 = new Vector3(); const tempBox = new Box3(); const tempMat = new Matrix4(); const tempSegment = new Line3(); const startRotationRadians = new Vector3(); export class CorePlayer { private _pressed = { forward: false, backward: false, left: false, right: false, }; private _onGround = false; private _velocity = new Vector3(); public readonly capsuleInfo = { radius: CapsuleSopOperation.DEFAULT_PARAMS.radius, segment: new Line3( new Vector3(0, 0, 0), new Vector3( 0, -(CapsuleSopOperation.DEFAULT_PARAMS.height - 2 * CapsuleSopOperation.DEFAULT_PARAMS.radius), 0 ) ), }; // private _meshName: string | undefined; // private _mesh: Mesh | undefined; public object!: Object3D; public collider!: MeshWithBVH; public startPosition = new Vector3(0, 5, 0); public startRotation = new Vector3(0, 0, 0); public jumpAllowed = true; public jumpStrength = 10; public runAllowed = true; public runSpeedMult = 2; private _running = false; public speed = 10; public physicsSteps = 5; public gravity = new Vector3(0, -30, 0); private _azimuthalAngle = 0; private _resetYMax = -25; private _resetRequiredCallback: ResetRequiredCallback = () => { return this.object.position.y < this._resetYMax; }; constructor(options: CorePlayerOptions) { this.setOptions(options); } setOptions(options: CorePlayerOptions) { this._setObject(options.object); this.setCollider(options.collider); } private _setObject(object: Object3D) { this.object = object; this.object.matrixAutoUpdate = true; } setCollider(collider: MeshWithBVH) { this.collider = collider; } setCapsule(capsuleOptions: CapsuleOptions) { this.capsuleInfo.radius = capsuleOptions.radius; this.capsuleInfo.segment.end.y = -(capsuleOptions.height - 2 * capsuleOptions.radius); } reset() { this.stop(); this.object.position.copy(this.startPosition); startRotationRadians.copy(this.startRotation).multiplyScalar(MathUtils.DEG2RAD); this.object.rotation.setFromVector3(startRotationRadians); this.object.updateMatrix(); this.object.updateWorldMatrix(true, true); this.object.updateMatrixWorld(true); } stop() { this._pressed.forward = false; this._pressed.backward = false; this._pressed.left = false; this._pressed.right = false; this._running = false; this._velocity.set(0, 0, 0); } setResetRequiredCallback(callback: ResetRequiredCallback) { this._resetRequiredCallback = callback; } setAzimuthalAngle(angle: number) { this._azimuthalAngle = angle; } update(delta: number) { const deltaBounded = Math.min(delta, 0.1); for (let i = 0; i < this.physicsSteps; i++) { this._updateStep(deltaBounded / this.physicsSteps); } } private _updateStep(delta: number) { if (!this._onGround) { tmpGravity.copy(this.gravity).multiplyScalar(delta); this._velocity.add(tmpGravity); } this.object.position.addScaledVector(this._velocity, delta); // move the player const angle = this._azimuthalAngle; const speed = this.speed * delta * (this._running ? this.runSpeedMult : 1); tempVector2.set(0, 0, 0); if (this._pressed.forward) { tempVector1.set(0, 0, -1).applyAxisAngle(upVector, angle); tempVector2.add(tempVector1); } if (this._pressed.backward) { tempVector1.set(0, 0, 1).applyAxisAngle(upVector, angle); tempVector2.add(tempVector1); } if (this._pressed.left) { tempVector1.set(-1, 0, 0).applyAxisAngle(upVector, angle); tempVector2.add(tempVector1); } if (this._pressed.right) { tempVector1.set(1, 0, 0).applyAxisAngle(upVector, angle); tempVector2.add(tempVector1); } tempVector2.normalize().multiplyScalar(speed); this.object.position.add(tempVector2); this.object.updateMatrixWorld(); // adjust player position based on collisions const capsuleInfo = this.capsuleInfo; tempBox.makeEmpty(); tempMat.copy(this.collider.matrixWorld).invert(); tempSegment.copy(capsuleInfo.segment); // get the position of the capsule in the local space of the collider tempSegment.start.applyMatrix4(this.object.matrixWorld).applyMatrix4(tempMat); tempSegment.end.applyMatrix4(this.object.matrixWorld).applyMatrix4(tempMat); // get the axis aligned bounding box of the capsule tempBox.expandByPoint(tempSegment.start); tempBox.expandByPoint(tempSegment.end); tempBox.min.addScalar(-capsuleInfo.radius); tempBox.max.addScalar(capsuleInfo.radius); const intersectsBounds = ( box: Box3, isLeaf: boolean, score: number | undefined, depth: number, nodeIndex: number ) => { return box.intersectsBox(tempBox); }; const intersectsTriangle = (tri: ExtendedTriangle) => { // check if the triangle is intersecting the capsule and adjust the // capsule position if it is. const triPoint = tempVector3; const capsulePoint = tempVector4; const distance = tri.closestPointToSegment(tempSegment, triPoint, capsulePoint) as number; if (distance < capsuleInfo.radius) { const depth = capsuleInfo.radius - distance; const direction = capsulePoint.sub(triPoint).normalize(); tempSegment.start.addScaledVector(direction, depth); tempSegment.end.addScaledVector(direction, depth); } }; this.collider.geometry.boundsTree.shapecast({ intersectsBounds, intersectsTriangle, }); // get the adjusted position of the capsule collider in world space after checking // triangle collisions and moving it. capsuleInfo.segment.start is assumed to be // the origin of the player model. const newPosition = tempVector5; // tempSegment.start.y += capsuleInfo.radius; newPosition.copy(tempSegment.start); newPosition.applyMatrix4(this.collider.matrixWorld); // check how much the collider was moved const deltaVector = tempVector2; deltaVector.subVectors(newPosition, this.object.position); // if the player was primarily adjusted vertically we assume it's on something we should consider ground this._onGround = deltaVector.y > Math.abs(delta * this._velocity.y * 0.25); const offset = Math.max(0.0, deltaVector.length() - 1e-5); deltaVector.normalize().multiplyScalar(offset); // adjust the player model this.object.position.add(deltaVector); if (!this._onGround) { deltaVector.normalize(); this._velocity.addScaledVector(deltaVector, -deltaVector.dot(this._velocity)); } else { this._velocity.set(0, 0, 0); } // if the player has fallen too far below the level reset their position to the start if (this._resetRequiredCallback()) { this.reset(); } } setForward(state: boolean) { this._pressed.forward = state; } setBackward(state: boolean) { this._pressed.backward = state; } setLeft(state: boolean) { this._pressed.left = state; } setRight(state: boolean) { this._pressed.right = state; } jump() { if (this._onGround && this.jumpAllowed) { this._velocity.y = this.jumpStrength; } } setRun(state: boolean) { if (state) { if (this._onGround && this.runAllowed) { this._running = true; } } else { this._running = false; } } running() { return this._running; } }