add vr template project
This commit is contained in:
@@ -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(name : String) -> bool:
|
||||
return 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwydogs6is45x
|
||||
@@ -0,0 +1,10 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cjyilbm4ucc7s"]
|
||||
|
||||
[ext_resource type="Script" 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="."]
|
||||
@@ -0,0 +1,99 @@
|
||||
@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(name : String) -> bool:
|
||||
return 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://pc8g5reafi7c
|
||||
@@ -0,0 +1,171 @@
|
||||
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
|
||||
|
||||
|
||||
## 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 p_point:
|
||||
transform = p_point.transform
|
||||
elif p_precise:
|
||||
transform = p_what.global_transform.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:
|
||||
what.add_collision_exception_with(collision_hand)
|
||||
collision_hand.add_collision_exception_with(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:
|
||||
transform = point.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
|
||||
if is_instance_valid(collision_hand):
|
||||
what.remove_collision_exception_with(collision_hand)
|
||||
collision_hand.remove_collision_exception_with(what)
|
||||
|
||||
# Report the release
|
||||
print_verbose("%s> released by %s", [what.name, by.name])
|
||||
what.released.emit(what, by)
|
||||
|
||||
|
||||
# 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dldogfjp88w5w
|
||||
@@ -0,0 +1,216 @@
|
||||
class_name XRToolsGrabDriver
|
||||
extends RemoteTransform3D
|
||||
|
||||
|
||||
## Grab state
|
||||
enum State {
|
||||
LERP,
|
||||
SNAP,
|
||||
}
|
||||
|
||||
|
||||
## Drive state
|
||||
var state : State = State.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.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.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.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:
|
||||
State.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 = State.SNAP
|
||||
if primary: primary.set_arrived()
|
||||
if secondary: secondary.set_arrived()
|
||||
|
||||
# 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 == State.SNAP:
|
||||
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
|
||||
|
||||
|
||||
# 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 = State.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 = State.SNAP
|
||||
driver.target = p_target
|
||||
driver.primary = p_grab
|
||||
driver.global_transform = p_grab.by.global_transform * p_grab.transform.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)
|
||||
|
||||
# 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://40m5rmpwo51
|
||||
@@ -0,0 +1,28 @@
|
||||
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.
|
||||
|
||||
|
||||
## 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d3qll1uorg88f
|
||||
@@ -0,0 +1,213 @@
|
||||
@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
|
||||
|
||||
|
||||
## 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()
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
# If left hand then verify left controller
|
||||
if hand == Hand.LEFT and controller.tracker != "left_hand":
|
||||
return false
|
||||
|
||||
# If right hand then verify right controller
|
||||
if hand == Hand.RIGHT and controller.tracker != "right_hand":
|
||||
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://obchv1tx4crh
|
||||
@@ -0,0 +1,7 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://c25yxb0vt53vc"]
|
||||
|
||||
[ext_resource type="Script" 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")
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ctw7nbntd5pcj"]
|
||||
|
||||
[ext_resource type="Script" 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://1vp1ger1vuws
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ca3daqmpo0tua"]
|
||||
|
||||
[ext_resource type="Script" 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")
|
||||
@@ -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://fbex26bgf88h
|
||||
@@ -0,0 +1,7 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dh8grd7s3n8kg"]
|
||||
|
||||
[ext_resource type="Script" 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")
|
||||
@@ -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
|
||||
hand = XRToolsHand.find_instance(controller)
|
||||
collision_hand = XRToolsCollisionHand.find_instance(controller)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c25o68ugepdhc
|
||||
@@ -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(name : String) -> bool:
|
||||
return 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cluve1c5lcb4c
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dc5t2qgmhb2nf"]
|
||||
|
||||
[ext_resource type="Script" 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(name : String) -> bool:
|
||||
return 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://bxu3usl4mkf7j
|
||||
@@ -0,0 +1,35 @@
|
||||
@tool
|
||||
class_name XRToolsHighlightRing
|
||||
extends MeshInstance3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(name : String) -> bool:
|
||||
return 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://d0xve6l1upti8
|
||||
@@ -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 )
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://da2qgxxwwitl6"]
|
||||
|
||||
[ext_resource type="Script" 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")
|
||||
@@ -0,0 +1,35 @@
|
||||
@tool
|
||||
class_name XRToolsHighlightVisible
|
||||
extends Node3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(name : String) -> bool:
|
||||
return 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://d1t1vxg2tf3j8
|
||||
@@ -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(name : String) -> bool:
|
||||
return name == "XRToolsInteractableArea"
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxhdix8ctxemr
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dm2jhqcty3h3u
|
||||
@@ -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://b1rqc51t4qb2h
|
||||
@@ -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://dsml8dpqkkacm
|
||||
@@ -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(name : String) -> bool:
|
||||
return 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://bceqvu4k2dhhb
|
||||
@@ -0,0 +1,724 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://lauwp8okd1vh"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://addons/godot-xr-tools/objects/keyboard/virtual_key_char.gd" id="2_n0nlg"]
|
||||
[ext_resource type="Script" 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"]
|
||||
@@ -0,0 +1,400 @@
|
||||
@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 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(name : String) -> bool:
|
||||
return 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
|
||||
emit_signal("action_pressed", 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:
|
||||
emit_signal("highlight_updated", 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dovapeepq3sdr
|
||||
@@ -0,0 +1,11 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://c8l60rnugru40"]
|
||||
|
||||
[ext_resource type="Script" 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="."]
|
||||
@@ -0,0 +1,107 @@
|
||||
@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(name : String) -> bool:
|
||||
return 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:
|
||||
# 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dpuiqviey3b11
|
||||
@@ -0,0 +1,321 @@
|
||||
@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(name : String) -> bool:
|
||||
return name == "XRToolsSnapZone"
|
||||
|
||||
|
||||
func _ready():
|
||||
# Set collision shape radius
|
||||
$CollisionShape3D.shape.radius = grab_distance
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
# 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
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://wx42ci3ia2yi
|
||||
@@ -0,0 +1,23 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://ce7vysyvondf8"]
|
||||
|
||||
[ext_resource type="Script" 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"]
|
||||
@@ -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(name : String) -> bool:
|
||||
return 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bd2s7jvwxy0qa
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dpy1eg3i331se"]
|
||||
|
||||
[ext_resource type="Script" 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")
|
||||
@@ -0,0 +1,614 @@
|
||||
@tool
|
||||
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
|
||||
@export 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
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
# 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_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 = "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)
|
||||
|
||||
|
||||
## 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()
|
||||
_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
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
# Construct the new screen material
|
||||
if material:
|
||||
# Copy custom material
|
||||
_screen_material = material.duplicate()
|
||||
else:
|
||||
# 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
|
||||
|
||||
# Update our viewport texture, it will have changed
|
||||
_dirty |= _DIRTY_ALBEDO
|
||||
|
||||
# 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:
|
||||
# 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://4xvdmrnws44w
|
||||
@@ -0,0 +1,47 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://clujaf3u776a3"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.gd" id="1"]
|
||||
[ext_resource type="Script" 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")
|
||||
@@ -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://dru0ogdcxwjuf
|
||||
@@ -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")
|
||||
@@ -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(name : String) -> bool:
|
||||
return name == "XRToolsWindArea"
|
||||
@@ -0,0 +1 @@
|
||||
uid://drvmixiuxlccx
|
||||
@@ -0,0 +1,11 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://nack1qite6lx"]
|
||||
|
||||
[ext_resource type="Script" 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="."]
|
||||
@@ -0,0 +1,64 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
|
||||
class_name XRToolsWorldGrabArea
|
||||
extends Area3D
|
||||
|
||||
|
||||
## XR Tools World-Grab Area
|
||||
##
|
||||
## This script adds world-grab areas to an environment
|
||||
##
|
||||
## For world-grab to work, the player must have an [XRToolsMovementWorldGrab]
|
||||
## node configured appropriately.
|
||||
|
||||
|
||||
## If true, the grip control must be held to keep holding the climbable
|
||||
var press_to_hold : bool = true
|
||||
|
||||
## Dictionary of temporary grab-handles indexed by the pickup node.
|
||||
var grab_locations := {}
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(name : String) -> bool:
|
||||
return name == "XRToolsWorldGrabArea"
|
||||
|
||||
|
||||
# Called by XRToolsFunctionPickup
|
||||
func is_picked_up() -> bool:
|
||||
return false
|
||||
|
||||
func can_pick_up(_by: Node3D) -> bool:
|
||||
return true
|
||||
|
||||
# Called by XRToolsFunctionPickup when user presses the action button while holding this object
|
||||
func action():
|
||||
pass
|
||||
|
||||
# Ignore highlighting requests from XRToolsFunctionPickup
|
||||
func request_highlight(_from, _on) -> void:
|
||||
pass
|
||||
|
||||
# Called by XRToolsFunctionPickup when this is picked up by a controller
|
||||
func pick_up(by: Node3D) -> 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())
|
||||
@@ -0,0 +1 @@
|
||||
uid://cvkttssme73fm
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://57q7hhomocdh"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot-xr-tools/objects/world_grab_area.gd" id="1_uxhq5"]
|
||||
|
||||
[node name="WorldGrabArea" type="Area3D"]
|
||||
collision_layer = 262144
|
||||
collision_mask = 524288
|
||||
script = ExtResource("1_uxhq5")
|
||||
Reference in New Issue
Block a user