270 lines
7.8 KiB
GDScript
270 lines
7.8 KiB
GDScript
@tool
|
|
class_name XRToolsMovementClimb
|
|
extends XRToolsMovementProvider
|
|
|
|
|
|
## XR Tools Movement Provider for Climbing
|
|
##
|
|
## This script provides climbing movement for the player. To add climbing
|
|
## support, the player must also have [XRToolsFunctionPickup] nodes attached
|
|
## to the left and right controllers, and an [XRToolsPlayerBody] under the
|
|
## [XROrigin3D].
|
|
##
|
|
## Climbable objects can inherit from the climbable scene, or be [StaticBody]
|
|
## objects with the [XRToolsClimbable] script attached to them.
|
|
##
|
|
## When climbing, the global velocity of the [XRToolsPlayerBody] is averaged,
|
|
## and upon release the velocity is applied to the [XRToolsPlayerBody] with an
|
|
## optional fling multiplier, so the player can fling themselves up walls if
|
|
## desired.
|
|
|
|
|
|
## Signal invoked when the player starts climing
|
|
signal player_climb_start
|
|
|
|
## Signal invoked when the player ends climbing
|
|
signal player_climb_end
|
|
|
|
|
|
## Distance at which grabs snap
|
|
const SNAP_DISTANCE : float = 1.0
|
|
|
|
|
|
## Movement provider order
|
|
@export var order : int = 15
|
|
|
|
## Push forward when flinging
|
|
@export var forward_push : float = 1.0
|
|
|
|
## Velocity multiplier when flinging up walls
|
|
@export var fling_multiplier : float = 1.0
|
|
|
|
## Averages for velocity measurement
|
|
@export var velocity_averages : int = 5
|
|
|
|
|
|
# Left climbing handle
|
|
var _left_handle : Node3D
|
|
|
|
# Right climbing handle
|
|
var _right_handle : Node3D
|
|
|
|
# Dominant handle (moving the player)
|
|
var _dominant : Node3D
|
|
|
|
|
|
# Velocity averager
|
|
@onready var _averager := XRToolsVelocityAveragerLinear.new(velocity_averages)
|
|
|
|
# Left pickup node
|
|
@onready var _left_pickup_node := XRToolsFunctionPickup.find_left(self)
|
|
|
|
# Right pickup node
|
|
@onready var _right_pickup_node := XRToolsFunctionPickup.find_right(self)
|
|
|
|
# Left controller
|
|
@onready var _left_controller := XRHelpers.get_left_controller(self)
|
|
|
|
# Right controller
|
|
@onready var _right_controller := XRHelpers.get_right_controller(self)
|
|
|
|
# Left collision hand
|
|
@onready var _left_collision_hand := XRToolsCollisionHand.find_left(self)
|
|
|
|
# Right collision hand
|
|
@onready var _right_collision_hand := XRToolsCollisionHand.find_right(self)
|
|
|
|
|
|
# Add support for is_xr_class on XRTools classes
|
|
func is_xr_class(name : String) -> bool:
|
|
return name == "XRToolsMovementClimb" or super(name)
|
|
|
|
|
|
## Called when the node enters the scene tree for the first time.
|
|
func _ready():
|
|
# In Godot 4 we must now manually call our super class ready function
|
|
super()
|
|
|
|
# Do not initialise if in the editor
|
|
if Engine.is_editor_hint():
|
|
return
|
|
|
|
# Connect pickup funcitons
|
|
if _left_pickup_node.connect("has_picked_up", _on_left_picked_up):
|
|
push_error("Unable to connect left picked up signal")
|
|
if _right_pickup_node.connect("has_picked_up", _on_right_picked_up):
|
|
push_error("Unable to connect right picked up signal")
|
|
if _left_pickup_node.connect("has_dropped", _on_left_dropped):
|
|
push_error("Unable to connect left dropped signal")
|
|
if _right_pickup_node.connect("has_dropped", _on_right_dropped):
|
|
push_error("Unable to connect right dropped signal")
|
|
|
|
|
|
## Perform player physics movement
|
|
func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
|
|
# Disable climbing if requested
|
|
if disabled or !enabled:
|
|
_set_climbing(false, player_body)
|
|
return
|
|
|
|
# Check for climbing handles being deleted while held
|
|
if not is_instance_valid(_left_handle):
|
|
_left_handle = null
|
|
if not is_instance_valid(_right_handle):
|
|
_right_handle = null
|
|
if not is_instance_valid(_dominant):
|
|
_dominant = null
|
|
|
|
# Snap grabs if too far
|
|
if _left_handle:
|
|
var left_pickup_pos := _left_controller.global_position
|
|
var left_grab_pos = _left_handle.global_position
|
|
if left_pickup_pos.distance_to(left_grab_pos) > SNAP_DISTANCE:
|
|
_left_pickup_node.drop_object()
|
|
if _right_handle:
|
|
var right_pickup_pos := _right_controller.global_position
|
|
var right_grab_pos := _right_handle.global_position
|
|
if right_pickup_pos.distance_to(right_grab_pos) > SNAP_DISTANCE:
|
|
_right_pickup_node.drop_object()
|
|
|
|
# Update climbing
|
|
_set_climbing(_dominant != null, player_body)
|
|
|
|
# Skip if not actively climbing
|
|
if !is_active:
|
|
return
|
|
|
|
# Calculate how much the player has moved
|
|
var offset := Vector3.ZERO
|
|
if _dominant == _left_handle:
|
|
var left_pickup_pos := _left_controller.global_position
|
|
var left_grab_pos := _left_handle.global_position
|
|
offset = left_pickup_pos - left_grab_pos
|
|
elif _dominant == _right_handle:
|
|
var right_pickup_pos := _right_controller.global_position
|
|
var right_grab_pos := _right_handle.global_position
|
|
offset = right_pickup_pos - right_grab_pos
|
|
|
|
# Move the player by the offset
|
|
var old_position := player_body.global_position
|
|
player_body.move_and_collide(-offset)
|
|
player_body.velocity = Vector3.ZERO
|
|
|
|
# Update the players average-velocity data
|
|
var distance := player_body.global_position - old_position
|
|
_averager.add_distance(delta, distance)
|
|
|
|
# Report exclusive motion performed (to bypass gravity)
|
|
return true
|
|
|
|
|
|
## Start or stop climbing
|
|
func _set_climbing(active: bool, player_body: XRToolsPlayerBody) -> void:
|
|
# Skip if no change
|
|
if active == is_active:
|
|
return
|
|
|
|
# Update state
|
|
is_active = active
|
|
|
|
# Handle state change
|
|
if is_active:
|
|
_averager.clear()
|
|
player_body.override_player_height(self, 0.0)
|
|
emit_signal("player_climb_start")
|
|
else:
|
|
# Calculate the forward direction (based on camera-forward)
|
|
var dir_forward = -player_body.camera_node.global_transform.basis.z \
|
|
.slide(player_body.up_player) \
|
|
.normalized()
|
|
|
|
# Set player velocity based on averaged velocity, fling multiplier,
|
|
# and a forward push
|
|
var velocity := _averager.velocity()
|
|
player_body.velocity = (velocity * fling_multiplier) + (dir_forward * forward_push)
|
|
|
|
player_body.override_player_height(self)
|
|
emit_signal("player_climb_end")
|
|
|
|
|
|
## Handler for left controller picked up
|
|
func _on_left_picked_up(what : Node3D) -> void:
|
|
# Get the climbable
|
|
var climbable = what as XRToolsClimbable
|
|
if not climbable:
|
|
return
|
|
|
|
# Get the handle
|
|
_left_handle = climbable.get_grab_handle(_left_pickup_node)
|
|
if not _left_handle:
|
|
return
|
|
|
|
# Switch dominance to the left handle
|
|
_dominant = _left_handle
|
|
|
|
# If collision hands present then target the handle
|
|
if _left_collision_hand:
|
|
_left_collision_hand.add_target_override(_left_handle, 0)
|
|
|
|
|
|
## Handler for right controller picked up
|
|
func _on_right_picked_up(what : Node3D) -> void:
|
|
# Get the climbable
|
|
var climbable = what as XRToolsClimbable
|
|
if not climbable:
|
|
return
|
|
|
|
# Get the handle
|
|
_right_handle = climbable.get_grab_handle(_right_pickup_node)
|
|
if not _right_handle:
|
|
return
|
|
|
|
# Switch dominance to the right handle
|
|
_dominant = _right_handle
|
|
|
|
# If collision hands present then target the handle
|
|
if _right_collision_hand:
|
|
_right_collision_hand.add_target_override(_right_handle, 0)
|
|
|
|
|
|
## Handler for left controller dropped
|
|
func _on_left_dropped() -> void:
|
|
# If collision hands present then clear handle target
|
|
if _left_collision_hand:
|
|
_left_collision_hand.remove_target_override(_left_handle)
|
|
|
|
# Release handle and transfer dominance
|
|
_left_handle = null
|
|
_dominant = _right_handle
|
|
|
|
|
|
## Handler for righ controller dropped
|
|
func _on_right_dropped() -> void:
|
|
# If collision hands present then clear handle target
|
|
if _right_collision_hand:
|
|
_right_collision_hand.remove_target_override(_right_handle)
|
|
|
|
# Release handle and transfer dominance
|
|
_right_handle = null
|
|
_dominant = _left_handle
|
|
|
|
|
|
# This method verifies the movement provider has a valid configuration.
|
|
func _get_configuration_warnings() -> PackedStringArray:
|
|
var warnings := super()
|
|
|
|
# Verify the left controller pickup
|
|
if !XRToolsFunctionPickup.find_left(self):
|
|
warnings.append("Unable to find left XRToolsFunctionPickup node")
|
|
|
|
# Verify the right controller pickup
|
|
if !XRToolsFunctionPickup.find_right(self):
|
|
warnings.append("Unable to find right XRToolsFunctionPickup node")
|
|
|
|
# Verify velocity averages
|
|
if velocity_averages < 2:
|
|
warnings.append("Minimum of 2 velocity averages needed")
|
|
|
|
# Return warnings
|
|
return warnings
|