0.前言
相信大家在看threejs官网案例的时候会觉得里面那个fps的小例子碰撞检测感觉很丝滑,还有一定的类似物理的效果,不像之前简单的利用射线进行碰撞检测顶到墙之后劈里啪啦卡顿。
之前一个大哥吧这个案例简单解析了一下,也介绍了一下八叉树的理论知识
three.js案例解析之游戏帧碰撞检测https://blog.csdn.net/web22050702/article/details/125301514这里就不赘述八叉树的理论和这个案例的解析了,那这儿就跟据这个案例搞一个碰撞检测的控制器CollisionController,方便大家直接用。下面的连接是本文的完整代码,需要js版本请复制代码到ts官网playground中根据自己需要的js版本自行编译。
1.先说用法
先引入
import { CollisionController } from './octreeCollision'
然后使用,格式如下
new CollisionController(capsuleParam: iCapsule, camera: PerspectiveCamera, collisionGroup: Object3D<Event>, canvas: HTMLCanvasElement): CollisionController
说人话就是new 一个CollisionController实例,传入相机体积参数(同Capsule)、需要控制的相机(PerspectiveCamera)、需要进行碰撞检测的模型(Object3D)、渲染画布(canvas)
// 控制器
private controls () {
this.controls = new CollisionController(
{
start: new Vector3(0, 0.35, 0),
end: new Vector3(0, 1, 0),
radius: 0.35
},
this.camera,
this.scene.children[this.scene.children.length - 1],
this.renderer.domElement)
this.controls.update()
}
然后在渲染循环中对这个控制器进行更新
// 渲染循环
private renderLoop () {
this.renderer.render(this.scene, this.camera)
this.controls?.update()
}
2.再说内容
如何创建一个threejs项目这里就不赘述,我这儿有个vue3+ts+threejs的项目模板,有需要的话可以fork一下,里面后续也会更新一些实用的小工具
threejs-standard-framework: vue框架threejs标准模板 (gitee.com)https://gitee.com/Susiia/threejs-standard-framework新建octreeCollision.ts,首先我们需要引入的东西有
import { Clock, Object3D, PerspectiveCamera, Vector3 } from 'three'
import { Octree } from 'three/examples/jsm/math/Octree.js'
import { Capsule } from 'three/examples/jsm/math/Capsule.js'
创建构造函数需要的接口(碰撞体积参数类型)
interface iCapsule{
start:Vector3,
end:Vector3,
radius:number
}
创建CollisionController类
class CollisionController {
constructor (capsuleParam:iCapsule, camera:PerspectiveCamera, collisionGroup:Object3D, canvas:HTMLCanvasElement) {
}
}
声明私有变量
private clock:Clock // 时钟
private camera:PerspectiveCamera // 相机
private collisionGroup:Object3D// 需要计算碰撞检测的组
private canvas:HTMLCanvasElement
private GRAVITY:number // 重力
private STEPS_PER_FRAME:number // 每秒步数
private worldOctree:Octree// 环境八叉树
private _playerCollider!: Capsule // 玩家碰撞体积(公开方法,setter和getter在下面)
private playerOnFloor:boolean;// 玩家是不是在地面上
private playerVelocity:Vector3;// 玩家速度
private playerDirection:Vector3;// 玩家方向
private eventStates = { // 事件状态
KeyW: false,
KeyA: false,
KeyS: false,
KeyD: false,
Space: false,
mouseDown: false
}
在构造函数中对声明的变量进行初始化
constructor (capsuleParam:iCapsule, camera:PerspectiveCamera, collisionGroup:Object3D, canvas:HTMLCanvasElement) {
this.clock = new Clock()
this.GRAVITY = 30
this.STEPS_PER_FRAME = 5
this.worldOctree = new Octree()
this.playerOnFloor = false
this.playerCollider = new Capsule(capsuleParam.start, capsuleParam.end, capsuleParam.radius)
this.playerVelocity = new Vector3()
this.playerDirection = new Vector3()
this.camera = camera
this.camera.rotation.order = 'YXZ' // 相机旋转方式需要调整一下
this.collisionGroup = collisionGroup
this.canvas = canvas
// 将需要进行碰撞检测的Object3D加入worldOctree
this.worldOctree.fromGraphNode(this.collisionGroup)
this.initEventListener() // 开启事件侦听
}
事件侦听方法:
// 初始化按键侦听
private initEventListener () {
// 键盘按下
document.addEventListener('keydown', (event) => {
switch (event.code) {
case 'KeyW':
this.eventStates[event.code] = true
break
case 'KeyA':
this.eventStates[event.code] = true
break
case 'KeyS':
this.eventStates[event.code] = true
break
case 'KeyD':
this.eventStates[event.code] = true
break
case 'Space':
this.eventStates[event.code] = true
break
default:
break
}
})
// 键盘抬起
document.addEventListener('keyup', (event) => {
switch (event.code) {
case 'KeyW':
this.eventStates[event.code] = false
break
case 'KeyA':
this.eventStates[event.code] = false
break
case 'KeyS':
this.eventStates[event.code] = false
break
case 'KeyD':
this.eventStates[event.code] = false
break
case 'Space':
this.eventStates[event.code] = false
break
default:
break
}
})
// 鼠标按下
this.canvas.addEventListener('mousedown', () => {
// this.canvas.requestPointerLock()
this.eventStates.mouseDown = true
})
// 鼠标抬起
this.canvas.addEventListener('mouseup', () => {
// NOTE:exitPointerLock在ts中会报错, ts3.1一些浏览器厂商特定的类型从lib.d.ts中被移除,其中包括退出PointerLock状态的方法exitPointerLock(),https://www.lanqiao.cn/library/TypeScript/breaking-changes/typescript-3.1
// this.canvas.exitPointerLock()
this.eventStates.mouseDown = false
})
// 鼠标移动
this.canvas.addEventListener('mousemove', (event) => {
if (this.eventStates.mouseDown) {
this.camera.rotation.y -= event.movementX / 500
this.camera.rotation.x -= event.movementY / 500
}
})
}
创建一个控制器方法
// 控制器
private controls (deltaTime:number) {
// gives a bit of air control
const speedDelta = deltaTime * (this.playerOnFloor ? 25 : 8)
if (this.eventStates.KeyW) {
this.playerVelocity.add(this.getForwardVector().multiplyScalar(speedDelta))
}
if (this.eventStates.KeyS) {
this.playerVelocity.add(this.getForwardVector().multiplyScalar(-speedDelta))
}
if (this.eventStates.KeyA) {
this.playerVelocity.add(this.getSideVector().multiplyScalar(-speedDelta))
}
if (this.eventStates.KeyD) {
this.playerVelocity.add(this.getSideVector().multiplyScalar(speedDelta))
}
if (this.playerOnFloor) {
if (this.eventStates.Space) {
this.playerVelocity.y = 15
}
}
}
创建一个public方法出去,用来更新控制器
// 更新控制器
public update () {
const deltaTime = Math.min(0.05, this.clock.getDelta()) / this.STEPS_PER_FRAME
for (let i = 0; i < this.STEPS_PER_FRAME; i++) {
this.controls(deltaTime)
this.updatePlayer(deltaTime)
}
}
创建一个用来更新玩家的方法
// 更新玩家
private updatePlayer (deltaTime:number) {
let damping = Math.exp(-4 * deltaTime) - 1
if (!this.playerOnFloor) {
this.playerVelocity.y -= this.GRAVITY * deltaTime
damping *= 0.1
}
this.playerVelocity.addScaledVector(this.playerVelocity, damping)
const deltaPosition = this.playerVelocity.clone().multiplyScalar(deltaTime)
this.playerCollider.translate(deltaPosition)
this.playerCollisions()
this.camera.position.copy(this.playerCollider.end)
}
创建获得当前相机前后和左右方向的方法
// 获得前后方向
private getForwardVector () {
this.camera.getWorldDirection(this.playerDirection)
this.playerDirection.y = 0
this.playerDirection.normalize()
return this.playerDirection
}
// 获得左右方向
private getSideVector () {
this.camera.getWorldDirection(this.playerDirection)
this.playerDirection.y = 0
this.playerDirection.normalize()
this.playerDirection.cross(this.camera.up)
return this.playerDirection
}
创建碰撞检测方法
// 玩家碰撞
private playerCollisions () {
const result = this.worldOctree.capsuleIntersect(this.playerCollider)
this.playerOnFloor = false
if (result) {
this.playerOnFloor = result.normal.y > 0
if (!this.playerOnFloor) {
this.playerVelocity.addScaledVector(result.normal, -result.normal.dot(this.playerVelocity))
}
this.playerCollider.translate(result.normal.multiplyScalar(result.depth))
}
}
到此我们这个控制器就写好了。