Skip to content

Commit

Permalink
Add world-grab movement support
Browse files Browse the repository at this point in the history
  • Loading branch information
Malcolmnixon committed Nov 2, 2023
1 parent 40a1d59 commit 6a84924
Show file tree
Hide file tree
Showing 14 changed files with 810 additions and 83 deletions.
1 change: 1 addition & 0 deletions VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
- Upgraded project to Godot 4.1 as the new minimum version.
- Added reporting of stage load errors.
- Blend player height changes and prevent the player from standing up under a low ceiling.
- Added support for world-grab movement.

# 4.2.1
- Fixed snap-zones showing highlight when disabled.
Expand Down
217 changes: 217 additions & 0 deletions addons/godot-xr-tools/functions/movement_world_grab.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
@tool
class_name XRToolsMovementWorldGrab
extends XRToolsMovementProvider


## XR Tools Movement Provider for World-Grab
##
## This script provides world-grab movement for the player. To add world-grab
## support, the player must also have [XRToolsFunctionPickup] nodes attached
## to the left and right controllers, and an [XRToolsPlayerBody] under the
## [XROrigin3D].
##
## World-Grab areas inherit from the world_grab_area scene, or be [Area3D]
## nodes with the [XRToolsWorldGrabArea] script attached to them.


## Signal invoked when the player starts world-grab movement
signal player_world_grab_start

## Signal invoked when the player ends world-grab movement
signal player_world_grab_end


## Movement provider order
@export var order : int = 15

## Smallest world scale
@export var world_scale_min := 0.5

## Largest world scale
@export var world_scale_max := 2.0


# Left world-grab handle
var _left_handle : Node3D

# Right world-grab handle
var _right_handle : Node3D


# 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)


# Add support for is_xr_class on XRTools classes
func is_xr_class(name : String) -> bool:
return name == "XRToolsMovementGrabWorld" 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 world-grab movement if requested
if disabled or !enabled:
_set_world_grab_moving(false)
return

# Always set velocity to zero if enabled
player_body.velocity = Vector3.ZERO

# Check for world-grab 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

# Disable world-grab movement if not holding the world
if not _left_handle and not _right_handle:
_set_world_grab_moving(false)
return

# World grabbed
_set_world_grab_moving(true)

# Handle world-grab movement
var offset := Vector3.ZERO
if _left_handle and not _right_handle:
# Left-hand movement only
var left_pickup_pos := _left_controller.global_position
var left_grab_pos := _left_handle.global_position
offset = left_pickup_pos - left_grab_pos
elif _right_handle and not _left_handle:
# Right-hand movement only
var right_pickup_pos := _right_controller.global_position
var right_grab_pos := _right_handle.global_position
offset = right_pickup_pos - right_grab_pos
else:
# Get the world-grab handle positions
var left_grab_pos := _left_handle.global_position
var right_grab_pos := _right_handle.global_position
var grab_l2r := (right_grab_pos - left_grab_pos).slide(player_body.up_player)
var grab_mid := (left_grab_pos + right_grab_pos) * 0.5

# Get the pickup positions
var left_pickup_pos := _left_controller.global_position
var right_pickup_pos := _right_controller.global_position
var pickup_l2r := (right_pickup_pos - left_pickup_pos).slide(player_body.up_player)
var pickup_mid := (left_pickup_pos + right_pickup_pos) * 0.5

# Apply rotation
var angle := grab_l2r.signed_angle_to(pickup_l2r, player_body.up_player)
player_body.rotate_player(angle)

# Apply scale
var new_world_scale := XRServer.world_scale * grab_l2r.length() / pickup_l2r.length()
new_world_scale = clamp(new_world_scale, world_scale_min, world_scale_max)
XRServer.world_scale = new_world_scale

# Apply offset
offset = pickup_mid - grab_mid

# Move the player by the offset
var old_position := player_body.global_position
player_body.move_body(-offset / delta)
player_body.velocity = Vector3.ZERO
#player_body.move_and_collide(-offset)

# Report exclusive motion performed (to bypass gravity)
return true


## Start or stop world-grab movement
func _set_world_grab_moving(active: bool) -> void:
# Skip if no change
if active == is_active:
return

# Update state
is_active = active

# Handle state change
if is_active:
emit_signal("player_world_grab_start")
else:
emit_signal("player_world_grab_end")


## Handler for left controller picked up
func _on_left_picked_up(what : Node3D) -> void:
# Get the world-grab area
var world_grab_area = what as XRToolsWorldGrabArea
if not world_grab_area:
return

# Get the handle
_left_handle = world_grab_area.get_grab_handle(_left_pickup_node)
if not _left_handle:
return


## Handler for right controller picked up
func _on_right_picked_up(what : Node3D) -> void:
# Get the world-grab area
var world_grab_area = what as XRToolsWorldGrabArea
if not world_grab_area:
return

# Get the handle
_right_handle = world_grab_area.get_grab_handle(_right_pickup_node)
if not _right_handle:
return


## Handler for left controller dropped
func _on_left_dropped() -> void:
# Release handle and transfer dominance
_left_handle = null


## Handler for righ controller dropped
func _on_right_dropped() -> void:
# Release handle and transfer dominance
_right_handle = null


# 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")

# Return warnings
return warnings
6 changes: 6 additions & 0 deletions addons/godot-xr-tools/functions/movement_world_grab.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://dg3gr6ofd8yx4"]

[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_world_grab.gd" id="1_0qp8q"]

[node name="MovementWorldGrab" type="Node" groups=["movement_providers"]]
script = ExtResource("1_0qp8q")
64 changes: 64 additions & 0 deletions addons/godot-xr-tools/objects/world_grab_area.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
@tool
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
class_name XRToolsWorldGrabArea
extends Area3D


## XR Tools World-Grab Area
##
## This script adds world-grab areas to an environment
##
## For world-grab to work, the player must have an [XRToolsMovementWorldGrab]
## node configured appropriately.


## If true, the grip control must be held to keep holding the climbable
var press_to_hold : bool = true

## Dictionary of temporary grab-handles indexed by the pickup node.
var grab_locations := {}


# Add support for is_xr_class on XRTools classes
func is_xr_class(name : String) -> bool:
return name == "XRToolsWorldGrabArea"


# Called by XRToolsFunctionPickup
func is_picked_up() -> bool:
return false

func can_pick_up(_by: Node3D) -> bool:
return true

# Called by XRToolsFunctionPickup when user presses the action button while holding this object
func action():
pass

# Ignore highlighting requests from XRToolsFunctionPickup
func request_highlight(_from, _on) -> void:
pass

# Called by XRToolsFunctionPickup when this is picked up by a controller
func pick_up(by: Node3D, _with_controller: XRController3D) -> void:
# Get the ID to save the grab handle under
var id = by.get_instance_id()

# Get or construct the grab handle
var handle = grab_locations.get(id)
if not handle:
handle = Node3D.new()
add_child(handle)
grab_locations[id] = handle

# Set the handles global transform. As it's a child of this
# climbable it will move as the climbable moves
handle.global_transform = by.global_transform

# Called by XRToolsFunctionPickup when this is let go by a controller
func let_go(_p_linear_velocity: Vector3, _p_angular_velocity: Vector3) -> void:
pass

# Get the grab handle
func get_grab_handle(p: Node3D) -> Node3D:
return grab_locations.get(p.get_instance_id())
6 changes: 6 additions & 0 deletions addons/godot-xr-tools/objects/world_grab_area.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://57q7hhomocdh"]

[ext_resource type="Script" path="res://addons/godot-xr-tools/objects/world_grab_area.gd" id="1_uxhq5"]

[node name="WorldGrabArea" type="Area3D"]
script = ExtResource("1_uxhq5")
Loading

0 comments on commit 6a84924

Please sign in to comment.