Godot interaction system init
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
extends Interactable
|
||||
class_name Button3D
|
||||
|
||||
signal pressed(button: Button3D)
|
||||
signal held(button: Button3D)
|
||||
signal released(button: Button3D)
|
||||
|
||||
@export var pressDepth: float = 0.05
|
||||
@export var pressSpeed: float = 10.0
|
||||
@export var autoReset: bool = true
|
||||
@export var buttonMesh: MeshInstance3D
|
||||
@export var holdMode: bool = false
|
||||
@export var pressArea: Area3D
|
||||
|
||||
var isPressed: bool = false
|
||||
var offset: float = 0.0
|
||||
var pressing_body: Node3D = null
|
||||
|
||||
func _ready() -> void:
|
||||
if pressArea:
|
||||
pressArea.monitoring = true
|
||||
pressArea.monitorable = true
|
||||
|
||||
func beginInteract(player, hitPosition: Vector3):
|
||||
if holdMode:
|
||||
isPressed = true
|
||||
offset = pressDepth
|
||||
emit_signal("pressed", self)
|
||||
else:
|
||||
if isPressed:
|
||||
return
|
||||
isPressed = true
|
||||
offset = pressDepth
|
||||
emit_signal("pressed", self)
|
||||
|
||||
func updateInteract(player):
|
||||
if holdMode and isPressed:
|
||||
emit_signal("held", self)
|
||||
|
||||
func endInteract(player):
|
||||
if holdMode:
|
||||
isPressed = false
|
||||
offset = 0.0
|
||||
emit_signal("released", self)
|
||||
else:
|
||||
if autoReset:
|
||||
offset = 0.0
|
||||
|
||||
func _physics_process(delta):
|
||||
var target = offset
|
||||
var current = buttonMesh.position.z
|
||||
var newZ = lerp(current, -target, delta * pressSpeed)
|
||||
buttonMesh.position.z = newZ
|
||||
|
||||
if autoReset and isPressed and abs(newZ) < 0.001:
|
||||
isPressed = false
|
||||
|
||||
#func _physics_process(delta):
|
||||
## Detect rigidbody overlap
|
||||
#var bodies = pressArea.get_overlapping_bodies()
|
||||
#
|
||||
#if bodies.size() > 0:
|
||||
#var body = bodies[0]
|
||||
#
|
||||
#if pressing_body == null:
|
||||
#pressing_body = body
|
||||
#isPressed = true
|
||||
#offset = pressDepth
|
||||
#emit_signal("pressed", self)
|
||||
#else:
|
||||
#if holdMode:
|
||||
#emit_signal("held", self)
|
||||
#else:
|
||||
#if pressing_body != null:
|
||||
#pressing_body = null
|
||||
#if autoReset:
|
||||
#offset = 0.0
|
||||
#isPressed = false
|
||||
#emit_signal("released", self)
|
||||
#
|
||||
## Animate button
|
||||
#var target = offset
|
||||
#var current = buttonMesh.position.z
|
||||
#var newZ = lerp(current, -target, delta * pressSpeed)
|
||||
#buttonMesh.position.z = newZ
|
||||
#
|
||||
#if autoReset and isPressed and abs(newZ) < 0.001:
|
||||
#isPressed = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://diphc7baic3px
|
||||
@@ -0,0 +1,17 @@
|
||||
extends CanvasLayer
|
||||
class_name Crosshair
|
||||
|
||||
@export var normalTexture: Texture2D
|
||||
@export var hoverTexture: Texture2D
|
||||
|
||||
@onready var normal = $NormalCrosshair
|
||||
@onready var hover = $HoverCrosshair
|
||||
|
||||
func _ready():
|
||||
normal.texture = normalTexture
|
||||
hover.texture = hoverTexture
|
||||
hover.visible = false
|
||||
|
||||
func setHover(active: bool):
|
||||
normal.visible = !active
|
||||
hover.visible = active
|
||||
@@ -0,0 +1 @@
|
||||
uid://dd8ys8xg3q3sw
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
extends Interactable
|
||||
class_name Door
|
||||
|
||||
@export var body: RigidBody3D
|
||||
@export var springStrength: float = 200.0
|
||||
@export var springDamping: float= 50.0
|
||||
@export var maxGrabDistance: float = 6.0
|
||||
@export var mouseForce: float = 0.02
|
||||
|
||||
@export var hinge: HingeJoint3D
|
||||
@export var hingeAngularUpperLimit: float = 120
|
||||
@export var hingeAngularLowerLimit: float = -120
|
||||
|
||||
@export var snapAngleThreshold: float = 1
|
||||
@export var snapVelocityThreshold: float = 5.0
|
||||
|
||||
@export var mouseHorizontalFactor: float = 1.0
|
||||
@export var mouseVerticalFactor: float = 1.0
|
||||
|
||||
var dragging: bool = false
|
||||
var grabLocal: Vector3 = Vector3.ZERO
|
||||
var handOffset: Vector3 = Vector3.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
hinge.set_param(HingeJoint3D.PARAM_LIMIT_LOWER, deg_to_rad(hingeAngularLowerLimit))
|
||||
hinge.set_param(HingeJoint3D.PARAM_LIMIT_UPPER, deg_to_rad(hingeAngularUpperLimit))
|
||||
|
||||
func _physics_process(_delta):
|
||||
if dragging:
|
||||
return
|
||||
|
||||
var angle = get_hinge_angle()
|
||||
var ang_vel = get_hinge_ang_vel()
|
||||
|
||||
if abs(rad_to_deg(angle)) > snapAngleThreshold:
|
||||
return
|
||||
|
||||
if abs(ang_vel) > snapVelocityThreshold:
|
||||
return
|
||||
|
||||
# Strong damping near closed position
|
||||
var damping = springDamping * 4.0
|
||||
|
||||
# PD controller torque toward angle 0
|
||||
var torque = -angle * springStrength - ang_vel * damping
|
||||
|
||||
var axis = hinge.global_transform.basis.y
|
||||
body.apply_torque(axis * torque)
|
||||
|
||||
func beginInteract(player, hitPosition: Vector3):
|
||||
dragging = true
|
||||
handOffset = Vector3.ZERO
|
||||
grabLocal = body.to_local(hitPosition)
|
||||
player.setMouseFactors(mouseHorizontalFactor, mouseVerticalFactor)
|
||||
|
||||
func updateInteract(player):
|
||||
if !dragging:
|
||||
return
|
||||
|
||||
var worldGrab = body.to_global(grabLocal)
|
||||
|
||||
handOffset += player.camera.global_basis.x * player.interactionMouseDelta.x * mouseForce
|
||||
handOffset += -player.camera.global_basis.y * player.interactionMouseDelta.y * mouseForce
|
||||
handOffset = handOffset.limit_length(0.3)
|
||||
|
||||
var targetHand = player.hand.global_position + handOffset
|
||||
|
||||
var error = targetHand - worldGrab
|
||||
|
||||
var handDistance = player.hand.global_position.distance_to(worldGrab)
|
||||
if handDistance > maxGrabDistance:
|
||||
player.stopInteraction()
|
||||
return
|
||||
|
||||
var offset = worldGrab - body.global_position
|
||||
|
||||
var pointVelocity = body.linear_velocity + body.angular_velocity.cross(offset)
|
||||
|
||||
var hingeAxis = body.global_basis.y
|
||||
|
||||
var tangent = hingeAxis.cross(offset)
|
||||
if tangent.length_squared() < 0.0001:
|
||||
return
|
||||
tangent = tangent.normalized()
|
||||
|
||||
var tangentError = tangent * error.dot(tangent)
|
||||
var tangentVelocity = tangent * pointVelocity.dot(tangent)
|
||||
|
||||
var force = tangentError * springStrength - tangentVelocity * springDamping
|
||||
|
||||
body.apply_force(force, offset)
|
||||
|
||||
func endInteract(player):
|
||||
dragging = false
|
||||
handOffset = Vector3.ZERO
|
||||
player.setMouseFactors(1.0, 1.0)
|
||||
|
||||
#func get_hinge_angle() -> float:
|
||||
#var body_basis = body.global_transform.basis
|
||||
#var hinge_basis = hinge.global_transform.basis
|
||||
#
|
||||
## Body forward vector in world space
|
||||
#var forward = body_basis.z
|
||||
#
|
||||
## Convert forward vector into hinge local space
|
||||
#var local_forward = hinge_basis.inverse() * forward
|
||||
#
|
||||
## Angle around hinge axis (Y)
|
||||
#return atan2(local_forward.x, local_forward.z)
|
||||
|
||||
func get_hinge_angle() -> float:
|
||||
var hinge_axis = hinge.global_transform.basis.y.normalized()
|
||||
|
||||
# Door forward direction
|
||||
var fwd = body.global_transform.basis.z.normalized()
|
||||
|
||||
# Reference forward direction (door closed direction)
|
||||
var ref = hinge.global_transform.basis.z.normalized()
|
||||
|
||||
# Remove hinge axis component (project onto hinge plane)
|
||||
var fwd_proj = (fwd - hinge_axis * fwd.dot(hinge_axis)).normalized()
|
||||
var ref_proj = (ref - hinge_axis * ref.dot(hinge_axis)).normalized()
|
||||
|
||||
# Signed angle
|
||||
var angle = atan2(
|
||||
hinge_axis.dot(ref_proj.cross(fwd_proj)),
|
||||
ref_proj.dot(fwd_proj)
|
||||
)
|
||||
|
||||
return angle
|
||||
|
||||
|
||||
func get_hinge_ang_vel() -> float:
|
||||
var axis = hinge.global_transform.basis.y.normalized()
|
||||
return body.angular_velocity.dot(axis)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cyw8ng7alo12e
|
||||
@@ -0,0 +1,19 @@
|
||||
extends Node3D
|
||||
|
||||
class_name Interactable
|
||||
|
||||
static func find(node: Node) -> Interactable:
|
||||
while node:
|
||||
if node is Interactable:
|
||||
return node
|
||||
node = node.get_parent()
|
||||
return null
|
||||
|
||||
func beginInteract(player, hitPosition: Vector3):
|
||||
pass
|
||||
|
||||
func updateInteract(player):
|
||||
pass
|
||||
|
||||
func endInteract(player):
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://boc1b3v02ssn5
|
||||
@@ -0,0 +1,107 @@
|
||||
extends Interactable
|
||||
class_name Lever
|
||||
|
||||
@export var body: RigidBody3D
|
||||
@export var hinge: HingeJoint3D
|
||||
|
||||
@export var angleOn: float = -45.0
|
||||
@export var angleOff: float = 0.0
|
||||
|
||||
@export var snapStrength: float = 200.0
|
||||
@export var maxGrabDistance: float = 4.0
|
||||
@export var mouseForce: float = 0.015
|
||||
|
||||
var dragging := false
|
||||
var grabLocal := Vector3.ZERO
|
||||
var handOffset := Vector3.ZERO
|
||||
var targetAngle := 0.0
|
||||
|
||||
func _ready():
|
||||
hinge.set_param(HingeJoint3D.PARAM_LIMIT_LOWER, deg_to_rad(angleOn))
|
||||
hinge.set_param(HingeJoint3D.PARAM_LIMIT_UPPER, deg_to_rad(angleOff))
|
||||
targetAngle = angleOff
|
||||
|
||||
|
||||
func _physics_process(delta):
|
||||
if dragging:
|
||||
return
|
||||
|
||||
var angle = rad_to_deg(get_hinge_angle())
|
||||
|
||||
# Snap to nearest end
|
||||
var mid = (angleOn + angleOff) * 0.5
|
||||
if angle < mid:
|
||||
targetAngle = angleOn
|
||||
else:
|
||||
targetAngle = angleOff
|
||||
|
||||
var angleError = deg_to_rad(targetAngle) - get_hinge_angle()
|
||||
|
||||
# HARD SNAP — no spring
|
||||
var axis = hinge.global_transform.basis.x
|
||||
var torque = snapStrength * sign(angleError)
|
||||
|
||||
body.apply_torque(axis * torque)
|
||||
|
||||
|
||||
func beginInteract(player, hitPosition: Vector3):
|
||||
dragging = true
|
||||
handOffset = Vector3.ZERO
|
||||
grabLocal = body.to_local(hitPosition)
|
||||
|
||||
|
||||
func updateInteract(player):
|
||||
if !dragging:
|
||||
return
|
||||
|
||||
var worldGrab = body.to_global(grabLocal)
|
||||
|
||||
# Only vertical mouse movement (Y)
|
||||
handOffset += -player.camera.global_basis.y * player.interactionMouseDelta.y * mouseForce
|
||||
handOffset = handOffset.limit_length(0.25)
|
||||
|
||||
var targetHand = player.hand.global_position + handOffset
|
||||
var error = targetHand - worldGrab
|
||||
|
||||
var handDistance = player.hand.global_position.distance_to(worldGrab)
|
||||
if handDistance > maxGrabDistance:
|
||||
endInteract(player)
|
||||
player.dragging = false
|
||||
return
|
||||
|
||||
var offset = worldGrab - body.global_position
|
||||
var pointVelocity = body.linear_velocity + body.angular_velocity.cross(offset)
|
||||
|
||||
# Correct hinge axis (X)
|
||||
var hingeAxis = body.global_basis.x
|
||||
var tangent = hingeAxis.cross(offset)
|
||||
if tangent.length_squared() < 0.0001:
|
||||
return
|
||||
tangent = tangent.normalized()
|
||||
|
||||
var tangentError = tangent * error.dot(tangent)
|
||||
var tangentVelocity = tangent * pointVelocity.dot(tangent)
|
||||
|
||||
var force = tangentError * snapStrength
|
||||
body.apply_force(force, offset)
|
||||
|
||||
|
||||
func endInteract(player):
|
||||
dragging = false
|
||||
handOffset = Vector3.ZERO
|
||||
|
||||
|
||||
func get_hinge_angle() -> float:
|
||||
var body_basis = body.global_transform.basis
|
||||
var hinge_basis = hinge.global_transform.basis
|
||||
|
||||
# X-axis rotation → use UP vector
|
||||
var up = body_basis.y
|
||||
var local_up = hinge_basis.inverse() * up
|
||||
|
||||
return atan2(local_up.z, local_up.y)
|
||||
|
||||
|
||||
func get_hinge_ang_vel() -> float:
|
||||
var axis = hinge.global_transform.basis.x
|
||||
return body.angular_velocity.dot(axis)
|
||||
@@ -0,0 +1 @@
|
||||
uid://2vyt44um2tty
|
||||
@@ -0,0 +1,97 @@
|
||||
extends Interactable
|
||||
class_name Pickupable
|
||||
|
||||
@export var springStrength: float = 150.0
|
||||
@export var springDamping: float = 50.0
|
||||
|
||||
@export var rotateStrength: float = 5.0
|
||||
@export var rotateDamping: float = 5.0
|
||||
|
||||
@export var throwForce: float = 10.0
|
||||
@export var maxDistance: float = 4.0
|
||||
@export var body: RigidBody3D
|
||||
@export var snapAnchor: Marker3D
|
||||
|
||||
@export_range(0.01, 1000.0, 0.01) var bodyMass: float = 1.0
|
||||
|
||||
var dragging: bool = false
|
||||
var rotating: bool = false
|
||||
var playerRef
|
||||
|
||||
var grabLocal: Vector3
|
||||
var holdDistance: float
|
||||
var rotationOffset: Basis
|
||||
|
||||
func _ready():
|
||||
if body:
|
||||
body.mass = bodyMass
|
||||
|
||||
func beginInteract(player, hitPosition: Vector3):
|
||||
playerRef = player
|
||||
dragging = true
|
||||
|
||||
grabLocal = body.to_local(hitPosition)
|
||||
var holdPoint = player.hand
|
||||
holdDistance = holdPoint.global_position.distance_to(hitPosition)
|
||||
rotationOffset = holdPoint.global_basis.inverse() * body.global_basis
|
||||
|
||||
func updateInteract(player):
|
||||
if !dragging:
|
||||
return
|
||||
|
||||
# Toggle rotation mode
|
||||
if Input.is_action_just_pressed("rotate"):
|
||||
rotating = true
|
||||
playerRef.setMouseFactors(0.0, 0.0)
|
||||
|
||||
if Input.is_action_just_released("rotate"):
|
||||
rotating = false
|
||||
playerRef.setMouseFactors(1.0, 1.0)
|
||||
|
||||
var holdPoint = player.hand
|
||||
|
||||
# Release if too far away
|
||||
if body.global_position.distance_to(player.global_position) > maxDistance:
|
||||
endInteract(player)
|
||||
return
|
||||
|
||||
# Target position
|
||||
var targetPosition = holdPoint.global_position + (-holdPoint.global_basis.z * holdDistance)
|
||||
var offset = targetPosition - body.global_position
|
||||
|
||||
# Linear spring
|
||||
body.apply_force(offset * springStrength - body.linear_velocity * springDamping, body.to_global(grabLocal) - body.global_position)
|
||||
|
||||
if rotating:
|
||||
var delta = player.interactionMouseDelta
|
||||
|
||||
rotationOffset = Basis(Vector3.UP, deg_to_rad(-delta.x * 0.35)) * rotationOffset
|
||||
rotationOffset = Basis(Vector3.RIGHT, deg_to_rad(delta.y * 0.35)) * rotationOffset
|
||||
|
||||
# Angular spring
|
||||
var targetBasis = holdPoint.global_basis * rotationOffset
|
||||
|
||||
var currentQuat = body.global_basis.get_rotation_quaternion()
|
||||
var targetQuat = targetBasis.get_rotation_quaternion()
|
||||
var diff = targetQuat * currentQuat.inverse()
|
||||
|
||||
if diff.dot(Quaternion.IDENTITY) < 0:
|
||||
diff = -diff
|
||||
|
||||
var angle = diff.get_angle()
|
||||
|
||||
if angle > 0.001:
|
||||
body.apply_torque(diff.get_axis() * angle * rotateStrength - body.angular_velocity * rotateDamping)
|
||||
|
||||
func endInteract(player):
|
||||
dragging = false
|
||||
rotating = false
|
||||
player.setMouseFactors(1.0, 1.0)
|
||||
playerRef = null
|
||||
|
||||
func throw():
|
||||
if !dragging:
|
||||
return
|
||||
|
||||
var direction = -playerRef.camera.global_basis.z
|
||||
body.apply_impulse(direction * throwForce)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b143brnl7gf4f
|
||||
@@ -0,0 +1,143 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
@export var speed: float = 6.0
|
||||
@export var jumpVelocity: float = 5.0
|
||||
@export var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") as float
|
||||
|
||||
@export var acceleration: float = 20.0
|
||||
@export var airAcceleration: float = 4.0
|
||||
@export var friction: float = 10.0
|
||||
|
||||
var mouseBaseSensitivity: float = 0.004
|
||||
var mouseSensitivity: float = 0.0
|
||||
@export var mouseSensitivityMultiplier: float = 0.2
|
||||
|
||||
@export var head: Node3D
|
||||
@export var camera: Camera3D
|
||||
@export var raycast: RayCast3D
|
||||
@export var hand: Marker3D
|
||||
@export var crosshair: CanvasLayer
|
||||
|
||||
var lateJumpTime: float = 0.15
|
||||
var lateJumpTimer: float = 0.0
|
||||
|
||||
var currentInteractable: Interactable
|
||||
var dragging: bool = false
|
||||
|
||||
var pitch: float = 0.0
|
||||
|
||||
var playerId: String
|
||||
var displayName: String
|
||||
|
||||
var mouseDelta: Vector2 = Vector2.ZERO
|
||||
var interactionMouseDelta: Vector2 = Vector2.ZERO
|
||||
|
||||
var mouseHorizontalFactor: float = 1.0
|
||||
var mouseVerticalFactor: float = 1.0
|
||||
|
||||
var targetMouseHorizontalFactor: float = 1.0
|
||||
var targetMouseVerticalFactor: float = 1.0
|
||||
@export var mouseFactorLerpSpeed: float = 2.0
|
||||
|
||||
func _ready():
|
||||
mouseSensitivity = mouseBaseSensitivity * mouseSensitivityMultiplier
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
else:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
if dragging and currentInteractable:
|
||||
interactionMouseDelta += event.relative
|
||||
mouseDelta += event.relative
|
||||
|
||||
func _physics_process(delta):
|
||||
mouseHorizontalFactor = lerp(mouseHorizontalFactor, targetMouseHorizontalFactor, delta * mouseFactorLerpSpeed)
|
||||
mouseVerticalFactor = lerp(mouseVerticalFactor, targetMouseVerticalFactor, delta * mouseFactorLerpSpeed)
|
||||
|
||||
if mouseDelta != Vector2.ZERO:
|
||||
rotate_y(-mouseDelta.x * mouseSensitivity * mouseHorizontalFactor)
|
||||
|
||||
pitch -= mouseDelta.y * mouseSensitivity * mouseVerticalFactor
|
||||
pitch = clamp(pitch, deg_to_rad(-90), deg_to_rad(90))
|
||||
head.rotation.x = pitch
|
||||
|
||||
mouseDelta = Vector2.ZERO
|
||||
|
||||
if !is_on_floor():
|
||||
velocity.y -= gravity * delta
|
||||
lateJumpTimer -= delta
|
||||
else:
|
||||
lateJumpTimer = lateJumpTime
|
||||
|
||||
# Interaction search
|
||||
if !dragging:
|
||||
if raycast.is_colliding():
|
||||
currentInteractable = Interactable.find(raycast.get_collider())
|
||||
crosshair.setHover(true)
|
||||
else:
|
||||
currentInteractable = null
|
||||
crosshair.setHover(false)
|
||||
|
||||
# Start interaction
|
||||
if !dragging and currentInteractable:
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
dragging = true
|
||||
currentInteractable.beginInteract(self, raycast.get_collision_point())
|
||||
|
||||
# Update interaction
|
||||
if dragging and currentInteractable:
|
||||
if Input.is_action_just_pressed("throw"):
|
||||
if currentInteractable is Pickupable:
|
||||
currentInteractable.throw()
|
||||
stopInteraction()
|
||||
return
|
||||
|
||||
currentInteractable.updateInteract(self)
|
||||
interactionMouseDelta = Vector2.ZERO
|
||||
|
||||
# End interaction
|
||||
if dragging and currentInteractable:
|
||||
if Input.is_action_just_released("interact"):
|
||||
stopInteraction()
|
||||
|
||||
# Late jump buffering
|
||||
if Input.is_action_just_pressed("jump") and lateJumpTimer > 0.0:
|
||||
velocity.y = jumpVelocity
|
||||
|
||||
# Movement
|
||||
var inputDir := Input.get_vector("left", "right", "forward", "backward")
|
||||
var direction := (transform.basis * Vector3(inputDir.x, 0, inputDir.y)).normalized()
|
||||
|
||||
if is_on_floor():
|
||||
if direction != Vector3.ZERO:
|
||||
velocity.x = direction.x * speed
|
||||
velocity.z = direction.z * speed
|
||||
else:
|
||||
velocity.x = lerp(velocity.x, 0.0, delta * friction)
|
||||
velocity.z = lerp(velocity.z, 0.0, delta * friction)
|
||||
else:
|
||||
velocity.x = lerp(velocity.x, direction.x * speed, delta * airAcceleration)
|
||||
velocity.z = lerp(velocity.z, direction.z * speed, delta * airAcceleration)
|
||||
|
||||
move_and_slide()
|
||||
|
||||
func setMouseFactors(horizontal: float, vertical: float):
|
||||
targetMouseHorizontalFactor = clamp(horizontal, 0.0, 1.0)
|
||||
targetMouseVerticalFactor = clamp(vertical, 0.0, 1.0)
|
||||
|
||||
func stopInteraction():
|
||||
if currentInteractable:
|
||||
currentInteractable.endInteract(self)
|
||||
|
||||
currentInteractable = null
|
||||
dragging = false
|
||||
interactionMouseDelta = Vector2.ZERO
|
||||
@@ -0,0 +1 @@
|
||||
uid://c1r1jpfh3vsn2
|
||||
@@ -0,0 +1,52 @@
|
||||
class_name Socket
|
||||
extends Node3D
|
||||
|
||||
@export var snapPoint: Marker3D
|
||||
@export var snapDistance: float = 0.5
|
||||
@export var lockOnSnap: bool = true
|
||||
|
||||
var candidate: Pickupable = null
|
||||
var snappedObject: Pickupable = null
|
||||
|
||||
func is_in_range(object: Pickupable) -> bool:
|
||||
if object == null:
|
||||
return false
|
||||
|
||||
if object.snapAnchor == null:
|
||||
return false
|
||||
|
||||
var distance := object.snapAnchor.global_position.distance_to(
|
||||
snapPoint.global_position
|
||||
)
|
||||
|
||||
return distance <= snapDistance
|
||||
|
||||
func getSnapTransform(object: Pickupable) -> Transform3D:
|
||||
var objectTransform := object.body.global_transform
|
||||
var anchorTransform := object.snapAnchor.global_transform
|
||||
|
||||
# Where is the SnapAnchor relative to the Rigidbody?
|
||||
var anchorOffset := objectTransform.affine_inverse() * anchorTransform
|
||||
|
||||
# We want the SnapAnchor to have exactly the same
|
||||
# transform as the Socket's SnapPoint.
|
||||
return snapPoint.global_transform * anchorOffset.affine_inverse()
|
||||
|
||||
func snapObject(object: Pickupable):
|
||||
if not is_in_range(object):
|
||||
return
|
||||
|
||||
var targetTransform := getSnapTransform(object)
|
||||
|
||||
var body := object.body
|
||||
|
||||
body.linear_velocity = Vector3.ZERO
|
||||
body.angular_velocity = Vector3.ZERO
|
||||
|
||||
body.global_transform = targetTransform
|
||||
|
||||
if lockOnSnap:
|
||||
body.freeze = true
|
||||
|
||||
snappedObject = object
|
||||
candidate = null
|
||||
@@ -0,0 +1 @@
|
||||
uid://xd03vepkva1l
|
||||
@@ -0,0 +1,132 @@
|
||||
extends AudioStreamPlayer3D
|
||||
class_name SpatialAudioPlayer
|
||||
|
||||
@export var maxRaycastDistance: float = 30.0
|
||||
@export var updateFrequencySeconds: float = 0.5
|
||||
@export var maxReverbWetness: float = 0.5
|
||||
@export var wallLowpassCutoffAmount: int = 600
|
||||
|
||||
var raycastArray: Array = []
|
||||
var distanceArray: Array = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
var lastUpdateTime: float = 0.0
|
||||
var updateDistances: bool = true
|
||||
var currentRaycastIndex: int = 0
|
||||
|
||||
var audioBusIndex = null
|
||||
var audioBusName: String = ""
|
||||
|
||||
var reverbEffect: AudioEffectReverb
|
||||
var lowpassFilterEffect: AudioEffectLowPassFilter
|
||||
|
||||
var targetLowpassCutoff: float = 20000
|
||||
var targetReverbRoomSize: float = 0.0
|
||||
var targetReverbWetness: float = 0.0
|
||||
var targetVolumeDb: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
audioBusIndex = AudioServer.bus_count
|
||||
audioBusName = "SpatialBus#"+str(audioBusIndex)
|
||||
AudioServer.add_bus(audioBusIndex)
|
||||
AudioServer.set_bus_name(audioBusIndex, audioBusName)
|
||||
AudioServer.set_bus_send(audioBusIndex, bus)
|
||||
self.bus = audioBusName
|
||||
|
||||
AudioServer.add_bus_effect(audioBusIndex, AudioEffectReverb.new(), 0)
|
||||
reverbEffect = AudioServer.get_bus_effect(audioBusIndex, 0)
|
||||
AudioServer.add_bus_effect(audioBusIndex, AudioEffectLowPassFilter.new(), 1)
|
||||
lowpassFilterEffect = AudioServer.get_bus_effect(audioBusIndex, 1)
|
||||
|
||||
targetVolumeDb = volume_db
|
||||
volume_db = -60
|
||||
|
||||
$RayCastDown.target_position = Vector3(0, -maxRaycastDistance, 0)
|
||||
$RayCastLeft.target_position = Vector3(maxRaycastDistance, 0, 0)
|
||||
$RayCastRight.target_position = Vector3(-maxRaycastDistance, 0, 0)
|
||||
$RayCastForward.target_position = Vector3(0, 0, maxRaycastDistance)
|
||||
$RayCastForwardLeft.target_position = Vector3(0, 0, maxRaycastDistance)
|
||||
$RayCastForwardRight.target_position = Vector3(0, 0, maxRaycastDistance)
|
||||
$RayCastBackwardRight.target_position = Vector3(0, 0, -maxRaycastDistance)
|
||||
$RayCastBackwardLeft.target_position = Vector3(0, 0, -maxRaycastDistance)
|
||||
$RayCastBackward.target_position = Vector3(0, 0, -maxRaycastDistance)
|
||||
$RayCastUp.target_position = Vector3(0, maxRaycastDistance, 0)
|
||||
|
||||
raycastArray.append($RayCastDown)
|
||||
raycastArray.append($RayCastLeft)
|
||||
raycastArray.append($RayCastRight)
|
||||
raycastArray.append($RayCastForward)
|
||||
raycastArray.append($RayCastForwardLeft)
|
||||
raycastArray.append($RayCastForwardRight)
|
||||
raycastArray.append($RayCastBackwardRight)
|
||||
raycastArray.append($RayCastBackwardLeft)
|
||||
raycastArray.append($RayCastBackward)
|
||||
raycastArray.append($RayCastUp)
|
||||
|
||||
play()
|
||||
|
||||
func onUpdateRaycastDistance(raycast: RayCast3D, raycastIndex: int) -> void:
|
||||
raycast.force_raycast_update()
|
||||
var collider = raycast.get_collider()
|
||||
if collider != null:
|
||||
distanceArray[raycastIndex] = self.global_position.distance_to(raycast.get_collision_point())
|
||||
else:
|
||||
distanceArray[raycastIndex] = -1
|
||||
raycast.enabled = false
|
||||
|
||||
func onUpdateSpatialAudio(player: Node3D) -> void:
|
||||
onUpdateReverb(player)
|
||||
onUpdateLowpassFilter(player)
|
||||
|
||||
func onUpdateReverb(player: Node3D) -> void:
|
||||
if reverbEffect != null:
|
||||
var roomSize: float = 0.0
|
||||
var wetness: float = 0.0
|
||||
for distance in distanceArray:
|
||||
if distance >= 0:
|
||||
roomSize += (distance / maxRaycastDistance) / (float(distanceArray.size()))
|
||||
roomSize = min(roomSize, 1.0)
|
||||
else:
|
||||
wetness -= 1.0 / float(distanceArray.size())
|
||||
wetness = max(wetness, 0.0)
|
||||
targetReverbWetness = wetness
|
||||
targetReverbRoomSize = roomSize
|
||||
|
||||
func onUpdateLowpassFilter(player : Node3D):
|
||||
if lowpassFilterEffect != null:
|
||||
var query = PhysicsRayQueryParameters3D.create(self.global_position,self.global_position + (player.global_position - self.global_position).normalized() * maxRaycastDistance, $RayCastForward.get_collision_mask())
|
||||
var spaceState = get_world_3d().direct_space_state
|
||||
var result = spaceState.intersect_ray(query)
|
||||
var lowpassCutoff = 20000
|
||||
if !result.is_empty():
|
||||
var rayDistance = self.global_position.distance_to(result["position"])
|
||||
var distanceToPlayer = self.global_position.distance_to(player.global_position)
|
||||
var wallToPlayerRatio = rayDistance / max(distanceToPlayer, 0.001)
|
||||
if rayDistance < distanceToPlayer:
|
||||
lowpassCutoff = wallLowpassCutoffAmount * wallToPlayerRatio
|
||||
targetLowpassCutoff = lowpassCutoff
|
||||
|
||||
func lerpParameters(delta: float) -> void:
|
||||
volume_db = lerp(volume_db, targetVolumeDb, delta)
|
||||
lowpassFilterEffect.cutoff_hz = lerp(lowpassFilterEffect.cutoff_hz, targetLowpassCutoff, delta * 5.0)
|
||||
reverbEffect.wet = lerp(reverbEffect.wet, targetReverbRoomSize * maxReverbWetness, delta * 5.0)
|
||||
reverbEffect.room_size = lerp(reverbEffect.room_size, targetReverbRoomSize, delta * 5.0)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
lastUpdateTime += delta
|
||||
|
||||
if updateDistances:
|
||||
onUpdateRaycastDistance(raycastArray[currentRaycastIndex], currentRaycastIndex)
|
||||
currentRaycastIndex += 1
|
||||
if currentRaycastIndex >= distanceArray.size():
|
||||
currentRaycastIndex = 0
|
||||
updateDistances = false
|
||||
|
||||
if lastUpdateTime > updateFrequencySeconds:
|
||||
var playerCamera = get_viewport().get_camera_3d()
|
||||
if playerCamera != null:
|
||||
onUpdateSpatialAudio(playerCamera)
|
||||
updateDistances = true
|
||||
lastUpdateTime = 0.0
|
||||
|
||||
lerpParameters(delta)
|
||||
print("Audio:", global_position)
|
||||
print("Camera:", get_viewport().get_camera_3d().global_position)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d07hgc6eewoup
|
||||
Reference in New Issue
Block a user