@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
264 lines (237 loc) • 7.85 kB
text/typescript
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;
}
}