init
This commit is contained in:
109
addons/godot-xr-tools/objects/climbable.gd
Normal file
109
addons/godot-xr-tools/objects/climbable.gd
Normal file
@@ -0,0 +1,109 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
|
||||
class_name XRToolsClimbable
|
||||
extends Node3D
|
||||
|
||||
|
||||
## XR Tools Climbable Object
|
||||
##
|
||||
## This script adds climbing support to any [StaticBody3D].
|
||||
##
|
||||
## For climbing to work, the player must have an [XRToolsMovementClimb] node
|
||||
## configured appropriately.
|
||||
|
||||
|
||||
## If true, the grip control must be held to keep holding the climbable
|
||||
var press_to_hold : bool = true
|
||||
|
||||
|
||||
## Array of permanent grab points.
|
||||
var _grab_points : Array[XRToolsGrabPoint] = []
|
||||
|
||||
## Dictionary of temporary grabs keyed by the pickup node
|
||||
var _grab_temps : Dictionary = {}
|
||||
|
||||
## Dictionary of active grabs keyed by the pickup node
|
||||
var _grabs : Dictionary = {}
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsClimbable"
|
||||
|
||||
|
||||
# Called when the node becomes "ready"
|
||||
func _ready() -> void:
|
||||
# Get all grab points
|
||||
for child in get_children():
|
||||
var grab_point := child as XRToolsGrabPoint
|
||||
if grab_point:
|
||||
_grab_points.push_back(grab_point)
|
||||
|
||||
|
||||
# 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) -> void:
|
||||
# Get the best permanent grab-point
|
||||
var point := _get_grab_point(by)
|
||||
if not point:
|
||||
# Get a temporary grab-point for the pickup
|
||||
point = _grab_temps.get(by)
|
||||
if not point:
|
||||
# Create a new temporary grab-point childed to the climbable
|
||||
point = Node3D.new()
|
||||
add_child(point)
|
||||
_grab_temps[by] = point
|
||||
|
||||
# Set the temporary to the current positon
|
||||
point.global_transform = by.global_transform
|
||||
|
||||
# Save the grab
|
||||
_grabs[by] = point
|
||||
|
||||
|
||||
# Called by XRToolsFunctionPickup when this is let go by a controller
|
||||
func let_go(by: Node3D, _p_linear_velocity: Vector3, _p_angular_velocity: Vector3) -> void:
|
||||
_grabs.erase(by)
|
||||
|
||||
|
||||
# Get the grab handle
|
||||
func get_grab_handle(by: Node3D) -> Node3D:
|
||||
return _grabs.get(by)
|
||||
|
||||
|
||||
## Find the most suitable grab-point for the grabber
|
||||
func _get_grab_point(by : Node3D) -> Node3D:
|
||||
# Find the best grab-point
|
||||
var fitness := 0.0
|
||||
var point : XRToolsGrabPoint = null
|
||||
for p in _grab_points:
|
||||
var f := p.can_grab(by, null)
|
||||
if f > fitness:
|
||||
fitness = f
|
||||
point = p
|
||||
|
||||
# Resolve redirection
|
||||
while point is XRToolsGrabPointRedirect:
|
||||
point = point.target
|
||||
|
||||
# Return the best grab point
|
||||
print_verbose("%s> picked grab-point %s" % [name, point])
|
||||
return point
|
||||
1
addons/godot-xr-tools/objects/climbable.gd.uid
Normal file
1
addons/godot-xr-tools/objects/climbable.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bqb3cy0ekk685
|
||||
10
addons/godot-xr-tools/objects/climbable.tscn
Normal file
10
addons/godot-xr-tools/objects/climbable.tscn
Normal file
@@ -0,0 +1,10 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cjyilbm4ucc7s"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bqb3cy0ekk685" path="res://addons/godot-xr-tools/objects/climbable.gd" id="1"]
|
||||
|
||||
[node name="Climbable" type="StaticBody3D"]
|
||||
collision_layer = 262145
|
||||
collision_mask = 0
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
187
addons/godot-xr-tools/objects/force_body/force_body.gd
Normal file
187
addons/godot-xr-tools/objects/force_body/force_body.gd
Normal file
@@ -0,0 +1,187 @@
|
||||
@tool
|
||||
class_name XRToolsForceBody
|
||||
extends AnimatableBody3D
|
||||
|
||||
|
||||
## XRTools Force Body script
|
||||
##
|
||||
## This script enhances AnimatableBody3D with move_and_slide and the ability
|
||||
## to push bodies by emparting forces on them.
|
||||
|
||||
|
||||
## Force Body Collision
|
||||
class ForceBodyCollision:
|
||||
## Collider object
|
||||
var collider : Node3D
|
||||
|
||||
## Collision point
|
||||
var position : Vector3
|
||||
|
||||
## Collision normal
|
||||
var normal : Vector3
|
||||
|
||||
|
||||
## Enables or disables pushing bodies
|
||||
@export var push_bodies : bool = true
|
||||
|
||||
## Control the stiffness of the body
|
||||
@export var stiffness : float = 10.0
|
||||
|
||||
## Control the maximum push force
|
||||
@export var maximum_force : float = 1.0
|
||||
|
||||
## Maximum slides
|
||||
@export var max_slides : int = 4
|
||||
|
||||
|
||||
## Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsForceBody"
|
||||
|
||||
|
||||
## This function moves and slides along the [param move] vector. It returns
|
||||
## information about the last collision, or null if no collision
|
||||
func move_and_slide(move : Vector3) -> ForceBodyCollision:
|
||||
# Make sure this is off or weird shit happens...
|
||||
sync_to_physics = false
|
||||
|
||||
# Loop performing the movement steps
|
||||
var step_move := move
|
||||
var ret : ForceBodyCollision = null
|
||||
for step in max_slides:
|
||||
# Take the next step
|
||||
var collision := move_and_collide(step_move)
|
||||
|
||||
# If we didn't collide with anything then we have finished the entire
|
||||
# move_and_slide operation
|
||||
if not collision:
|
||||
break
|
||||
|
||||
# Save relevant collision information
|
||||
var collider := collision.get_collider()
|
||||
var postion := collision.get_position()
|
||||
var normal := collision.get_normal()
|
||||
|
||||
# Save the collision information
|
||||
if not ret:
|
||||
ret = ForceBodyCollision.new()
|
||||
|
||||
ret.collider = collider
|
||||
ret.position = postion
|
||||
ret.normal = normal
|
||||
|
||||
# Calculate the next move
|
||||
var next_move := collision.get_remainder().slide(normal)
|
||||
|
||||
# Handle pushing bodies
|
||||
if push_bodies:
|
||||
var body := collider as RigidBody3D
|
||||
if body:
|
||||
# Calculate the momentum lost by the collision
|
||||
var lost_momentum := step_move - next_move
|
||||
|
||||
# TODO: We should consider the velocity of the body such that
|
||||
# we never push it away faster than our own velocity.
|
||||
|
||||
# Apply the lost momentum as an impulse to the body we hit
|
||||
body.apply_impulse(
|
||||
(lost_momentum * stiffness).limit_length(maximum_force),
|
||||
position - body.global_position)
|
||||
|
||||
# Update the remaining movement
|
||||
step_move = next_move
|
||||
|
||||
# Prevent bouncing back along movement path
|
||||
if next_move.dot(move) <= 0:
|
||||
break
|
||||
|
||||
# Return the last collision data
|
||||
return ret
|
||||
|
||||
|
||||
## Attempts to rotate our object until it collides
|
||||
func rotate_and_collide( \
|
||||
target_global_basis : Basis, \
|
||||
step_angle : float = deg_to_rad(5.0) \
|
||||
) -> ForceBodyCollision:
|
||||
# Make sure this is off or weird shit happens...
|
||||
sync_to_physics = false
|
||||
|
||||
var ret : ForceBodyCollision = null
|
||||
|
||||
var space = PhysicsServer3D.body_get_space(get_rid())
|
||||
var direct_state = PhysicsServer3D.space_get_direct_state(space)
|
||||
|
||||
# We don't seem to have a rotational movement query for collisions,
|
||||
# so best we can do is to rotate in steps and test
|
||||
var from_quat : Quaternion = Quaternion(global_basis)
|
||||
var to_quat : Quaternion = Quaternion(target_global_basis)
|
||||
var angle : float = from_quat.angle_to(to_quat)
|
||||
var steps : float = ceil(angle / step_angle)
|
||||
|
||||
# Convert collision exceptions to a RID array
|
||||
var exception_rids : Array[RID]
|
||||
for collision_exception in get_collision_exceptions():
|
||||
# It is our responsibility to remove exceptions before freeing the object, but sometimes
|
||||
# that is hard.
|
||||
if is_instance_valid(collision_exception):
|
||||
exception_rids.push_back(collision_exception.get_rid())
|
||||
else:
|
||||
push_warning("freed object still exists in a collision exception")
|
||||
|
||||
# Prevent collisions with ourselves
|
||||
exception_rids.push_back(get_rid())
|
||||
|
||||
# Find our shape ids
|
||||
var shape_rids : Array[RID]
|
||||
for node in get_children(true):
|
||||
if node is CollisionShape3D:
|
||||
var col_shape : CollisionShape3D = node
|
||||
if not col_shape.disabled:
|
||||
shape_rids.push_back(col_shape.shape.get_rid())
|
||||
|
||||
# Our physics query
|
||||
var query : PhysicsShapeQueryParameters3D = PhysicsShapeQueryParameters3D.new()
|
||||
query.collide_with_areas = false
|
||||
query.collide_with_bodies = true
|
||||
query.collision_mask = collision_mask
|
||||
query.exclude = exception_rids
|
||||
|
||||
# Check our collisions
|
||||
var step : float = 0.0
|
||||
var new_quat : Quaternion = from_quat
|
||||
var t = global_transform
|
||||
while step < steps and not ret:
|
||||
step += 1.0
|
||||
|
||||
var test_quat : Quaternion = from_quat.slerp(to_quat, step / steps)
|
||||
t.basis = Basis(test_quat)
|
||||
query.transform = t
|
||||
|
||||
for rid in shape_rids:
|
||||
query.shape_rid = rid
|
||||
var collision = direct_state.get_rest_info(query)
|
||||
if not collision.is_empty():
|
||||
ret = ForceBodyCollision.new()
|
||||
ret.collider = instance_from_id(collision["collider_id"])
|
||||
ret.position = collision["point"]
|
||||
ret.normal = collision["normal"]
|
||||
|
||||
# TODO May need to see about applying a rotational force
|
||||
# if pushbodies is true
|
||||
|
||||
break
|
||||
|
||||
if not ret:
|
||||
# No collision, we can rotate this far!
|
||||
new_quat = test_quat
|
||||
|
||||
# Update our rotation to our last successful rotation
|
||||
global_basis = Basis(new_quat)
|
||||
|
||||
# Return the last collision data
|
||||
return ret
|
||||
|
||||
|
||||
func _ready():
|
||||
process_physics_priority = -90
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1148cmpripqt
|
||||
235
addons/godot-xr-tools/objects/grab_points/grab.gd
Normal file
235
addons/godot-xr-tools/objects/grab_points/grab.gd
Normal file
@@ -0,0 +1,235 @@
|
||||
class_name Grab
|
||||
extends Grabber
|
||||
|
||||
|
||||
## Grab Class
|
||||
##
|
||||
## This class encodes information about an active grab. Additionally it applies
|
||||
## hand poses and collision exceptions as appropriate.
|
||||
|
||||
|
||||
## Priority for grip poses
|
||||
const GRIP_POSE_PRIORITY := 100
|
||||
|
||||
## Priority for grip targeting
|
||||
const GRIP_TARGET_PRIORITY := 100
|
||||
|
||||
|
||||
## Grab target
|
||||
var what : XRToolsPickable
|
||||
|
||||
## Grab point information
|
||||
var point : XRToolsGrabPoint
|
||||
|
||||
## Hand grab point information
|
||||
var hand_point : XRToolsGrabPointHand
|
||||
|
||||
## Grab transform
|
||||
var transform : Transform3D
|
||||
|
||||
## Position drive strength
|
||||
var drive_position : float = 1.0
|
||||
|
||||
## Angle drive strength
|
||||
var drive_angle : float = 1.0
|
||||
|
||||
## Aim drive strength
|
||||
var drive_aim : float = 0.0
|
||||
|
||||
## Has target arrived at grab point
|
||||
var _arrived : bool = false
|
||||
|
||||
## Collision exceptions we manage
|
||||
var _collision_exceptions : Array[RID]
|
||||
|
||||
|
||||
## Initialize the grab
|
||||
func _init(
|
||||
p_grabber : Grabber,
|
||||
p_what : XRToolsPickable,
|
||||
p_point : XRToolsGrabPoint,
|
||||
p_precise : bool) -> void:
|
||||
|
||||
# Copy the grabber information
|
||||
by = p_grabber.by
|
||||
pickup = p_grabber.pickup
|
||||
controller = p_grabber.controller
|
||||
hand = p_grabber.hand
|
||||
collision_hand = p_grabber.collision_hand
|
||||
|
||||
# Set the point
|
||||
what = p_what
|
||||
point = p_point
|
||||
hand_point = p_point as XRToolsGrabPointHand
|
||||
|
||||
# Calculate the grab transform
|
||||
if hand_point:
|
||||
# Get our adjusted grab point (palm position)
|
||||
transform = hand_point.get_palm_transform()
|
||||
elif point:
|
||||
transform = point.transform
|
||||
elif p_precise:
|
||||
transform = p_what.global_transform.affine_inverse() * by.global_transform
|
||||
else:
|
||||
transform = Transform3D.IDENTITY
|
||||
|
||||
# Set the drive parameters
|
||||
if hand_point:
|
||||
drive_position = hand_point.drive_position
|
||||
drive_angle = hand_point.drive_angle
|
||||
drive_aim = hand_point.drive_aim
|
||||
|
||||
# Apply collision exceptions
|
||||
if collision_hand:
|
||||
collision_hand.max_distance_reached.connect(_on_max_distance_reached)
|
||||
_add_collision_exceptions(what)
|
||||
|
||||
|
||||
## Set the target as arrived at the grab-point
|
||||
func set_arrived() -> void:
|
||||
# Ignore if already arrived
|
||||
if _arrived:
|
||||
return
|
||||
|
||||
# Set arrived and apply any hand pose
|
||||
print_verbose("%s> arrived at %s" % [what.name, point])
|
||||
_arrived = true
|
||||
_set_hand_pose()
|
||||
|
||||
# Report the grab
|
||||
print_verbose("%s> grabbed by %s", [what.name, by.name])
|
||||
what.grabbed.emit(what, by)
|
||||
|
||||
|
||||
## Set the grab point
|
||||
func set_grab_point(p_point : XRToolsGrabPoint) -> void:
|
||||
# Skip if no change
|
||||
if p_point == point:
|
||||
return
|
||||
|
||||
# Remove any current pose override
|
||||
_clear_hand_pose()
|
||||
|
||||
# Update the grab point
|
||||
point = p_point
|
||||
hand_point = point as XRToolsGrabPointHand
|
||||
|
||||
# Update the transform
|
||||
if point:
|
||||
# Get our adjusted grab point (palm position)
|
||||
transform = p_point.get_palm_transform()
|
||||
|
||||
# Apply the new hand grab-point settings
|
||||
if hand_point:
|
||||
drive_position = hand_point.drive_position
|
||||
drive_angle = hand_point.drive_angle
|
||||
drive_aim = hand_point.drive_aim
|
||||
|
||||
# Apply any pose overrides
|
||||
if _arrived:
|
||||
_set_hand_pose()
|
||||
|
||||
# Report switch
|
||||
print_verbose("%s> switched grab point to %s", [what.name, point.name])
|
||||
what.released.emit(what, by)
|
||||
what.grabbed.emit(what, by)
|
||||
|
||||
|
||||
## Release the grip
|
||||
func release() -> void:
|
||||
# Clear any hand pose
|
||||
_clear_hand_pose()
|
||||
|
||||
# Remove collision exceptions with a small delay
|
||||
if is_instance_valid(collision_hand) and not _collision_exceptions.is_empty():
|
||||
# Use RIDs instead of the objects directly in case they get freed while
|
||||
# we are waiting for the object to fall away
|
||||
var copy : Array[RID] = _collision_exceptions.duplicate()
|
||||
_collision_exceptions.clear()
|
||||
|
||||
# Delay removing our exceptions to give the object time to fall away
|
||||
collision_hand.get_tree().create_timer(0.5).timeout \
|
||||
.connect(_remove_collision_exceptions \
|
||||
.bind(copy) \
|
||||
.bind(collision_hand.get_rid()))
|
||||
|
||||
# Report the release
|
||||
print_verbose("%s> released by %s", [what.name, by.name])
|
||||
what.released.emit(what, by)
|
||||
|
||||
|
||||
# Hand has moved too far away from object, can no longer hold on to it.
|
||||
func _on_max_distance_reached() -> void:
|
||||
pickup.drop_object()
|
||||
|
||||
|
||||
# Set hand-pose overrides
|
||||
func _set_hand_pose() -> void:
|
||||
# Skip if not hand
|
||||
if not is_instance_valid(hand) or not is_instance_valid(hand_point):
|
||||
return
|
||||
|
||||
# Apply the hand-pose
|
||||
if hand_point.hand_pose:
|
||||
hand.add_pose_override(hand_point, GRIP_POSE_PRIORITY, hand_point.hand_pose)
|
||||
|
||||
# Apply hand snapping
|
||||
if hand_point.snap_hand:
|
||||
hand.add_target_override(hand_point, GRIP_TARGET_PRIORITY)
|
||||
|
||||
|
||||
# Clear any hand-pose overrides
|
||||
func _clear_hand_pose() -> void:
|
||||
# Skip if not hand
|
||||
if not is_instance_valid(hand) or not is_instance_valid(hand_point):
|
||||
return
|
||||
|
||||
# Remove hand-pose
|
||||
hand.remove_pose_override(hand_point)
|
||||
|
||||
# Remove hand snapping
|
||||
hand.remove_target_override(hand_point)
|
||||
|
||||
|
||||
# Add collision exceptions for the grabbed object and any of its children
|
||||
func _add_collision_exceptions(from : Node):
|
||||
if not is_instance_valid(collision_hand):
|
||||
return
|
||||
|
||||
if not is_instance_valid(from):
|
||||
return
|
||||
|
||||
# If this is a physics body, add an exception
|
||||
if from is PhysicsBody3D:
|
||||
# Make sure we don't collide with what we're holding
|
||||
_collision_exceptions.push_back(from.get_rid())
|
||||
PhysicsServer3D.body_add_collision_exception(collision_hand.get_rid(), from.get_rid())
|
||||
PhysicsServer3D.body_add_collision_exception(from.get_rid(), collision_hand.get_rid())
|
||||
|
||||
# Check all children
|
||||
for child in from.get_children():
|
||||
_add_collision_exceptions(child)
|
||||
|
||||
|
||||
# Remove the exceptions in our passed array. We call this with a small delay
|
||||
# to give an object a chance to drop away from the hand before it starts
|
||||
# colliding.
|
||||
# It is possible that another object is picked up in the meanwhile
|
||||
# and we thus fill _collision_exceptions with new content.
|
||||
# Hence using a copy of this list at the time of dropping the object.
|
||||
#
|
||||
# Note, this is static because our grab object gets destroyed before this code gets run.
|
||||
static func _remove_collision_exceptions( \
|
||||
on_collision_hand : RID, \
|
||||
exceptions : Array[RID]):
|
||||
|
||||
# This can be improved by checking if we're still colliding and only
|
||||
# removing those objects from our exception list that are not.
|
||||
# If any are left, we can restart a new timer.
|
||||
# This will also allow us to use a much smaller timer interval
|
||||
|
||||
# For now we'll remove all.
|
||||
|
||||
for body : RID in exceptions:
|
||||
PhysicsServer3D.body_remove_collision_exception(on_collision_hand, body)
|
||||
PhysicsServer3D.body_remove_collision_exception(body, on_collision_hand)
|
||||
1
addons/godot-xr-tools/objects/grab_points/grab.gd.uid
Normal file
1
addons/godot-xr-tools/objects/grab_points/grab.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c6pkfifsm2fy1
|
||||
239
addons/godot-xr-tools/objects/grab_points/grab_driver.gd
Normal file
239
addons/godot-xr-tools/objects/grab_points/grab_driver.gd
Normal file
@@ -0,0 +1,239 @@
|
||||
class_name XRToolsGrabDriver
|
||||
extends RemoteTransform3D
|
||||
|
||||
|
||||
## Grab state
|
||||
enum GrabState {
|
||||
LERP,
|
||||
SNAP,
|
||||
}
|
||||
|
||||
|
||||
## Drive state
|
||||
var state : GrabState = GrabState.SNAP
|
||||
|
||||
## Target pickable
|
||||
var target : XRToolsPickable
|
||||
|
||||
## Primary grab information
|
||||
var primary : Grab = null
|
||||
|
||||
## Secondary grab information
|
||||
var secondary : Grab = null
|
||||
|
||||
## Lerp start position
|
||||
var lerp_start : Transform3D
|
||||
|
||||
## Lerp total duration
|
||||
var lerp_duration : float = 1.0
|
||||
|
||||
## Lerp time
|
||||
var lerp_time : float = 0.0
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _physics_process(delta : float) -> void:
|
||||
# Skip if no primary node
|
||||
if not is_instance_valid(primary):
|
||||
return
|
||||
|
||||
# Set destination from primary grab
|
||||
var destination := primary.by.global_transform * primary.transform.affine_inverse()
|
||||
|
||||
# If present, apply secondary-node contributions
|
||||
if is_instance_valid(secondary):
|
||||
# Calculate lerp coefficients based on drive strengths
|
||||
var position_lerp := _vote(primary.drive_position, secondary.drive_position)
|
||||
var angle_lerp := _vote(primary.drive_angle, secondary.drive_angle)
|
||||
|
||||
# Calculate the transform from secondary grab
|
||||
var x1 := destination
|
||||
var x2 := secondary.by.global_transform * secondary.transform.affine_inverse()
|
||||
|
||||
# Independently lerp the angle and position
|
||||
destination = Transform3D(
|
||||
x1.basis.slerp(x2.basis, angle_lerp),
|
||||
x1.origin.lerp(x2.origin, position_lerp))
|
||||
|
||||
# Test if we need to apply aiming
|
||||
if secondary.drive_aim > 0.0:
|
||||
# Convert destination from global to primary-local
|
||||
destination = primary.by.global_transform.affine_inverse() * destination
|
||||
|
||||
# Calculate the from and to vectors in primary-local space
|
||||
var secondary_from := destination * secondary.transform.origin
|
||||
var secondary_to := primary.by.to_local(secondary.by.global_position)
|
||||
|
||||
# Build shortest arc
|
||||
secondary_from = secondary_from.normalized()
|
||||
secondary_to = secondary_to.normalized()
|
||||
var spherical := Quaternion(secondary_from, secondary_to)
|
||||
|
||||
# Build aim-rotation
|
||||
var rotate := Basis.IDENTITY.slerp(Basis(spherical), secondary.drive_aim)
|
||||
destination = Transform3D(rotate, Vector3.ZERO) * destination
|
||||
|
||||
# Convert destination from primary-local to global
|
||||
destination = primary.by.global_transform * destination
|
||||
|
||||
# Handle update
|
||||
match state:
|
||||
GrabState.LERP:
|
||||
# Progress the lerp
|
||||
lerp_time += delta
|
||||
if lerp_time < lerp_duration:
|
||||
# Interpolate from lerp_start to destination
|
||||
destination = lerp_start.interpolate_with(
|
||||
destination,
|
||||
lerp_time / lerp_duration)
|
||||
else:
|
||||
# Lerp completed
|
||||
state = GrabState.SNAP
|
||||
_update_weight()
|
||||
if primary: primary.set_arrived()
|
||||
if secondary: secondary.set_arrived()
|
||||
|
||||
if global_transform.is_equal_approx(destination):
|
||||
return
|
||||
|
||||
# Apply the destination transform
|
||||
global_transform = destination
|
||||
force_update_transform()
|
||||
if is_instance_valid(target):
|
||||
target.force_update_transform()
|
||||
|
||||
|
||||
## Set the secondary grab point
|
||||
func add_grab(p_grab : Grab) -> void:
|
||||
# Set the secondary grab
|
||||
if p_grab.hand_point and p_grab.hand_point.mode == XRToolsGrabPointHand.Mode.PRIMARY:
|
||||
print_verbose("%s> new primary grab %s" % [target.name, p_grab.by.name])
|
||||
secondary = primary
|
||||
primary = p_grab
|
||||
else:
|
||||
print_verbose("%s> new secondary grab %s" % [target.name, p_grab.by.name])
|
||||
secondary = p_grab
|
||||
|
||||
# If snapped then report arrived at the new grab
|
||||
if state == GrabState.SNAP:
|
||||
_update_weight()
|
||||
p_grab.set_arrived()
|
||||
|
||||
|
||||
## Get the grab information for the grab node
|
||||
func get_grab(by : Node3D) -> Grab:
|
||||
if primary and primary.by == by:
|
||||
return primary
|
||||
|
||||
if secondary and secondary.by == by:
|
||||
return secondary
|
||||
|
||||
return null
|
||||
|
||||
|
||||
func remove_grab(p_grab : Grab) -> void:
|
||||
# Remove the appropriate grab
|
||||
if p_grab == primary:
|
||||
# Remove primary (secondary promoted)
|
||||
print_verbose("%s> %s (primary) released" % [target.name, p_grab.by.name])
|
||||
primary = secondary
|
||||
secondary = null
|
||||
elif p_grab == secondary:
|
||||
# Remove secondary
|
||||
print_verbose("%s> %s (secondary) released" % [target.name, p_grab.by.name])
|
||||
secondary = null
|
||||
|
||||
if state == GrabState.SNAP:
|
||||
_update_weight()
|
||||
|
||||
|
||||
# Discard the driver
|
||||
func discard():
|
||||
remote_path = NodePath()
|
||||
queue_free()
|
||||
|
||||
|
||||
# Create the driver to lerp the target from its current location to the
|
||||
# primary grab-point.
|
||||
static func create_lerp(
|
||||
p_target : Node3D,
|
||||
p_grab : Grab,
|
||||
p_lerp_speed : float) -> XRToolsGrabDriver:
|
||||
|
||||
print_verbose("%s> lerping %s" % [p_target.name, p_grab.by.name])
|
||||
|
||||
# Construct the driver lerping from the current position
|
||||
var driver := XRToolsGrabDriver.new()
|
||||
driver.name = p_target.name + "_driver"
|
||||
driver.top_level = true
|
||||
driver.process_physics_priority = -80
|
||||
driver.state = GrabState.LERP
|
||||
driver.target = p_target
|
||||
driver.primary = p_grab
|
||||
driver.global_transform = p_target.global_transform
|
||||
|
||||
# Calculate the start and duration
|
||||
var end := p_grab.by.global_transform * p_grab.transform
|
||||
var delta := end.origin - p_target.global_position
|
||||
driver.lerp_start = p_target.global_transform
|
||||
driver.lerp_duration = delta.length() / p_lerp_speed
|
||||
|
||||
# Add the driver as a neighbor of the target as RemoteTransform3D nodes
|
||||
# cannot be descendands of the targets they drive.
|
||||
p_target.get_parent().add_child(driver)
|
||||
driver.remote_path = driver.get_path_to(p_target)
|
||||
|
||||
# Return the driver
|
||||
return driver
|
||||
|
||||
|
||||
# Create the driver to instantly snap to the primary grab-point.
|
||||
static func create_snap(
|
||||
p_target : Node3D,
|
||||
p_grab : Grab) -> XRToolsGrabDriver:
|
||||
|
||||
print_verbose("%s> snapping to %s" % [p_target.name, p_grab.by.name])
|
||||
|
||||
# Construct the driver snapped to the held position
|
||||
var driver := XRToolsGrabDriver.new()
|
||||
driver.name = p_target.name + "_driver"
|
||||
driver.top_level = true
|
||||
driver.process_physics_priority = -80
|
||||
driver.state = GrabState.SNAP
|
||||
driver.target = p_target
|
||||
driver.primary = p_grab
|
||||
driver.global_transform = p_grab.by.global_transform * p_grab.transform.affine_inverse()
|
||||
|
||||
# Snapped to grab-point so report arrived
|
||||
p_grab.set_arrived()
|
||||
|
||||
# Add the driver as a neighbor of the target as RemoteTransform3D nodes
|
||||
# cannot be descendands of the targets they drive.
|
||||
p_target.get_parent().add_child(driver)
|
||||
driver.remote_path = driver.get_path_to(p_target)
|
||||
|
||||
driver._update_weight()
|
||||
|
||||
# Return the driver
|
||||
return driver
|
||||
|
||||
|
||||
# Calculate the lerp voting from a to b
|
||||
static func _vote(a : float, b : float) -> float:
|
||||
if a == 0.0 and b == 0.0:
|
||||
return 0.0
|
||||
|
||||
return b / (a + b)
|
||||
|
||||
|
||||
# Update the weight on collision hands
|
||||
func _update_weight():
|
||||
if primary:
|
||||
var weight : float = target.mass
|
||||
if secondary:
|
||||
# Each hand carries half the weight
|
||||
weight = weight / 2.0
|
||||
if secondary.collision_hand:
|
||||
secondary.collision_hand.set_held_weight(weight)
|
||||
|
||||
if primary.collision_hand:
|
||||
primary.collision_hand.set_held_weight(weight)
|
||||
@@ -0,0 +1 @@
|
||||
uid://blacqnxi11y3x
|
||||
47
addons/godot-xr-tools/objects/grab_points/grab_point.gd
Normal file
47
addons/godot-xr-tools/objects/grab_points/grab_point.gd
Normal file
@@ -0,0 +1,47 @@
|
||||
class_name XRToolsGrabPoint
|
||||
extends Marker3D
|
||||
|
||||
|
||||
## XR Tools Grab Point Base Script
|
||||
##
|
||||
## This script is the base for all grab points. Pickable object extending from
|
||||
## [XRToolsPickable] can have numerous grab points to control where the object
|
||||
## is grabbed from.
|
||||
|
||||
|
||||
# Signal emitted when the user presses the action button while holding this grab point
|
||||
signal action_pressed(pickable, grab_point)
|
||||
|
||||
# Signal emitted when the user releases the action button while holding this grab point
|
||||
signal action_released(pickable, grab_point)
|
||||
|
||||
|
||||
## If true, the grab point is enabled for grabbing
|
||||
@export var enabled : bool = true
|
||||
|
||||
|
||||
## Evaluate fitness of the proposed grab, with 0.0 for not allowed.
|
||||
func can_grab(grabber : Node3D, _current : XRToolsGrabPoint) -> float:
|
||||
if not enabled:
|
||||
return 0.0
|
||||
|
||||
# Return the distance-weighted fitness
|
||||
return _weight(grabber)
|
||||
|
||||
|
||||
# Return a distance-weighted fitness weight in the range (0.0 - max]
|
||||
func _weight(grabber : Node3D, max : float = 1.0) -> float:
|
||||
var distance := global_position.distance_to(grabber.global_position)
|
||||
return max / (1.0 + distance)
|
||||
|
||||
|
||||
# action is called when user presses the action button while holding this grab point
|
||||
func action(pickable : XRToolsPickable):
|
||||
# let interested parties know
|
||||
action_pressed.emit(pickable, self)
|
||||
|
||||
|
||||
# action_release is called when user releases the action button while holding this grab point
|
||||
func action_release(pickable : XRToolsPickable):
|
||||
# let interested parties know
|
||||
action_released.emit(pickable, self)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8uwrf51jd5xm
|
||||
236
addons/godot-xr-tools/objects/grab_points/grab_point_hand.gd
Normal file
236
addons/godot-xr-tools/objects/grab_points/grab_point_hand.gd
Normal file
@@ -0,0 +1,236 @@
|
||||
@tool
|
||||
class_name XRToolsGrabPointHand
|
||||
extends XRToolsGrabPoint
|
||||
|
||||
|
||||
## XR Tools Grab Point Hand Script
|
||||
##
|
||||
## This script allows specifying a grab point for a specific hand. Additionally
|
||||
## the grab point can be used to control the pose of the hand, and to allow the
|
||||
## grab point position to be fine-tuned in the editor.
|
||||
|
||||
|
||||
## Hand for this grab point
|
||||
enum Hand {
|
||||
LEFT, ## Left hand
|
||||
RIGHT, ## Right hand
|
||||
}
|
||||
|
||||
## Grab mode for this grab point
|
||||
enum Mode {
|
||||
GENERAL, ## General grab point
|
||||
PRIMARY, ## Primary-hand grab point
|
||||
SECONDARY ## Secondary-hand grab point
|
||||
}
|
||||
|
||||
## Hand preview option
|
||||
enum PreviewMode {
|
||||
CLOSED, ## Preview hand closed
|
||||
OPEN, ## Preview hand open
|
||||
}
|
||||
|
||||
|
||||
## Left hand scene path (for editor preview)
|
||||
const LEFT_HAND_PATH := "res://addons/godot-xr-tools/hands/scenes/lowpoly/left_hand_low.tscn"
|
||||
|
||||
## Right hand scene path (for editor preview)
|
||||
const RIGHT_HAND_PATH := "res://addons/godot-xr-tools/hands/scenes/lowpoly/right_hand_low.tscn"
|
||||
|
||||
|
||||
## Grab-point handle
|
||||
@export var handle : String
|
||||
|
||||
## Which hand this grab point is for
|
||||
@export var hand : Hand: set = _set_hand
|
||||
|
||||
## Hand grab mode
|
||||
@export var mode : Mode = Mode.GENERAL
|
||||
|
||||
## Snap the hand mesh to the grab-point
|
||||
@export var snap_hand : bool = true
|
||||
|
||||
## Hand pose
|
||||
@export var hand_pose : XRToolsHandPoseSettings: set = _set_hand_pose
|
||||
|
||||
## If true, the hand is shown in the editor
|
||||
@export var editor_preview_mode : PreviewMode = PreviewMode.CLOSED: set = _set_editor_preview_mode
|
||||
|
||||
## How much this grab-point drives the position
|
||||
@export var drive_position : float = 1.0
|
||||
|
||||
## How much this grab-point drives the angle
|
||||
@export var drive_angle : float = 1.0
|
||||
|
||||
## How much this grab-point drives the aim
|
||||
@export var drive_aim : float = 0.0
|
||||
|
||||
## Hand to use for editor preview
|
||||
var _editor_preview_hand : XRToolsHand
|
||||
|
||||
# Adjust the grab point from our old aim positioning, to our palm positioning.
|
||||
func get_palm_transform(global : bool = false) -> Transform3D:
|
||||
var adj_transform : Transform3D = global_transform if global else transform
|
||||
|
||||
# Historically our hands have always been positioned based on our aim,
|
||||
# So we apply our old hardcoded offset, but adjusted for our palm center.
|
||||
var aim_offset := Transform3D()
|
||||
aim_offset.origin = Vector3(-0.02 if hand == Hand.LEFT else 0.02, -0.05, 0.10)
|
||||
adj_transform = adj_transform * aim_offset
|
||||
|
||||
return adj_transform
|
||||
|
||||
|
||||
## Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# If in the editor then update the preview
|
||||
if Engine.is_editor_hint():
|
||||
_update_editor_preview()
|
||||
|
||||
|
||||
## Test if a grabber can grab by this grab-point
|
||||
func can_grab(grabber : Node3D, current : XRToolsGrabPoint) -> float:
|
||||
# Skip if not enabled
|
||||
if not enabled:
|
||||
return 0.0
|
||||
|
||||
# Verify the hand matches
|
||||
if not _is_correct_hand(grabber):
|
||||
return 0.0
|
||||
|
||||
# Fail if the hand grab is not permitted
|
||||
if not _is_valid_hand_grab(current):
|
||||
return 0.0
|
||||
|
||||
# Get the distance-weighted fitness in the range (0.0 - 0.5], but boost
|
||||
# to [0.5 - 1.0] for valid "specific" grabs.
|
||||
var fitness := _weight(grabber, 0.5)
|
||||
if mode != Mode.GENERAL:
|
||||
fitness += 0.5
|
||||
|
||||
# Return the grab fitness
|
||||
return fitness
|
||||
|
||||
|
||||
func _set_hand(new_value : Hand) -> void:
|
||||
hand = new_value
|
||||
if Engine.is_editor_hint():
|
||||
_update_editor_preview()
|
||||
|
||||
|
||||
func _set_hand_pose(new_value : XRToolsHandPoseSettings) -> void:
|
||||
# Unsubscribe from the old hand-pose changed signal
|
||||
if Engine.is_editor_hint() and hand_pose:
|
||||
hand_pose.changed.disconnect(_update_editor_preview)
|
||||
|
||||
# Save the hand pose
|
||||
hand_pose = new_value
|
||||
|
||||
# Update the editor preview
|
||||
if Engine.is_editor_hint() and hand_pose:
|
||||
hand_pose.changed.connect(_update_editor_preview)
|
||||
_update_editor_preview()
|
||||
|
||||
|
||||
func _set_editor_preview_mode(new_value : PreviewMode) -> void:
|
||||
editor_preview_mode = new_value
|
||||
if Engine.is_editor_hint():
|
||||
_update_editor_preview()
|
||||
|
||||
|
||||
func _update_editor_preview() -> void:
|
||||
# Discard any existing hand model
|
||||
if _editor_preview_hand:
|
||||
remove_child(_editor_preview_hand)
|
||||
_editor_preview_hand.queue_free()
|
||||
_editor_preview_hand = null
|
||||
|
||||
# Pick the hand scene
|
||||
var hand_path := LEFT_HAND_PATH if hand == Hand.LEFT else RIGHT_HAND_PATH
|
||||
var hand_scene : PackedScene = load(hand_path)
|
||||
if !hand_scene:
|
||||
return
|
||||
|
||||
# Construct the model
|
||||
_editor_preview_hand = hand_scene.instantiate()
|
||||
_editor_preview_hand.hand_offset_mode = 4 # Disabled
|
||||
|
||||
# Set the pose
|
||||
if hand_pose:
|
||||
_editor_preview_hand.add_pose_override(self, 0.0, hand_pose)
|
||||
|
||||
# Set the grip override
|
||||
if editor_preview_mode == PreviewMode.CLOSED:
|
||||
_editor_preview_hand.force_grip_trigger(1.0, 1.0)
|
||||
else:
|
||||
_editor_preview_hand.force_grip_trigger(0.0, 0.0)
|
||||
|
||||
# Add the editor-preview hand as a child
|
||||
add_child(_editor_preview_hand, false, Node.INTERNAL_MODE_BACK)
|
||||
|
||||
# Keep this backwards compatible,
|
||||
# position the hand according to the original aim logic
|
||||
var hand_node : Node3D = _editor_preview_hand.get_child(0)
|
||||
if hand_node:
|
||||
var custom_offset := Transform3D()
|
||||
custom_offset.origin = Vector3(-0.03 if hand == Hand.LEFT else 0.03, -0.05, 0.15)
|
||||
hand_node.transform = custom_offset
|
||||
|
||||
|
||||
# Is the grabber for the correct hand
|
||||
func _is_correct_hand(grabber : Node3D) -> bool:
|
||||
# Find the controller
|
||||
var controller := _get_grabber_controller(grabber)
|
||||
if not controller:
|
||||
return false
|
||||
|
||||
# Get the positional tracker
|
||||
var tracker := XRServer.get_tracker(controller.tracker) as XRPositionalTracker
|
||||
|
||||
# If left hand then verify left controller
|
||||
if hand == Hand.LEFT and tracker.hand != XRPositionalTracker.TRACKER_HAND_LEFT:
|
||||
return false
|
||||
|
||||
# If right hand then verify right controller
|
||||
if hand == Hand.RIGHT and tracker.hand != XRPositionalTracker.TRACKER_HAND_RIGHT:
|
||||
return false
|
||||
|
||||
# Controller matches hand
|
||||
return true
|
||||
|
||||
|
||||
# Test if hand grab is permitted
|
||||
func _is_valid_hand_grab(current : XRToolsGrabPoint) -> bool:
|
||||
# Not a valid hand grab if currently held by something other than a hand
|
||||
var current_hand := current as XRToolsGrabPointHand
|
||||
if current and not current_hand:
|
||||
return false
|
||||
|
||||
# Not valid if grabbing the same named handle
|
||||
if handle and current_hand and handle == current_hand.handle:
|
||||
return false
|
||||
|
||||
# Not valid if attempting PRIMARY grab while current is PRIMARY
|
||||
if mode == Mode.PRIMARY and current_hand and current_hand.mode == Mode.PRIMARY:
|
||||
return false
|
||||
|
||||
# Not valid if attempting SECONDARY grab while no current
|
||||
if mode == Mode.SECONDARY and not current_hand:
|
||||
return false
|
||||
|
||||
# Hand is allowed to grab
|
||||
return true
|
||||
|
||||
|
||||
# Get the controller associated with a grabber
|
||||
static func _get_grabber_controller(grabber : Node3D) -> XRController3D:
|
||||
# Ensure the grabber is valid
|
||||
if not is_instance_valid(grabber):
|
||||
return null
|
||||
|
||||
# Ensure the pickup is a function pickup for a controller
|
||||
var pickup := grabber as XRToolsFunctionPickup
|
||||
if not pickup:
|
||||
return null
|
||||
|
||||
# Get the controller associated with the pickup
|
||||
return pickup.get_controller()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dx3swqg2owe4p
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://c25yxb0vt53vc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dx3swqg2owe4p" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand.gd" id="1"]
|
||||
|
||||
[node name="GrabPointHandLeft" type="Marker3D"]
|
||||
visible = false
|
||||
script = ExtResource("1")
|
||||
hand_offset_mode = null
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ctw7nbntd5pcj"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dx3swqg2owe4p" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand.gd" id="1"]
|
||||
|
||||
[node name="GrabPointHandRight" type="Marker3D"]
|
||||
visible = false
|
||||
script = ExtResource("1")
|
||||
hand = 1
|
||||
@@ -0,0 +1,17 @@
|
||||
@tool
|
||||
class_name XRToolsGrabPointRedirect
|
||||
extends XRToolsGrabPoint
|
||||
|
||||
|
||||
## Grab point to redirect grabbing to
|
||||
@export var target : XRToolsGrabPoint
|
||||
|
||||
|
||||
## Evaluate fitness of the proposed grab, with 0.0 for not allowed.
|
||||
func can_grab(grabber : Node3D, current : XRToolsGrabPoint) -> float:
|
||||
# Fail if no target
|
||||
if not is_instance_valid(target):
|
||||
return 0.0
|
||||
|
||||
# Consult the target
|
||||
return target.can_grab(grabber, current)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdrjykm7xjl0n
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ca3daqmpo0tua"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cdrjykm7xjl0n" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_redirect.gd" id="1_vyuch"]
|
||||
|
||||
[node name="GrabPointRedirect" type="Marker3D"]
|
||||
script = ExtResource("1_vyuch")
|
||||
49
addons/godot-xr-tools/objects/grab_points/grab_point_snap.gd
Normal file
49
addons/godot-xr-tools/objects/grab_points/grab_point_snap.gd
Normal file
@@ -0,0 +1,49 @@
|
||||
@tool
|
||||
class_name XRToolsGrabPointSnap
|
||||
extends XRToolsGrabPoint
|
||||
|
||||
|
||||
## XR Tools Grab Point Snap Script
|
||||
##
|
||||
## This script allows specifying a grab point for snap zones. It supports
|
||||
## group-filters if different points are required for different snap zones.
|
||||
|
||||
|
||||
## Require grab-by to be in the specified group
|
||||
@export var require_group : String = ""
|
||||
|
||||
## Deny grab-by if in the specified group
|
||||
@export var exclude_group : String = ""
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Add a Position3D child to help editor visibility
|
||||
if Engine.is_editor_hint():
|
||||
add_child(Marker3D.new())
|
||||
|
||||
|
||||
## Evaluate fitness of the proposed grab, with 0.0 for not allowed.
|
||||
func can_grab(grabber : Node3D, current : XRToolsGrabPoint) -> float:
|
||||
# Skip if not enabled or current grab
|
||||
if not enabled or current:
|
||||
return 0.0
|
||||
|
||||
# Ensure the pickup is valid
|
||||
if not is_instance_valid(grabber):
|
||||
return 0.0
|
||||
|
||||
# Ensure the grabber is a snap-zone
|
||||
if not grabber is XRToolsSnapZone:
|
||||
return 0.0
|
||||
|
||||
# Refuse if the grabber is not in the required group
|
||||
if not require_group.is_empty() and not grabber.is_in_group(require_group):
|
||||
return 0.0
|
||||
|
||||
# Refuse if the grabber is in the excluded group
|
||||
if not exclude_group.is_empty() and grabber.is_in_group(exclude_group):
|
||||
return 0.0
|
||||
|
||||
# Return the distance-weighted fitness
|
||||
return _weight(grabber)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dn7grmvmgjiqv
|
||||
@@ -0,0 +1,7 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dh8grd7s3n8kg"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dn7grmvmgjiqv" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_snap.gd" id="1"]
|
||||
|
||||
[node name="GrabPointSnap" type="Marker3D"]
|
||||
visible = false
|
||||
script = ExtResource("1")
|
||||
32
addons/godot-xr-tools/objects/grab_points/grabber.gd
Normal file
32
addons/godot-xr-tools/objects/grab_points/grabber.gd
Normal file
@@ -0,0 +1,32 @@
|
||||
class_name Grabber
|
||||
|
||||
|
||||
## Grabber Class
|
||||
##
|
||||
## This class contains relevant information for a grabber including any
|
||||
## assocated pickup, controller, and hand nodes.
|
||||
|
||||
|
||||
## Grabber node
|
||||
var by : Node3D
|
||||
|
||||
## Pickup associated with the grabber
|
||||
var pickup : XRToolsFunctionPickup
|
||||
|
||||
## Controller associated with the grabber
|
||||
var controller : XRController3D
|
||||
|
||||
## Hand associated with the grabber
|
||||
var hand : XRToolsHand
|
||||
|
||||
## Collision hand associated with the grabber
|
||||
var collision_hand : XRToolsCollisionHand
|
||||
|
||||
## Initialize the grabber
|
||||
func _init(p_by : Node3D) -> void:
|
||||
by = p_by
|
||||
pickup = p_by as XRToolsFunctionPickup
|
||||
controller = pickup.get_controller() if pickup else null
|
||||
if controller:
|
||||
hand = XRToolsHand.find_instance(controller)
|
||||
collision_hand = XRToolsCollisionHand.find_instance(controller)
|
||||
1
addons/godot-xr-tools/objects/grab_points/grabber.gd.uid
Normal file
1
addons/godot-xr-tools/objects/grab_points/grabber.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bu86qn5rnylas
|
||||
37
addons/godot-xr-tools/objects/hand_pose_area.gd
Normal file
37
addons/godot-xr-tools/objects/hand_pose_area.gd
Normal file
@@ -0,0 +1,37 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
|
||||
class_name XRToolsHandPoseArea
|
||||
extends Area3D
|
||||
|
||||
|
||||
## XR Tools Hand Pose Area
|
||||
##
|
||||
## This area works with the XRToolsFunctionPoseArea to control the pose
|
||||
## of the VR hands.
|
||||
|
||||
|
||||
## Priority level for this hand pose area
|
||||
@export var pose_priority : int
|
||||
|
||||
## Left hand pose settings (XRToolsHandPoseSettings)
|
||||
@export var left_pose : XRToolsHandPoseSettings
|
||||
|
||||
## Right hand pose settings (XRToolsHandPoseSettings)
|
||||
@export var right_pose : XRToolsHandPoseSettings
|
||||
|
||||
## Array of grabpoints this hand pose area disables when active
|
||||
@export var grabpoints : Array[XRToolsGrabPointHand]
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsHandPoseArea"
|
||||
|
||||
# Disables grabpoints
|
||||
func disable_grab_points():
|
||||
for grabpoint in grabpoints:
|
||||
grabpoint.enabled = false
|
||||
|
||||
# Enables grabpoints
|
||||
func enable_grab_points():
|
||||
for grabpoint in grabpoints:
|
||||
grabpoint.enabled = true
|
||||
1
addons/godot-xr-tools/objects/hand_pose_area.gd.uid
Normal file
1
addons/godot-xr-tools/objects/hand_pose_area.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://colsb5moktl1e
|
||||
9
addons/godot-xr-tools/objects/hand_pose_area.tscn
Normal file
9
addons/godot-xr-tools/objects/hand_pose_area.tscn
Normal file
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dc5t2qgmhb2nf"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://colsb5moktl1e" path="res://addons/godot-xr-tools/objects/hand_pose_area.gd" id="1"]
|
||||
|
||||
[node name="HandPoseArea" type="Area3D"]
|
||||
collision_layer = 2097152
|
||||
collision_mask = 0
|
||||
monitoring = false
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,57 @@
|
||||
@tool
|
||||
class_name XRToolsHighlightMaterial
|
||||
extends Node
|
||||
|
||||
|
||||
## Mesh to highlight
|
||||
@export var highlight_mesh_instance : NodePath
|
||||
|
||||
## Material to set
|
||||
@export var highlight_material : Resource
|
||||
|
||||
|
||||
var _original_materials = Array()
|
||||
var _highlight_mesh_instance: MeshInstance3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsHighlightMaterial"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Get the mesh to highlight
|
||||
_highlight_mesh_instance = get_node(highlight_mesh_instance)
|
||||
|
||||
# Save the materials
|
||||
if _highlight_mesh_instance:
|
||||
# if we can find a node remember which materials are currently set on each surface
|
||||
for i in range(0, _highlight_mesh_instance.get_surface_override_material_count()):
|
||||
_original_materials.push_back(_highlight_mesh_instance.get_surface_override_material(i))
|
||||
|
||||
# Hook the highlight update
|
||||
get_parent().connect("highlight_updated", _on_highlight_updated)
|
||||
|
||||
|
||||
# Called when the pickable highlight changes
|
||||
func _on_highlight_updated(_pickable, enable: bool) -> void:
|
||||
# Set the materials
|
||||
if _highlight_mesh_instance:
|
||||
for i in range(0, _highlight_mesh_instance.get_surface_override_material_count()):
|
||||
if enable:
|
||||
_highlight_mesh_instance.set_surface_override_material(i, highlight_material)
|
||||
else:
|
||||
_highlight_mesh_instance.set_surface_override_material(i, _original_materials[i])
|
||||
|
||||
|
||||
# This method verifies the node
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Verify parent supports highlighting
|
||||
var parent := get_parent()
|
||||
if not parent or not parent.has_signal("highlight_updated"):
|
||||
warnings.append("Parent does not support highlighting")
|
||||
|
||||
return warnings
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdl5kcr2wvnav
|
||||
35
addons/godot-xr-tools/objects/highlight/highlight_ring.gd
Normal file
35
addons/godot-xr-tools/objects/highlight/highlight_ring.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
@tool
|
||||
class_name XRToolsHighlightRing
|
||||
extends MeshInstance3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsHighlightRing"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Turn off until requested
|
||||
if not Engine.is_editor_hint():
|
||||
visible = false
|
||||
|
||||
# Hook the highlight update
|
||||
get_parent().connect("highlight_updated", _on_highlight_updated)
|
||||
|
||||
|
||||
# Called when the pickable highlight changes
|
||||
func _on_highlight_updated(_pickable, enable: bool) -> void:
|
||||
visible = enable
|
||||
|
||||
|
||||
# This method verifies the node
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Verify parent supports highlighting
|
||||
var parent := get_parent()
|
||||
if not parent or not parent.has_signal("highlight_updated"):
|
||||
warnings.append("Parent does not support highlighting")
|
||||
|
||||
return warnings
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8waaso7b6bsr
|
||||
10
addons/godot-xr-tools/objects/highlight/highlight_ring.tres
Normal file
10
addons/godot-xr-tools/objects/highlight/highlight_ring.tres
Normal file
@@ -0,0 +1,10 @@
|
||||
[gd_resource type="StandardMaterial3D" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot-xr-tools/images/ring.png" type="Texture2D" id=1]
|
||||
|
||||
[resource]
|
||||
flags_transparent = true
|
||||
flags_unshaded = true
|
||||
flags_no_depth_test = true
|
||||
params_billboard_mode = 1
|
||||
albedo_texture = ExtResource( 1 )
|
||||
12
addons/godot-xr-tools/objects/highlight/highlight_ring.tscn
Normal file
12
addons/godot-xr-tools/objects/highlight/highlight_ring.tscn
Normal file
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://da2qgxxwwitl6"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b8waaso7b6bsr" path="res://addons/godot-xr-tools/objects/highlight/highlight_ring.gd" id="1"]
|
||||
[ext_resource type="Material" path="res://addons/godot-xr-tools/objects/highlight/highlight_ring.tres" id="2"]
|
||||
|
||||
[sub_resource type="QuadMesh" id="1"]
|
||||
size = Vector2(0.05, 0.05)
|
||||
|
||||
[node name="HighlightRing" type="MeshInstance3D"]
|
||||
mesh = SubResource("1")
|
||||
surface_material_override/0 = ExtResource("2")
|
||||
script = ExtResource("1")
|
||||
35
addons/godot-xr-tools/objects/highlight/highlight_visible.gd
Normal file
35
addons/godot-xr-tools/objects/highlight/highlight_visible.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
@tool
|
||||
class_name XRToolsHighlightVisible
|
||||
extends Node3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsHighlightVisible"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Turn off until requested
|
||||
if not Engine.is_editor_hint():
|
||||
visible = false
|
||||
|
||||
# Hook the highlight update
|
||||
get_parent().connect("highlight_updated", _on_highlight_updated)
|
||||
|
||||
|
||||
# Called when the pickable highlight changes
|
||||
func _on_highlight_updated(_pickable, enable: bool) -> void:
|
||||
visible = enable
|
||||
|
||||
|
||||
# This method verifies the node
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Verify parent supports highlighting
|
||||
var parent := get_parent()
|
||||
if not parent or not parent.has_signal("highlight_updated"):
|
||||
warnings.append("Parent does not support highlighting")
|
||||
|
||||
return warnings
|
||||
@@ -0,0 +1 @@
|
||||
uid://0b7wkxi4vymb
|
||||
12
addons/godot-xr-tools/objects/interactable_area.gd
Normal file
12
addons/godot-xr-tools/objects/interactable_area.gd
Normal file
@@ -0,0 +1,12 @@
|
||||
@tool
|
||||
class_name XRToolsInteractableArea
|
||||
extends Area3D
|
||||
|
||||
|
||||
## Signal when pointer event occurs on area
|
||||
signal pointer_event(event)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsInteractableArea"
|
||||
1
addons/godot-xr-tools/objects/interactable_area.gd.uid
Normal file
1
addons/godot-xr-tools/objects/interactable_area.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://e7gdpxabj45k
|
||||
7
addons/godot-xr-tools/objects/interactable_body.gd
Normal file
7
addons/godot-xr-tools/objects/interactable_body.gd
Normal file
@@ -0,0 +1,7 @@
|
||||
class_name XRToolsInteractableBody
|
||||
extends Node3D
|
||||
# This should extend from PhysicsBody3D but https://github.com/godotengine/godot/issues/46073
|
||||
|
||||
|
||||
## Signal when pointer event occurs on body
|
||||
signal pointer_event(event)
|
||||
1
addons/godot-xr-tools/objects/interactable_body.gd.uid
Normal file
1
addons/godot-xr-tools/objects/interactable_body.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bcxvnq76xbncx
|
||||
150
addons/godot-xr-tools/objects/keyboard/virtual_key.gd
Normal file
150
addons/godot-xr-tools/objects/keyboard/virtual_key.gd
Normal file
@@ -0,0 +1,150 @@
|
||||
@tool
|
||||
class_name XRToolsVirtualKey
|
||||
extends Node2D
|
||||
|
||||
|
||||
## Key pressed event
|
||||
signal pressed
|
||||
|
||||
## Key released event
|
||||
signal released
|
||||
|
||||
|
||||
## Key location
|
||||
@export var key_size := Vector2(32, 32) : set = _set_key_size
|
||||
|
||||
## Key text
|
||||
@export var key_text := "" : set = _set_key_text
|
||||
|
||||
## Key normal color
|
||||
@export var key_normal := Color(0.1, 0.1, 0.1) : set = _set_key_normal
|
||||
|
||||
## Key highlight color
|
||||
@export var key_highlight := Color(0.2, 0.2, 0.2) : set = _set_key_highlight
|
||||
|
||||
## Text normal color
|
||||
@export var text_normal := Color(1.0, 1.0, 1.0) : set = _set_text_normal
|
||||
|
||||
## Text highlight color
|
||||
@export var text_highlight := Color(0.0, 0.0, 0.0) : set = _set_text_highlight
|
||||
|
||||
## Key highlighted
|
||||
@export var highlighted := false : set = _set_highlighted
|
||||
|
||||
|
||||
# TouchScreenButton node
|
||||
var _button : TouchScreenButton
|
||||
|
||||
# ColorRect node
|
||||
var _color : ColorRect
|
||||
|
||||
# Label node
|
||||
var _label : Label
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Construct the ColorRect node
|
||||
_color = ColorRect.new()
|
||||
|
||||
# Construct the Label node
|
||||
_label = Label.new()
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
|
||||
# Construct the TouchScreenButton node
|
||||
_button = TouchScreenButton.new()
|
||||
_button.shape = RectangleShape2D.new()
|
||||
|
||||
# Attach the nodes
|
||||
_color.add_child(_label)
|
||||
_button.add_child(_color)
|
||||
add_child(_button)
|
||||
|
||||
# Handle button presses
|
||||
_button.pressed.connect(_on_button_pressed)
|
||||
_button.released.connect(_on_button_released)
|
||||
|
||||
# Apply initial updates
|
||||
_update_key_size()
|
||||
_update_key_text()
|
||||
_update_highlighted()
|
||||
|
||||
|
||||
func _on_button_pressed() -> void:
|
||||
pressed.emit()
|
||||
|
||||
|
||||
func _on_button_released() -> void:
|
||||
released.emit()
|
||||
|
||||
|
||||
func _set_key_size(p_key_size : Vector2) -> void:
|
||||
key_size = p_key_size
|
||||
if is_inside_tree():
|
||||
_update_key_size()
|
||||
|
||||
|
||||
func _set_key_text(p_key_text : String) -> void:
|
||||
key_text = p_key_text
|
||||
if is_inside_tree():
|
||||
_update_key_text()
|
||||
|
||||
|
||||
func _set_key_normal(p_key_normal : Color) -> void:
|
||||
key_normal = p_key_normal
|
||||
if is_inside_tree():
|
||||
_update_highlighted()
|
||||
|
||||
|
||||
func _set_key_highlight(p_key_highlight : Color) -> void:
|
||||
key_highlight = p_key_highlight
|
||||
if is_inside_tree():
|
||||
_update_highlighted()
|
||||
|
||||
|
||||
func _set_text_normal(p_text_normal : Color) -> void:
|
||||
text_normal = p_text_normal
|
||||
if is_inside_tree():
|
||||
_update_highlighted()
|
||||
|
||||
|
||||
func _set_text_highlight(p_text_highlight : Color) -> void:
|
||||
text_highlight = p_text_highlight
|
||||
if is_inside_tree():
|
||||
_update_highlighted()
|
||||
|
||||
|
||||
func _set_highlighted(p_highlighted : bool) -> void:
|
||||
highlighted = p_highlighted
|
||||
if is_inside_tree():
|
||||
_update_highlighted()
|
||||
|
||||
|
||||
func _update_key_size() -> void:
|
||||
var half_size := key_size / 2
|
||||
|
||||
# Set the TouchScreenButton position and shape size
|
||||
_button.position = half_size
|
||||
_button.shape.size = key_size
|
||||
|
||||
# Size and position the ColorRect
|
||||
_color.size = key_size
|
||||
_color.position = -half_size
|
||||
|
||||
# Size the label
|
||||
_label.size = key_size
|
||||
|
||||
|
||||
func _update_key_text() -> void:
|
||||
_label.text = key_text
|
||||
|
||||
|
||||
func _update_highlighted() -> void:
|
||||
# Pick colors
|
||||
var key := key_highlight if highlighted else key_normal
|
||||
var text := text_highlight if highlighted else text_normal
|
||||
|
||||
# Set colors
|
||||
_color.color = key
|
||||
_label.add_theme_color_override("font_color", text)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bk7d6s38ejigt
|
||||
45
addons/godot-xr-tools/objects/keyboard/virtual_key_char.gd
Normal file
45
addons/godot-xr-tools/objects/keyboard/virtual_key_char.gd
Normal file
@@ -0,0 +1,45 @@
|
||||
@tool
|
||||
class_name XRToolsVirtualKeyChar
|
||||
extends XRToolsVirtualKey
|
||||
|
||||
|
||||
## Godot scan-code text
|
||||
@export var scan_code_text := ""
|
||||
|
||||
## Unicode character
|
||||
@export var unicode := 0
|
||||
|
||||
## Shift modifier
|
||||
@export var shift_modifier := false
|
||||
|
||||
|
||||
# Keyboard associated with this button
|
||||
var _keyboard : XRToolsVirtualKeyboard2D
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Call the base
|
||||
super()
|
||||
|
||||
# Find the keyboard
|
||||
_keyboard = XRTools.find_xr_ancestor(
|
||||
self,
|
||||
"*",
|
||||
"XRToolsVirtualKeyboard2D")
|
||||
|
||||
# Handle button presses
|
||||
pressed.connect(_on_pressed)
|
||||
released.connect(_on_released)
|
||||
|
||||
|
||||
# Handler for button pressed
|
||||
func _on_pressed() -> void:
|
||||
highlighted = true
|
||||
if _keyboard:
|
||||
_keyboard.on_key_pressed(scan_code_text, unicode, shift_modifier)
|
||||
|
||||
|
||||
# Handler for button released
|
||||
func _on_released() -> void:
|
||||
highlighted = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://tae1jc8su0i6
|
||||
102
addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.gd
Normal file
102
addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.gd
Normal file
@@ -0,0 +1,102 @@
|
||||
@tool
|
||||
class_name XRToolsVirtualKeyboard2D
|
||||
extends CanvasLayer
|
||||
|
||||
|
||||
## Enumeration of keyboard view modes
|
||||
enum KeyboardMode {
|
||||
LOWER_CASE, ## Lower-case keys mode
|
||||
UPPER_CASE, ## Upper-case keys mode
|
||||
ALTERNATE ## Alternate keys mode
|
||||
}
|
||||
|
||||
|
||||
# Shift button down
|
||||
var _shift_down := false
|
||||
|
||||
# Caps button down
|
||||
var _caps_down := false
|
||||
|
||||
# Alt button down
|
||||
var _alt_down := false
|
||||
|
||||
# Current keyboard mode
|
||||
var _mode: int = KeyboardMode.LOWER_CASE
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsVirtualKeyboard2D"
|
||||
|
||||
|
||||
# Handle key pressed from VirtualKey
|
||||
func on_key_pressed(scan_code_text: String, unicode: int, shift: bool):
|
||||
# Find the scan code
|
||||
var scan_code := OS.find_keycode_from_string(scan_code_text)
|
||||
|
||||
# Create the InputEventKey
|
||||
var input := InputEventKey.new()
|
||||
input.physical_keycode = scan_code
|
||||
input.unicode = unicode if unicode else scan_code
|
||||
input.pressed = true
|
||||
input.keycode = scan_code
|
||||
input.shift_pressed = shift
|
||||
|
||||
# Dispatch the input event
|
||||
Input.parse_input_event(input)
|
||||
|
||||
# Pop any temporary shift key
|
||||
if _shift_down:
|
||||
_shift_down = false
|
||||
_update_visible()
|
||||
|
||||
|
||||
func _on_toggle_shift_pressed() -> void:
|
||||
# Update toggle keys
|
||||
_shift_down = not _shift_down
|
||||
_caps_down = false
|
||||
_alt_down = false
|
||||
_update_visible()
|
||||
|
||||
|
||||
func _on_toggle_caps_pressed() -> void:
|
||||
# Update toggle keys
|
||||
_caps_down = not _caps_down
|
||||
_shift_down = false
|
||||
_alt_down = false
|
||||
_update_visible()
|
||||
|
||||
|
||||
func _on_toggle_alt_pressed() -> void:
|
||||
# Update toggle keys
|
||||
_alt_down = not _alt_down
|
||||
_shift_down = false
|
||||
_caps_down = false
|
||||
_update_visible()
|
||||
|
||||
|
||||
# Update switching the visible case keys
|
||||
func _update_visible() -> void:
|
||||
# Ensure the control buttons are set correctly
|
||||
$Background/Standard/ToggleShift.highlighted = _shift_down
|
||||
$Background/Standard/ToggleCaps.highlighted = _caps_down
|
||||
$Background/Standard/ToggleAlt.highlighted = _alt_down
|
||||
|
||||
# Evaluate the new mode
|
||||
var new_mode: int
|
||||
if _alt_down:
|
||||
new_mode = KeyboardMode.ALTERNATE
|
||||
elif _shift_down or _caps_down:
|
||||
new_mode = KeyboardMode.UPPER_CASE
|
||||
else:
|
||||
new_mode = KeyboardMode.LOWER_CASE
|
||||
|
||||
# Skip if no change
|
||||
if new_mode == _mode:
|
||||
return
|
||||
|
||||
# Update the visible mode
|
||||
_mode = new_mode
|
||||
$Background/LowerCase.visible = _mode == KeyboardMode.LOWER_CASE
|
||||
$Background/UpperCase.visible = _mode == KeyboardMode.UPPER_CASE
|
||||
$Background/Alternate.visible = _mode == KeyboardMode.ALTERNATE
|
||||
@@ -0,0 +1 @@
|
||||
uid://phmvrwkudp7u
|
||||
724
addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.tscn
Normal file
724
addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.tscn
Normal file
@@ -0,0 +1,724 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://lauwp8okd1vh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://phmvrwkudp7u" path="res://addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://tae1jc8su0i6" path="res://addons/godot-xr-tools/objects/keyboard/virtual_key_char.gd" id="2_n0nlg"]
|
||||
[ext_resource type="Script" uid="uid://bk7d6s38ejigt" path="res://addons/godot-xr-tools/objects/keyboard/virtual_key.gd" id="3_h05ve"]
|
||||
|
||||
[node name="VirtualKeyboard2D" type="CanvasLayer"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
offset_right = 400.0
|
||||
offset_bottom = 200.0
|
||||
color = Color(0.12549, 0.12549, 0.12549, 0.752941)
|
||||
|
||||
[node name="Standard" type="Control" parent="Background"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 0
|
||||
offset_left = 4.0
|
||||
offset_top = 4.0
|
||||
offset_right = 394.0
|
||||
offset_bottom = 149.0
|
||||
|
||||
[node name="VirtualKey1" type="Node2D" parent="Background/Standard"]
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "1"
|
||||
unicode = 49
|
||||
key_text = "1"
|
||||
|
||||
[node name="VirtualKey2" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(40, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "2"
|
||||
unicode = 50
|
||||
key_text = "2"
|
||||
|
||||
[node name="VirtualKey3" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(80, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "3"
|
||||
unicode = 51
|
||||
key_text = "3"
|
||||
|
||||
[node name="VirtualKey4" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(120, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "4"
|
||||
unicode = 52
|
||||
key_text = "4"
|
||||
|
||||
[node name="VirtualKey5" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(160, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "5"
|
||||
unicode = 53
|
||||
key_text = "5"
|
||||
|
||||
[node name="VirtualKey6" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(200, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "6"
|
||||
unicode = 54
|
||||
key_text = "6"
|
||||
|
||||
[node name="VirtualKey7" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(240, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "7"
|
||||
unicode = 55
|
||||
key_text = "7"
|
||||
|
||||
[node name="VirtualKey8" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(280, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "8"
|
||||
unicode = 56
|
||||
key_text = "8"
|
||||
|
||||
[node name="VirtualKey9" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(320, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "9"
|
||||
unicode = 57
|
||||
key_text = "9"
|
||||
|
||||
[node name="VirtualKey0" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(360, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "0"
|
||||
unicode = 48
|
||||
key_text = "0"
|
||||
|
||||
[node name="ToggleShift" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(0, 120)
|
||||
script = ExtResource("3_h05ve")
|
||||
key_size = Vector2(50, 32)
|
||||
key_text = "SHIFT"
|
||||
key_highlight = Color(1, 1, 1, 1)
|
||||
|
||||
[node name="ToggleCaps" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(0, 160)
|
||||
script = ExtResource("3_h05ve")
|
||||
key_size = Vector2(45, 32)
|
||||
key_text = "CAPS"
|
||||
key_highlight = Color(1, 1, 1, 1)
|
||||
|
||||
[node name="ToggleAlt" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(55, 160)
|
||||
script = ExtResource("3_h05ve")
|
||||
key_size = Vector2(35, 32)
|
||||
key_text = "ALT"
|
||||
key_highlight = Color(1, 1, 1, 1)
|
||||
|
||||
[node name="VirtualKeyBackspace" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(340, 120)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "BackSpace"
|
||||
key_size = Vector2(52, 32)
|
||||
key_text = "BKSP"
|
||||
|
||||
[node name="VirtualKeySpace" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(100, 160)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Space"
|
||||
unicode = 32
|
||||
key_size = Vector2(190, 32)
|
||||
key_text = "Space"
|
||||
|
||||
[node name="VirtualKeyPeriod" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(300, 160)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Period"
|
||||
unicode = 46
|
||||
key_text = "."
|
||||
|
||||
[node name="VirtualKeyEnter" type="Node2D" parent="Background/Standard"]
|
||||
position = Vector2(340, 160)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Enter"
|
||||
key_size = Vector2(52, 32)
|
||||
key_text = "Enter"
|
||||
|
||||
[node name="LowerCase" type="Node2D" parent="Background"]
|
||||
position = Vector2(4, 44)
|
||||
|
||||
[node name="VirtualKeyLowerQ" type="Node2D" parent="Background/LowerCase"]
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Q"
|
||||
unicode = 113
|
||||
key_text = "q"
|
||||
|
||||
[node name="VirtualKeyLowerW" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(40, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "W"
|
||||
unicode = 119
|
||||
key_text = "w"
|
||||
|
||||
[node name="VirtualKeyLowerE" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(80, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "E"
|
||||
unicode = 101
|
||||
key_text = "e"
|
||||
|
||||
[node name="VirtualKeyLowerR" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(120, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "R"
|
||||
unicode = 114
|
||||
key_text = "r"
|
||||
|
||||
[node name="VirtualKeyLowerT" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(160, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "T"
|
||||
unicode = 116
|
||||
key_text = "t"
|
||||
|
||||
[node name="VirtualKeyLowerY" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(200, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Y"
|
||||
unicode = 121
|
||||
key_text = "y"
|
||||
|
||||
[node name="VirtualKeyLowerU" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(240, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "U"
|
||||
unicode = 117
|
||||
key_text = "u"
|
||||
|
||||
[node name="VirtualKeyLowerI" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(280, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "I"
|
||||
unicode = 105
|
||||
key_text = "i"
|
||||
|
||||
[node name="VirtualKeyLowerO" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(320, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "O"
|
||||
unicode = 111
|
||||
key_text = "o"
|
||||
|
||||
[node name="VirtualKeyLowerP" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(360, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "P"
|
||||
unicode = 112
|
||||
key_text = "p"
|
||||
|
||||
[node name="VirtualKeyLowerA" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(20, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "A"
|
||||
unicode = 97
|
||||
key_text = "a"
|
||||
|
||||
[node name="VirtualKeyLowerS" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(60, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "S"
|
||||
unicode = 115
|
||||
key_text = "s"
|
||||
|
||||
[node name="VirtualKeyLowerD" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(100, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "D"
|
||||
unicode = 100
|
||||
key_text = "d"
|
||||
|
||||
[node name="VirtualKeyLowerF" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(140, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "F"
|
||||
unicode = 102
|
||||
key_text = "f"
|
||||
|
||||
[node name="VirtualKeyLowerG" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(180, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "G"
|
||||
unicode = 103
|
||||
key_text = "g"
|
||||
|
||||
[node name="VirtualKeyLowerH" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(220, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "H"
|
||||
unicode = 104
|
||||
key_text = "h"
|
||||
|
||||
[node name="VirtualKeyLowerJ" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(260, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "J"
|
||||
unicode = 106
|
||||
key_text = "j"
|
||||
|
||||
[node name="VirtualKeyLowerK" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(300, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "K"
|
||||
unicode = 107
|
||||
key_text = "k"
|
||||
|
||||
[node name="VirtualKeyLowerL" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(340, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "L"
|
||||
unicode = 108
|
||||
key_text = "l"
|
||||
|
||||
[node name="VirtualKeyLowerZ" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(60, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Z"
|
||||
unicode = 122
|
||||
key_text = "z"
|
||||
|
||||
[node name="VirtualKeyLowerX" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(100, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "X"
|
||||
unicode = 120
|
||||
key_text = "x"
|
||||
|
||||
[node name="VirtualKeyLowerC" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(140, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "C"
|
||||
unicode = 99
|
||||
key_text = "c"
|
||||
|
||||
[node name="VirtualKeyLowerV" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(180, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "V"
|
||||
unicode = 118
|
||||
key_text = "v"
|
||||
|
||||
[node name="VirtualKeyLowerB" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(220, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "B"
|
||||
unicode = 98
|
||||
key_text = "b"
|
||||
|
||||
[node name="VirtualKeyLowerN" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(260, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "N"
|
||||
unicode = 110
|
||||
key_text = "n"
|
||||
|
||||
[node name="VirtualKeyLowerM" type="Node2D" parent="Background/LowerCase"]
|
||||
position = Vector2(300, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "M"
|
||||
unicode = 109
|
||||
key_text = "m"
|
||||
|
||||
[node name="UpperCase" type="Node2D" parent="Background"]
|
||||
visible = false
|
||||
position = Vector2(4, 44)
|
||||
|
||||
[node name="VirtualKeyUpperQ" type="Node2D" parent="Background/UpperCase"]
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Q"
|
||||
unicode = 81
|
||||
shift_modifier = true
|
||||
key_text = "Q"
|
||||
|
||||
[node name="VirtualKeyUpperW" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(40, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "W"
|
||||
unicode = 87
|
||||
shift_modifier = true
|
||||
key_text = "W"
|
||||
|
||||
[node name="VirtualKeyUpperE" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(80, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "E"
|
||||
unicode = 69
|
||||
shift_modifier = true
|
||||
key_text = "E"
|
||||
|
||||
[node name="VirtualKeyUpperR" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(120, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "R"
|
||||
unicode = 82
|
||||
shift_modifier = true
|
||||
key_text = "R"
|
||||
|
||||
[node name="VirtualKeyUpperT" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(160, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "T"
|
||||
unicode = 84
|
||||
shift_modifier = true
|
||||
key_text = "T"
|
||||
|
||||
[node name="VirtualKeyUpperY" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(200, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Y"
|
||||
unicode = 89
|
||||
shift_modifier = true
|
||||
key_text = "Y"
|
||||
|
||||
[node name="VirtualKeyUpperU" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(240, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "U"
|
||||
unicode = 85
|
||||
shift_modifier = true
|
||||
key_text = "U"
|
||||
|
||||
[node name="VirtualKeyUpperI" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(280, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "I"
|
||||
unicode = 73
|
||||
shift_modifier = true
|
||||
key_text = "I"
|
||||
|
||||
[node name="VirtualKeyUpperO" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(320, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "O"
|
||||
unicode = 79
|
||||
shift_modifier = true
|
||||
key_text = "O"
|
||||
|
||||
[node name="VirtualKeyUpperP" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(360, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "P"
|
||||
unicode = 80
|
||||
shift_modifier = true
|
||||
key_text = "P"
|
||||
|
||||
[node name="VirtualKeyUpperA" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(20, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "A"
|
||||
unicode = 65
|
||||
shift_modifier = true
|
||||
key_text = "A"
|
||||
|
||||
[node name="VirtualKeyUpperS" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(60, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "S"
|
||||
unicode = 83
|
||||
shift_modifier = true
|
||||
key_text = "S"
|
||||
|
||||
[node name="VirtualKeyUpperD" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(100, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "D"
|
||||
unicode = 68
|
||||
shift_modifier = true
|
||||
key_text = "D"
|
||||
|
||||
[node name="VirtualKeyUpperF" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(140, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "F"
|
||||
unicode = 70
|
||||
shift_modifier = true
|
||||
key_text = "F"
|
||||
|
||||
[node name="VirtualKeyUpperG" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(180, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "G"
|
||||
unicode = 71
|
||||
shift_modifier = true
|
||||
key_text = "G"
|
||||
|
||||
[node name="VirtualKeyUpperH" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(220, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "H"
|
||||
unicode = 72
|
||||
shift_modifier = true
|
||||
key_text = "H"
|
||||
|
||||
[node name="VirtualKeyUpperJ" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(260, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "J"
|
||||
unicode = 74
|
||||
shift_modifier = true
|
||||
key_text = "J"
|
||||
|
||||
[node name="VirtualKeyUpperK" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(300, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "K"
|
||||
unicode = 75
|
||||
shift_modifier = true
|
||||
key_text = "K"
|
||||
|
||||
[node name="VirtualKeyUpperL" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(340, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "L"
|
||||
unicode = 76
|
||||
shift_modifier = true
|
||||
key_text = "L"
|
||||
|
||||
[node name="VirtualKeyUpperZ" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(60, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Z"
|
||||
unicode = 90
|
||||
shift_modifier = true
|
||||
key_text = "Z"
|
||||
|
||||
[node name="VirtualKeyUpperX" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(100, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "X"
|
||||
unicode = 88
|
||||
shift_modifier = true
|
||||
key_text = "X"
|
||||
|
||||
[node name="VirtualKeyUpperC" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(140, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "C"
|
||||
unicode = 67
|
||||
shift_modifier = true
|
||||
key_text = "C"
|
||||
|
||||
[node name="VirtualKeyUpperV" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(180, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "V"
|
||||
unicode = 86
|
||||
shift_modifier = true
|
||||
key_text = "V"
|
||||
|
||||
[node name="VirtualKeyUpperB" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(220, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "B"
|
||||
unicode = 66
|
||||
shift_modifier = true
|
||||
key_text = "B"
|
||||
|
||||
[node name="VirtualKeyUpperN" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(260, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "N"
|
||||
unicode = 78
|
||||
shift_modifier = true
|
||||
key_text = "N"
|
||||
|
||||
[node name="VirtualKeyUpperM" type="Node2D" parent="Background/UpperCase"]
|
||||
position = Vector2(300, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "M"
|
||||
unicode = 77
|
||||
shift_modifier = true
|
||||
key_text = "M"
|
||||
|
||||
[node name="Alternate" type="Node2D" parent="Background"]
|
||||
visible = false
|
||||
position = Vector2(4, 44)
|
||||
|
||||
[node name="VirtualKeyPlus" type="Node2D" parent="Background/Alternate"]
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Plus"
|
||||
unicode = 43
|
||||
key_text = "+"
|
||||
|
||||
[node name="VirtualKeyAsterisk" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(40, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Asterisk"
|
||||
unicode = 42
|
||||
key_text = "*"
|
||||
|
||||
[node name="VirtualKeyDivision" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(80, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Division"
|
||||
unicode = 247
|
||||
key_text = "÷"
|
||||
|
||||
[node name="VirtualKeyEqual" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(120, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Equal"
|
||||
unicode = 61
|
||||
key_text = "="
|
||||
|
||||
[node name="VirtualKeySlash" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(160, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Slash"
|
||||
unicode = 47
|
||||
key_text = "/"
|
||||
|
||||
[node name="VirtualKeyUnderScore" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(200, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "UnderScore"
|
||||
unicode = 95
|
||||
key_text = "_"
|
||||
|
||||
[node name="VirtualKeyLess" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(240, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Less"
|
||||
unicode = 60
|
||||
key_text = "<"
|
||||
|
||||
[node name="VirtualKeyGreater" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(280, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Greater"
|
||||
unicode = 62
|
||||
key_text = ">"
|
||||
|
||||
[node name="VirtualKeyBracketLeft" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(320, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "BracketLeft"
|
||||
unicode = 91
|
||||
key_text = "["
|
||||
|
||||
[node name="VirtualKeyBracketRight" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(360, 0)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "BracketRight"
|
||||
unicode = 93
|
||||
key_text = "]"
|
||||
|
||||
[node name="VirtualKeyExclam" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(20, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Exclam"
|
||||
unicode = 33
|
||||
key_text = "!"
|
||||
|
||||
[node name="VirtualKeyAt" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(60, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "At"
|
||||
unicode = 64
|
||||
key_text = "@"
|
||||
|
||||
[node name="VirtualKeyNumberSign" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(100, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "NumberSign"
|
||||
unicode = 35
|
||||
key_text = "#"
|
||||
|
||||
[node name="VirtualKeyDollar" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(140, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Dollar"
|
||||
unicode = 36
|
||||
key_text = "$"
|
||||
|
||||
[node name="VirtualKeyPercent" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(180, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Percent"
|
||||
unicode = 37
|
||||
key_text = "%"
|
||||
|
||||
[node name="VirtualKeyCircumflex" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(220, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "AsciiCircum"
|
||||
unicode = 94
|
||||
key_text = "^"
|
||||
|
||||
[node name="VirtualKeyAmpersand" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(260, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Ampersand"
|
||||
unicode = 38
|
||||
key_text = "&"
|
||||
|
||||
[node name="VirtualKeyParenLeft" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(300, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "ParenLeft"
|
||||
unicode = 40
|
||||
key_text = "("
|
||||
|
||||
[node name="VirtualKeyParenRight" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(340, 40)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "ParenRight"
|
||||
unicode = 41
|
||||
key_text = ")"
|
||||
|
||||
[node name="VirtualKeyMinus" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(60, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Minus"
|
||||
unicode = 45
|
||||
key_text = "-"
|
||||
|
||||
[node name="VirtualKeyApostrophe" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(100, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Apostrophe"
|
||||
unicode = 39
|
||||
key_text = "'"
|
||||
|
||||
[node name="VirtualKeyQuoteDbl" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(140, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "QuoteDbl"
|
||||
unicode = 34
|
||||
key_text = "\""
|
||||
|
||||
[node name="VirtualKeyColon" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(180, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Colon"
|
||||
unicode = 58
|
||||
key_text = ":"
|
||||
|
||||
[node name="VirtualKeySemicolon" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(220, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Semicolon"
|
||||
unicode = 59
|
||||
key_text = ";"
|
||||
|
||||
[node name="VirtualKeyComma" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(260, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Comma"
|
||||
unicode = 44
|
||||
key_text = ","
|
||||
|
||||
[node name="VirtualKeyQuestion" type="Node2D" parent="Background/Alternate"]
|
||||
position = Vector2(300, 80)
|
||||
script = ExtResource("2_n0nlg")
|
||||
scan_code_text = "Question"
|
||||
unicode = 63
|
||||
key_text = "?"
|
||||
|
||||
[connection signal="pressed" from="Background/Standard/ToggleShift" to="." method="_on_toggle_shift_pressed"]
|
||||
[connection signal="pressed" from="Background/Standard/ToggleCaps" to="." method="_on_toggle_caps_pressed"]
|
||||
[connection signal="pressed" from="Background/Standard/ToggleAlt" to="." method="_on_toggle_alt_pressed"]
|
||||
439
addons/godot-xr-tools/objects/pickable.gd
Normal file
439
addons/godot-xr-tools/objects/pickable.gd
Normal file
@@ -0,0 +1,439 @@
|
||||
@tool
|
||||
class_name XRToolsPickable
|
||||
extends RigidBody3D
|
||||
|
||||
|
||||
## XR Tools Pickable Object
|
||||
##
|
||||
## This script allows a [RigidBody3D] to be picked up by an
|
||||
## [XRToolsFunctionPickup] attached to a players controller.
|
||||
##
|
||||
## Additionally pickable objects may support being snapped into
|
||||
## [XRToolsSnapZone] areas.
|
||||
##
|
||||
## Grab-points can be defined by adding different types of [XRToolsGrabPoint]
|
||||
## child nodes controlling hand and snap-zone grab locations.
|
||||
|
||||
|
||||
# Signal emitted when this object is picked up (held by a player or snap-zone)
|
||||
signal picked_up(pickable)
|
||||
|
||||
# Signal emitted when this object is dropped
|
||||
signal dropped(pickable)
|
||||
|
||||
# Signal emitted when this object is grabbed (primary or secondary)
|
||||
signal grabbed(pickable, by)
|
||||
|
||||
# Signal emitted when this object is released (primary or secondary)
|
||||
signal released(pickable, by)
|
||||
|
||||
# Signal emitted when the user presses the action button while holding this object
|
||||
signal action_pressed(pickable)
|
||||
|
||||
# Signal emitted when the user releases the action button while holding this object
|
||||
signal action_released(pickable)
|
||||
|
||||
# Signal emitted when the highlight state changes
|
||||
signal highlight_updated(pickable, enable)
|
||||
|
||||
|
||||
## Method used to grab object at range
|
||||
enum RangedMethod {
|
||||
NONE, ## Ranged grab is not supported
|
||||
SNAP, ## Object snaps to holder
|
||||
LERP, ## Object lerps to holder
|
||||
}
|
||||
|
||||
enum ReleaseMode {
|
||||
ORIGINAL = -1, ## Preserve original mode when picked up
|
||||
UNFROZEN = 0, ## Release and unfreeze
|
||||
FROZEN = 1, ## Release and freeze
|
||||
}
|
||||
|
||||
enum SecondHandGrab {
|
||||
IGNORE, ## Ignore second grab
|
||||
SWAP, ## Swap to second hand
|
||||
SECOND, ## Second hand grab
|
||||
}
|
||||
|
||||
|
||||
# Default layer for held objects is 17:held-object
|
||||
const DEFAULT_LAYER := 0b0000_0000_0000_0001_0000_0000_0000_0000
|
||||
|
||||
|
||||
## If true, the pickable supports being picked up
|
||||
@export var enabled : bool = true
|
||||
|
||||
## If true, the grip control must be held to keep the object picked up
|
||||
@export var press_to_hold : bool = true
|
||||
|
||||
## Layer for this object while picked up
|
||||
@export_flags_3d_physics var picked_up_layer : int = DEFAULT_LAYER
|
||||
|
||||
## Release mode to use when releasing the object
|
||||
@export var release_mode : ReleaseMode = ReleaseMode.ORIGINAL
|
||||
|
||||
## Method used to perform a ranged grab
|
||||
@export var ranged_grab_method : RangedMethod = RangedMethod.SNAP: set = _set_ranged_grab_method
|
||||
|
||||
## Second hand grab mode
|
||||
@export var second_hand_grab : SecondHandGrab = SecondHandGrab.IGNORE
|
||||
|
||||
## Speed for ranged grab
|
||||
@export var ranged_grab_speed : float = 20.0
|
||||
|
||||
## Refuse pick-by when in the specified group
|
||||
@export var picked_by_exclude : String = ""
|
||||
|
||||
## Require pick-by to be in the specified group
|
||||
@export var picked_by_require : String = ""
|
||||
|
||||
|
||||
## If true, the object can be picked up at range
|
||||
var can_ranged_grab: bool = true
|
||||
|
||||
## Frozen state to restore to when dropped
|
||||
var restore_freeze : bool = false
|
||||
|
||||
# Count of 'is_closest' grabbers
|
||||
var _closest_count: int = 0
|
||||
|
||||
# Grab Driver to control position while grabbed
|
||||
var _grab_driver: XRToolsGrabDriver = null
|
||||
|
||||
# Array of grab points
|
||||
var _grab_points : Array[XRToolsGrabPoint] = []
|
||||
|
||||
# Dictionary of nodes requesting highlight
|
||||
var _highlight_requests : Dictionary = {}
|
||||
|
||||
# Is this node highlighted
|
||||
var _highlighted : bool = false
|
||||
|
||||
|
||||
# Remember some state so we can return to it when the user drops the object
|
||||
@onready var original_collision_mask : int = collision_mask
|
||||
@onready var original_collision_layer : int = collision_layer
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsPickable"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Get all grab points
|
||||
for child in get_children():
|
||||
var grab_point := child as XRToolsGrabPoint
|
||||
if grab_point:
|
||||
_grab_points.push_back(grab_point)
|
||||
|
||||
|
||||
# Called when the node exits the tree
|
||||
func _exit_tree():
|
||||
# Skip if not picked up
|
||||
if not is_instance_valid(_grab_driver):
|
||||
return
|
||||
|
||||
# Release primary grab
|
||||
if _grab_driver.primary:
|
||||
_grab_driver.primary.release()
|
||||
|
||||
# Release secondary grab
|
||||
if _grab_driver.secondary:
|
||||
_grab_driver.secondary.release()
|
||||
|
||||
|
||||
# Test if this object can be picked up
|
||||
func can_pick_up(by: Node3D) -> bool:
|
||||
# Refuse if not enabled
|
||||
if not enabled:
|
||||
return false
|
||||
|
||||
# Allow if not held by anything
|
||||
if not is_picked_up():
|
||||
return true
|
||||
|
||||
# Fail if second hand grabbing isn't allowed
|
||||
if second_hand_grab == SecondHandGrab.IGNORE:
|
||||
return false
|
||||
|
||||
# Fail if either pickup isn't by a hand
|
||||
if not _grab_driver.primary.pickup or not by is XRToolsFunctionPickup:
|
||||
return false
|
||||
|
||||
# Allow second hand grab
|
||||
return true
|
||||
|
||||
|
||||
# Test if this object is picked up
|
||||
func is_picked_up() -> bool:
|
||||
return _grab_driver and _grab_driver.primary
|
||||
|
||||
|
||||
# action is called when user presses the action button while holding this object
|
||||
func action():
|
||||
# let interested parties know
|
||||
action_pressed.emit(self)
|
||||
|
||||
|
||||
func controller_action(controller : XRController3D):
|
||||
# Let the grab points know about the action
|
||||
if (
|
||||
_grab_driver.primary and _grab_driver.primary.point
|
||||
and _grab_driver.primary.controller == controller
|
||||
):
|
||||
_grab_driver.primary.point.action(self)
|
||||
|
||||
if (
|
||||
_grab_driver.secondary and _grab_driver.secondary.point
|
||||
and _grab_driver.secondary.controller == controller
|
||||
):
|
||||
_grab_driver.secondary.point.action(self)
|
||||
|
||||
|
||||
# action_release is called when user releases the action button while holding this object
|
||||
func action_release():
|
||||
# let interested parties know
|
||||
action_released.emit(self)
|
||||
|
||||
|
||||
func controller_action_release(controller : XRController3D):
|
||||
# Let the grab points know about the action release
|
||||
if (
|
||||
_grab_driver.primary and _grab_driver.primary.point
|
||||
and _grab_driver.primary.controller == controller
|
||||
):
|
||||
_grab_driver.primary.point.action_release(self)
|
||||
|
||||
if (
|
||||
_grab_driver.secondary and _grab_driver.secondary.point
|
||||
and _grab_driver.secondary.controller == controller
|
||||
):
|
||||
_grab_driver.secondary.point.action_release(self)
|
||||
|
||||
|
||||
## This method requests highlighting of the [XRToolsPickable].
|
||||
## If [param from] is null then all highlighting requests are cleared,
|
||||
## otherwise the highlight request is associated with the specified node.
|
||||
func request_highlight(from : Node, on : bool = true) -> void:
|
||||
# Save if we are highlighted
|
||||
var old_highlighted := _highlighted
|
||||
|
||||
# Update the highlight requests dictionary
|
||||
if not from:
|
||||
_highlight_requests.clear()
|
||||
elif on:
|
||||
_highlight_requests[from] = from
|
||||
else:
|
||||
_highlight_requests.erase(from)
|
||||
|
||||
# Update the highlighted state
|
||||
_highlighted = _highlight_requests.size() > 0
|
||||
|
||||
# Report any changes
|
||||
if _highlighted != old_highlighted:
|
||||
highlight_updated.emit(self, _highlighted)
|
||||
|
||||
|
||||
func drop():
|
||||
# Skip if not picked up
|
||||
if not is_picked_up():
|
||||
return
|
||||
|
||||
# Request secondary grabber to drop
|
||||
if _grab_driver.secondary:
|
||||
_grab_driver.secondary.by.drop_object()
|
||||
|
||||
# Request primary grabber to drop
|
||||
_grab_driver.primary.by.drop_object()
|
||||
|
||||
|
||||
func drop_and_free():
|
||||
drop()
|
||||
queue_free()
|
||||
|
||||
|
||||
# Called when this object is picked up
|
||||
func pick_up(by: Node3D) -> void:
|
||||
# Skip if not enabled
|
||||
if not enabled:
|
||||
return
|
||||
|
||||
# Find the grabber information
|
||||
var grabber := Grabber.new(by)
|
||||
|
||||
# Test if we're already picked up:
|
||||
if is_picked_up():
|
||||
# Ignore if we don't support second-hand grab
|
||||
if second_hand_grab == SecondHandGrab.IGNORE:
|
||||
print_verbose("%s> second-hand grab not enabled" % name)
|
||||
return
|
||||
|
||||
# Ignore if either pickup isn't by a hand
|
||||
if not _grab_driver.primary.pickup or not grabber.pickup:
|
||||
return
|
||||
|
||||
# Construct the second grab
|
||||
if second_hand_grab != SecondHandGrab.SWAP:
|
||||
# Grab the object
|
||||
var by_grab_point := _get_grab_point(by, _grab_driver.primary.point)
|
||||
var grab := Grab.new(grabber, self, by_grab_point, true)
|
||||
_grab_driver.add_grab(grab)
|
||||
|
||||
# Report the secondary grab
|
||||
grabbed.emit(self, by)
|
||||
return
|
||||
|
||||
# Swapping hands, let go with the primary grab
|
||||
print_verbose("%s> letting go to swap hands" % name)
|
||||
let_go(_grab_driver.primary.by, Vector3.ZERO, Vector3.ZERO)
|
||||
|
||||
# Remember the mode before pickup
|
||||
match release_mode:
|
||||
ReleaseMode.UNFROZEN:
|
||||
restore_freeze = false
|
||||
|
||||
ReleaseMode.FROZEN:
|
||||
restore_freeze = true
|
||||
|
||||
_:
|
||||
restore_freeze = freeze
|
||||
|
||||
# turn off physics on our pickable object
|
||||
freeze = true
|
||||
collision_layer = picked_up_layer
|
||||
collision_mask = 0
|
||||
|
||||
# Find a suitable primary hand grab
|
||||
var by_grab_point := _get_grab_point(by, null)
|
||||
|
||||
# Construct the grab driver
|
||||
if by.picked_up_ranged:
|
||||
if ranged_grab_method == RangedMethod.LERP:
|
||||
var grab := Grab.new(grabber, self, by_grab_point, false)
|
||||
_grab_driver = XRToolsGrabDriver.create_lerp(self, grab, ranged_grab_speed)
|
||||
else:
|
||||
var grab := Grab.new(grabber, self, by_grab_point, false)
|
||||
_grab_driver = XRToolsGrabDriver.create_snap(self, grab)
|
||||
else:
|
||||
var grab := Grab.new(grabber, self, by_grab_point, true)
|
||||
_grab_driver = XRToolsGrabDriver.create_snap(self, grab)
|
||||
|
||||
# Report picked up and grabbed
|
||||
picked_up.emit(self)
|
||||
grabbed.emit(self, by)
|
||||
|
||||
|
||||
# Called when this object is dropped
|
||||
func let_go(by: Node3D, p_linear_velocity: Vector3, p_angular_velocity: Vector3) -> void:
|
||||
# Skip if not picked up
|
||||
if not is_picked_up():
|
||||
return
|
||||
|
||||
# Get the grab information
|
||||
var grab := _grab_driver.get_grab(by)
|
||||
if not grab:
|
||||
return
|
||||
|
||||
# Remove the grab from the driver and release the grab
|
||||
_grab_driver.remove_grab(grab)
|
||||
grab.release()
|
||||
|
||||
# Test if still grabbing
|
||||
if _grab_driver.primary:
|
||||
# Test if we need to swap grab-points
|
||||
if is_instance_valid(_grab_driver.primary.hand_point):
|
||||
# Verify the current primary grab point is a valid primary grab point
|
||||
if _grab_driver.primary.hand_point.mode != XRToolsGrabPointHand.Mode.SECONDARY:
|
||||
return
|
||||
|
||||
# Find a more suitable grab-point
|
||||
var new_grab_point := _get_grab_point(_grab_driver.primary.by, null)
|
||||
print_verbose("%s> held only by secondary, swapping grab points" % name)
|
||||
switch_active_grab_point(new_grab_point)
|
||||
|
||||
# Grab is still good
|
||||
return
|
||||
|
||||
# Drop the grab-driver
|
||||
print_verbose("%s> dropping" % name)
|
||||
_grab_driver.discard()
|
||||
_grab_driver = null
|
||||
|
||||
# Restore RigidBody mode
|
||||
freeze = restore_freeze
|
||||
collision_mask = original_collision_mask
|
||||
collision_layer = original_collision_layer
|
||||
|
||||
# Set velocity
|
||||
linear_velocity = p_linear_velocity
|
||||
angular_velocity = p_angular_velocity
|
||||
|
||||
# let interested parties know
|
||||
dropped.emit(self)
|
||||
|
||||
|
||||
## Get the node currently holding this object
|
||||
func get_picked_up_by() -> Node3D:
|
||||
# Skip if not picked up
|
||||
if not is_picked_up():
|
||||
return null
|
||||
|
||||
# Get the primary pickup
|
||||
return _grab_driver.primary.by
|
||||
|
||||
|
||||
## Get the controller currently holding this object
|
||||
func get_picked_up_by_controller() -> XRController3D:
|
||||
# Skip if not picked up
|
||||
if not is_picked_up():
|
||||
return null
|
||||
|
||||
# Get the primary pickup controller
|
||||
return _grab_driver.primary.controller
|
||||
|
||||
|
||||
## Get the active grab-point this object is held by
|
||||
func get_active_grab_point() -> XRToolsGrabPoint:
|
||||
# Skip if not picked up
|
||||
if not is_picked_up():
|
||||
return null
|
||||
|
||||
return _grab_driver.primary.point
|
||||
|
||||
|
||||
## Switch the active grab-point for this object
|
||||
func switch_active_grab_point(grab_point : XRToolsGrabPoint):
|
||||
# Skip if not picked up
|
||||
if not is_picked_up():
|
||||
return null
|
||||
|
||||
# Apply the grab point
|
||||
_grab_driver.primary.set_grab_point(grab_point)
|
||||
|
||||
|
||||
## Find the most suitable grab-point for the grabber
|
||||
func _get_grab_point(grabber : Node3D, current : XRToolsGrabPoint) -> XRToolsGrabPoint:
|
||||
# Find the best grab-point
|
||||
var fitness := 0.0
|
||||
var point : XRToolsGrabPoint = null
|
||||
for p in _grab_points:
|
||||
var f := p.can_grab(grabber, current)
|
||||
if f > fitness:
|
||||
fitness = f
|
||||
point = p
|
||||
|
||||
# Resolve redirection
|
||||
while point is XRToolsGrabPointRedirect:
|
||||
point = point.target
|
||||
|
||||
# Return the best grab point
|
||||
print_verbose("%s> picked grab-point %s" % [name, point])
|
||||
return point
|
||||
|
||||
|
||||
func _set_ranged_grab_method(new_value: int) -> void:
|
||||
ranged_grab_method = new_value
|
||||
can_ranged_grab = new_value != RangedMethod.NONE
|
||||
1
addons/godot-xr-tools/objects/pickable.gd.uid
Normal file
1
addons/godot-xr-tools/objects/pickable.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bfrlpbsqg5lqr
|
||||
11
addons/godot-xr-tools/objects/pickable.tscn
Normal file
11
addons/godot-xr-tools/objects/pickable.tscn
Normal file
@@ -0,0 +1,11 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://c8l60rnugru40"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bfrlpbsqg5lqr" path="res://addons/godot-xr-tools/objects/pickable.gd" id="1"]
|
||||
|
||||
[node name="PickableObject" type="RigidBody3D"]
|
||||
collision_layer = 4
|
||||
collision_mask = 196615
|
||||
freeze_mode = 1
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
110
addons/godot-xr-tools/objects/return_to_snap_zone.gd
Normal file
110
addons/godot-xr-tools/objects/return_to_snap_zone.gd
Normal file
@@ -0,0 +1,110 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
|
||||
class_name XRToolsReturnToSnapZone
|
||||
extends Node
|
||||
|
||||
|
||||
## XR Tools Return to Snap Zone
|
||||
##
|
||||
## This node can be added to an XRToolsPickable to make it return to a specified
|
||||
## snap-zone when the object is dropped.
|
||||
|
||||
|
||||
## Snap zone path
|
||||
@export var snap_zone_path : NodePath
|
||||
|
||||
## Return delay
|
||||
@export var return_delay : float = 1.0
|
||||
|
||||
|
||||
# Pickable object to control
|
||||
var _pickable : XRToolsPickable
|
||||
|
||||
# Snap zone to return to
|
||||
var _snap_zone : XRToolsSnapZone
|
||||
|
||||
# Return counter
|
||||
var _return_counter : float = 0.0
|
||||
|
||||
# Is the pickable held
|
||||
var _held := false
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsReturnToSnapZone"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
# Get the pickable (parent of this node)
|
||||
_pickable = get_parent() as XRToolsPickable
|
||||
if _pickable:
|
||||
_pickable.picked_up.connect(_on_picked_up)
|
||||
_pickable.dropped.connect(_on_dropped)
|
||||
|
||||
# Get the optional snap-zone
|
||||
_snap_zone = get_node_or_null(snap_zone_path)
|
||||
if not _snap_zone:
|
||||
set_process(false)
|
||||
|
||||
|
||||
# Handle the return counter
|
||||
func _process(delta : float) -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Update return time and skip if still waiting
|
||||
_return_counter += delta
|
||||
if _return_counter < return_delay:
|
||||
return
|
||||
|
||||
# Stop counting
|
||||
set_process(false)
|
||||
|
||||
# If the snap-zone is empty then snap to it
|
||||
if not _snap_zone.has_snapped_object():
|
||||
_snap_zone.pick_up_object(_pickable)
|
||||
|
||||
|
||||
# Set the snap-zone
|
||||
func set_snap_zone(snap_zone : XRToolsSnapZone) -> void:
|
||||
# Set the snap zone
|
||||
_snap_zone = snap_zone
|
||||
_return_counter = 0.0
|
||||
|
||||
# Control counting
|
||||
if _snap_zone and not _held:
|
||||
set_process(true)
|
||||
else:
|
||||
set_process(false)
|
||||
|
||||
|
||||
# Handle the object being picked up
|
||||
func _on_picked_up(_pickable) -> void:
|
||||
# Set held and stop counting
|
||||
_held = true
|
||||
set_process(false)
|
||||
|
||||
|
||||
# Handle the object being dropped
|
||||
func _on_dropped(_pickable) -> void:
|
||||
# Clear held and reset counter
|
||||
_held = false
|
||||
_return_counter = 0.0
|
||||
|
||||
# Start counter if snap-zone specified
|
||||
if _snap_zone:
|
||||
set_process(true)
|
||||
|
||||
|
||||
# This method verifies the pose area has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Verify this node is a child of a pickable
|
||||
if not get_parent() is XRToolsPickable:
|
||||
warnings.append("Must be a child of a pickable")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/objects/return_to_snap_zone.gd.uid
Normal file
1
addons/godot-xr-tools/objects/return_to_snap_zone.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bvnrc8puaxqel
|
||||
176
addons/godot-xr-tools/objects/snap_path.gd
Normal file
176
addons/godot-xr-tools/objects/snap_path.gd
Normal file
@@ -0,0 +1,176 @@
|
||||
@tool
|
||||
class_name XRToolsSnapPath
|
||||
extends XRToolsSnapZone
|
||||
|
||||
|
||||
## An [XRToolsSnapZone] that allows [XRToolsPickable] to be placed along a
|
||||
## child [Path3D] node. They can either be placed along any point in the curve
|
||||
## or at discrete intervals by setting "snap_interval" above '0.0'.
|
||||
##
|
||||
## Note: Attached [XRToolsPickable]s will face the +Z axis.
|
||||
|
||||
|
||||
## Real world distance between intervals in Meters.
|
||||
## Enabled when not 0
|
||||
@export var snap_interval := 0.0:
|
||||
set(v): snap_interval = absf(v)
|
||||
|
||||
@onready var path : Path3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
|
||||
for c in get_children():
|
||||
if c is Path3D:
|
||||
path = c
|
||||
break
|
||||
|
||||
|
||||
func has_snap_interval() -> bool:
|
||||
return !is_equal_approx(snap_interval, 0.0)
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
# Check for Path3D child
|
||||
for c in get_children():
|
||||
if c is Path3D:
|
||||
path = c
|
||||
return[]
|
||||
return["This node requires a Path3D child node to define its shape."]
|
||||
|
||||
|
||||
# Called when a target in our grab area is dropped
|
||||
func _on_target_dropped(target: Node3D) -> void:
|
||||
# Skip if invalid
|
||||
if !enabled or !path or !target.can_pick_up(self) or \
|
||||
!is_instance_valid(target) or \
|
||||
is_instance_valid(picked_up_object):
|
||||
return
|
||||
|
||||
# Make a zone that will destruct once its object has left
|
||||
var zone = _make_temp_zone()
|
||||
var offset = _find_offset(path, target.global_position)
|
||||
|
||||
# if snap guide
|
||||
if _has_snap_guide(target):
|
||||
# comply with guide
|
||||
offset = _find_closest_offset_with_length(path.curve, offset, _get_snap_guide(target).length)
|
||||
|
||||
# too large to place on path
|
||||
if is_equal_approx(offset, -1.0):
|
||||
return
|
||||
|
||||
# if snap_interval has been set, use it
|
||||
if has_snap_interval():
|
||||
offset = snappedf(offset, snap_interval)
|
||||
|
||||
# set position
|
||||
zone.position = path.curve.sample_baked(offset)
|
||||
|
||||
# Add zone as a child
|
||||
path.add_child(zone)
|
||||
zone.owner = path
|
||||
|
||||
# Connect self-destruct with lambda
|
||||
zone.has_dropped.connect(func(): zone.queue_free(), Object.ConnectFlags.CONNECT_ONE_SHOT)
|
||||
|
||||
# Use Pickable's Shapes as our Shapes
|
||||
for c in target.get_children():
|
||||
if c is CollisionShape3D:
|
||||
PhysicsServer3D.area_add_shape(zone.get_rid(), c.shape.get_rid(), c.transform)
|
||||
|
||||
# Force pickup
|
||||
zone.pick_up_object(target)
|
||||
|
||||
|
||||
# Make a zone that dies on dropping objects
|
||||
func _make_temp_zone():
|
||||
var zone = XRToolsSnapZone.new()
|
||||
|
||||
# connect lambda to play stash sounds when temp zone picks up
|
||||
if has_node("AudioStreamPlayer3D"):
|
||||
zone.has_picked_up.connect(
|
||||
func(object):
|
||||
$AudioStreamPlayer3D.stream = stash_sound
|
||||
$AudioStreamPlayer3D.play()
|
||||
)
|
||||
|
||||
# XRToolsSnapZone manaul copy
|
||||
zone.enabled = true
|
||||
zone.stash_sound = stash_sound
|
||||
zone.grab_distance = grab_distance
|
||||
zone.snap_mode = snap_mode
|
||||
zone.snap_require = snap_require
|
||||
zone.snap_exclude = snap_exclude
|
||||
zone.grab_require = grab_require
|
||||
zone.grab_exclude = grab_exclude
|
||||
zone.initial_object = NodePath()
|
||||
|
||||
# CollisionObject3D manual copy
|
||||
zone.disable_mode = disable_mode
|
||||
zone.collision_layer = collision_layer
|
||||
zone.collision_mask = collision_mask
|
||||
zone.collision_priority = collision_priority
|
||||
|
||||
return zone
|
||||
|
||||
|
||||
func _has_snap_guide(target: Node3D) -> bool:
|
||||
for c in target.get_children():
|
||||
if c is XRToolsSnapPathGuide:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _get_snap_guide(target: Node3D) -> Node3D:
|
||||
for c in target.get_children():
|
||||
if c is XRToolsSnapPathGuide:
|
||||
return c
|
||||
return null
|
||||
|
||||
|
||||
# Returns -1 if invalid
|
||||
# _offset should be in _curve's local coordinates
|
||||
func _find_closest_offset_with_length(_curve: Curve3D, _offset: float, _length: float) -> float:
|
||||
# p1 and p2 are the object's start and end respectively
|
||||
var p1 = _offset
|
||||
var p2 = _offset - _length
|
||||
|
||||
# a _curve's final point is its end, aka the furthest 'forward', which is why it is p1
|
||||
# path_p1 and path_p2 are the curve's start and end respectively
|
||||
var path_p1 := _curve.get_closest_offset(_curve.get_point_position(_curve.point_count-1))
|
||||
var path_p2 := _curve.get_closest_offset(_curve.get_point_position(0))
|
||||
|
||||
# if at front (or beyond)
|
||||
if is_equal_approx(p1, path_p1):
|
||||
# if too large
|
||||
if p2 < path_p2:
|
||||
return -1
|
||||
# if too far back
|
||||
elif p2 < path_p2:
|
||||
# check if snapping will over-extend
|
||||
if has_snap_interval():
|
||||
# snapping p1_new may move it further back, and out-of-bounds
|
||||
# larger snaps move the object further forward
|
||||
var p1_new = path_p2 + _length
|
||||
var ideal_snap = snappedf(p1_new, snap_interval)
|
||||
var more_snap = _snappedf_up(p1_new, snap_interval)
|
||||
# if ideal snap fits, take that
|
||||
if ideal_snap >= p1_new:
|
||||
return ideal_snap
|
||||
return more_snap
|
||||
return path_p2 + _length
|
||||
# otherwise: within bounds
|
||||
return p1
|
||||
|
||||
|
||||
## Round 'x' upwards to the nearest 'step'
|
||||
func _snappedf_up(x, step) -> float:
|
||||
return step * ceilf(x / step)
|
||||
|
||||
|
||||
func _find_offset(_path: Path3D, _global_position: Vector3) -> float:
|
||||
# transform target pos to local space
|
||||
var local_pos: Vector3 = _global_position * _path.global_transform
|
||||
return _path.curve.get_closest_offset(local_pos)
|
||||
1
addons/godot-xr-tools/objects/snap_path.gd.uid
Normal file
1
addons/godot-xr-tools/objects/snap_path.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bk0xen1ol7xb2
|
||||
32
addons/godot-xr-tools/objects/snap_path.tscn
Normal file
32
addons/godot-xr-tools/objects/snap_path.tscn
Normal file
@@ -0,0 +1,32 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://dsstvanwd58r0"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bk0xen1ol7xb2" path="res://addons/godot-xr-tools/objects/snap_path.gd" id="1_m211o"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_pik8g"]
|
||||
size = Vector3(0.1, 0.1, 1)
|
||||
|
||||
[sub_resource type="Curve3D" id="Curve3D_w68am"]
|
||||
_data = {
|
||||
"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0.5),
|
||||
"tilts": PackedFloat32Array(0, 0)
|
||||
}
|
||||
point_count = 2
|
||||
|
||||
[node name="SnapPath" type="Area3D"]
|
||||
collision_layer = 4
|
||||
collision_mask = 65540
|
||||
script = ExtResource("1_m211o")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("BoxShape3D_pik8g")
|
||||
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."]
|
||||
unit_size = 3.0
|
||||
max_db = 1.0
|
||||
max_distance = 100.0
|
||||
|
||||
[node name="Path3D" type="Path3D" parent="."]
|
||||
curve = SubResource("Curve3D_w68am")
|
||||
|
||||
[connection signal="body_entered" from="." to="." method="_on_snap_zone_body_entered"]
|
||||
[connection signal="body_exited" from="." to="." method="_on_snap_zone_body_exited"]
|
||||
14
addons/godot-xr-tools/objects/snap_path_guide.gd
Normal file
14
addons/godot-xr-tools/objects/snap_path_guide.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
class_name XRToolsSnapPathGuide
|
||||
extends Marker3D
|
||||
|
||||
|
||||
## XRToolsSnapRailGuide depicts a guide for [XRToolsSnapPath] to judge the
|
||||
## length of an [XRToolsPickable], helping place pickables within its bounds.
|
||||
## Add as a child node to any [XRToolsPickable], then move negatively along
|
||||
## the Z-Axis to define a length.
|
||||
|
||||
|
||||
var length : float:
|
||||
get:
|
||||
return abs(position.z)
|
||||
1
addons/godot-xr-tools/objects/snap_path_guide.gd.uid
Normal file
1
addons/godot-xr-tools/objects/snap_path_guide.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dmdr4j8plvntu
|
||||
336
addons/godot-xr-tools/objects/snap_zone.gd
Normal file
336
addons/godot-xr-tools/objects/snap_zone.gd
Normal file
@@ -0,0 +1,336 @@
|
||||
@tool
|
||||
class_name XRToolsSnapZone
|
||||
extends Area3D
|
||||
|
||||
|
||||
## Signal emitted when the snap-zone picks something up
|
||||
signal has_picked_up(what)
|
||||
|
||||
## Signal emitted when the snap-zone drops something
|
||||
signal has_dropped
|
||||
|
||||
# Signal emitted when the highlight state changes
|
||||
signal highlight_updated(pickable, enable)
|
||||
|
||||
# Signal emitted when the highlight state changes
|
||||
signal close_highlight_updated(pickable, enable)
|
||||
|
||||
|
||||
## Enumeration of snap mode
|
||||
enum SnapMode {
|
||||
DROPPED, ## Snap only when the object is dropped
|
||||
RANGE, ## Snap whenever an object is in range
|
||||
}
|
||||
|
||||
|
||||
## Enable or disable snap-zone
|
||||
@export var enabled : bool = true: set = _set_enabled
|
||||
|
||||
## Optional audio stream to play when a object snaps to the zone
|
||||
@export var stash_sound : AudioStream
|
||||
|
||||
## Grab distance
|
||||
@export var grab_distance : float = 0.3: set = _set_grab_distance
|
||||
|
||||
## Snap mode
|
||||
@export var snap_mode : SnapMode = SnapMode.DROPPED: set = _set_snap_mode
|
||||
|
||||
## Require snap items to be in specified group
|
||||
@export var snap_require : String = ""
|
||||
|
||||
## Deny snapping items in the specified group
|
||||
@export var snap_exclude : String = ""
|
||||
|
||||
## Require grab-by to be in the specified group
|
||||
@export var grab_require : String = ""
|
||||
|
||||
## Deny grab-by
|
||||
@export var grab_exclude : String= ""
|
||||
|
||||
## Initial object in snap zone
|
||||
@export var initial_object : NodePath
|
||||
|
||||
|
||||
# Public fields
|
||||
var closest_object : Node3D = null
|
||||
var picked_up_object : Node3D = null
|
||||
var picked_up_ranged : bool = true
|
||||
|
||||
|
||||
# Private fields
|
||||
var _object_in_grab_area = Array()
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsSnapZone"
|
||||
|
||||
|
||||
func _ready():
|
||||
# Set collision shape radius
|
||||
if has_node("CollisionShape3D") and "radius" in $CollisionShape3D.shape:
|
||||
$CollisionShape3D.shape.radius = grab_distance
|
||||
|
||||
# Add important connections
|
||||
if not body_entered.is_connected(_on_snap_zone_body_entered):
|
||||
body_entered.connect(_on_snap_zone_body_entered)
|
||||
if not body_exited.is_connected(_on_snap_zone_body_exited):
|
||||
body_exited.connect(_on_snap_zone_body_exited)
|
||||
|
||||
# Perform updates
|
||||
_update_snap_mode()
|
||||
|
||||
# Perform the initial object check when next idle
|
||||
if not Engine.is_editor_hint():
|
||||
_initial_object_check.call_deferred()
|
||||
|
||||
|
||||
# Called on each frame to update the pickup
|
||||
func _process(_delta):
|
||||
# Skip if in editor or not enabled
|
||||
if Engine.is_editor_hint() or not enabled:
|
||||
return
|
||||
|
||||
# Skip if we aren't doing range-checking
|
||||
if snap_mode != SnapMode.RANGE:
|
||||
return
|
||||
|
||||
# Skip if already holding a valid object
|
||||
if is_instance_valid(picked_up_object):
|
||||
return
|
||||
|
||||
# Check for any object in range that can be grabbed
|
||||
for o in _object_in_grab_area:
|
||||
# skip objects that can not be picked up
|
||||
if not o.can_pick_up(self):
|
||||
continue
|
||||
|
||||
# pick up our target
|
||||
pick_up_object(o)
|
||||
return
|
||||
|
||||
|
||||
# Pickable Method: snap-zone can be grabbed if holding object
|
||||
func can_pick_up(by: Node3D) -> bool:
|
||||
# Refuse if not enabled
|
||||
if not enabled:
|
||||
return false
|
||||
|
||||
# Refuse if no object is held
|
||||
if not is_instance_valid(picked_up_object):
|
||||
return false
|
||||
|
||||
# Refuse if the grab-by is not in the required group
|
||||
if not grab_require.is_empty() and not by.is_in_group(grab_require):
|
||||
return false
|
||||
|
||||
# Refuse if the grab-by is in the excluded group
|
||||
if not grab_exclude.is_empty() and by.is_in_group(grab_exclude):
|
||||
return false
|
||||
|
||||
# Grab is permitted
|
||||
return true
|
||||
|
||||
|
||||
# Pickable Method: Snap points can't be picked up
|
||||
func is_picked_up() -> bool:
|
||||
return false
|
||||
|
||||
|
||||
# Pickable Method: Gripper-actions can't occur on snap zones
|
||||
func action():
|
||||
pass
|
||||
|
||||
|
||||
# Ignore highlighting requests from XRToolsFunctionPickup
|
||||
func request_highlight(from : Node, on : bool = true) -> void:
|
||||
if is_instance_valid(picked_up_object):
|
||||
picked_up_object.request_highlight(from, on)
|
||||
|
||||
|
||||
# Pickable Method: Object being grabbed from this snap zone
|
||||
func pick_up(_by: Node3D) -> void:
|
||||
pass
|
||||
|
||||
|
||||
# Pickable Method: Player never graps snap-zone
|
||||
func let_go(_by: Node3D, _p_linear_velocity: Vector3, _p_angular_velocity: Vector3) -> void:
|
||||
pass
|
||||
|
||||
|
||||
# Pickup Method: Drop the currently picked up object
|
||||
func drop_object() -> void:
|
||||
if not is_instance_valid(picked_up_object):
|
||||
return
|
||||
|
||||
# let go of this object
|
||||
picked_up_object.let_go(self, Vector3.ZERO, Vector3.ZERO)
|
||||
picked_up_object = null
|
||||
has_dropped.emit()
|
||||
highlight_updated.emit(self, enabled)
|
||||
|
||||
|
||||
# Check for an initial object pickup
|
||||
func _initial_object_check() -> void:
|
||||
# Check for an initial object
|
||||
if initial_object:
|
||||
# Force pick-up the initial object
|
||||
pick_up_object(get_node(initial_object))
|
||||
else:
|
||||
# Show highlight when empty and enabled
|
||||
highlight_updated.emit(self, enabled)
|
||||
|
||||
# Stop any audio from initial pickup
|
||||
var audio := get_node("AudioStreamPlayer3D") if has_node("AudioStreamPlayer3D") else null
|
||||
|
||||
# Only stop if the user doesn't intend to auto-play
|
||||
if audio is AudioStreamPlayer3D and !audio.autoplay:
|
||||
audio.stop()
|
||||
|
||||
|
||||
# Called when a body enters the snap zone
|
||||
func _on_snap_zone_body_entered(target: Node3D) -> void:
|
||||
# Ignore objects already known about
|
||||
if _object_in_grab_area.find(target) >= 0:
|
||||
return
|
||||
|
||||
# Reject objects which don't support picking up
|
||||
if not target.has_method('pick_up'):
|
||||
return
|
||||
|
||||
# Reject objects not in the required snap group
|
||||
if not snap_require.is_empty() and not target.is_in_group(snap_require):
|
||||
return
|
||||
|
||||
# Reject objects in the excluded snap group
|
||||
if not snap_exclude.is_empty() and target.is_in_group(snap_exclude):
|
||||
return
|
||||
|
||||
# Reject climbable objects
|
||||
if target is XRToolsClimbable:
|
||||
return
|
||||
|
||||
# Add to the list of objects in grab area
|
||||
_object_in_grab_area.push_back(target)
|
||||
|
||||
# If this snap zone is configured to snap objects that are dropped, then
|
||||
# start listening for the objects dropped signal
|
||||
if snap_mode == SnapMode.DROPPED and target.has_signal("dropped"):
|
||||
target.connect("dropped", _on_target_dropped, CONNECT_DEFERRED)
|
||||
|
||||
# Show highlight when something could be snapped
|
||||
if not is_instance_valid(picked_up_object):
|
||||
close_highlight_updated.emit(self, enabled)
|
||||
|
||||
|
||||
# Called when a body leaves the snap zone
|
||||
func _on_snap_zone_body_exited(target: Node3D) -> void:
|
||||
# Ensure the object is not in our list
|
||||
_object_in_grab_area.erase(target)
|
||||
|
||||
# Stop listening for dropped signals
|
||||
if target.has_signal("dropped") and target.is_connected("dropped", _on_target_dropped):
|
||||
target.disconnect("dropped", _on_target_dropped)
|
||||
|
||||
# Hide highlight when nothing could be snapped
|
||||
if _object_in_grab_area.is_empty():
|
||||
close_highlight_updated.emit(self, false)
|
||||
|
||||
|
||||
# Test if this snap zone has a picked up object
|
||||
func has_snapped_object() -> bool:
|
||||
return is_instance_valid(picked_up_object)
|
||||
|
||||
|
||||
# Pick up the specified object
|
||||
func pick_up_object(target: Node3D) -> void:
|
||||
# check if already holding an object
|
||||
if is_instance_valid(picked_up_object):
|
||||
# skip if holding the target object
|
||||
if picked_up_object == target:
|
||||
return
|
||||
# holding something else? drop it
|
||||
drop_object()
|
||||
|
||||
# skip if target null or freed
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
|
||||
# Pick up our target. Note, target may do instant drop_and_free
|
||||
picked_up_object = target
|
||||
if has_node("AudioStreamPlayer3D"):
|
||||
var player = get_node("AudioStreamPlayer3D")
|
||||
if is_instance_valid(player):
|
||||
if player.playing:
|
||||
player.stop()
|
||||
player.stream = stash_sound
|
||||
player.play()
|
||||
|
||||
target.pick_up(self)
|
||||
|
||||
# If object picked up then emit signal
|
||||
if is_instance_valid(picked_up_object):
|
||||
has_picked_up.emit(picked_up_object)
|
||||
highlight_updated.emit(self, false)
|
||||
|
||||
|
||||
# Called when the enabled property has been modified
|
||||
func _set_enabled(p_enabled: bool) -> void:
|
||||
enabled = p_enabled
|
||||
if is_inside_tree:
|
||||
highlight_updated.emit(
|
||||
self,
|
||||
enabled and not is_instance_valid(picked_up_object))
|
||||
|
||||
|
||||
# Called when the grab distance has been modified
|
||||
func _set_grab_distance(new_value: float) -> void:
|
||||
grab_distance = new_value
|
||||
if is_inside_tree() and $CollisionShape3D:
|
||||
$CollisionShape3D.shape.radius = grab_distance
|
||||
|
||||
|
||||
# Called when the snap mode property has been modified
|
||||
func _set_snap_mode(new_value: SnapMode) -> void:
|
||||
snap_mode = new_value
|
||||
if is_inside_tree():
|
||||
_update_snap_mode()
|
||||
|
||||
|
||||
# Handle changes to the snap mode
|
||||
func _update_snap_mode() -> void:
|
||||
match snap_mode:
|
||||
SnapMode.DROPPED:
|
||||
# Disable _process as we aren't using RANGE pickups
|
||||
set_process(false)
|
||||
|
||||
# Start monitoring all objects in range for drop
|
||||
for o in _object_in_grab_area:
|
||||
o.connect("dropped", _on_target_dropped, CONNECT_DEFERRED)
|
||||
|
||||
SnapMode.RANGE:
|
||||
# Enable _process to scan for RANGE pickups
|
||||
set_process(true)
|
||||
|
||||
# Clear any dropped signal hooks
|
||||
for o in _object_in_grab_area:
|
||||
o.disconnect("dropped", _on_target_dropped)
|
||||
|
||||
|
||||
# Called when a target in our grab area is dropped
|
||||
func _on_target_dropped(target: Node3D) -> void:
|
||||
# Skip if not enabled
|
||||
if not enabled:
|
||||
return
|
||||
|
||||
# Skip if already holding a valid object
|
||||
if is_instance_valid(picked_up_object):
|
||||
return
|
||||
|
||||
# Skip if the target is not valid
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
|
||||
# Pick up the target if we can
|
||||
if target.can_pick_up(self):
|
||||
pick_up_object(target)
|
||||
1
addons/godot-xr-tools/objects/snap_zone.gd.uid
Normal file
1
addons/godot-xr-tools/objects/snap_zone.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cquqe4f1m1sw6
|
||||
23
addons/godot-xr-tools/objects/snap_zone.tscn
Normal file
23
addons/godot-xr-tools/objects/snap_zone.tscn
Normal file
@@ -0,0 +1,23 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://ce7vysyvondf8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="1"]
|
||||
resource_local_to_scene = true
|
||||
radius = 0.1
|
||||
|
||||
[node name="SnapZone" type="Area3D"]
|
||||
collision_layer = 4
|
||||
collision_mask = 65540
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("1")
|
||||
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."]
|
||||
unit_size = 3.0
|
||||
max_db = 1.0
|
||||
max_distance = 100.0
|
||||
|
||||
[connection signal="body_entered" from="." to="." method="_on_snap_zone_body_entered"]
|
||||
[connection signal="body_exited" from="." to="." method="_on_snap_zone_body_exited"]
|
||||
29
addons/godot-xr-tools/objects/teleport_area.gd
Normal file
29
addons/godot-xr-tools/objects/teleport_area.gd
Normal file
@@ -0,0 +1,29 @@
|
||||
@tool
|
||||
class_name XRToolsTeleportArea
|
||||
extends Area3D
|
||||
|
||||
|
||||
## Target node
|
||||
@export var target : Node3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsTeleportArea"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Handle body entered
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
# Handle body entering area
|
||||
func _on_body_entered(body : Node3D) -> void:
|
||||
# Test if the body is the player
|
||||
var player_body := body as XRToolsPlayerBody
|
||||
if not player_body:
|
||||
return
|
||||
|
||||
# Teleport the player
|
||||
player_body.teleport(target.global_transform)
|
||||
1
addons/godot-xr-tools/objects/teleport_area.gd.uid
Normal file
1
addons/godot-xr-tools/objects/teleport_area.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://153oneb2r3uq
|
||||
8
addons/godot-xr-tools/objects/teleport_area.tscn
Normal file
8
addons/godot-xr-tools/objects/teleport_area.tscn
Normal file
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dpy1eg3i331se"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://153oneb2r3uq" path="res://addons/godot-xr-tools/objects/teleport_area.gd" id="1_0awk1"]
|
||||
|
||||
[node name="TeleportArea" type="Area3D"]
|
||||
collision_layer = 0
|
||||
collision_mask = 524288
|
||||
script = ExtResource("1_0awk1")
|
||||
658
addons/godot-xr-tools/objects/viewport_2d_in_3d.gd
Normal file
658
addons/godot-xr-tools/objects/viewport_2d_in_3d.gd
Normal file
@@ -0,0 +1,658 @@
|
||||
@tool
|
||||
class_name XRToolsViewport2DIn3D
|
||||
extends Node3D
|
||||
|
||||
|
||||
## XR ToolsViewport 2D in 3D
|
||||
##
|
||||
## This script manages a 2D scene rendered as a texture on a 3D quad.
|
||||
##
|
||||
## Pointer and keyboard input are mapped into the 2D scene.
|
||||
|
||||
|
||||
## Signal for pointer events
|
||||
signal pointer_event(event)
|
||||
|
||||
|
||||
## Transparent property
|
||||
enum TransparancyMode {
|
||||
OPAQUE, ## Render opaque
|
||||
TRANSPARENT, ## Render transparent
|
||||
SCISSOR, ## Render using alpha-scissor
|
||||
}
|
||||
|
||||
## Viewport Update Mode
|
||||
enum UpdateMode {
|
||||
UPDATE_ONCE, ## Update once (redraw triggered if set again to UPDATE_ONCE)
|
||||
UPDATE_ALWAYS, ## Update on every frame
|
||||
UPDATE_THROTTLED, ## Update at throttled rate
|
||||
}
|
||||
|
||||
|
||||
# The following dirty flags are private (leading _) to suppress them in the
|
||||
# generated documentation. Unfortunately gdlint complaints on private constants
|
||||
# (see https://github.com/Scony/godot-gdscript-toolkit/issues/223). Until this
|
||||
# is fixed we suppress the rule.
|
||||
# gdlint: disable=constant-name
|
||||
|
||||
# State dirty flags
|
||||
const _DIRTY_NONE := 0x0000 # Everything up to date
|
||||
const _DIRTY_MATERIAL := 0x0001 # Material needs update
|
||||
const _DIRTY_SCENE := 0x0002 # Scene needs update
|
||||
const _DIRTY_SIZE := 0x0004 # Viewport size needs update
|
||||
const _DIRTY_ALBEDO := 0x0008 # Albedo texture needs update
|
||||
const _DIRTY_UPDATE := 0x0010 # Update mode needs update
|
||||
const _DIRTY_TRANSPARENCY := 0x0020 # Transparency needs update
|
||||
const _DIRTY_ALPHA_SCISSOR := 0x0040 # Alpha scissor needs update
|
||||
const _DIRTY_UNSHADED := 0x0080 # Shade mode needs update
|
||||
const _DIRTY_FILTERED := 0x0100 # Filter mode needs update
|
||||
const _DIRTY_SURFACE := 0x0200 # Surface material needs update
|
||||
const _DIRTY_REDRAW := 0x0400 # Redraw required
|
||||
const _DIRTY_ALL := 0x07FF # All dirty
|
||||
|
||||
# Default layer of 1:static-world, 21:pointable, 23:ui-objects
|
||||
const DEFAULT_LAYER := 0b0000_0000_0101_0000_0000_0000_0000_0001
|
||||
|
||||
|
||||
# Physics property group
|
||||
@export_group("Physics")
|
||||
|
||||
## Physical screen size property
|
||||
@export var screen_size : Vector2 = Vector2(3.0, 2.0): set = set_screen_size
|
||||
|
||||
## Viewport collision enabled property
|
||||
@export var enabled : bool = true: set = set_enabled
|
||||
|
||||
## Collision layer
|
||||
@export_flags_3d_physics var collision_layer : int = DEFAULT_LAYER: set = set_collision_layer
|
||||
|
||||
# Content property group
|
||||
@export_group("Content")
|
||||
|
||||
## Scene property
|
||||
@export var scene : PackedScene: set = set_scene
|
||||
|
||||
## Viewport size property
|
||||
@export var viewport_size : Vector2 = Vector2(300.0, 200.0): set = set_viewport_size
|
||||
|
||||
## Update Mode property
|
||||
@export var update_mode : UpdateMode = UpdateMode.UPDATE_ALWAYS: set = set_update_mode
|
||||
|
||||
## Update throttle property
|
||||
@export var throttle_fps : float = 30.0
|
||||
|
||||
# Input property group
|
||||
@export_group("Input")
|
||||
|
||||
## Allow physical keyboard input to viewport
|
||||
@export var input_keyboard : bool = true
|
||||
|
||||
## Allow gamepad input to viewport
|
||||
@export var input_gamepad : bool = false
|
||||
|
||||
# Rendering property group
|
||||
@export_group("Rendering")
|
||||
|
||||
## Custom material template
|
||||
@export var material : StandardMaterial3D = null: set = set_material
|
||||
|
||||
## Transparent property
|
||||
var transparent : TransparancyMode = TransparancyMode.TRANSPARENT: set = set_transparent
|
||||
|
||||
## Alpha Scissor Threshold property (ignored when custom material provided)
|
||||
var alpha_scissor_threshold : float = 0.25: set = set_alpha_scissor_threshold
|
||||
|
||||
## Unshaded flag (ignored when custom material provided)
|
||||
var unshaded : bool = false: set = set_unshaded
|
||||
|
||||
## Filtering flag (ignored when custom material provided)
|
||||
var filter : bool = true: set = set_filter
|
||||
|
||||
|
||||
var is_ready : bool = false
|
||||
var scene_node : Node
|
||||
var scene_properties_keys: PackedStringArray = []
|
||||
var scene_properties : Array[Dictionary] = []
|
||||
# Needed to apply custom properties of the scene before it is instanced, as these are set on ready,
|
||||
# But at this point in time the scene is not instanced yet
|
||||
var scene_proxy_configuration: Dictionary = {}
|
||||
var viewport_texture : ViewportTexture
|
||||
var time_since_last_update : float = 0.0
|
||||
var _screen_material : StandardMaterial3D
|
||||
var _dirty := _DIRTY_ALL
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsViewport2DIn3D"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
is_ready = true
|
||||
|
||||
# Listen for pointer events on the screen body
|
||||
$StaticBody3D.connect("pointer_event", _on_pointer_event)
|
||||
|
||||
# Update enabled based on visibility
|
||||
visibility_changed.connect(_on_visibility_changed)
|
||||
|
||||
# Apply physics properties
|
||||
_update_screen_size()
|
||||
_update_enabled()
|
||||
_update_collision_layer()
|
||||
|
||||
# Update the render objects
|
||||
_update_render()
|
||||
|
||||
|
||||
# Provide custom property information
|
||||
func _get_property_list() -> Array[Dictionary]:
|
||||
# Select visibility of properties
|
||||
var show_transparency := not material
|
||||
var show_alpha_scissor := not material and transparent == TransparancyMode.SCISSOR
|
||||
var show_unshaded := not material
|
||||
var show_filter := not material
|
||||
|
||||
var extra_properties : Array[Dictionary] = [
|
||||
{
|
||||
name = "Rendering",
|
||||
type = TYPE_NIL,
|
||||
usage = PROPERTY_USAGE_GROUP
|
||||
},
|
||||
{
|
||||
name = "transparent",
|
||||
type = TYPE_BOOL,
|
||||
usage = PROPERTY_USAGE_DEFAULT if show_transparency else PROPERTY_USAGE_NO_EDITOR
|
||||
},
|
||||
{
|
||||
name = "alpha_scissor_threshold",
|
||||
type = TYPE_FLOAT,
|
||||
usage = PROPERTY_USAGE_DEFAULT if show_alpha_scissor else PROPERTY_USAGE_NO_EDITOR,
|
||||
hint = PROPERTY_HINT_RANGE,
|
||||
hint_string = "0.0,1.0"
|
||||
},
|
||||
{
|
||||
name = "unshaded",
|
||||
type = TYPE_BOOL,
|
||||
usage = PROPERTY_USAGE_DEFAULT if show_unshaded else PROPERTY_USAGE_NO_EDITOR
|
||||
},
|
||||
{
|
||||
name = "filter",
|
||||
type = TYPE_BOOL,
|
||||
usage = PROPERTY_USAGE_DEFAULT if show_filter else PROPERTY_USAGE_NO_EDITOR
|
||||
},
|
||||
# Store the scene property keys on the disk, so that even before the scene is loaded we
|
||||
# know about the custom properties
|
||||
{
|
||||
name = "scene_properties_keys",
|
||||
type = TYPE_PACKED_STRING_ARRAY,
|
||||
usage = PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_STORAGE
|
||||
}
|
||||
]
|
||||
|
||||
# Add all the custom properties of the subscene so they show up in the editor
|
||||
if scene_properties_keys.size() > 0:
|
||||
extra_properties.append_array(scene_properties)
|
||||
|
||||
return extra_properties
|
||||
|
||||
|
||||
# Forward setting and getting of custom properties of the child scene
|
||||
func _get(property: StringName) -> Variant:
|
||||
if scene_properties_keys.has(property):
|
||||
|
||||
var return_value: Variant = null
|
||||
|
||||
# If our scene is already instanced then get the property directly
|
||||
if is_instance_valid(scene_node):
|
||||
return_value = scene_node.get(property)
|
||||
# If it is not instanced, we use the proxy configuration
|
||||
elif scene_proxy_configuration.has(property):
|
||||
return_value = scene_proxy_configuration[property]
|
||||
|
||||
# Special handling is required for NodePaths, as they are relative to the scene
|
||||
if return_value is NodePath and !return_value.is_absolute():
|
||||
var path_string : String = str(return_value)
|
||||
# Remove the additional leading ../../
|
||||
return_value = NodePath(path_string.substr(6, -1))
|
||||
|
||||
return return_value
|
||||
# Keep normal behaviour
|
||||
return null
|
||||
|
||||
|
||||
func _set(property: StringName, value: Variant):
|
||||
if scene_properties_keys.has(property):
|
||||
|
||||
# Special handling is required for NodePaths, as they are relative to the scene
|
||||
if value is NodePath and !value.is_absolute():
|
||||
# Add the additional leading ../../
|
||||
value = NodePath("../../" + str(value))
|
||||
|
||||
# If our scene is already instanced then set the property directly
|
||||
if is_instance_valid(scene_node):
|
||||
scene_node.set(property, value)
|
||||
# If it is not instanced yet, store it to the proxy configuration,
|
||||
# which will get applied on scene load
|
||||
else:
|
||||
scene_proxy_configuration[property] = value
|
||||
return true
|
||||
# Keep normal behaviour
|
||||
return false
|
||||
|
||||
|
||||
# Allow revert of custom properties
|
||||
func _property_can_revert(property : StringName) -> bool:
|
||||
match property:
|
||||
"alpha_scissor_threshold":
|
||||
return true
|
||||
"unshaded":
|
||||
return true
|
||||
"filter":
|
||||
return true
|
||||
_:
|
||||
return false
|
||||
|
||||
|
||||
# Provide revert values for custom properties
|
||||
func _property_get_revert(property : StringName): # Variant
|
||||
match property:
|
||||
"alpha_scissor_threshold":
|
||||
return 0.25
|
||||
"unshaded":
|
||||
return false
|
||||
"filter":
|
||||
return true
|
||||
|
||||
|
||||
# When the scene_node changes, update the property list
|
||||
func _update_scene_property_list():
|
||||
scene_properties = []
|
||||
scene_properties_keys = []
|
||||
if is_instance_valid(scene_node):
|
||||
|
||||
# If the scene is queued for deletion, clear the scene proxy configuration
|
||||
if scene_node.is_queued_for_deletion():
|
||||
scene_proxy_configuration = {}
|
||||
else:
|
||||
# Extract relevant properties of the provided scene to display in the editor (forwarded)
|
||||
var node_script: Script = scene_node.get_script() as Script
|
||||
if node_script:
|
||||
var all_properties := node_script.get_script_property_list()
|
||||
|
||||
# Join this with the custom property list of the object created by the script
|
||||
if scene_node.has_method("_get_property_list"):
|
||||
all_properties.append_array(scene_node.call("_get_property_list"))
|
||||
|
||||
for property in all_properties:
|
||||
# Filter out only the properties that are supposed to be stored, or are used for grouping
|
||||
if property["usage"] & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_GROUP \
|
||||
| PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_SUBGROUP):
|
||||
scene_properties.append(property)
|
||||
scene_properties_keys.append(property["name"])
|
||||
|
||||
notify_property_list_changed()
|
||||
|
||||
|
||||
## Get the 2D scene instance
|
||||
func get_scene_instance() -> Node:
|
||||
return scene_node
|
||||
|
||||
|
||||
## Connect a 2D scene signal
|
||||
func connect_scene_signal(which : String, callback : Callable, flags : int = 0):
|
||||
if scene_node:
|
||||
scene_node.connect(which, callback, flags)
|
||||
|
||||
|
||||
# Handle pointer event from screen-body
|
||||
func _on_pointer_event(event : XRToolsPointerEvent) -> void:
|
||||
pointer_event.emit(event)
|
||||
|
||||
|
||||
# Handler for input events
|
||||
func _input(event):
|
||||
# Map keyboard events to the viewport if enabled
|
||||
if input_keyboard and (event is InputEventKey or event is InputEventShortcut):
|
||||
$Viewport.push_input(event)
|
||||
return
|
||||
|
||||
# Map gamepad events to the viewport if enable
|
||||
if input_gamepad and (event is InputEventJoypadButton or event is InputEventJoypadMotion):
|
||||
$Viewport.push_input(event)
|
||||
return
|
||||
|
||||
|
||||
# Process event
|
||||
func _process(delta):
|
||||
# Process screen refreshing
|
||||
if Engine.is_editor_hint():
|
||||
# Perform periodic material refreshes to handle the user modifying the
|
||||
# material properties in the editor
|
||||
time_since_last_update += delta
|
||||
if time_since_last_update > 1.0:
|
||||
time_since_last_update = 0.0
|
||||
# Trigger material refresh
|
||||
_dirty = _DIRTY_MATERIAL
|
||||
_update_render()
|
||||
elif update_mode == UpdateMode.UPDATE_THROTTLED:
|
||||
# Perform throttled updates of the viewport
|
||||
var frame_time = 1.0 / throttle_fps
|
||||
time_since_last_update += delta
|
||||
if time_since_last_update > frame_time:
|
||||
time_since_last_update = 0.0
|
||||
# Trigger update
|
||||
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
else:
|
||||
# This is no longer needed
|
||||
set_process(false)
|
||||
|
||||
|
||||
# Handle visibility changed
|
||||
func _on_visibility_changed() -> void:
|
||||
# Fire visibility changed in scene
|
||||
if scene_node:
|
||||
scene_node.propagate_notification(
|
||||
CanvasItem.NOTIFICATION_VISIBILITY_CHANGED)
|
||||
|
||||
# Update collision and rendering based on visibility
|
||||
_update_enabled()
|
||||
_dirty |= _DIRTY_UPDATE
|
||||
_update_render()
|
||||
|
||||
|
||||
## Set screen size property
|
||||
func set_screen_size(new_size: Vector2) -> void:
|
||||
screen_size = new_size
|
||||
if is_ready:
|
||||
_update_screen_size()
|
||||
|
||||
|
||||
## Set enabled property
|
||||
func set_enabled(is_enabled: bool) -> void:
|
||||
enabled = is_enabled
|
||||
if is_ready:
|
||||
_update_enabled()
|
||||
|
||||
|
||||
## Set collision layer property
|
||||
func set_collision_layer(new_layer: int) -> void:
|
||||
collision_layer = new_layer
|
||||
if is_ready:
|
||||
_update_collision_layer()
|
||||
|
||||
|
||||
## Set scene property
|
||||
func set_scene(new_scene: PackedScene) -> void:
|
||||
scene = new_scene
|
||||
_dirty |= _DIRTY_SCENE
|
||||
if is_ready:
|
||||
_update_render()
|
||||
|
||||
|
||||
## Set viewport size property
|
||||
func set_viewport_size(new_size: Vector2) -> void:
|
||||
viewport_size = new_size
|
||||
_dirty |= _DIRTY_SIZE
|
||||
if is_ready:
|
||||
_update_render()
|
||||
|
||||
|
||||
## Set update mode property
|
||||
func set_update_mode(new_update_mode: UpdateMode) -> void:
|
||||
update_mode = new_update_mode
|
||||
_dirty |= _DIRTY_UPDATE
|
||||
if is_ready:
|
||||
_update_render()
|
||||
|
||||
|
||||
## Set material property
|
||||
func set_material(new_material: StandardMaterial3D) -> void:
|
||||
material = new_material
|
||||
notify_property_list_changed()
|
||||
|
||||
# Discard our screen material, _update_render will create a new one.
|
||||
_screen_material = null
|
||||
|
||||
_dirty |= _DIRTY_MATERIAL
|
||||
if is_ready:
|
||||
_update_render()
|
||||
|
||||
|
||||
## Set transparent property
|
||||
func set_transparent(new_transparent: TransparancyMode) -> void:
|
||||
transparent = new_transparent
|
||||
notify_property_list_changed()
|
||||
_dirty |= _DIRTY_TRANSPARENCY
|
||||
if is_ready:
|
||||
_update_render()
|
||||
|
||||
|
||||
## Set the alpha scisser threshold
|
||||
func set_alpha_scissor_threshold(new_threshold: float) -> void:
|
||||
alpha_scissor_threshold = new_threshold
|
||||
_dirty |= _DIRTY_ALPHA_SCISSOR
|
||||
if is_ready:
|
||||
_update_render()
|
||||
|
||||
|
||||
## Set the unshaded property
|
||||
func set_unshaded(new_unshaded : bool) -> void:
|
||||
unshaded = new_unshaded
|
||||
_dirty |= _DIRTY_UNSHADED
|
||||
if is_ready:
|
||||
_update_render()
|
||||
|
||||
|
||||
## Set filter property
|
||||
func set_filter(new_filter: bool) -> void:
|
||||
filter = new_filter
|
||||
_dirty |= _DIRTY_FILTERED
|
||||
if is_ready:
|
||||
_update_render()
|
||||
|
||||
|
||||
# Screen size update handler
|
||||
func _update_screen_size() -> void:
|
||||
$Screen.mesh.size = screen_size
|
||||
$StaticBody3D.screen_size = screen_size
|
||||
$StaticBody3D/CollisionShape3D.shape.size = Vector3(
|
||||
screen_size.x,
|
||||
screen_size.y,
|
||||
0.02)
|
||||
|
||||
|
||||
# Enabled update handler
|
||||
func _update_enabled() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
$StaticBody3D/CollisionShape3D.disabled = !enabled or not is_visible_in_tree()
|
||||
|
||||
|
||||
# Collision layer update handler
|
||||
func _update_collision_layer() -> void:
|
||||
$StaticBody3D.collision_layer = collision_layer
|
||||
|
||||
|
||||
# This complex function processes the render dirty flags and performs the
|
||||
# minimal number of updates to get the render objects into the correct state.
|
||||
func _update_render() -> void:
|
||||
# Handle material change
|
||||
if _dirty & _DIRTY_MATERIAL:
|
||||
_dirty &= ~_DIRTY_MATERIAL
|
||||
|
||||
if material:
|
||||
# We can't use our material directly because each instance uses
|
||||
# it's own ViewportTexture. So we duplicate our material.
|
||||
if not _screen_material:
|
||||
_screen_material = material.duplicate()
|
||||
else:
|
||||
# We should only get here if we're in our editor.
|
||||
# We can't detect when our material changes,
|
||||
# so we need to check for changed properties.
|
||||
for property in ClassDB.class_get_property_list("BaseMaterial3D", true):
|
||||
# If any of the material properties we do not manage changed, update them.
|
||||
if property.name != "albedo_texture":
|
||||
var was_value = _screen_material.get(property.name)
|
||||
var new_value = material.get(property.name)
|
||||
if was_value != new_value:
|
||||
_screen_material.set(property.name, new_value)
|
||||
elif not _screen_material:
|
||||
# Create new local material
|
||||
_screen_material = StandardMaterial3D.new()
|
||||
|
||||
# Disable culling
|
||||
_screen_material.params_cull_mode = StandardMaterial3D.CULL_DISABLED
|
||||
|
||||
# Ensure local material is configured
|
||||
_dirty |= _DIRTY_TRANSPARENCY | \
|
||||
_DIRTY_ALPHA_SCISSOR | \
|
||||
_DIRTY_UNSHADED | \
|
||||
_DIRTY_FILTERED
|
||||
|
||||
# Ensure new material renders viewport onto surface
|
||||
_dirty |= _DIRTY_ALBEDO | _DIRTY_SURFACE
|
||||
|
||||
# If we have no screen material then skip everything else
|
||||
if not _screen_material:
|
||||
return
|
||||
|
||||
# Handle scene change
|
||||
if _dirty & _DIRTY_SCENE:
|
||||
_dirty &= ~_DIRTY_SCENE
|
||||
|
||||
# Out with the old
|
||||
if is_instance_valid(scene_node):
|
||||
if scene_node.property_list_changed.is_connected(_update_scene_property_list):
|
||||
scene_node.property_list_changed.disconnect(_update_scene_property_list)
|
||||
$Viewport.remove_child(scene_node)
|
||||
scene_node.queue_free()
|
||||
_update_scene_property_list()
|
||||
|
||||
# In with the new
|
||||
if scene:
|
||||
# Instantiate provided scene
|
||||
scene_node = scene.instantiate()
|
||||
_update_scene_property_list()
|
||||
scene_node.property_list_changed.connect(_update_scene_property_list)
|
||||
|
||||
# Apply the scene proxy configuration on the first load
|
||||
for key in scene_properties_keys:
|
||||
if scene_proxy_configuration.has(key):
|
||||
scene_node.set(key, scene_proxy_configuration[key])
|
||||
|
||||
# Finally add it to the scene, so values are available in _ready
|
||||
$Viewport.add_child(scene_node)
|
||||
elif $Viewport.get_child_count() == 1:
|
||||
# Use already-provided scene
|
||||
scene_node = $Viewport.get_child(0)
|
||||
|
||||
# Ensure the new scene is rendered at least once
|
||||
_dirty |= _DIRTY_REDRAW
|
||||
|
||||
# Handle viewport size change
|
||||
if _dirty & _DIRTY_SIZE:
|
||||
_dirty &= ~_DIRTY_SIZE
|
||||
|
||||
# Set the viewport size
|
||||
$Viewport.size = viewport_size
|
||||
$StaticBody3D.viewport_size = viewport_size
|
||||
|
||||
# Perform redraw to let viewport texture update correctly after changing the viewport's size
|
||||
_dirty |= _DIRTY_REDRAW
|
||||
|
||||
# Handle albedo change:
|
||||
if _dirty & _DIRTY_ALBEDO:
|
||||
_dirty &= ~_DIRTY_ALBEDO
|
||||
|
||||
# Set the screen material to use the viewport for the albedo channel
|
||||
viewport_texture = $Viewport.get_texture()
|
||||
_screen_material.albedo_texture = viewport_texture
|
||||
|
||||
# Handle update mode change
|
||||
if _dirty & _DIRTY_UPDATE:
|
||||
_dirty &= ~_DIRTY_UPDATE
|
||||
|
||||
# Apply update rules
|
||||
if Engine.is_editor_hint():
|
||||
# Update once. Process function used for editor refreshes
|
||||
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
set_process(true)
|
||||
elif update_mode == UpdateMode.UPDATE_ONCE or not is_visible_in_tree():
|
||||
# Update once. Process function not used
|
||||
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
set_process(false)
|
||||
elif update_mode == UpdateMode.UPDATE_ALWAYS:
|
||||
# Update always. Process function not used
|
||||
$Viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
set_process(false)
|
||||
elif update_mode == UpdateMode.UPDATE_THROTTLED:
|
||||
# Update once. Process function triggers periodic refresh
|
||||
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
set_process(true)
|
||||
|
||||
# Handle transparency update
|
||||
if _dirty & _DIRTY_TRANSPARENCY:
|
||||
_dirty &= ~_DIRTY_TRANSPARENCY
|
||||
|
||||
# If using a temporary material then update transparency
|
||||
if _screen_material and not material:
|
||||
# Set the transparancy mode
|
||||
match transparent:
|
||||
TransparancyMode.OPAQUE:
|
||||
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED
|
||||
TransparancyMode.TRANSPARENT:
|
||||
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
TransparancyMode.SCISSOR:
|
||||
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
||||
|
||||
# Set the viewport background transparency mode and force a redraw
|
||||
$Viewport.transparent_bg = transparent != TransparancyMode.OPAQUE
|
||||
_dirty |= _DIRTY_REDRAW
|
||||
|
||||
# Handle alpha scissor update
|
||||
if _dirty & _DIRTY_ALPHA_SCISSOR:
|
||||
_dirty &= ~_DIRTY_ALPHA_SCISSOR
|
||||
|
||||
# If using a temporary material with alpha-scissor then update
|
||||
if _screen_material and not material and transparent == TransparancyMode.SCISSOR:
|
||||
_screen_material.params_alpha_scissor_threshold = alpha_scissor_threshold
|
||||
|
||||
# Handle unshaded update
|
||||
if _dirty & _DIRTY_UNSHADED:
|
||||
_dirty &= ~_DIRTY_UNSHADED
|
||||
|
||||
# If using a temporary material then update the shading mode and force a redraw
|
||||
if _screen_material and not material:
|
||||
_screen_material.shading_mode = (
|
||||
BaseMaterial3D.SHADING_MODE_UNSHADED if unshaded else
|
||||
BaseMaterial3D.SHADING_MODE_PER_PIXEL)
|
||||
#_dirty |= _DIRTY_REDRAW
|
||||
|
||||
# Handle filter update
|
||||
if _dirty & _DIRTY_FILTERED:
|
||||
_dirty &= ~_DIRTY_FILTERED
|
||||
|
||||
# If using a temporary material then update the filter mode and force a redraw
|
||||
if _screen_material and not material:
|
||||
_screen_material.texture_filter = (
|
||||
BaseMaterial3D.TEXTURE_FILTER_LINEAR if filter else
|
||||
BaseMaterial3D.TEXTURE_FILTER_NEAREST)
|
||||
#_dirty |= _DIRTY_REDRAW
|
||||
|
||||
# Handle surface material update
|
||||
if _dirty & _DIRTY_SURFACE:
|
||||
_dirty &= ~_DIRTY_SURFACE
|
||||
|
||||
# Set the screen to render using the new screen material
|
||||
$Screen.set_surface_override_material(0, _screen_material)
|
||||
|
||||
# Handle forced redraw of the viewport
|
||||
if _dirty & _DIRTY_REDRAW:
|
||||
_dirty &= ~_DIRTY_REDRAW
|
||||
|
||||
# Force a redraw of the viewport
|
||||
if Engine.is_editor_hint() or update_mode == UpdateMode.UPDATE_ONCE:
|
||||
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
1
addons/godot-xr-tools/objects/viewport_2d_in_3d.gd.uid
Normal file
1
addons/godot-xr-tools/objects/viewport_2d_in_3d.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://eavk7267igp7
|
||||
47
addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn
Normal file
47
addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn
Normal file
@@ -0,0 +1,47 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://clujaf3u776a3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://eavk7267igp7" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://cpl5yiyf1ypft" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d_body.gd" id="2"]
|
||||
|
||||
[sub_resource type="QuadMesh" id="1"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector2(3, 2)
|
||||
|
||||
[sub_resource type="ViewportTexture" id="ViewportTexture_ufe4n"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_fvml0"]
|
||||
transparency = 1
|
||||
cull_mode = 2
|
||||
albedo_texture = SubResource("ViewportTexture_ufe4n")
|
||||
texture_filter = 1
|
||||
|
||||
[sub_resource type="BoxShape3D" id="4"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(3, 2, 0.02)
|
||||
|
||||
[node name="Viewport2Din3D" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
alpha_scissor_threshold = 0.25
|
||||
unshaded = false
|
||||
filter = true
|
||||
|
||||
[node name="Viewport" type="SubViewport" parent="."]
|
||||
disable_3d = true
|
||||
transparent_bg = true
|
||||
gui_embed_subwindows = true
|
||||
size = Vector2i(300, 200)
|
||||
render_target_update_mode = 1
|
||||
|
||||
[node name="Screen" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("1")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_fvml0")
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="."]
|
||||
collision_layer = 5242881
|
||||
collision_mask = 0
|
||||
script = ExtResource("2")
|
||||
viewport_size = Vector2(300, 200)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.01)
|
||||
shape = SubResource("4")
|
||||
217
addons/godot-xr-tools/objects/viewport_2d_in_3d_body.gd
Normal file
217
addons/godot-xr-tools/objects/viewport_2d_in_3d_body.gd
Normal file
@@ -0,0 +1,217 @@
|
||||
extends XRToolsInteractableBody
|
||||
|
||||
|
||||
## Screen size
|
||||
@export var screen_size = Vector2(3.0, 2.0)
|
||||
|
||||
## Viewport size
|
||||
@export var viewport_size = Vector2(100.0, 100.0)
|
||||
|
||||
|
||||
# Current mouse mask
|
||||
var _mouse_mask := 0
|
||||
|
||||
# Viewport node
|
||||
var _viewport : Viewport
|
||||
|
||||
# Dictionary of pointers to touch-index
|
||||
var _touches := {}
|
||||
|
||||
# Dictionary of pressed pointers
|
||||
var _presses := {}
|
||||
|
||||
# Dominant pointer (index == 0)
|
||||
var _dominant : Node3D
|
||||
|
||||
# Mouse pointer
|
||||
var _mouse : Node3D
|
||||
|
||||
# Last mouse position
|
||||
var _mouse_last := Vector2.ZERO
|
||||
|
||||
|
||||
func _ready():
|
||||
# Get viewport node
|
||||
_viewport = get_node("../Viewport")
|
||||
|
||||
# Subscribe to pointer events
|
||||
pointer_event.connect(_on_pointer_event)
|
||||
|
||||
|
||||
## Convert intersection point to screen coordinate
|
||||
func global_to_viewport(p_at : Vector3) -> Vector2:
|
||||
var t = $CollisionShape3D.global_transform
|
||||
var at = t.affine_inverse() * p_at
|
||||
|
||||
# Convert to screen space
|
||||
at.x = ((at.x / screen_size.x) + 0.5) * viewport_size.x
|
||||
at.y = (0.5 - (at.y / screen_size.y)) * viewport_size.y
|
||||
|
||||
return Vector2(at.x, at.y)
|
||||
|
||||
|
||||
# Pointer event handler
|
||||
func _on_pointer_event(event : XRToolsPointerEvent) -> void:
|
||||
# Ignore if we have no viewport
|
||||
if not is_instance_valid(_viewport):
|
||||
return
|
||||
|
||||
# Get the pointer and event type
|
||||
var pointer := event.pointer
|
||||
var type := event.event_type
|
||||
|
||||
# Get the touch-index [0..]
|
||||
var index : int = _touches.get(pointer, -1)
|
||||
|
||||
# Create a new touch-index if necessary
|
||||
if index < 0 or type == XRToolsPointerEvent.Type.ENTERED:
|
||||
# Clear any stale pointer information
|
||||
_touches.erase(pointer)
|
||||
_presses.erase(pointer)
|
||||
|
||||
# Assign a new touch-index for the pointer
|
||||
index = _next_touch_index()
|
||||
_touches[pointer] = index
|
||||
|
||||
# Detect dominant pointer
|
||||
if index == 0:
|
||||
_dominant = pointer
|
||||
|
||||
# Get the viewport positions
|
||||
var at := global_to_viewport(event.position)
|
||||
var last := global_to_viewport(event.last_position)
|
||||
|
||||
# Get/update pressed state
|
||||
var pressed : bool
|
||||
match type:
|
||||
XRToolsPointerEvent.Type.PRESSED:
|
||||
_presses[pointer] = true
|
||||
pressed = true
|
||||
|
||||
XRToolsPointerEvent.Type.RELEASED:
|
||||
_presses.erase(pointer)
|
||||
pressed = false
|
||||
|
||||
_:
|
||||
pressed = _presses.has(pointer)
|
||||
|
||||
# Dispatch touch events
|
||||
match type:
|
||||
XRToolsPointerEvent.Type.PRESSED:
|
||||
_report_touch_down(index, at)
|
||||
|
||||
XRToolsPointerEvent.Type.RELEASED:
|
||||
_report_touch_up(index, at)
|
||||
|
||||
XRToolsPointerEvent.Type.MOVED:
|
||||
_report_touch_move(index, pressed, last, at)
|
||||
|
||||
# If the current mouse isn't pressed then consider switching to a new one
|
||||
if not _presses.has(_mouse):
|
||||
if type == XRToolsPointerEvent.Type.PRESSED and pointer is XRToolsFunctionPointer:
|
||||
# Switch to pressed laser-pointer
|
||||
_mouse = pointer
|
||||
elif type == XRToolsPointerEvent.Type.EXITED and pointer == _mouse:
|
||||
# Current mouse leaving, switch to dominant
|
||||
_mouse = _dominant
|
||||
elif not _mouse and _dominant:
|
||||
# No mouse, pick the dominant
|
||||
_mouse = _dominant
|
||||
|
||||
# Fire mouse events
|
||||
if pointer == _mouse:
|
||||
match type:
|
||||
XRToolsPointerEvent.Type.PRESSED:
|
||||
_report_mouse_down(at)
|
||||
|
||||
XRToolsPointerEvent.Type.RELEASED:
|
||||
_report_mouse_up( at)
|
||||
|
||||
XRToolsPointerEvent.Type.MOVED:
|
||||
_report_mouse_move(pressed, last, at)
|
||||
|
||||
# Clear pointer information on exit
|
||||
if type == XRToolsPointerEvent.Type.EXITED:
|
||||
# Clear pointer information
|
||||
_touches.erase(pointer)
|
||||
_presses.erase(pointer)
|
||||
if pointer == _dominant:
|
||||
_dominant = null
|
||||
if pointer == _mouse:
|
||||
_mouse = null
|
||||
|
||||
|
||||
# Report touch-down event
|
||||
func _report_touch_down(index : int, at : Vector2) -> void:
|
||||
var event := InputEventScreenTouch.new()
|
||||
event.index = index
|
||||
event.position = at
|
||||
event.pressed = true
|
||||
_viewport.push_input(event)
|
||||
|
||||
|
||||
# Report touch-up event
|
||||
func _report_touch_up(index : int, at : Vector2) -> void:
|
||||
var event := InputEventScreenTouch.new()
|
||||
event.index = index
|
||||
event.position = at
|
||||
event.pressed = false
|
||||
_viewport.push_input(event)
|
||||
|
||||
|
||||
# Report touch-move event
|
||||
func _report_touch_move(index : int, pressed : bool, from : Vector2, to : Vector2) -> void:
|
||||
var event := InputEventScreenDrag.new()
|
||||
event.index = index
|
||||
event.position = to
|
||||
event.pressure = 1.0 if pressed else 0.0
|
||||
event.relative = to - from
|
||||
_viewport.push_input(event)
|
||||
|
||||
|
||||
# Report mouse-down event
|
||||
func _report_mouse_down(at : Vector2) -> void:
|
||||
var event := InputEventMouseButton.new()
|
||||
event.button_index = 1
|
||||
event.pressed = true
|
||||
event.position = at
|
||||
event.global_position = at
|
||||
event.button_mask = 1
|
||||
_viewport.push_input(event)
|
||||
|
||||
|
||||
# Report mouse-up event
|
||||
func _report_mouse_up(at : Vector2) -> void:
|
||||
var event := InputEventMouseButton.new()
|
||||
event.button_index = 1
|
||||
event.pressed = false
|
||||
event.position = at
|
||||
event.global_position = at
|
||||
event.button_mask = 0
|
||||
_viewport.push_input(event)
|
||||
|
||||
|
||||
# Report mouse-move event
|
||||
func _report_mouse_move(pressed : bool, from : Vector2, to : Vector2) -> void:
|
||||
var event := InputEventMouseMotion.new()
|
||||
event.position = to
|
||||
event.global_position = to
|
||||
event.relative = to - from
|
||||
event.button_mask = 1 if pressed else 0
|
||||
event.pressure = 1.0 if pressed else 0.0
|
||||
_viewport.push_input(event)
|
||||
|
||||
|
||||
# Find the next free touch index
|
||||
func _next_touch_index() -> int:
|
||||
# Get the current touches
|
||||
var current := _touches.values()
|
||||
current.sort()
|
||||
|
||||
# Look for a hole
|
||||
for touch in current.size():
|
||||
if current[touch] != touch:
|
||||
return touch
|
||||
|
||||
# No hole so add to end
|
||||
return current.size()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpl5yiyf1ypft
|
||||
44
addons/godot-xr-tools/objects/virtual_keyboard.tscn
Normal file
44
addons/godot-xr-tools/objects/virtual_keyboard.tscn
Normal file
@@ -0,0 +1,44 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://dgdb1texynduw"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://lauwp8okd1vh" path="res://addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.tscn" id="2"]
|
||||
|
||||
[sub_resource type="QuadMesh" id="QuadMesh_iets3"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector2(1.5, 0.75)
|
||||
|
||||
[sub_resource type="ViewportTexture" id="ViewportTexture_ncnoj"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_kjc12"]
|
||||
cull_mode = 2
|
||||
shading_mode = 0
|
||||
albedo_texture = SubResource("ViewportTexture_ncnoj")
|
||||
texture_filter = 1
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_twvoe"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(1.5, 0.75, 0.02)
|
||||
|
||||
[node name="VirtualKeyboard" instance=ExtResource("1")]
|
||||
screen_size = Vector2(1.5, 0.75)
|
||||
scene = ExtResource("2")
|
||||
viewport_size = Vector2(400, 200)
|
||||
update_mode = 2
|
||||
throttle_fps = 15.0
|
||||
transparent = 0
|
||||
unshaded = true
|
||||
|
||||
[node name="Viewport" parent="." index="0"]
|
||||
transparent_bg = false
|
||||
size = Vector2i(400, 200)
|
||||
|
||||
[node name="Screen" parent="." index="1"]
|
||||
mesh = SubResource("QuadMesh_iets3")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_kjc12")
|
||||
|
||||
[node name="StaticBody3D" parent="." index="2"]
|
||||
screen_size = Vector2(1.5, 0.75)
|
||||
viewport_size = Vector2(400, 200)
|
||||
|
||||
[node name="CollisionShape3D" parent="StaticBody3D" index="0"]
|
||||
shape = SubResource("BoxShape3D_twvoe")
|
||||
14
addons/godot-xr-tools/objects/wind_area.gd
Normal file
14
addons/godot-xr-tools/objects/wind_area.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
class_name XRToolsWindArea
|
||||
extends Area3D
|
||||
|
||||
## Vector (direction and magnitude) of wind in this area
|
||||
@export var wind_vector : Vector3 = Vector3.ZERO
|
||||
|
||||
## Wind drag factor
|
||||
@export var drag : float = 1.0
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsWindArea"
|
||||
1
addons/godot-xr-tools/objects/wind_area.gd.uid
Normal file
1
addons/godot-xr-tools/objects/wind_area.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c7l0klrkdolyw
|
||||
11
addons/godot-xr-tools/objects/wind_area.tscn
Normal file
11
addons/godot-xr-tools/objects/wind_area.tscn
Normal file
@@ -0,0 +1,11 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://nack1qite6lx"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c7l0klrkdolyw" path="res://addons/godot-xr-tools/objects/wind_area.gd" id="1"]
|
||||
|
||||
[node name="WindArea" type="Area3D"]
|
||||
collision_layer = 524288
|
||||
collision_mask = 0
|
||||
monitoring = false
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
64
addons/godot-xr-tools/objects/world_grab_area.gd
Normal file
64
addons/godot-xr-tools/objects/world_grab_area.gd
Normal file
@@ -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(xr_name: String) -> bool:
|
||||
return xr_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) -> 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(_by: Node3D, _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())
|
||||
1
addons/godot-xr-tools/objects/world_grab_area.gd.uid
Normal file
1
addons/godot-xr-tools/objects/world_grab_area.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://curcpmtyhxa1b
|
||||
8
addons/godot-xr-tools/objects/world_grab_area.tscn
Normal file
8
addons/godot-xr-tools/objects/world_grab_area.tscn
Normal file
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://57q7hhomocdh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://curcpmtyhxa1b" path="res://addons/godot-xr-tools/objects/world_grab_area.gd" id="1_uxhq5"]
|
||||
|
||||
[node name="WorldGrabArea" type="Area3D"]
|
||||
collision_layer = 262144
|
||||
collision_mask = 524288
|
||||
script = ExtResource("1_uxhq5")
|
||||
Reference in New Issue
Block a user