crude player physics & collision response

This commit is contained in:
a dinosaur 2024-08-16 00:27:35 +10:00
parent 60ced3691d
commit 6b92b538a5
6 changed files with 206 additions and 34 deletions

View File

@ -0,0 +1,58 @@
import simd
struct AABB {
private var _bounds: simd_float2x3
var lower: SIMD3<Float> {
get { _bounds[0] }
set(row) { self._bounds[0] = row }
}
var upper: SIMD3<Float> {
get { _bounds[1] }
set(row) { self._bounds[1] = row }
}
var center: SIMD3<Float> {
get { (self._bounds[0] + self._bounds[1]) / 2 }
}
var size: SIMD3<Float> {
get { self._bounds[1] - self._bounds[0] }
}
var left: Float { self._bounds[0].x }
var bottom: Float { self._bounds[0].y }
var far: Float { self._bounds[0].z }
var right: Float { self._bounds[1].x }
var top: Float { self._bounds[1].y }
var near: Float { self._bounds[1].z }
private init(bounds: simd_float2x3) {
self._bounds = bounds
}
init(from: SIMD3<Float>, to: SIMD3<Float>) {
self.init(bounds: .init(from, to))
}
static func fromUnitCube(position: SIMD3<Float> = .zero, scale: SIMD3<Float> = .one) -> Self {
self.init(
from: position - scale,
to: position + scale)
}
func touching(_ other: Self) -> Bool{
let distLower = other._bounds[0] - self._bounds[1] // x: left, y: bottom, z: far
let distUpper = self._bounds[0] - other._bounds[1] // x: right, y: top, z: near
if distLower.x > 0 || distUpper.x > 0 { return false }
if distLower.y > 0 || distUpper.y > 0 { return false }
if distLower.z > 0 || distUpper.z > 0 { return false }
return true
}
}
extension AABB {
static func + (lhs: Self, rhs: SIMD3<Float>) -> Self {
.init(bounds: lhs._bounds + .init(rhs, rhs))
}
}

View File

@ -9,6 +9,7 @@ add_executable(Voxelotl MACOSX_BUNDLE
FloatExtensions.swift
Matrix4x4.swift
Rectangle.swift
AABB.swift
NSImageLoader.swift
Renderer.swift

View File

@ -1,16 +1,39 @@
import simd
struct Instance {
var position: SIMD3<Float> = .zero
var scale: SIMD3<Float> = .one
var rotation: simd_quatf = .identity
var color: SIMD4<Float> = .one
struct Box {
var geometry: AABB
var color: SIMD4<Float> = .one
}
struct Instance {
let position: SIMD3<Float>
let scale: SIMD3<Float>
let rotation: simd_quatf
let color: SIMD4<Float>
init(
position: SIMD3<Float> = .zero,
scale: SIMD3<Float> = .one,
rotation: simd_quatf = .identity,
color: SIMD4<Float> = .one
) {
self.position = position
self.scale = scale
self.rotation = rotation
self.color = color
}
}
let boxes: [Box] = [
Box(geometry: .fromUnitCube(position: .init(0, -1, 0) * 2, scale: .init(10, 0.1, 10) * 2)),
Box(geometry: .fromUnitCube(position: .init(-2.5, 0, -3) * 2, scale: .init(repeating: 2)), color: .init(1, 0.5, 0.75, 1)),
Box(geometry: .fromUnitCube(position: .init(-2.5, -0.5, -5) * 2, scale: .init(repeating: 2)), color: .init(0.75, 1, 1, 1))
]
class Game: GameDelegate {
private var fpsCalculator = FPSCalculator()
var camera = Camera(fov: 60, size: .one, range: 0.03...25)
var camera = Camera(fov: 60, size: .one, range: 0.06...50)
var player = Player()
var projection: matrix_float4x4 = .identity
@ -24,7 +47,7 @@ class Game: GameDelegate {
print("FPS: \(fps)")
}
player.update(deltaTime: deltaTime)
player.update(deltaTime: deltaTime, boxes: boxes)
camera.position = player.position
camera.rotation =
simd_quatf(angle: player.rotation.y, axis: .init(1, 0, 0)) *
@ -34,16 +57,18 @@ class Game: GameDelegate {
func draw(_ renderer: Renderer, _ time: GameTime) {
let totalTime = Float(time.total.asFloat)
let instances: [Instance] = [
var instances: [Instance] = boxes.map {
Instance(
position: .init(0, sin(totalTime * 1.5) * 0.5, -2),
scale: .init(repeating: 0.25),
position: $0.geometry.center,
scale: $0.geometry.size * 0.5,
color: $0.color)
}
instances.append(
Instance(
position: .init(0, sin(totalTime * 1.5) * 0.5, -2) * 2,
scale: .init(repeating: 0.5),
rotation: .init(angle: totalTime * 3.0, axis: .init(0, 1, 0)),
color: .init(0.5, 0.5, 1, 1)),
Instance(position: .init(0, -1, 0), scale: .init(10, 0.1, 10)),
Instance(position: .init(-2.5, 0, -3), color: .init(1, 0.5, 0.75, 1)),
Instance(position: .init(-2.5, -0.5, -5), color: .init(0.75, 1, 1, 1))
]
color: .init(0.5, 0.5, 1, 1)))
renderer.batch(instances: instances, camera: self.camera)
}

View File

@ -192,6 +192,8 @@ public extension GameController.Pad.State {
var rightStick: SIMD2<Float> {
.init(axis(.rightStickX), axis(.rightStickY))
}
var leftTrigger: Float { axis(.leftTrigger) }
var rightTrigger: Float { axis(.rightTrigger) }
}
public extension FloatingPoint {

View File

@ -1,36 +1,122 @@
import simd
struct Player {
static let height: Float = 1.8
static let radius: Float = 0.4
static let bounds = AABB(
from: .init(-Self.radius, 0, -Self.radius),
to: .init(Self.radius, Self.height, Self.radius))
static let eyeLevel: Float = 1.4
static let epsilon = Float.ulpOfOne * 10
static let speedCoeff: Float = 720
static let gravityCoeff: Float = 12
static let jumpVelocity: Float = 9
private var _position = SIMD3<Float>.zero
private var _velocity = SIMD3<Float>.zero
private var _rotation = SIMD2<Float>.zero
public var position: SIMD3<Float> { self._position }
private var _onGround: Bool = false
public var position: SIMD3<Float> { self._position + .init(0, Self.eyeLevel, 0) }
public var rotation: SIMD2<Float> { self._rotation }
mutating func update(deltaTime: Float) {
mutating func update(deltaTime: Float, boxes: [Box]) {
if let pad = GameController.current?.state {
// Turning input
let turning = pad.rightStick.radialDeadzone(min: 0.1, max: 1)
_rotation += turning * deltaTime * 3.0
if _rotation.x < 0.0 {
_rotation.x += .pi * 2
if self._rotation.x < 0.0 {
self._rotation.x += .pi * 2
} else if _rotation.x > .pi * 2 {
_rotation.x -= .pi * 2
self._rotation.x -= .pi * 2
}
_rotation.y = _rotation.y.clamp(-.pi * 0.5, .pi * 0.5)
self._rotation.y = self._rotation.y.clamp(-.pi * 0.5, .pi * 0.5)
let movement = pad.leftStick.cardinalDeadzone(min: 0.1, max: 1)
if self._onGround {
// Movement on ground
let movement = pad.leftStick.cardinalDeadzone(min: 0.1, max: 1)
let rotc = cos(self._rotation.x), rots = sin(self._rotation.x)
self._velocity.x += (movement.x * rotc - movement.y * rots) * Self.speedCoeff * deltaTime
self._velocity.z += (movement.y * rotc + movement.x * rots) * Self.speedCoeff * deltaTime
let rotc = cos(_rotation.x), rots = sin(_rotation.x)
_position += .init(
movement.x * rotc - movement.y * rots,
0,
movement.y * rotc + movement.x * rots
) * deltaTime * 3.0
// Jumping
if pad.pressed(.east) {
self._velocity.y = Self.jumpVelocity
self._onGround = false
}
}
// Flying
self._velocity.y += pad.rightTrigger * 36 * deltaTime
// Reset
if pad.pressed(.back) {
_position = .zero
_rotation = .zero
self._position = .zero
self._velocity = .zero
self._rotation = .zero
self._onGround = false
}
}
// Apply gravity
self._velocity.y -= Self.gravityCoeff * deltaTime
// Move & handle collision
let checkCollision = { (position: SIMD3<Float>) -> Optional<AABB> in
for box in boxes {
let bounds = Self.bounds + position
if bounds.touching(box.geometry) {
return box.geometry
}
}
return nil
}
self._position.x += _velocity.x * deltaTime
if let aabb = checkCollision(self._position) {
if _velocity.x < 0 {
self._position.x = aabb.right + Self.radius + Self.epsilon
printErr("-x")
} else {
self._position.x = aabb.left - Self.radius - Self.epsilon
printErr("+x")
}
self._velocity.x = 0
}
self._position.z += _velocity.z * deltaTime
if let aabb = checkCollision(self._position) {
if _velocity.z < 0 {
self._position.z = aabb.near + Self.radius + Self.epsilon
printErr("-x")
} else {
self._position.z = aabb.far - Self.radius - Self.epsilon
printErr("+x")
}
self._velocity.z = 0
}
self._position.y += _velocity.y * deltaTime
if let aabb = checkCollision(self._position) {
if _velocity.y < 0 {
self._position.y = aabb.top + Self.epsilon
if !self._onGround { printErr("-y") }
self._onGround = true
} else {
self._position.y = aabb.bottom - Self.height - Self.epsilon
self._onGround = false
printErr("+y")
}
self._velocity.y = 0
} else {
self._onGround = false
}
// Ground friction
if self._onGround {
self._velocity.x *= 25 * deltaTime
self._velocity.z *= 25 * deltaTime
}
}
}

View File

@ -350,10 +350,10 @@ public class Renderer {
matrix_float4x4(instance.rotation) *
.scale(instance.scale),
color: .init(
UInt8(instance.color.x * 0xFF),
UInt8(instance.color.y * 0xFF),
UInt8(instance.color.z * 0xFF),
UInt8(instance.color.w * 0xFF)))
UInt8(truncating: NSNumber(value: instance.color.x * 0xFF)),
UInt8(truncating: NSNumber(value: instance.color.y * 0xFF)),
UInt8(truncating: NSNumber(value: instance.color.z * 0xFF)),
UInt8(truncating: NSNumber(value: instance.color.w * 0xFF))))
}
// Ideal as long as our uniforms total 4 KB or less