UNPKG

ggabcd-meshwalk

Version:

MeshWalk.js is a JS library which helps your TPS game development with three.js.

466 lines (402 loc) 15 kB
import { THREE, onInstallHandlers } from "../install.js"; import EventDispatcher from "./EventDispatcher.js"; import { testSegmentTriangle, isIntersectionSphereTriangle, } from "../math/collision.js"; const PI_HALF = Math.PI * 0.5; const PI_ONE_HALF = Math.PI * 1.5; let direction2D; let wallNormal2D; let groundingHead; let groundingTo; let point1; let point2; let direction; let translateScoped; let translate; onInstallHandlers.push(() => { direction2D = new THREE.Vector2(); wallNormal2D = new THREE.Vector2(); groundingHead = new THREE.Vector3(); groundingTo = new THREE.Vector3(); point1 = new THREE.Vector3(); point2 = new THREE.Vector3(); direction = new THREE.Vector3(); translateScoped = new THREE.Vector3(); translate = new THREE.Vector3(); }); export class CharacterController extends EventDispatcher { constructor(object3d, radius, head, height) { super(); this.isCharacterController = true; this.object = object3d; this.headObject = head; this.height = height; this.fallVelocity = -20; // 下落速度 this.jumpDuration = 1000; // 跳跃过程时间 this.headHeight = 5; this.center = this.object.position.clone(); this.radius = radius; this.groundPadding = 0.5; this.maxSlopeGradient = Math.cos(50 * THREE.Math.DEG2RAD); this.isGrounded = false; this.isOnSlope = false; this.isIdling = false; this.isRunning = false; this.isWalking = false; this.isJumping = false; this.direction = 0; // 0 to 2PI(=360deg) in rad this.movementSpeed = 10; // Meters Per Second this.walkSpeed = 10; // Meters Per Second this.runSpeed = 30; // Meters Per Second this.moveAction = "run"; this.velocity = new THREE.Vector3(0, -10, 0); this.currentJumpPower = 0; this.jumpStartTime = 0; this.groundHeight = 0; this.groundNormal = new THREE.Vector3(); this.collisionCandidate; this.contactInfo = []; let isFirstUpdate = true; let wasGrounded; let wasOnSlope; // let wasIdling; let wasRunning; let wasJumping; let lastMoveAction; this._events = () => { // 首次执行内容 if (isFirstUpdate) { isFirstUpdate = false; wasGrounded = this.isGrounded; wasOnSlope = this.isOnSlope; // wasIdling = this.isIdling; wasRunning = this.isRunning; wasJumping = this.isJumping; return; } const moveName = this.isWalking ? "walk" : "run"; if (!wasRunning && !this.isRunning && this.isGrounded && !this.isIdling) { this.isIdling = true; this.dispatchEvent({ type: "startIdling" }); } else if ( (!wasRunning && this.isRunning && !this.isJumping && this.isGrounded) || (!wasGrounded && this.isGrounded && this.isRunning) || (wasOnSlope && !this.isOnSlope && this.isRunning && this.isGrounded) ) { this.isIdling = false; if (this.isWalking) { this.dispatchEvent({ type: "startWalking" }); } else { this.dispatchEvent({ type: "startRunning" }); } } else if (!wasJumping && this.isJumping) { this.isIdling = false; this.dispatchEvent({ type: "startJumping" }); } else if (!wasOnSlope && this.isOnSlope) { this.dispatchEvent({ type: "startSliding" }); } else if (wasGrounded && !this.isGrounded && !this.isJumping) { this.dispatchEvent({ type: "startFalling" }); } else if (wasRunning && this.isRunning && lastMoveAction !== moveName) { lastMoveAction = moveName; if (this.isWalking) { this.dispatchEvent({ type: "startWalking" }); } else { this.dispatchEvent({ type: "startRunning" }); } } if (!wasGrounded && this.isGrounded) { // 先出现startIdling有问题 // TODO 更改以在此事件后 n 秒开始 startIdling // this.dispatchEvent ({type:'endJumping'}); } wasGrounded = this.isGrounded; wasOnSlope = this.isOnSlope; // wasIdling = this.isIdling; wasRunning = this.isRunning; wasJumping = this.isJumping; }; } update(dt) { // 重置状态 this.isGrounded = false; this.isOnSlope = false; this.groundHeight = -Infinity; this.groundNormal.set(0, 1, 0); this._updateGrounding(); this._updateJumping(); this._updatePosition(dt); this._collisionDetection(); this._solvePosition(); this._updateVelocity(); this._events(); } _updateVelocity() { const frontDirection = -Math.cos(this.direction); const rightDirection = -Math.sin(this.direction); let isHittingCeiling = false; const moveSpeed = this.isWalking ? this.walkSpeed : this.runSpeed; this.velocity.set( rightDirection * moveSpeed * this.isRunning, this.fallVelocity, frontDirection * moveSpeed * this.isRunning ); // 处理自动应用的速度,例如陡坡和自由落体 if (this.contactInfo.length === 0 && !this.isJumping) { // 没有碰撞,所以自由落体 return; } else if (this.isGrounded && !this.isOnSlope && !this.isJumping) { // 如果你在正常的地面上,除了在跳跃开始时 this.velocity.y = 0; } else if (this.isOnSlope) { // TODO 0.2 是一个神奇的数字,所以想想如何从几何上找到它。 const slidingDownVelocity = this.fallVelocity; const horizontalSpeed = (-slidingDownVelocity / (1 - this.groundNormal.y)) * 0.2; this.velocity.x = this.groundNormal.x * horizontalSpeed; this.velocity.y = this.fallVelocity; this.velocity.z = this.groundNormal.z * horizontalSpeed; } else if (!this.isGrounded && !this.isOnSlope && this.isJumping) { // 跳跃处理 this.velocity.y = this.currentJumpPower * -this.fallVelocity; } // 面向墙壁时将向墙壁的速度设置为0的处理 // vs 墙壁和在墙上滑动 direction2D.set(rightDirection, frontDirection); // const frontAngle = Math.atan2( direction2D.y, direction2D.x ); const negativeFrontAngle = Math.atan2(-direction2D.y, -direction2D.x); for (let i = 0, l = this.contactInfo.length; i < l; i++) { const normal = this.contactInfo[i].face.normal; // var distance = this.contactInfo[ i ].distance; if (this.maxSlopeGradient < normal.y || this.isOnSlope) { // 由于人脸是在地面上,所以没有像墙一样碰撞的可能性。 // 不衰减速度 continue; } if (!isHittingCeiling && normal.y < 0) { isHittingCeiling = true; } wallNormal2D.set(normal.x, normal.z).normalize(); const wallAngle = Math.atan2(wallNormal2D.y, wallNormal2D.x); if ( Math.abs(negativeFrontAngle - wallAngle) >= PI_HALF && // 90deg Math.abs(negativeFrontAngle - wallAngle) <= PI_ONE_HALF // 270deg ) { // 面与行进方向相反,点为背面的墙 // 不衰减速度 continue; } // 如果不满足以上条件,人脸就是墙 // 找到墙壁法线并将指向相反方向的速度向量设置为0 wallNormal2D.set( direction2D.dot(wallNormal2D) * wallNormal2D.x, direction2D.dot(wallNormal2D) * wallNormal2D.y ); direction2D.sub(wallNormal2D); const moveSpeed = this.isWalking ? this.walkSpeed : this.runSpeed; this.velocity.x = direction2D.x * moveSpeed * this.isRunning; this.velocity.z = direction2D.y * moveSpeed * this.isRunning; } // 如果在跳跃时撞到天花板,中断跳跃 if (isHittingCeiling) { this.velocity.y = Math.min(0, this.velocity.y); this.isJumping = false; } } _updateGrounding() { // “从头顶到几乎无限向下的分段 (segment)”与“三角形(triangle)” // 进行交叉判断 // 如果与面部的交点在“头顶”和“地下填充(groundPadding)”之间 // isGrounded认定在地面上 // // ___ // / | \ // | | | player sphere 玩家 // \_|_/ // | //---[+]---- ground 地面 // | // | // | segment (玩家头部到无限) let groundContactInfo; let groundContactInfoTmp; const faces = this.collisionCandidate; groundingHead.set( this.center.x, this.center.y + this.radius, this.center.z ); groundingTo.set(this.center.x, this.center.y - 1e10, this.center.z); for (let i = 0, l = faces.length; i < l; i++) { groundContactInfoTmp = testSegmentTriangle( groundingHead, groundingTo, faces[i].a, faces[i].b, faces[i].c ); if (groundContactInfoTmp && !groundContactInfo) { groundContactInfo = groundContactInfoTmp; groundContactInfo.face = faces[i]; } else if ( groundContactInfoTmp && groundContactInfoTmp.contactPoint.y > groundContactInfo.contactPoint.y ) { groundContactInfo = groundContactInfoTmp; groundContactInfo.face = faces[i]; } } if (!groundContactInfo) { return; } this.groundHeight = groundContactInfo.contactPoint.y; // this.groundNormal.copy(groundContactInfo.face.normal); const top = groundingHead.y; const bottom = this.center.y - this.radius - this.groundPadding; // 跳跃和向上移动时不要强迫地面 if (this.isJumping && 0 < this.currentJumpPower) { this.isOnSlope = false; this.isGrounded = false; return; } this.isGrounded = bottom <= this.groundHeight && this.groundHeight <= top; this.isOnSlope = this.groundNormal.y <= this.maxSlopeGradient; if (this.isGrounded) { this.isJumping = false; } } _updatePosition(dt) { // 暂时忽略墙壁等(速度*时间) // 推进中心坐标 // 与墙壁的碰撞检测会在下一步进行,这里就不做了 // 如果是isGrounded,强制将y的值调整到地面 const groundedY = this.groundHeight + this.radius; const x = this.center.x + this.velocity.x * dt; const y = this.center.y + this.velocity.y * dt; const z = this.center.z + this.velocity.z * dt; this.center.set(x, this.isGrounded ? groundedY : y, z); } _collisionDetection() { // 从可能相交的人脸列表中(collisionCandidate) // 提取实际相交的墙面 // 添加到 this.contactInfo const faces = this.collisionCandidate; this.contactInfo.length = 0; for (let i = 0, l = faces.length; i < l; i++) { const contactInfo = isIntersectionSphereTriangle( this, faces[i].a, faces[i].b, faces[i].c, faces[i].normal ); if (!contactInfo) continue; contactInfo.face = faces[i]; this.contactInfo.push(contactInfo); } } _calHeadPosition(position) { const center = { x: position.x, y: position.y + 1 + this.height, z: position.z, }; return center; } _solvePosition() { // 用 updatePosition() 运行中心后 // 如果它与墙壁碰撞并咬入它 // 这里从墙中挤出 let face; let normal; // let distance; if (this.contactInfo.length === 0) { // 没有冲突 // 使用 center 的值退出 const newPosition = { x: this.center.x, y: this.center.y - this.radius, z: this.center.z, }; this.object.position.copy(newPosition); this.headObject.position.copy(this._calHeadPosition(newPosition)); return; } // // vs 墙壁和在墙上滑动 translate.set(0, 0, 0); for (let i = 0, l = this.contactInfo.length; i < l; i++) { face = this.contactInfo[i].face; normal = this.contactInfo[i].face.normal; // distance = this.contactInfo[ i ].distance; // if ( 0 <= distance ) { // // 交差点までの距離が 0 以上ならこのフェイスとは衝突していない // // 無視する // continue; // } if (this.maxSlopeGradient < normal.y) { // 这个三角形是地面或斜坡,而不是墙壁或天花板 // 面是非陡坡,即地面。 // 忽略接地过程,因为它在 updatePosition () 中解决 continue; } // 面是否为陡坡 const isSlopeFace = this.maxSlopeGradient <= face.normal.y && face.normal.y < 1; // 如果您在跳跃下降过程中遇到陡坡,则跳跃结束 if (this.isJumping && 0 >= this.currentJumpPower && isSlopeFace) { this.isJumping = false; this.isGrounded = true; // console.log( 'jump end' ); } if (this.isGrounded || this.isOnSlope) { // 如果您在地面上,y(垂直)方向保持不变 // 通过仅更改 x、z(水平)方向进行拉伸 // http://gamedev.stackexchange.com/questions/80293/how-do-i-resolve-a-sphere-triangle-collision-in-a-given-direction point1.copy(normal).multiplyScalar(-this.radius).add(this.center); direction.set(normal.x, 0, normal.z).normalize(); const plainD = face.a.dot(normal); const t = (plainD - (normal.x * point1.x + normal.y * point1.y + normal.z * point1.z)) / (normal.x * direction.x + normal.y * direction.y + normal.z * direction.z); point2.copy(direction).multiplyScalar(t).add(point1); translateScoped.subVectors(point2, point1); if (Math.abs(translate.x) > Math.abs(translateScoped.x)) { translate.x += translateScoped.x; } if (Math.abs(translate.z) > Math.abs(translateScoped.z)) { translate.z += translateScoped.z; } // break; continue; } } const newPosition = { x: this.center.x, y: this.center.y - this.radius, z: this.center.z, }; this.object.position.copy(newPosition); this.headObject.position.copy(this._calHeadPosition(newPosition)); } setDirection() {} jump() { if (this.isJumping || !this.isGrounded || this.isOnSlope) return; this.jumpStartTime = performance.now(); this.currentJumpPower = 1; this.isJumping = true; } _updateJumping() { if (!this.isJumping) return; const elapsed = performance.now() - this.jumpStartTime; const progress = elapsed / this.jumpDuration; this.currentJumpPower = Math.cos(Math.min(progress, 1) * Math.PI); } }