UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

237 lines • 10.1 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Quaternion, Ray, Vector2, Vector3 } from "three"; import { Mathf } from "../engine/engine_math.js"; import { RaycastOptions } from "../engine/engine_physics.js"; import { serializable } from "../engine/engine_serialization.js"; import { getWorldPosition } from "../engine/engine_three_utils.js"; import { getParam } from "../engine/engine_utils.js"; import { Animator } from "./Animator.js"; import { CapsuleCollider } from "./Collider.js"; import { Behaviour } from "./Component.js"; import { Rigidbody } from "./RigidBody.js"; const debug = getParam("debugcharactercontroller"); /** * @category Camera Controls * @group Components */ export class CharacterController extends Behaviour { center = new Vector3(0, 0, 0); radius = .5; height = 2; _rigidbody = null; get rigidbody() { if (this._rigidbody) return this._rigidbody; this._rigidbody = this.gameObject.getComponent(Rigidbody); if (!this._rigidbody) this._rigidbody = this.gameObject.addComponent(Rigidbody); return this.rigidbody; } _activeGroundCollisions; awake() { this._activeGroundCollisions = new Set(); } onEnable() { const rb = this.rigidbody; let collider = this.gameObject.getComponent(CapsuleCollider); if (!collider) collider = this.gameObject.addComponent(CapsuleCollider); collider.center.copy(this.center); collider.radius = this.radius; collider.height = this.height; // discard any rotation besides Y axis const wForward = new Vector3(0, 0, 1); const wRight = new Vector3(1, 0, 0); const wUp = new Vector3(0, 1, 0); const fwd = this.gameObject.getWorldDirection(new Vector3()); fwd.y = 0; const sign = wRight.dot(fwd) < 0 ? -1 : 1; const angleY = wForward.angleTo(fwd) * sign; this.gameObject.setRotationFromAxisAngle(wUp, angleY); rb.lockRotationX = true; rb.lockRotationY = true; rb.lockRotationZ = true; } move(vec) { this.gameObject.position.add(vec); } onCollisionEnter(col) { // contacts can be empty, ignoring such collision results in a stuck grounded state // namely caused by mesh colliders if (col.contacts.length == 0 || col.contacts.some(contact => contact.normal.y > 0.2)) { this._activeGroundCollisions.add(col); if (debug) { console.log(`Collision(${this._activeGroundCollisions.size}): ${col.contacts.map(c => c.normal.y.toFixed(2)).join(", ")} - ${this.isGrounded}`); } } } onCollisionExit(col) { this._activeGroundCollisions.delete(col); if (debug) { console.log(`Collision(${this._activeGroundCollisions.size}) - ${this.isGrounded}`); } } get isGrounded() { return this._activeGroundCollisions.size > 0; } _contactVelocity = new Vector3(); get contactVelocity() { this._contactVelocity.set(0, 0, 0); for (const col of this._activeGroundCollisions) { const vel = this.context.physics.engine?.getLinearVelocity(col.collider); if (!vel) continue; // const friction = col.collider.sharedMaterial?.dynamicFriction || 1; this._contactVelocity.x += vel.x; this._contactVelocity.y += vel.y; this._contactVelocity.z += vel.z; } return this._contactVelocity; } } __decorate([ serializable(Vector3) ], CharacterController.prototype, "center", void 0); __decorate([ serializable() ], CharacterController.prototype, "radius", void 0); __decorate([ serializable() ], CharacterController.prototype, "height", void 0); /** * @category Camera Controls * @category Interactivity * @group Components */ export class CharacterControllerInput extends Behaviour { controller; movementSpeed = 2; rotationSpeed = 2; jumpForce = 1; doubleJumpForce = 2; animator; lookForward = true; awake() { this._currentRotation = new Quaternion(); } update() { const input = this.context.input; if (input.isKeyPressed("KeyW")) this.moveInput.y += 1; else if (input.isKeyPressed("KeyS")) this.moveInput.y -= 1; if (input.isKeyPressed("KeyD")) this.lookInput.x += 1; else if (input.isKeyPressed("KeyA")) this.lookInput.x -= 1; this.jumpInput ||= input.isKeyDown("Space"); } move(move) { this.moveInput.add(move); } look(look) { this.lookInput.add(look); } jump() { this.jumpInput = true; } lookInput = new Vector2(0, 0); moveInput = new Vector2(0, 0); jumpInput = false; onBeforeRender() { this.handleInput(this.moveInput, this.lookInput, this.jumpInput); this.lookInput.set(0, 0); this.moveInput.set(0, 0); this.jumpInput = false; } _currentSpeed = new Vector3(0, 0, 0); _currentAngularSpeed = new Vector3(0, 0, 0); _temp = new Vector3(0, 0, 0); _jumpCount = 0; _currentRotation; handleInput(move, look, jump) { if (this.controller?.isGrounded) { this._jumpCount = 0; if (this.doubleJumpForce > 0) this.animator?.setBool("doubleJump", false); } this._currentSpeed.z += move.y * this.movementSpeed * this.context.time.deltaTime; this.animator?.setBool("running", move.length() > 0.01); this.animator?.setBool("jumping", this.controller?.isGrounded === true && jump); this._temp.copy(this._currentSpeed); this._temp.applyQuaternion(this.gameObject.quaternion); if (this.controller) this.controller.move(this._temp); else this.gameObject.position.add(this._temp); this._currentAngularSpeed.y += Mathf.toRadians(-look.x * this.rotationSpeed) * this.context.time.deltaTime; if (this.lookForward && Math.abs(this._currentAngularSpeed.y) < .01) { const forwardVector = this.context.mainCameraComponent.forward; forwardVector.y = 0; forwardVector.normalize(); this._currentRotation.setFromUnitVectors(new Vector3(0, 0, 1), forwardVector); this.gameObject.quaternion.slerp(this._currentRotation, this.context.time.deltaTime * 10); } this.gameObject.rotateY(this._currentAngularSpeed.y); this._currentSpeed.multiplyScalar(1 - this.context.time.deltaTime * 10); this._currentAngularSpeed.y *= 1 - this.context.time.deltaTime * 10; if (this.controller && jump && this.jumpForce > 0) { let canJump = this.controller?.isGrounded; if (this.doubleJumpForce > 0 && !this.controller?.isGrounded && this._jumpCount === 1) { canJump = true; this.animator?.setBool("doubleJump", true); } if (canJump) { this._jumpCount += 1; // TODO: factor in mass const rb = this.controller.rigidbody; // const fullJumpHoldLength = .1; const factor = this._jumpCount === 2 ? this.doubleJumpForce : this.jumpForce; // Mathf.clamp((this.context.time.time - this._jumpDownTime), 0, fullJumpHoldLength) / fullJumpHoldLength; rb.applyImpulse(new Vector3(0, 1, 0).multiplyScalar(factor)); } } if (this.controller) { // TODO: should probably raycast to the ground or check if we're still in the jump animation const verticalSpeed = this.controller?.rigidbody.getVelocity().y; if (verticalSpeed < -1) { if (!this._raycastOptions.ray) this._raycastOptions.ray = new Ray(); this._raycastOptions.ray.origin.copy(getWorldPosition(this.gameObject)); this._raycastOptions.ray.direction.set(0, -1, 0); const currentLayer = this.layer; this.gameObject.layers.disableAll(); this.gameObject.layers.set(2); const hits = this.context.physics.raycast(this._raycastOptions); this.gameObject.layers.set(currentLayer); if ((hits.length && hits[0].distance > 2 || verticalSpeed < -10)) { this.animator?.setBool("falling", true); } } else this.animator?.setBool("falling", false); } } _raycastOptions = new RaycastOptions(); } __decorate([ serializable(CharacterController) ], CharacterControllerInput.prototype, "controller", void 0); __decorate([ serializable() ], CharacterControllerInput.prototype, "movementSpeed", void 0); __decorate([ serializable() ], CharacterControllerInput.prototype, "rotationSpeed", void 0); __decorate([ serializable() ], CharacterControllerInput.prototype, "jumpForce", void 0); __decorate([ serializable() ], CharacterControllerInput.prototype, "doubleJumpForce", void 0); __decorate([ serializable(Animator) ], CharacterControllerInput.prototype, "animator", void 0); //# sourceMappingURL=CharacterController.js.map