90 lines
2.3 KiB
TypeScript
90 lines
2.3 KiB
TypeScript
|
|
import type { Vec3 } from "../math/Vec3"
|
||
|
|
import { CollisionWorld, type CollisionWorld as World } from "./CollisionWorld"
|
||
|
|
|
||
|
|
export type Character = {
|
||
|
|
position: Vec3
|
||
|
|
yaw: number
|
||
|
|
velocityY: number
|
||
|
|
onGround: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
export type CharacterInput = {
|
||
|
|
forward: number
|
||
|
|
right: number
|
||
|
|
jump: boolean
|
||
|
|
run: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
export type CharacterConfig = {
|
||
|
|
radius: number
|
||
|
|
speed: number
|
||
|
|
runMultiplier: number
|
||
|
|
gravity: number
|
||
|
|
jumpSpeed: number
|
||
|
|
eyeHeight: number
|
||
|
|
}
|
||
|
|
|
||
|
|
export namespace CharacterController {
|
||
|
|
export function update(
|
||
|
|
character: Character,
|
||
|
|
input: CharacterInput,
|
||
|
|
dt: number,
|
||
|
|
world: World,
|
||
|
|
config: CharacterConfig,
|
||
|
|
): void {
|
||
|
|
if (input.jump && character.onGround) {
|
||
|
|
character.velocityY = config.jumpSpeed
|
||
|
|
character.onGround = false
|
||
|
|
}
|
||
|
|
const steps = moveSubsteps(input, dt, config)
|
||
|
|
for (let i = 0; i < steps; i++) {
|
||
|
|
moveHorizontal(character, input, dt / steps, config)
|
||
|
|
CollisionWorld.pushOut(world, character.position, config.radius)
|
||
|
|
}
|
||
|
|
character.velocityY -= config.gravity * dt
|
||
|
|
character.position.y += character.velocityY * dt
|
||
|
|
const ground = CollisionWorld.groundHeight(world, character.position)
|
||
|
|
if (character.position.y <= ground) {
|
||
|
|
character.position.y = ground
|
||
|
|
character.velocityY = 0
|
||
|
|
character.onGround = true
|
||
|
|
} else {
|
||
|
|
character.onGround = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function moveSubsteps(
|
||
|
|
input: CharacterInput,
|
||
|
|
dt: number,
|
||
|
|
config: CharacterConfig,
|
||
|
|
): number {
|
||
|
|
const distance =
|
||
|
|
config.speed *
|
||
|
|
runFactor(input, config) *
|
||
|
|
dt *
|
||
|
|
Math.hypot(input.forward, input.right)
|
||
|
|
return Math.max(1, Math.ceil(distance / config.radius))
|
||
|
|
}
|
||
|
|
|
||
|
|
function moveHorizontal(
|
||
|
|
character: Character,
|
||
|
|
input: CharacterInput,
|
||
|
|
dt: number,
|
||
|
|
config: CharacterConfig,
|
||
|
|
): void {
|
||
|
|
const speed = config.speed * runFactor(input, config) * dt
|
||
|
|
const forwardX = Math.sin(character.yaw)
|
||
|
|
const forwardZ = -Math.cos(character.yaw)
|
||
|
|
const rightX = Math.cos(character.yaw)
|
||
|
|
const rightZ = Math.sin(character.yaw)
|
||
|
|
character.position.x +=
|
||
|
|
(forwardX * input.forward + rightX * input.right) * speed
|
||
|
|
character.position.z +=
|
||
|
|
(forwardZ * input.forward + rightZ * input.right) * speed
|
||
|
|
}
|
||
|
|
|
||
|
|
function runFactor(input: CharacterInput, config: CharacterConfig): number {
|
||
|
|
return input.run ? config.runMultiplier : 1
|
||
|
|
}
|
||
|
|
}
|