@matematrolii/sketchbook
Version:
3D matematrolii playground built on three.js and cannon.js
479 lines (421 loc) • 15.2 kB
text/typescript
import * as THREE from "three";
// @ts-ignore
import * as CANNON from "cannon";
import * as _ from "lodash";
import * as Utils from "../core/FunctionLibrary";
import { VectorSpringSimulator } from "../physics/spring_simulation/VectorSpringSimulator";
import { RelativeSpringSimulator } from "../physics/spring_simulation/RelativeSpringSimulator";
import { World } from "../world/World";
import { IWorldEntity } from "../interfaces/IWorldEntity";
import { SphereCollider } from "../physics/colliders/SphereCollider";
import { EntityType } from "../enums/EntityType";
import { CollisionGroups } from "../enums/CollisionGroups";
import { GroundImpactData } from "./GroundImpactData";
import { Death } from './character_states/Death'
import { EventType } from "../enums/EventType";
import { ModelType } from "../enums/WorldType";
export class Bullet extends THREE.Object3D implements IWorldEntity {
public updateOrder: number = 1;
public entityType: EntityType = EntityType.Bullet;
public groundImpactData: GroundImpactData = new GroundImpactData();
// Movement
public velocity: THREE.Vector3 = new THREE.Vector3();
public arcadeVelocityInfluence: THREE.Vector3 = new THREE.Vector3();
public velocityTarget: THREE.Vector3 = new THREE.Vector3();
public arcadeVelocityIsAdditive: boolean = false;
public defaultVelocitySimulatorDamping: number = 0.8;
public defaultVelocitySimulatorMass: number = 50;
public velocitySimulator: VectorSpringSimulator;
public moveSpeed: number = 4;
public angularVelocity: number = 0;
public orientation: THREE.Vector3 = new THREE.Vector3(0, 0, 1);
public orientationTarget: THREE.Vector3 = new THREE.Vector3(0, 0, 1);
public defaultRotationSimulatorDamping: number = 0.5;
public defaultRotationSimulatorMass: number = 10;
public rotationSimulator: RelativeSpringSimulator;
public viewVector: THREE.Vector3;
public bulletBox: SphereCollider;
// Ray casting
public rayResult: CANNON.RaycastResult = new CANNON.RaycastResult();
public rayHasHit: boolean = false;
public rayCastLength: number = 0;
public raySafeOffset: number = 0;
public raycastBox: THREE.Mesh;
public world: World;
private physicsEnabled: boolean = true;
public prevStep: number = 0;
public enemyNameFound = null;
constructor() {
super();
const ballRadius = 0.15;
const ballSegments = 8;
const ballMaterial = new THREE.MeshLambertMaterial({ color: 0xdddddd });
const ballGeometry = new THREE.SphereGeometry(ballRadius, ballSegments, ballSegments);
this.velocitySimulator = new VectorSpringSimulator(
60,
this.defaultVelocitySimulatorMass,
this.defaultVelocitySimulatorDamping
);
this.rotationSimulator = new RelativeSpringSimulator(
60,
this.defaultRotationSimulatorMass,
this.defaultRotationSimulatorDamping
);
this.viewVector = new THREE.Vector3();
// Physics
// Player Capsule
this.bulletBox = new SphereCollider({
mass: 1,
position: new CANNON.Vec3(),
radius: ballRadius,
friction: 0,
});
// capsulePhysics.physical.collisionFilterMask = ~CollisionGroups.Trimesh;
this.bulletBox.body.shapes.forEach((shape) => {
// tslint:disable-next-line: no-bitwise
shape.collisionFilterMask = ~CollisionGroups.TrimeshColliders;
});
this.bulletBox.body.allowSleep = false;
// Move character to different collision group for raycasting
this.bulletBox.body.collisionFilterGroup = 2;
// Disable character rotation
this.bulletBox.body.fixedRotation = true;
this.bulletBox.body.updateMassProperties();
this.raycastBox = new THREE.Mesh(ballGeometry, ballMaterial);
this.raycastBox.visible = true;
// Physics pre/post step callback bindings
this.bulletBox.body.preStep = (body: CANNON.Body) => {
this.physicsPreStep(body, this);
};
this.bulletBox.body.postStep = (body: CANNON.Body) => {
this.physicsPostStep(body, this);
};
}
public setArcadeVelocityInfluence(
x: number,
y: number = x,
z: number = x
): void {
this.arcadeVelocityInfluence.set(x, y, z);
}
public setViewVector(vector: THREE.Vector3): void {
this.viewVector.copy(vector).normalize();
}
public setPosition(x: number, y: number, z: number): void {
if (this.physicsEnabled) {
this.bulletBox.body.previousPosition = new CANNON.Vec3(x, y, z);
this.bulletBox.body.position = new CANNON.Vec3(x, y, z);
this.bulletBox.body.interpolatedPosition = new CANNON.Vec3(x, y, z);
} else {
this.position.x = x;
this.position.y = y;
this.position.z = z;
}
}
public resetVelocity(): void {
this.velocity.x = 0;
this.velocity.y = 0;
this.velocity.z = 0;
this.bulletBox.body.velocity.x = 0;
this.bulletBox.body.velocity.y = 0;
this.bulletBox.body.velocity.z = 0;
this.velocitySimulator.init();
}
public setArcadeVelocityTarget(
velZ: number,
velX: number = 0,
velY: number = 0
): void {
this.velocityTarget.z = velZ;
this.velocityTarget.x = velX;
this.velocityTarget.y = velY;
}
public setOrientation(
vector: THREE.Vector3,
instantly: boolean = false
): void {
let lookVector = new THREE.Vector3().copy(vector).setY(0).normalize();
this.orientationTarget.copy(lookVector);
if (instantly) {
this.orientation.copy(lookVector);
}
}
public setPhysicsEnabled(value: boolean): void {
this.physicsEnabled = value;
if (value === true) {
this.world.physicsWorld.addBody(this.bulletBox.body);
} else {
this.world.physicsWorld.remove(this.bulletBox.body);
}
}
public update(timeStep: number): void {
if (this.prevStep !== undefined) {
this.prevStep += timeStep;
if (Math.floor(this.prevStep) > 5) {
this.world.remove(this);
}
}
}
public springRotation(timeStep: number): void {
// Spring rotation
// Figure out angle between current and target orientation
let angle = Utils.getSignedAngleBetweenVectors(
this.orientation,
this.orientationTarget
);
// Simulator
this.rotationSimulator.target = angle;
this.rotationSimulator.simulate(timeStep);
let rot = this.rotationSimulator.position;
// Updating values
this.orientation.applyAxisAngle(new THREE.Vector3(0, 1, 0), rot);
this.angularVelocity = this.rotationSimulator.velocity;
}
public physicsPreStep(body: CANNON.Body, character: Bullet): void {
this.feetRaycast();
if (character.rayHasHit) {
if (character.raycastBox.visible) {
character.raycastBox.position.x = character.rayResult.hitPointWorld.x;
character.raycastBox.position.y = character.rayResult.hitPointWorld.y;
character.raycastBox.position.z = character.rayResult.hitPointWorld.z;
}
} else {
if (character.raycastBox.visible) {
character.raycastBox.position.set(
body.position.x,
body.position.y - character.rayCastLength - character.raySafeOffset,
body.position.z
);
}
}
}
public feetRaycast(): void {
// Player ray casting
// Create ray
let body = this.bulletBox.body;
const start = new CANNON.Vec3(
body.position.x,
body.position.y,
body.position.z
);
const end = new CANNON.Vec3(
body.position.x,
body.position.y - this.rayCastLength - this.raySafeOffset,
body.position.z
);
// Raycast options
const rayCastOptions = {
collisionFilterMask: CollisionGroups.Default,
skipBackfaces: true /* ignore back faces */,
};
// Cast the ray
this.rayHasHit = this.world.physicsWorld.raycastClosest(
start,
end,
rayCastOptions,
this.rayResult
);
}
public physicsPostStep(body: CANNON.Body, character: Bullet): void {
// Get velocities
let simulatedVelocity = new THREE.Vector3(
body.velocity.x,
body.velocity.y,
body.velocity.z
);
// Take local velocity
let arcadeVelocity = new THREE.Vector3()
.copy(character.velocity)
.multiplyScalar(character.moveSpeed);
// Turn local into global
arcadeVelocity = Utils.appplyVectorMatrixXZ(
character.orientation,
arcadeVelocity
);
let newVelocity = new THREE.Vector3();
// Additive velocity mode
if (character.arcadeVelocityIsAdditive) {
newVelocity.copy(simulatedVelocity);
let globalVelocityTarget = Utils.appplyVectorMatrixXZ(
character.orientation,
character.velocityTarget
);
let add = new THREE.Vector3()
.copy(arcadeVelocity)
.multiply(character.arcadeVelocityInfluence);
if (
Math.abs(simulatedVelocity.x) <
Math.abs(globalVelocityTarget.x * character.moveSpeed) ||
Utils.haveDifferentSigns(simulatedVelocity.x, arcadeVelocity.x)
) {
newVelocity.x += add.x;
}
if (
Math.abs(simulatedVelocity.y) <
Math.abs(globalVelocityTarget.y * character.moveSpeed) ||
Utils.haveDifferentSigns(simulatedVelocity.y, arcadeVelocity.y)
) {
newVelocity.y += add.y;
}
if (
Math.abs(simulatedVelocity.z) <
Math.abs(globalVelocityTarget.z * character.moveSpeed) ||
Utils.haveDifferentSigns(simulatedVelocity.z, arcadeVelocity.z)
) {
newVelocity.z += add.z;
}
} else {
newVelocity = new THREE.Vector3(
THREE.MathUtils.lerp(
simulatedVelocity.x,
arcadeVelocity.x,
character.arcadeVelocityInfluence.x
),
THREE.MathUtils.lerp(
simulatedVelocity.y,
arcadeVelocity.y,
character.arcadeVelocityInfluence.y
),
THREE.MathUtils.lerp(
simulatedVelocity.z,
arcadeVelocity.z,
character.arcadeVelocityInfluence.z
)
);
}
// If we're hitting the ground, stick to ground
if (character.rayHasHit) {
// Flatten velocity
newVelocity.y = 0;
// Move on top of moving objects
if (character.rayResult.body.mass > 0) {
let pointVelocity = new CANNON.Vec3();
character.rayResult.body.getVelocityAtWorldPoint(
character.rayResult.hitPointWorld,
pointVelocity
);
newVelocity.add(Utils.threeVector(pointVelocity));
}
// Measure the normal vector offset from direct "up" vector
// and transform it into a matrix
let up = new THREE.Vector3(0, 1, 0);
let normal = new THREE.Vector3(
character.rayResult.hitNormalWorld.x,
character.rayResult.hitNormalWorld.y,
character.rayResult.hitNormalWorld.z
);
let q = new THREE.Quaternion().setFromUnitVectors(up, normal);
let m = new THREE.Matrix4().makeRotationFromQuaternion(q);
// Rotate the velocity vector
newVelocity.applyMatrix4(m);
// Compensate for gravity
// newVelocity.y -= body.world.physicsWorld.gravity.y / body.character.world.physicsFrameRate;
// Apply velocity
body.velocity.x = newVelocity.x;
body.velocity.y = newVelocity.y;
body.velocity.z = newVelocity.z;
// Ground character
body.position.y =
character.rayResult.hitPointWorld.y +
character.rayCastLength +
newVelocity.y / character.world.physicsFrameRate;
} else {
// If we're in air
body.velocity.x = newVelocity.x;
body.velocity.y = newVelocity.y;
body.velocity.z = newVelocity.z;
// Save last in-air information
character.groundImpactData.velocity.x = body.velocity.x;
character.groundImpactData.velocity.y = body.velocity.y;
character.groundImpactData.velocity.z = body.velocity.z;
}
}
public collideListener(colider: any) {
const that = this;
if (colider.body?.userData?.type === ModelType.ENEMY) {
if (!that.enemyNameFound) {
that.enemyNameFound = colider.body?.userData?.name;
}
setTimeout(function() {
if (that.world && that.world.characters) {
const character = that.world.characterNames[that.enemyNameFound];
if (that.world && character && !character.isHit) {
character.isHit = true;
// TODO: set somehow to true when hit
//(character.characterCapsule.body as any)?.userData?.isHit = true;
that.bulletBox.body.removeEventListener("collide", that.collideListener);
character.setState(new Death(character));
that.world.triggerEvent({
type: EventType.ENEMY,
targets: [`${character.characterCapsule.body.id}`]
});
that.world.remove(that);
}
}
}, 0);
}
}
public addToWorld(world: World): void {
if (_.includes(world.bullets, this)) {
console.warn("Adding item to a world in which it already exists.");
} else {
// Set world
this.world = world;
// Register character
world.bullets.push(this);
// Register physics
world.physicsWorld.addBody(this.bulletBox.body);
// listen for collisions
this.bulletBox.body.addEventListener("collide", this.collideListener.bind(this));
// Add to graphicsWorld
world.graphicsWorld.add(this);
world.graphicsWorld.add(this.raycastBox);
}
}
public removeFromWorld(world: World): void {
if (!_.includes(world.bullets, this)) {
console.warn("Removing Bullet from a world in which it isn't present.");
} else {
this.world = undefined;
// Remove from items
_.pull(world.bullets, this);
// Remove physics
world.physicsWorld.remove(this.bulletBox.body);
// Remove visuals
world.graphicsWorld.remove(this);
world.graphicsWorld.remove(this.raycastBox);
this.raycastBox.geometry.dispose();
if (!Array.isArray(this.raycastBox.material)) {
this.raycastBox.material.dispose();
}
this.raycastBox = undefined;
this.velocitySimulator = undefined;
this.rotationSimulator = undefined;
this.viewVector = undefined;
this.bulletBox = undefined;
this.updateOrder = undefined;
this.entityType = undefined;
this.velocity = undefined;
this.arcadeVelocityInfluence = undefined;
this.velocityTarget = undefined;
this.arcadeVelocityIsAdditive = undefined;
this.defaultVelocitySimulatorDamping = undefined;
this.defaultVelocitySimulatorMass = undefined;
this.moveSpeed = undefined;
this.angularVelocity = undefined;
this.orientation = undefined;
this.orientationTarget = undefined;
this.defaultRotationSimulatorDamping = undefined;
this.defaultRotationSimulatorMass = undefined;
this.rayResult = undefined;
this.rayHasHit = undefined;
this.rayCastLength = undefined;
this.raySafeOffset = undefined;
this.groundImpactData = undefined;
this.physicsEnabled = undefined;
this.type = undefined;
this.enemyNameFound = undefined;
this.prevStep = undefined;
}
}
}