From 6b92b538a54988966538ee4d52366c41bf4563be Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Fri, 16 Aug 2024 00:27:35 +1000 Subject: [PATCH] crude player physics & collision response --- Sources/Voxelotl/AABB.swift | 58 +++++++++++++ Sources/Voxelotl/CMakeLists.txt | 1 + Sources/Voxelotl/Game.swift | 55 ++++++++---- Sources/Voxelotl/GameController.swift | 2 + Sources/Voxelotl/Player.swift | 116 ++++++++++++++++++++++---- Sources/Voxelotl/Renderer.swift | 8 +- 6 files changed, 206 insertions(+), 34 deletions(-) create mode 100644 Sources/Voxelotl/AABB.swift diff --git a/Sources/Voxelotl/AABB.swift b/Sources/Voxelotl/AABB.swift new file mode 100644 index 0000000..1bdb0bc --- /dev/null +++ b/Sources/Voxelotl/AABB.swift @@ -0,0 +1,58 @@ +import simd + +struct AABB { + private var _bounds: simd_float2x3 + + var lower: SIMD3 { + get { _bounds[0] } + set(row) { self._bounds[0] = row } + } + var upper: SIMD3 { + get { _bounds[1] } + set(row) { self._bounds[1] = row } + } + var center: SIMD3 { + get { (self._bounds[0] + self._bounds[1]) / 2 } + } + var size: SIMD3 { + 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, to: SIMD3) { + self.init(bounds: .init(from, to)) + } + + static func fromUnitCube(position: SIMD3 = .zero, scale: SIMD3 = .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) -> Self { + .init(bounds: lhs._bounds + .init(rhs, rhs)) + } +} diff --git a/Sources/Voxelotl/CMakeLists.txt b/Sources/Voxelotl/CMakeLists.txt index a412f1a..b963708 100644 --- a/Sources/Voxelotl/CMakeLists.txt +++ b/Sources/Voxelotl/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(Voxelotl MACOSX_BUNDLE FloatExtensions.swift Matrix4x4.swift Rectangle.swift + AABB.swift NSImageLoader.swift Renderer.swift diff --git a/Sources/Voxelotl/Game.swift b/Sources/Voxelotl/Game.swift index b144983..2abbed8 100644 --- a/Sources/Voxelotl/Game.swift +++ b/Sources/Voxelotl/Game.swift @@ -1,16 +1,39 @@ import simd -struct Instance { - var position: SIMD3 = .zero - var scale: SIMD3 = .one - var rotation: simd_quatf = .identity - var color: SIMD4 = .one +struct Box { + var geometry: AABB + var color: SIMD4 = .one } +struct Instance { + let position: SIMD3 + let scale: SIMD3 + let rotation: simd_quatf + let color: SIMD4 + + init( + position: SIMD3 = .zero, + scale: SIMD3 = .one, + rotation: simd_quatf = .identity, + color: SIMD4 = .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) } diff --git a/Sources/Voxelotl/GameController.swift b/Sources/Voxelotl/GameController.swift index f8bcec5..65fddfd 100644 --- a/Sources/Voxelotl/GameController.swift +++ b/Sources/Voxelotl/GameController.swift @@ -192,6 +192,8 @@ public extension GameController.Pad.State { var rightStick: SIMD2 { .init(axis(.rightStickX), axis(.rightStickY)) } + var leftTrigger: Float { axis(.leftTrigger) } + var rightTrigger: Float { axis(.rightTrigger) } } public extension FloatingPoint { diff --git a/Sources/Voxelotl/Player.swift b/Sources/Voxelotl/Player.swift index 4eee088..86710da 100644 --- a/Sources/Voxelotl/Player.swift +++ b/Sources/Voxelotl/Player.swift @@ -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.zero + private var _velocity = SIMD3.zero private var _rotation = SIMD2.zero - public var position: SIMD3 { self._position } + private var _onGround: Bool = false + + public var position: SIMD3 { self._position + .init(0, Self.eyeLevel, 0) } public var rotation: SIMD2 { 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) -> Optional 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 + } } } diff --git a/Sources/Voxelotl/Renderer.swift b/Sources/Voxelotl/Renderer.swift index 60db3a2..feed28c 100644 --- a/Sources/Voxelotl/Renderer.swift +++ b/Sources/Voxelotl/Renderer.swift @@ -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