UNPKG

@matematrolii/sketchbook

Version:

3D matematrolii playground built on three.js and cannon.js

479 lines (421 loc) 15.2 kB
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; } } }