@react-three/p2
Version:
2D physics based hooks for react-three-fiber
358 lines (283 loc) • 10.3 kB
text/typescript
import type { Body } from 'p2-es'
import { Ray, RaycastResult, vec2 } from 'p2-es'
import type { Duplet } from './'
import type KinematicCharacterController from './KinematicCharacterController'
import type { RaycastControllerOptns } from './RaycastController'
import RaycastController from './RaycastController'
interface BodyWithUuid extends Body {
uuid: string
}
// math helpers
function sign(x: number) {
return x >= 0 ? 1 : -1
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value))
}
// constants
const ZERO = vec2.create()
interface PlatformControllerOptns extends RaycastControllerOptns {
controllers: { [uuid: string]: { controller: KinematicCharacterController } }
dstBetweenRays?: number
localWaypoints: Duplet[]
passengerMask: number
skinWidth?: number
speed?: number
}
export default class PlatformController extends RaycastController {
cyclic: boolean
//[Range(0,2)]
easeAmount: number
fromWaypointIndex: number
globalWaypoints: Duplet[]
localWaypoints: Duplet[]
nextMoveTime: number
passengerDictionary: { [key: string]: KinematicCharacterController }
passengerMask: number
passengerMovement: PassengerMovement[]
percentBetweenWaypoints: number
ray: Ray
raycastResult: RaycastResult
raysData: [from: Duplet, to: Duplet, hitDistance?: number][]
speed: number
time: number
waitTime: number
constructor(options: PlatformControllerOptns) {
super(options)
this.passengerMask = options.passengerMask || -1
this.localWaypoints = options.localWaypoints
this.globalWaypoints = []
this.speed = options.speed || 5
this.cyclic = false
this.waitTime = 0
// Range(0,2)
this.easeAmount = 0
this.fromWaypointIndex = 0
this.percentBetweenWaypoints = 0
this.nextMoveTime = 0
this.passengerMovement = []
this.passengerDictionary = {}
this.time = 0
this.ray = new Ray({
from: [0, 0],
mode: Ray.CLOSEST,
skipBackfaces: true,
to: [0, -1],
})
this.raycastResult = new RaycastResult()
this.raysData = []
this.globalWaypoints = new Array(this.localWaypoints.length)
for (let i = 0; i < this.localWaypoints.length; i++) {
const temp = vec2.create()
vec2.add(temp, this.localWaypoints[i], this.body.position)
this.globalWaypoints[i] = temp
}
Object.values(options.controllers).map((c) => {
const body = c.controller.body as unknown as BodyWithUuid
if (c.controller.constructor.name === 'KinematicCharacterController')
this.passengerDictionary[body.uuid] = c.controller
})
this.world.on('postStep', () => this.update(1 / 60))
}
calculatePassengerMovement(velocity: Duplet) {
const movedPassengers = new Set()
this.passengerMovement = []
const directionX = sign(velocity[0])
const directionY = sign(velocity[1])
// Vertically moving platform
if (velocity[1] !== 0) {
const rayLength = Math.abs(velocity[1]) + this.skinWidth
for (let i = 0; i < this.verticalRayCount; i++) {
const ray = this.ray
ray.collisionMask = this.passengerMask
vec2.copy(ray.from, directionY === -1 ? this.raycastOrigins.bottomLeft : this.raycastOrigins.topLeft)
vec2.set(ray.from, ray.from[0] + this.verticalRaySpacing * i, ray.from[1])
vec2.set(ray.to, ray.from[0], ray.from[1] + directionY * rayLength)
ray.update()
this.world.raycast(this.raycastResult, ray)
this.raysData[i] = [[...ray.from], [...ray.to], undefined]
if (this.raycastResult.body) {
const distance = this.raycastResult.getHitDistance(ray)
if (distance === 0) continue
const body = this.raycastResult.body as BodyWithUuid
if (!movedPassengers.has(body.uuid)) {
movedPassengers.add(body.uuid)
const pushX = directionY === 1 ? velocity[0] : 0
const pushY = velocity[1] - (distance - this.skinWidth) * directionY
this.passengerMovement.push(
new PassengerMovement({
moveBeforePlatform: true,
standingOnPlatform: directionY === 1,
uuid: body.uuid,
velocity: vec2.fromValues(pushX, pushY),
}),
)
}
}
this.raycastResult.reset()
}
}
// Horizontally moving platform
if (velocity[0] !== 0) {
const rayLength = Math.abs(velocity[0]) + this.skinWidth
for (let i = 0; i < this.horizontalRayCount; i++) {
const ray = this.ray
ray.collisionMask = this.passengerMask
vec2.copy(
ray.from,
directionX === -1 ? this.raycastOrigins.bottomLeft : this.raycastOrigins.bottomRight,
)
ray.from[1] += this.horizontalRaySpacing * i
vec2.copy(ray.to, ray.from)
ray.to[0] += directionX * rayLength
ray.update()
this.world.raycast(this.raycastResult, ray)
this.raysData[this.verticalRayCount + i] = [[...ray.from], [...ray.to], undefined]
if (this.raycastResult.body) {
const body = this.raycastResult.body as BodyWithUuid
const distance = this.raycastResult.getHitDistance(ray)
if (distance === 0) {
continue
}
if (!movedPassengers.has(body.uuid)) {
movedPassengers.add(body.uuid)
const pushX = velocity[0] - (distance - this.skinWidth) * directionX
const pushY = -this.skinWidth
this.passengerMovement.push(
new PassengerMovement({
moveBeforePlatform: true,
standingOnPlatform: false,
uuid: body.uuid,
velocity: vec2.fromValues(pushX, pushY),
}),
)
}
}
this.raycastResult.reset()
}
}
// Passenger on top of a horizontally or downward moving platform
if (directionY === -1 || (velocity[1] === 0 && velocity[0] !== 0)) {
const rayLength = this.skinWidth * 2
for (let i = 0; i < this.verticalRayCount; i++) {
const ray = this.ray
ray.collisionMask = this.passengerMask
vec2.set(
ray.from,
this.raycastOrigins.topLeft[0] + this.verticalRaySpacing * i,
this.raycastOrigins.topLeft[1],
)
vec2.set(ray.to, ray.from[0], ray.from[1] + rayLength)
ray.update()
this.world.raycast(this.raycastResult, ray)
this.raysData[this.verticalRayCount + this.horizontalRayCount + i] = [
[...ray.from],
[...ray.to],
undefined,
]
if (this.raycastResult.body) {
const distance = this.raycastResult.getHitDistance(ray)
if (distance === 0) {
continue
}
const body = this.raycastResult.body as BodyWithUuid
if (!movedPassengers.has(body.uuid)) {
movedPassengers.add(body.uuid)
const pushX = velocity[0]
const pushY = velocity[1]
this.passengerMovement.push(
new PassengerMovement({
moveBeforePlatform: false,
standingOnPlatform: true,
uuid: body.uuid,
velocity: vec2.fromValues(pushX, pushY),
}),
)
}
}
this.raycastResult.reset()
}
}
}
calculatePlatformMovement(deltaTime: number): Duplet {
if (this.time < this.nextMoveTime) {
return ZERO
}
const { globalWaypoints, speed } = this
this.fromWaypointIndex %= globalWaypoints.length
const toWaypointIndex = (this.fromWaypointIndex + 1) % globalWaypoints.length
const distanceBetweenWaypoints = vec2.distance(
globalWaypoints[this.fromWaypointIndex],
globalWaypoints[toWaypointIndex],
)
this.percentBetweenWaypoints += (deltaTime * speed) / distanceBetweenWaypoints
this.percentBetweenWaypoints = clamp(this.percentBetweenWaypoints, 0, 1)
const easedPercentBetweenWaypoints = this.ease(this.percentBetweenWaypoints)
const newPos = vec2.create()
vec2.lerp(
newPos,
globalWaypoints[this.fromWaypointIndex],
globalWaypoints[toWaypointIndex],
easedPercentBetweenWaypoints,
)
if (this.percentBetweenWaypoints >= 1) {
this.percentBetweenWaypoints = 0
this.fromWaypointIndex++
if (!this.cyclic) {
if (this.fromWaypointIndex >= globalWaypoints.length - 1) {
this.fromWaypointIndex = 0
globalWaypoints.reverse()
}
}
this.nextMoveTime = this.time + this.waitTime
}
const result = vec2.create()
vec2.subtract(result, newPos, this.body.position)
return result
}
ease(x: number) {
const a = this.easeAmount + 1
return Math.pow(x, a) / (Math.pow(x, a) + Math.pow(1 - x, a))
}
movePassengers(beforeMovePlatform: boolean) {
this.passengerMovement.map((passenger) => {
if (!(passenger.uuid in this.passengerDictionary)) {
return console.error('passenger uuid not in passengerDictionary')
}
if (passenger.moveBeforePlatform === beforeMovePlatform) {
this.passengerDictionary[passenger.uuid].moveWithZeroInput(
passenger.velocity,
passenger.standingOnPlatform,
)
}
})
}
update(deltaTime: number) {
this.time += deltaTime
super.updateRaycastOrigins()
const velocity = this.calculatePlatformMovement(deltaTime)
this.updateRaycastOrigins()
this.calculatePassengerMovement(velocity)
this.movePassengers(true)
vec2.set(this.body.position, this.body.position[0] + velocity[0], this.body.position[1] + velocity[1])
this.movePassengers(false)
}
}
type PassengerMovementOptns = {
moveBeforePlatform: boolean
standingOnPlatform: boolean
uuid: string
velocity: Duplet
}
class PassengerMovement {
moveBeforePlatform: boolean
standingOnPlatform: boolean
uuid: string
velocity: Duplet
constructor(options: PassengerMovementOptns) {
this.velocity = options.velocity || [0, 0]
this.standingOnPlatform = options.standingOnPlatform || false
this.moveBeforePlatform = options.moveBeforePlatform || false
this.uuid = options.uuid || ''
}
}