162 lines
6.1 KiB
GDScript
162 lines
6.1 KiB
GDScript
"""
|
|
Asset: Godot Simple Portal System
|
|
File: advanced_portal_teleport.gd
|
|
Description: An area which teleports a node through the parent node's portal. Checks entry velocity and handles RigidBody3D and CharacterBody3D physics. Can also handle a portal clone of the node if specified.
|
|
Instructions: For detailed documentation, see the README or visit: https://github.com/Donitzo/godot-simple-portal-system
|
|
Repository: https://github.com/Donitzo/godot-simple-portal-system
|
|
License: CC0 License
|
|
"""
|
|
|
|
extends Area3D
|
|
class_name AdvancedPortalTeleport
|
|
|
|
## Checks if the node is moving TOWARDS the portal before teleporting it.
|
|
@export var velocity_check:bool = true
|
|
## An additional velocity push given to RigidBody3Ds/CharacterBody3D exiting the portal.
|
|
@export var exit_push_velocity:float = 0
|
|
## Seconds to keep portal clones visible after the node leaves the teleporter.
|
|
@export var clone_keep_alive_seconds:float = 0.1
|
|
|
|
var _parent_portal:Portal
|
|
|
|
# Info about the nodes currently crossing the portal
|
|
var _crossing_nodes:Array = []
|
|
|
|
func _ready() -> void:
|
|
_parent_portal = get_parent() as Portal
|
|
if _parent_portal == null:
|
|
push_error("The PortalTeleport \"%s\" is not a child of a Portal instance" % name)
|
|
|
|
connect("area_entered", _on_area_entered)
|
|
connect("area_exited", _on_area_exited)
|
|
|
|
func _process(delta:float) -> void:
|
|
# Update nodes crossing the portal
|
|
for i in range(_crossing_nodes.size() - 1, -1, -1):
|
|
var crossing_node:Dictionary = _crossing_nodes[i]
|
|
|
|
if not is_instance_valid(crossing_node.node):
|
|
# Node has been freed, remove crossing_node
|
|
_crossing_nodes.remove_at(i)
|
|
continue
|
|
|
|
# If the portal has yet to leave the enter portal area, try to teleport it
|
|
if not crossing_node.left and _try_teleport(_crossing_nodes[i]):
|
|
# Switch portals so that the portal clone is placed at the entrance portal instead
|
|
crossing_node.clone_portal = _parent_portal.exit_portal
|
|
crossing_node.left = true
|
|
|
|
# Move the clone to the exit portal
|
|
if crossing_node.clone != null and crossing_node.clone_portal != null:
|
|
crossing_node.clone.global_transform = crossing_node.clone_portal.real_to_exit_transform(crossing_node.node.global_transform)
|
|
|
|
# If the node has left the enter portal, keep it a bit longer before erasing it
|
|
if crossing_node.left:
|
|
crossing_node.keep_alive_seconds -= delta
|
|
if crossing_node.keep_alive_seconds <= 0:
|
|
# Hide portal clone
|
|
if crossing_node.clone != null:
|
|
crossing_node.clone.visible = false
|
|
|
|
_crossing_nodes.remove_at(i)
|
|
|
|
# Try to teleport the crossing node, and return false otherwise
|
|
func _try_teleport(crossing_node:Dictionary) -> bool:
|
|
var node:Node3D = crossing_node.node
|
|
|
|
# Check if the node is moving towards the portal
|
|
if velocity_check:
|
|
if node is RigidBody3D:
|
|
var local_velocity:Vector3 = _parent_portal.global_transform.basis.inverse() * node.linear_velocity
|
|
if local_velocity.z >= 0:
|
|
return false
|
|
elif node is CharacterBody3D:
|
|
var local_velocity:Vector3 = _parent_portal.global_transform.basis.inverse() * node.velocity
|
|
if local_velocity.z >= 0:
|
|
return false
|
|
else:
|
|
var last_position = crossing_node.position
|
|
crossing_node.position = _parent_portal.to_local(node.global_position)
|
|
if last_position == null or last_position.z <= crossing_node.position.z:
|
|
return false
|
|
|
|
# Handle RigidBody3D physics
|
|
if node is RigidBody3D:
|
|
# Rotate rotation and velocity
|
|
node.linear_velocity = _parent_portal.real_to_exit_direction(node.linear_velocity)
|
|
node.angular_velocity *= _parent_portal.real_to_exit_transform(Transform3D.IDENTITY).basis.inverse()
|
|
|
|
# Additional push when exiting the portal
|
|
if exit_push_velocity > 0:
|
|
var exit_forward:Vector3 = _parent_portal.exit_portal.global_transform.basis.z.normalized()
|
|
node.linear_velocity += exit_forward * exit_push_velocity
|
|
|
|
# Handle CharacterBody3D physics
|
|
elif node is CharacterBody3D:
|
|
# Rotate velocity
|
|
node.velocity = _parent_portal.real_to_exit_direction(node.velocity)
|
|
|
|
# Additional push when exiting the portal
|
|
if exit_push_velocity > 0:
|
|
var exit_forward:Vector3 = _parent_portal.exit_portal.global_transform.basis.z.normalized()
|
|
node.velocity += exit_forward * exit_push_velocity
|
|
|
|
# Transform the position and orientation
|
|
node.global_transform = _parent_portal.real_to_exit_transform(node.global_transform)
|
|
|
|
return true
|
|
|
|
func _on_area_entered(area:Area3D) -> void:
|
|
if area.has_meta("teleportable_root"):
|
|
# The node may not teleport immediately if it's not heading TOWARDS the portal,
|
|
# so we keep a reference to it until it teleports or leaves.
|
|
# This also allows us to hide its portal clone after it leaves.
|
|
|
|
var root:Node3D = area.get_node(area.get_meta("teleportable_root"))
|
|
|
|
var crossing_node:Dictionary
|
|
if root.has_meta("crossing_node"):
|
|
# Node is crossing another portal, erase it from that portal and start using this one instead
|
|
crossing_node = root.get_meta("crossing_node")
|
|
crossing_node.teleporter._crossing_nodes.erase(crossing_node)
|
|
else:
|
|
# First portal
|
|
var clone:Node3D = area.get_node(area.get_meta("portal_clone")) if area.has_meta("portal_clone") else null
|
|
crossing_node = {
|
|
"node": root,
|
|
"clone": clone,
|
|
"clone_portal": null,
|
|
"teleporter": null,
|
|
"left": false,
|
|
"keep_alive_seconds": 0.0,
|
|
"position": null,
|
|
}
|
|
root.set_meta("crossing_node", crossing_node)
|
|
|
|
crossing_node.clone_portal = _parent_portal
|
|
crossing_node.teleporter = self
|
|
crossing_node.left = false
|
|
crossing_node.keep_alive_seconds = clone_keep_alive_seconds
|
|
crossing_node.position = null
|
|
|
|
# Show portal clone if it exists
|
|
if crossing_node.clone != null:
|
|
crossing_node.clone.visible = true
|
|
|
|
# Keep track of the node in this portal
|
|
_crossing_nodes.push_back(crossing_node)
|
|
|
|
# Try an initial teleport
|
|
if _try_teleport(crossing_node):
|
|
crossing_node.clone_portal = _parent_portal.exit_portal
|
|
crossing_node.left = true
|
|
|
|
func _on_area_exited(area:Area3D) -> void:
|
|
if area.has_meta("teleportable_root"):
|
|
var root:Node3D = area.get_node(area.get_meta("teleportable_root"))
|
|
var crossing_node:Dictionary = root.get_meta("crossing_node")
|
|
|
|
if crossing_node.teleporter == self:
|
|
# The node left the enter portal without teleporting (but don't erase it yet)
|
|
crossing_node.left = true
|