Lots of stuff I forgot to commit because Holidays

- D&D dice engine (see README)
- Markdown support
- Phantom camera
This commit is contained in:
Tony Bark 2023-12-24 20:39:57 -05:00
parent 9589acd877
commit 2b41f84b05
125 changed files with 13170 additions and 23 deletions

View file

@ -0,0 +1,5 @@
@tool
extends RefCounted
const PCAM_GROUP_NAME: StringName = "phantom_camera_group"
const PCAM_HOST_GROUP_NAME: StringName = "phantom_camera_host_group"

View file

@ -0,0 +1,477 @@
@tool
@icon("res://addons/phantom_camera/icons/PhantomCameraIcon2D.svg")
class_name PhantomCamera2D
extends Node2D
const Constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd")
var Properties = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_properties.gd").new()
const FOLLOW_GROUP_ZOOM_AUTO: StringName = Constants.FOLLOW_PARAMETERS_NAME + "auto_zoom"
const FOLLOW_GROUP_ZOOM_MIN: StringName = Constants.FOLLOW_PARAMETERS_NAME + "min_zoom"
const FOLLOW_GROUP_ZOOM_MAX: StringName = Constants.FOLLOW_PARAMETERS_NAME + "max_zoom"
const FOLLOW_GROUP_ZOOM_MARGIN: StringName = Constants.FOLLOW_PARAMETERS_NAME + "zoom_margin"
var follow_group_zoom_auto: bool
var follow_group_zoom_min: float = 1
var follow_group_zoom_max: float = 5
var follow_group_zoom_margin: Vector4
var _camera_offset: Vector2
func _get_property_list() -> Array:
var property_list: Array[Dictionary]
property_list.append_array(Properties.add_priority_properties())
property_list.append({
"name": Constants.ZOOM_PROPERTY_NAME,
"type": TYPE_VECTOR2,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT
})
property_list.append_array(Properties.add_follow_mode_property())
if Properties.follow_mode != Constants.FollowMode.NONE:
property_list.append_array(Properties.add_follow_target_property())
if Properties.follow_mode == Constants.FollowMode.GROUP:
property_list.append({
"name": FOLLOW_GROUP_ZOOM_AUTO,
"type": TYPE_BOOL,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT,
})
if follow_group_zoom_auto:
property_list.append({
"name": FOLLOW_GROUP_ZOOM_MIN,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0.01, 100, 0.01,",
"usage": PROPERTY_USAGE_DEFAULT,
})
property_list.append({
"name": FOLLOW_GROUP_ZOOM_MAX,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0.01, 100, 0.01,",
"usage": PROPERTY_USAGE_DEFAULT,
})
property_list.append({
"name": FOLLOW_GROUP_ZOOM_MARGIN,
"type": TYPE_VECTOR4,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0, 100, 0.01,",
"usage": PROPERTY_USAGE_DEFAULT,
})
if Properties.follow_has_target || Properties.has_follow_group:
property_list.append_array(Properties.add_follow_properties())
property_list.append_array(Properties.add_follow_framed())
property_list.append_array(Properties.add_tween_properties())
property_list.append_array(Properties.add_secondary_properties())
return property_list
func _set(property: StringName, value) -> bool:
Properties.set_priority_property(property, value, self)
# ZOOM
if property == Constants.ZOOM_PROPERTY_NAME:
if value.x == 0:
Properties.zoom.x = 0.001
else:
Properties.zoom.x = value.x
if value.y == 0:
Properties.zoom.y = 0.001
else:
Properties.zoom.y = value.y
# ZOOM CLAMP
if property == FOLLOW_GROUP_ZOOM_AUTO:
follow_group_zoom_auto = value
notify_property_list_changed()
if property == FOLLOW_GROUP_ZOOM_MIN:
if value > 0:
follow_group_zoom_min = value
else:
follow_group_zoom_min = 0
if property == FOLLOW_GROUP_ZOOM_MAX:
if value > 0:
follow_group_zoom_max = value
else:
follow_group_zoom_max = 0
if property == FOLLOW_GROUP_ZOOM_MARGIN:
follow_group_zoom_margin = value
Properties.set_follow_properties(property, value, self)
Properties.set_tween_properties(property, value, self)
Properties.set_secondary_properties(property, value, self)
return false
func _get(property: StringName):
if property == Constants.PRIORITY_PROPERTY_NAME: return Properties.priority
if property == Constants.ZOOM_PROPERTY_NAME: return Properties.zoom
if property == Constants.FOLLOW_MODE_PROPERTY_NAME: return Properties.follow_mode
if property == Constants.FOLLOW_TARGET_OFFSET_PROPERTY_NAME: return Properties.follow_target_offset_2D
if property == Constants.FOLLOW_TARGET_PROPERTY_NAME: return Properties.follow_target_path
if property == Constants.FOLLOW_GROUP_PROPERTY_NAME: return Properties.follow_group_paths
if property == Constants.FOLLOW_PATH_PROPERTY_NAME: return Properties.follow_path_path
if property == Constants.FOLLOW_FRAMED_DEAD_ZONE_HORIZONTAL_NAME: return Properties.follow_framed_dead_zone_width
if property == Constants.FOLLOW_FRAMED_DEAD_ZONE_VERTICAL_NAME: return Properties.follow_framed_dead_zone_height
if property == Constants.FOLLOW_VIEWFINDER_IN_PLAY_NAME: return Properties.show_viewfinder_in_play
if property == FOLLOW_GROUP_ZOOM_AUTO: return follow_group_zoom_auto
if property == FOLLOW_GROUP_ZOOM_MIN: return follow_group_zoom_min
if property == FOLLOW_GROUP_ZOOM_MAX: return follow_group_zoom_max
if property == FOLLOW_GROUP_ZOOM_MARGIN: return follow_group_zoom_margin
if property == Constants.FOLLOW_DAMPING_NAME: return Properties.follow_has_damping
if property == Constants.FOLLOW_DAMPING_VALUE_NAME: return Properties.follow_damping_value
if property == Constants.TWEEN_RESOURCE_PROPERTY_NAME: return Properties.tween_resource
if property == Constants.INACTIVE_UPDATE_MODE_PROPERTY_NAME: return Properties.inactive_update_mode
if property == Constants.TWEEN_ONLOAD_NAME: return Properties.tween_onload
###################
# Private Functions
###################
func _enter_tree() -> void:
Properties.is_2D = true
Properties.camera_enter_tree(self)
Properties.assign_pcam_host(self)
func _exit_tree() -> void:
if Properties.pcam_host_owner:
Properties.pcam_host_owner.pcam_removed_from_scene(self)
Properties.pcam_exit_tree(self)
func _physics_process(delta: float) -> void:
# print(follow_group_zoom_margin)
if not Properties.is_active:
match Properties.inactive_update_mode:
Constants.InactiveUpdateMode.NEVER:
return
# Constants.InactiveUpdateMode.EXPONENTIALLY:
# TODO
if not Properties.should_follow: return
match Properties.follow_mode:
Constants.FollowMode.GLUED:
if Properties.follow_target_node:
_interpolate_position(Properties.follow_target_node.position, delta)
Constants.FollowMode.SIMPLE:
if Properties.follow_target_node:
_interpolate_position(_target_position_with_offset(), delta)
Constants.FollowMode.GROUP:
if Properties.has_follow_group:
if Properties.follow_group_nodes_2D.size() == 1:
_interpolate_position(Properties.follow_group_nodes_2D[0].get_global_position(), delta)
else:
var rect: Rect2 = Rect2(Properties.follow_group_nodes_2D[0].get_global_position(), Vector2.ZERO)
for node in Properties.follow_group_nodes_2D:
rect = rect.expand(node.get_global_position())
if follow_group_zoom_auto:
rect = rect.grow_individual(
follow_group_zoom_margin.x,
follow_group_zoom_margin.y,
follow_group_zoom_margin.z,
follow_group_zoom_margin.w)
# else:
# rect = rect.grow_individual(-80, 0, 0, 0)
if follow_group_zoom_auto:
var screen_size: Vector2 = get_viewport_rect().size
if rect.size.x > rect.size.y * screen_size.aspect():
Properties.zoom = clamp(screen_size.x / rect.size.x, follow_group_zoom_min, follow_group_zoom_max) * Vector2.ONE
else:
Properties.zoom = clamp(screen_size.y / rect.size.y, follow_group_zoom_min, follow_group_zoom_max) * Vector2.ONE
_interpolate_position(rect.get_center(), delta)
Constants.FollowMode.PATH:
if Properties.follow_target_node and Properties.follow_path_node:
var path_position: Vector2 = Properties.follow_path_node.get_global_position()
_interpolate_position(
Properties.follow_path_node.curve.get_closest_point(
Properties.follow_target_node.get_global_position() - path_position
) + path_position, \
delta)
Constants.FollowMode.FRAMED:
if Properties.follow_target_node:
if not Engine.is_editor_hint():
Properties.viewport_position = (get_follow_target_node().get_global_transform_with_canvas().get_origin() + Properties.follow_target_offset_2D) / get_viewport_rect().size
if Properties.get_framed_side_offset() != Vector2.ZERO:
var glo_pos: Vector2
var target_position: Vector2 = _target_position_with_offset() + _camera_offset
var dead_zone_width: float = Properties.follow_framed_dead_zone_width
var dead_zone_height: float = Properties.follow_framed_dead_zone_height
if dead_zone_width == 0 || dead_zone_height == 0:
if dead_zone_width == 0 && dead_zone_height != 0:
_interpolate_position(_target_position_with_offset(), delta)
elif dead_zone_width != 0 && dead_zone_height == 0:
glo_pos = _target_position_with_offset()
glo_pos.x += target_position.x - global_position.x
_interpolate_position(glo_pos, delta)
else:
_interpolate_position(_target_position_with_offset(), delta)
else:
_interpolate_position(target_position, delta)
else:
_camera_offset = global_position - _target_position_with_offset()
else:
set_global_position(_target_position_with_offset())
func _target_position_with_offset() -> Vector2:
return Properties.follow_target_node.get_global_position() + Properties.follow_target_offset_2D
func _interpolate_position(position: Vector2, delta: float, target: Node2D = self) -> void:
if Properties.follow_has_damping:
target.set_global_position(
target.get_global_position().lerp(
position,
delta * Properties.follow_damping_value
)
)
else:
target.set_global_position(position)
##################
# Public Functions
##################
## Assigns the PhantomCamera2D to a new PhantomCameraHost.
func assign_pcam_host() -> void:
Properties.assign_pcam_host(self)
## Gets the current PhantomCameraHost this PhantomCamera2D is assigned to.
func get_pcam_host_owner() -> PhantomCameraHost:
return Properties.pcam_host_owner
## Assigns new Zoom value.
func set_zoom(value: Vector2) -> void:
Properties.zoom = value
## Gets current Zoom value.
func get_zoom() -> Vector2:
return Properties.zoom
## Assigns new Priority value.
func set_priority(value: int) -> void:
Properties.set_priority(value, self)
## Gets current Priority value.
func get_priority() -> int:
return Properties.priority
## Assigns a new PhantomCameraTween resource to the PhantomCamera2D
func set_tween_resource(value: PhantomCameraTween) -> void:
Properties.tween_resource = value
## Gets the PhantomCameraTween resource assigned to the PhantomCamera2D
## Returns null if there's nothing assigned to it.
func get_tween_resource() -> PhantomCameraTween:
return Properties.tween_resource
## Assigns a new Tween Duration value. The duration value is in seconds.
## Note: This will override and make the Tween Resource unique to this PhantomCamera2D.
func set_tween_duration(value: float) -> void:
if get_tween_resource():
Properties.tween_resource_default.duration = value
Properties.tween_resource_default.transition = Properties.tween_resource.transition
Properties.tween_resource_default.ease = Properties.tween_resource.ease
set_tween_resource(null) # Clears resource from PCam instance
else:
Properties.tween_resource_default.duration = value
## Gets the current Tween Duration value. The duration value is in seconds.
func get_tween_duration() -> float:
if get_tween_resource():
return get_tween_resource().duration
else:
return Properties.tween_resource_default.duration
## Assigns a new Tween Transition value.
## Note: This will override and make the Tween Resource unique to this PhantomCamera2D.
func set_tween_transition(value: Constants.TweenTransitions) -> void:
if get_tween_resource():
Properties.tween_resource_default.duration = Properties.tween_resource.duration
Properties.tween_resource_default.transition = value
Properties.tween_resource_default.ease = Properties.tween_resource.ease
set_tween_resource(null) # Clears resource from PCam instance
else:
Properties.tween_resource_default.transition = value
## Gets the current Tween Transition value.
func get_tween_transition() -> int:
if get_tween_resource():
return get_tween_resource().transition
else:
return Properties.tween_resource_default.transition
## Assigns a new Tween Ease value.
## Note: This will override and make the Tween Resource unique to this PhantomCamera2D.
func set_tween_ease(value: Constants.TweenEases) -> void:
if get_tween_resource():
Properties.tween_resource_default.duration = Properties.tween_resource.duration
Properties.tween_resource_default.transition = Properties.tween_resource.ease
Properties.tween_resource_default.ease = value
set_tween_resource(null) # Clears resource from PCam instance
else:
Properties.tween_resource_default.ease = value
## Gets the current Tween Ease value.
func get_tween_ease() -> int:
if get_tween_resource():
return get_tween_resource().ease
else:
return Properties.tween_resource_default.ease
## Gets current active state of the PhantomCamera2D.
## If it returns true, it means the PhantomCamera2D is what the Camera2D is currently following.
func is_active() -> bool:
return Properties.is_active
## Enables or disables the Tween on Load.
func set_tween_on_load(value: bool) -> void:
Properties.tween_onload = value
## Gets the current Tween On Load value.
func is_tween_on_load() -> bool:
return Properties.tween_onload
## Gets the current follow mode as an enum int based on Constants.FOLLOW_MODE enum.
## Note: Setting Follow Mode purposely not added. A separate PCam should be used instead.
func get_follow_mode() -> int:
return Properties.follow_mode
## Assigns a new Node2D as the Follow Target property.
func set_follow_target_node(value: Node2D) -> void:
Properties.follow_target_node = value
Properties.should_follow = true
## Erases the current Node2D from the Follow Target property.
func erase_follow_target_node() -> void:
Properties.should_follow = false
Properties.follow_target_node = null
## Gets the current Node2D target property.
func get_follow_target_node():
if Properties.follow_target_node:
return Properties.follow_target_node
else:
printerr("No Follow Target Node assigned")
## Assigns a new Path2D to the Follow Path property.
func set_follow_path(value: Path2D) -> void:
Properties.follow_path_node = value
## Erases the current Path2D from the Follow Path property.
func erase_follow_path() -> void:
Properties.follow_path_node = null
## Gets the current Path2D from the Follow Path property.
func get_follow_path():
if Properties.follow_path_node:
return Properties.follow_path_node
else:
printerr("No Follow Path assigned")
## Assigns a new Vector2 for the Follow Target Offset property.
func set_follow_target_offset(value: Vector2) -> void:
Properties.follow_target_offset_2D = value
## Gets the current Vector2 for the Follow Target Offset property.
func get_follow_target_offset() -> Vector2:
return Properties.follow_target_offset_2D
## Enables or disables Follow Damping.
func set_follow_has_damping(value: bool) -> void:
Properties.follow_has_damping = value
## Gets the currents Follow Damping property.
func get_follow_has_damping() -> bool:
return Properties.follow_has_damping
## Assigns new Damping value.
func set_follow_damping_value(value: float) -> void:
Properties.follow_damping_value = value
## Gets the currents Follow Damping value.
func get_follow_damping_value() -> float:
return Properties.follow_damping_value
## Adds a single Node2D to Follow Group array.
func append_follow_group_node(value: Node2D) -> void:
if not Properties.follow_group_nodes_2D.has(value):
Properties.follow_group_nodes_2D.append(value)
Properties.should_follow = true
Properties.has_follow_group = true
else:
printerr(value, " is already part of Follow Group")
## Adds an Array of type Node2D to Follow Group array.
func append_follow_group_node_array(value: Array[Node2D]) -> void:
for val in value:
if not Properties.follow_group_nodes_2D.has(val):
Properties.follow_group_nodes_2D.append(val)
Properties.should_follow = true
Properties.has_follow_group = true
else:
printerr(val, " is already part of Follow Group")
## Removes Node2D from Follow Group array.
func erase_follow_group_node(value: Node2D) -> void:
Properties.follow_group_nodes_2D.erase(value)
if Properties.follow_group_nodes_2D.size() < 1:
Properties.should_follow = false
Properties.has_follow_group = false
## Gets all Node2D from Follow Group array.
func get_follow_group_nodes() -> Array[Node2D]:
return Properties.follow_group_nodes_2D
## Enables or disables Auto zoom when using Group Follow.
func set_auto_zoom(value: bool) -> void:
follow_group_zoom_auto = value
## Gets Auto Zoom state.
func get_auto_zoom() -> bool:
return follow_group_zoom_auto
## Assigns new Min Auto Zoom value.
func set_min_auto_zoom(value: float) -> void:
follow_group_zoom_min = value
## Gets Min Auto Zoom value.
func get_min_auto_zoom() -> float:
return follow_group_zoom_min
## Assigns new Max Auto Zoom value.
func set_max_auto_zoom(value: float) -> void:
follow_group_zoom_max = value
## Gets Max Auto Zoom value.
func get_max_auto_zoom() -> float:
return follow_group_zoom_max
## Assigns new Zoom Auto Margin value.
func set_zoom_auto_margin(value: Vector4) -> void:
follow_group_zoom_margin = value
## Gets Zoom Auto Margin value.
func get_zoom_auto_margin() -> Vector4:
return follow_group_zoom_margin
## Gets Interactive Update Mode property.
func get_inactive_update_mode() -> String:
return Constants.InactiveUpdateMode.keys()[Properties.inactive_update_mode].capitalize()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
@tool
extends RefCounted
const PhantomCameraHost: Script = preload("res://addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd")
# Values
const CAMERA_2D_NODE_NAME: StringName = "Camera2D"
const CAMERA_3D_NODE_NAME: StringName = "Camera3D"
const PCAM_HOST_NODE_NAME: StringName = "PhantomCameraHost"
const PCAM_2D_NODE_NAME: StringName = "PhantomCamera2D"
const PCAM_3D_NODE_NAME: StringName = "PhantomCamera3D"
const COLOR_2D: Color = Color("8DA5F3")
const COLOR_3D: Color = Color("FC7F7F")
const COLOR_PCAM: Color = Color("3AB99A")
const PCAM_HOST_COLOR: Color = Color("E0E0E0")
# Primary
const PRIORITY_PROPERTY_NAME: StringName = "priority"
const PRIORITY_OVERRIDE: StringName = "priority_override"
const PCAM_HOST: StringName = "phantom_camera_host"
# Follow
const FOLLOW_MODE_PROPERTY_NAME: StringName = "follow_mode"
const FOLLOW_TARGET_PROPERTY_NAME: StringName = "follow_target"
const FOLLOW_GROUP_PROPERTY_NAME: StringName = "follow_group"
const FOLLOW_PATH_PROPERTY_NAME: StringName = "follow_path"
const FOLLOW_PARAMETERS_NAME: StringName = "follow_parameters/"
# Follow Parameters
const FOLLOW_DISTANCE_PROPERTY_NAME: StringName = FOLLOW_PARAMETERS_NAME + "distance"
const FOLLOW_DAMPING_NAME: StringName = FOLLOW_PARAMETERS_NAME + "damping"
const FOLLOW_DAMPING_VALUE_NAME: StringName = FOLLOW_PARAMETERS_NAME + "damping_value"
const FOLLOW_TARGET_OFFSET_PROPERTY_NAME: StringName = FOLLOW_PARAMETERS_NAME + "target_offset"
const FOLLOW_FRAMED_DEAD_ZONE_HORIZONTAL_NAME: StringName = FOLLOW_PARAMETERS_NAME + "dead_zone_horizontal"
const FOLLOW_FRAMED_DEAD_ZONE_VERTICAL_NAME: StringName = FOLLOW_PARAMETERS_NAME + "dead_zone_vertical"
const FOLLOW_VIEWFINDER_IN_PLAY_NAME: StringName = FOLLOW_PARAMETERS_NAME + "viewfinder_in_play"
const DEAD_ZONE_CHANGED_SIGNAL: StringName = "dead_zone_changed"
#Zoom
const ZOOM_PROPERTY_NAME: StringName = "zoom"
# Tween Resource
const TWEEN_RESOURCE_PROPERTY_NAME: StringName = "tween_parameters"
# Secondary
const TWEEN_ONLOAD_NAME: StringName = "tween_on_load"
const INACTIVE_UPDATE_MODE_PROPERTY_NAME: StringName = "inactive_update_mode"
enum FollowMode {
NONE = 0,
GLUED = 1,
SIMPLE = 2,
GROUP = 3,
PATH = 4,
FRAMED = 5,
THIRD_PERSON = 6,
}
enum TweenTransitions {
LINEAR = 0,
SINE = 1,
QUINT = 2,
QUART = 3,
QUAD = 4,
EXPO = 5,
ELASTIC = 6,
CUBIC = 7,
CIRC = 8,
BOUNCE = 9,
BACK = 10,
# CUSTOM = 11,
# NONE = 12,
}
enum TweenEases {
EASE_IN = 0,
EASE_OUT = 1,
EASE_IN_OUT = 2,
EASE_OUT_IN = 3,
}
enum InactiveUpdateMode {
ALWAYS,
NEVER,
# EXPONENTIALLY,
}

View file

@ -0,0 +1,488 @@
@tool
extends RefCounted
const Constants: Script = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd")
const PcamGroupNames: Script = preload("res://addons/phantom_camera/scripts/group_names.gd")
var is_2D: bool
var pcam_host_owner: PhantomCameraHost
var scene_has_multiple_pcam_hosts: bool
var pcam_host_group: Array[Node]
var is_active: bool
var priority_override: bool
var priority: int = 0
var tween_onload: bool = true
var has_tweened_onload: bool = true
# Follow
var should_follow: bool
var has_follow_group: bool
var follow_target_node: Node
var follow_target_path: NodePath
var follow_has_target: bool
var follow_has_path_target: bool
var follow_path_node: Node
var follow_path_path: NodePath
var follow_mode: Constants.FollowMode = Constants.FollowMode.NONE
var follow_target_offset_2D: Vector2
var follow_target_offset_3D: Vector3
var follow_has_damping: bool
var follow_damping_value: float = 10
# Follow Group
var follow_group_nodes_2D: Array[Node2D]
var follow_group_nodes_3D: Array[Node3D]
var follow_group_paths: Array[NodePath]
# Framed Follow
signal dead_zone_changed
var follow_framed_dead_zone_width: float
var follow_framed_dead_zone_height: float
var follow_framed_initial_set: bool
var show_viewfinder_in_play: bool
var viewport_position: Vector2
var zoom: Vector2 = Vector2.ONE
var tween_resource: PhantomCameraTween
var tween_resource_default: PhantomCameraTween = PhantomCameraTween.new()
var inactive_update_mode: Constants.InactiveUpdateMode = Constants.InactiveUpdateMode.ALWAYS
func camera_enter_tree(pcam: Node):
pcam.add_to_group(PcamGroupNames.PCAM_GROUP_NAME)
if pcam.Properties.follow_target_path and \
not pcam.get_parent() is SpringArm3D and \
is_instance_valid(pcam.get_node(pcam.Properties.follow_target_path)):
pcam.Properties.follow_target_node = pcam.get_node(pcam.Properties.follow_target_path)
elif follow_group_paths:
if is_2D:
follow_group_nodes_2D.clear()
else:
follow_group_nodes_3D.clear()
for path in follow_group_paths:
if not path.is_empty() and pcam.get_node(path):
should_follow = true
has_follow_group = true
if is_2D:
follow_group_nodes_2D.append(pcam.get_node(path))
else:
follow_group_nodes_3D.append(pcam.get_node(path))
if pcam.Properties.follow_path_path:
pcam.Properties.follow_path_node = pcam.get_node(pcam.Properties.follow_path_path)
func pcam_exit_tree(pcam: Node):
pcam.remove_from_group(PcamGroupNames.PCAM_GROUP_NAME)
#########################
# Add Properties
#########################
func add_multiple_hosts_properties() -> Array:
var _property_list: Array
if scene_has_multiple_pcam_hosts:
_property_list.append({
"name": Constants.PCAM_HOST,
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": ",".join(PackedStringArray(pcam_host_group)),
"usage": PROPERTY_USAGE_DEFAULT,
})
return _property_list
func add_priority_properties() -> Array:
var _property_list: Array
_property_list.append({
"name": Constants.PRIORITY_OVERRIDE,
"type": TYPE_BOOL,
})
_property_list.append({
"name": Constants.PRIORITY_PROPERTY_NAME,
"type": TYPE_INT,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT,
})
return _property_list
func add_follow_mode_property() -> Array:
var _property_list: Array
var follow_mode_keys: Array = Constants.FollowMode.keys()
if is_2D:
follow_mode_keys.remove_at(Constants.FollowMode.THIRD_PERSON)
_property_list.append({
"name": Constants.FOLLOW_MODE_PROPERTY_NAME,
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": ", ".join(PackedStringArray(follow_mode_keys)).capitalize(),
"usage": PROPERTY_USAGE_DEFAULT,
})
return _property_list
func add_follow_target_property() -> Array:
var _property_list: Array
if follow_mode == Constants.FollowMode.GROUP:
_property_list.append({
"name": Constants.FOLLOW_GROUP_PROPERTY_NAME,
"type": TYPE_ARRAY,
"hint": PROPERTY_HINT_TYPE_STRING,
"hint_string": TYPE_NODE_PATH,
"usage": PROPERTY_USAGE_DEFAULT,
})
else:
_property_list.append({
"name": Constants.FOLLOW_TARGET_PROPERTY_NAME,
"type": TYPE_NODE_PATH,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT,
})
if follow_mode == Constants.FollowMode.PATH:
_property_list.append({
"name": Constants.FOLLOW_PATH_PROPERTY_NAME,
"type": TYPE_NODE_PATH,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT,
})
return _property_list
func add_follow_properties() -> Array:
var _property_list: Array
if follow_mode != Constants.FollowMode.NONE:
if follow_mode == Constants.FollowMode.SIMPLE or \
follow_mode == Constants.FollowMode.GROUP or \
follow_mode == Constants.FollowMode.FRAMED or \
follow_mode == Constants.FollowMode.THIRD_PERSON:
if is_2D:
_property_list.append({
"name": Constants.FOLLOW_TARGET_OFFSET_PROPERTY_NAME,
"type": TYPE_VECTOR2,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT,
})
else:
_property_list.append({
"name": Constants.FOLLOW_TARGET_OFFSET_PROPERTY_NAME,
"type": TYPE_VECTOR3,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT,
})
if follow_mode != Constants.FollowMode.NONE:
_property_list.append({
"name": Constants.FOLLOW_DAMPING_NAME,
"type": TYPE_BOOL,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT,
})
if follow_has_damping:
_property_list.append({
"name": Constants.FOLLOW_DAMPING_VALUE_NAME,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0.01, 100, 0.01,",
"usage": PROPERTY_USAGE_DEFAULT,
})
return _property_list
func add_follow_framed() -> Array:
var _property_list: Array
if follow_mode == Constants.FollowMode.FRAMED:
_property_list.append({
"name": Constants.FOLLOW_FRAMED_DEAD_ZONE_HORIZONTAL_NAME,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0, 1, 0.01,",
"usage": PROPERTY_USAGE_DEFAULT,
})
_property_list.append({
"name": Constants.FOLLOW_FRAMED_DEAD_ZONE_VERTICAL_NAME,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0, 1, 0.01,",
"usage": PROPERTY_USAGE_DEFAULT,
})
_property_list.append({
"name": Constants.FOLLOW_VIEWFINDER_IN_PLAY_NAME,
"type": TYPE_BOOL,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT
})
return _property_list
func add_tween_properties() -> Array:
var _property_list: Array
_property_list.append({
"name": Constants.TWEEN_RESOURCE_PROPERTY_NAME,
"type": TYPE_OBJECT,
"hint": PROPERTY_HINT_RESOURCE_TYPE,
"hint_string": "PhantomCameraTween"
})
return _property_list
func add_secondary_properties() -> Array:
var _property_list: Array
_property_list.append({
"name": Constants.TWEEN_ONLOAD_NAME,
"type": TYPE_BOOL,
"hint": PROPERTY_HINT_NONE,
"usage": PROPERTY_USAGE_DEFAULT
})
_property_list.append({
"name": Constants.INACTIVE_UPDATE_MODE_PROPERTY_NAME,
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": ", ".join(PackedStringArray(Constants.InactiveUpdateMode.keys())).capitalize(),
})
return _property_list
#########################
# Set Properties
#########################
func set_phantom_host_property(property: StringName, value, pcam: Node):
if property == Constants.PCAM_HOST:
if value != null && value is int:
var host_node = instance_from_id(value)
pcam_host_owner = host_node
func set_priority_property(property: StringName, value, pcam: Node):
if Engine.is_editor_hint() and is_instance_valid(pcam_host_owner):
if property == Constants.PRIORITY_OVERRIDE:
if value == true:
priority_override = value
pcam_host_owner.pcam_priority_override(pcam)
else:
priority_override = value
pcam_host_owner.pcam_priority_updated(pcam)
pcam_host_owner.pcam_priority_override_disabled()
if property == Constants.PRIORITY_PROPERTY_NAME:
set_priority(value, pcam)
func set_follow_properties(property: StringName, value, pcam: Node):
if property == Constants.FOLLOW_MODE_PROPERTY_NAME:
follow_mode = value
if follow_mode != Constants.FollowMode.GROUP:
has_follow_group = false
if follow_mode == Constants.FollowMode.FRAMED:
follow_framed_initial_set = true
pcam.notify_property_list_changed()
# match value:
# Constants.FollowMode.NONE:
# set_process(pcam, false)
# _:
# set_process(pcam, true)
if property == Constants.FOLLOW_TARGET_PROPERTY_NAME:
if follow_mode != Constants.FollowMode.NONE:
should_follow = true
else:
should_follow = false
follow_target_path = value
var valueNodePath: NodePath = value as NodePath
if not valueNodePath.is_empty():
follow_has_target = true
if pcam.has_node(follow_target_path):
follow_target_node = pcam.get_node(follow_target_path)
else:
follow_has_target = false
follow_target_node = null
pcam.notify_property_list_changed()
if property == Constants.FOLLOW_PATH_PROPERTY_NAME:
follow_path_path = value
var valueNodePath: NodePath = value as NodePath
if not valueNodePath.is_empty():
follow_has_path_target = true
if pcam.has_node(follow_path_path):
follow_path_node = pcam.get_node(follow_path_path)
else:
follow_has_path_target = false
follow_path_node = null
pcam.notify_property_list_changed()
if property == Constants.FOLLOW_GROUP_PROPERTY_NAME:
if value and value.size() > 0:
# Clears the Array in case of reshuffling or updated Nodes
if is_2D:
follow_group_nodes_2D.clear()
else:
follow_group_nodes_3D.clear()
follow_group_paths = value as Array[NodePath]
if not follow_group_paths.is_empty():
for path in follow_group_paths:
if pcam.has_node(path):
should_follow = true
has_follow_group = true
var node: Node = pcam.get_node(path)
if node is Node2D or node is Node3D:
# Prevents duplicated nodes from being assigned to array
if is_2D:
if follow_group_nodes_2D.find(node):
follow_group_nodes_2D.append(node)
else:
if follow_group_nodes_3D.find(node):
follow_group_nodes_3D.append(node)
else:
printerr("Assigned non-Node3D to Follow Group")
pcam.notify_property_list_changed()
# Framed Follow
if property == Constants.FOLLOW_FRAMED_DEAD_ZONE_HORIZONTAL_NAME:
follow_framed_dead_zone_width = value
dead_zone_changed.emit()
if property == Constants.FOLLOW_FRAMED_DEAD_ZONE_VERTICAL_NAME:
follow_framed_dead_zone_height = value
dead_zone_changed.emit()
if property == Constants.FOLLOW_VIEWFINDER_IN_PLAY_NAME:
show_viewfinder_in_play = value
if property == Constants.FOLLOW_TARGET_OFFSET_PROPERTY_NAME:
if value is Vector3:
follow_target_offset_3D = value
else:
follow_target_offset_2D = value
if property == Constants.FOLLOW_DAMPING_NAME:
follow_has_damping = value
pcam.notify_property_list_changed()
if property == Constants.FOLLOW_DAMPING_VALUE_NAME:
follow_damping_value = value
func set_tween_properties(property: StringName, value, pcam: Node):
if property == Constants.TWEEN_RESOURCE_PROPERTY_NAME:
tween_resource = value
func set_secondary_properties(property: StringName, value, pcam: Node):
if property == Constants.TWEEN_ONLOAD_NAME:
tween_onload = value
if value == false:
has_tweened_onload = false
else:
has_tweened_onload = true
if property == Constants.INACTIVE_UPDATE_MODE_PROPERTY_NAME:
inactive_update_mode = value
func set_priority(value: int, pcam: Node) -> void:
if value < 0:
printerr("Phantom Camera's priority cannot be less than 0")
priority = 0
else:
priority = value
if pcam_host_owner:
pcam_host_owner.pcam_priority_updated(pcam)
# else:
## TODO - Add logic to handle Phantom Camera Host in scene
# printerr("Trying to change priority without a Phantom Camera Host - Please attached one to a Camera3D")
# pass
#########################
# Other Functions
#########################
func assign_pcam_host(pcam: Node) -> void:
pcam_host_group = pcam.get_tree().get_nodes_in_group(PcamGroupNames.PCAM_HOST_GROUP_NAME)
if pcam_host_group.size() == 1:
pcam_host_owner = pcam.Properties.pcam_host_group[0]
pcam_host_owner.pcam_added_to_scene(pcam)
# else:
# for camera_host in camera_host_group:
# print("Multiple PhantomCameraBases in scene")
# print(pcam_host_group)
# print(pcam.get_tree().get_nodes_in_group(PhantomCameraGroupNames.PHANTOM_CAMERA_HOST_GROUP_NAME))
# multiple_pcam_host_group.append(camera_host)
# return null
func toggle_priorty_override(pcam: Node) -> void:
if pcam_host_owner:
pcam_host_owner.pcam_priority_updated(pcam)
func assign_specific_pcam_host(pcam: Node, pcam_host: PhantomCameraHost) -> void:
pcam_host = pcam
func check_multiple_pcam_host_property(pcam: Node, multiple_host: bool = false) -> void:
if not multiple_host:
scene_has_multiple_pcam_hosts = false
else:
scene_has_multiple_pcam_hosts = true
pcam.notify_property_list_changed()
# pcam_host_group.append_array(host_group)
func get_framed_side_offset() -> Vector2:
var frame_out_bounds: Vector2
if viewport_position.x < 0.5 - follow_framed_dead_zone_width / 2:
# Is outside left edge
frame_out_bounds.x = -1
if viewport_position.y < 0.5 - follow_framed_dead_zone_height / 2:
# Is outside top edge
frame_out_bounds.y = 1
if viewport_position.x > 0.5 + follow_framed_dead_zone_width / 2:
# Is outside right edge
frame_out_bounds.x = 1
if viewport_position.y > 0.5001 + follow_framed_dead_zone_height / 2: # 0.501 to resolve an issue where the bottom vertical Dead Zone never becoming 0 when the Dead Zone Vertical parameter is set to 0
# Is outside bottom edge
frame_out_bounds.y = -1
return frame_out_bounds

View file

@ -0,0 +1,366 @@
@tool
@icon("res://addons/phantom_camera/icons/PhantomCameraHostIcon.svg")
class_name PhantomCameraHost
extends Node
const PcamGroupNames = preload("res://addons/phantom_camera/scripts/group_names.gd")
var _pcam_tween: Tween
var _tween_default_ease: Tween.EaseType
var _easing: Tween.TransitionType
var camera_2D: Camera2D
var camera_3D: Camera3D
var _pcam_list: Array[Node]
var _active_pcam: Node
var _active_pcam_priority: int = -1
var _active_pcam_missing: bool = true
var _active_pcam_has_damping: bool
var _prev_active_pcam_2D_transform: Transform2D
var _prev_active_pcam_3D_transform: Transform3D
var trigger_pcam_tween: bool
var tween_duration: float
var multiple_pcam_hosts: bool
var is_child_of_camera: bool = false
var _is_2D: bool
signal update_editor_viewfinder
var framed_viewfinder_scene = load("res://addons/phantom_camera/framed_viewfinder/framed_viewfinder_panel.tscn")
var framed_viewfinder_node: Control
var viewfinder_needed_check: bool = true
var camera_zoom: Vector2
var _prev_camera_h_offset: float
var _prev_camera_v_offset: float
var _prev_camera_fov: float
var _should_refresh_transform: bool
var _active_pcam_2D_glob_transform: Transform2D
var _active_pcam_3D_glob_transform: Transform3D
###################
# Private Functions
###################
func _enter_tree() -> void:
# camera = get_parent()
var parent = get_parent()
if parent is Camera2D or parent is Camera3D:
is_child_of_camera = true
if parent is Camera2D:
_is_2D = true
camera_2D = parent
else:
_is_2D = false
camera_3D = parent
add_to_group(PcamGroupNames.PCAM_HOST_GROUP_NAME)
# var already_multi_hosts: bool = multiple_pcam_hosts
_check_camera_host_amount()
if multiple_pcam_hosts:
printerr(
"Only one PhantomCameraHost can exist in a scene",
"\n",
"Multiple PhantomCameraHosts will be supported in https://github.com/MarcusSkov/phantom-camera/issues/26"
)
queue_free()
for pcam in _get_pcam_node_group():
if not multiple_pcam_hosts:
pcam_added_to_scene(pcam)
pcam.assign_pcam_host()
# else:
# pcam.Properties.check_multiple_pcam_host_property(pcam, pca,_host_group, true)
else:
printerr(name, " is not a child of a Camera2D or Camera3D")
func _exit_tree() -> void:
remove_from_group(PcamGroupNames.PCAM_HOST_GROUP_NAME)
_check_camera_host_amount()
for pcam in _get_pcam_node_group():
if not multiple_pcam_hosts:
pcam.Properties.check_multiple_pcam_host_property(pcam)
func _ready() -> void:
if not is_instance_valid(_active_pcam): return
if _is_2D:
_active_pcam_2D_glob_transform = _active_pcam.get_global_transform()
else:
_active_pcam_3D_glob_transform = _active_pcam.get_global_transform()
func _check_camera_host_amount():
if _get_pcam_host_group().size() > 1:
multiple_pcam_hosts = true
else:
multiple_pcam_hosts = false
func _assign_new_active_pcam(pcam: Node) -> void:
var no_previous_pcam: bool
if _active_pcam:
if _is_2D:
_prev_active_pcam_2D_transform = camera_2D.get_transform()
else:
_prev_active_pcam_3D_transform = camera_3D.get_transform()
_prev_camera_fov = camera_3D.get_fov()
_prev_camera_h_offset = camera_3D.get_h_offset()
_prev_camera_v_offset = camera_3D.get_v_offset()
_active_pcam.Properties.is_active = false
else:
no_previous_pcam = true
_active_pcam = pcam
_active_pcam_priority = pcam.get_priority()
_active_pcam_has_damping = pcam.Properties.follow_has_damping
_active_pcam.Properties.is_active = true
if _is_2D:
camera_zoom = camera_2D.get_zoom()
else:
if _active_pcam.get_camera_3D_resource():
camera_3D.set_cull_mask(_active_pcam.get_camera_cull_mask())
if no_previous_pcam:
if _is_2D:
_prev_active_pcam_2D_transform = _active_pcam.get_transform()
else:
_prev_active_pcam_3D_transform = _active_pcam.get_transform()
tween_duration = 0
trigger_pcam_tween = true
func _find_pcam_with_highest_priority() -> void:
for pcam in _pcam_list:
if pcam.get_priority() > _active_pcam_priority:
_assign_new_active_pcam(pcam)
_active_pcam_missing = false
func _tween_pcam(delta: float) -> void:
if _active_pcam.Properties.tween_onload == false && _active_pcam.Properties.has_tweened_onload == false:
trigger_pcam_tween = false
_reset_tween_on_load()
return
else:
_reset_tween_on_load()
tween_duration += delta
if _is_2D:
camera_2D.set_global_position(
_tween_interpolate_value(_prev_active_pcam_2D_transform.origin, _active_pcam_2D_glob_transform.origin)
)
camera_2D.set_zoom(
_tween_interpolate_value(camera_zoom, _active_pcam.Properties.zoom)
)
else:
camera_3D.set_global_position(
_tween_interpolate_value(_prev_active_pcam_3D_transform.origin, _active_pcam_3D_glob_transform.origin)
)
var prev_active_pcam_3D_basis = Quaternion(_prev_active_pcam_3D_transform.basis.orthonormalized())
camera_3D.set_quaternion(
Tween.interpolate_value(
prev_active_pcam_3D_basis, \
prev_active_pcam_3D_basis.inverse() * Quaternion(_active_pcam_3D_glob_transform.basis.orthonormalized()),
tween_duration, \
_active_pcam.get_tween_duration(), \
_active_pcam.get_tween_transition(),
_active_pcam.get_tween_ease(),
)
)
if _prev_camera_fov != _active_pcam.get_camera_fov() and _active_pcam.get_camera_3D_resource():
camera_3D.set_fov(
_tween_interpolate_value(_prev_camera_fov, _active_pcam.get_camera_fov())
)
if _prev_camera_h_offset != _active_pcam.get_camera_h_offset() and _active_pcam.get_camera_3D_resource():
camera_3D.set_h_offset(
_tween_interpolate_value(_prev_camera_h_offset, _active_pcam.get_camera_h_offset())
)
if _prev_camera_v_offset != _active_pcam.get_camera_v_offset() and _active_pcam.get_camera_3D_resource():
camera_3D.set_v_offset(
_tween_interpolate_value(_prev_camera_v_offset, _active_pcam.get_camera_v_offset())
)
func _tween_interpolate_value(from: Variant, to: Variant) -> Variant:
return Tween.interpolate_value(
from, \
to - from,
tween_duration, \
_active_pcam.get_tween_duration(), \
_active_pcam.get_tween_transition(),
_active_pcam.get_tween_ease(),
)
func _reset_tween_on_load() -> void:
for pcam in _get_pcam_node_group():
pcam.Properties.has_tweened_onload = true
if not _is_2D:
if _active_pcam.get_camera_3D_resource():
camera_3D.set_fov(_active_pcam.get_camera_fov())
camera_3D.set_h_offset(_active_pcam.get_camera_h_offset())
camera_3D.set_v_offset(_active_pcam.get_camera_v_offset())
func _pcam_follow(delta: float) -> void:
if not _active_pcam: return
if _is_2D:
camera_2D.set_global_transform(_active_pcam_2D_glob_transform)
if _active_pcam.Properties.has_follow_group:
if _active_pcam.Properties.follow_has_damping:
camera_2D.zoom = camera_2D.zoom.lerp(_active_pcam.Properties.zoom, delta * _active_pcam.Properties.follow_damping_value)
else:
camera_2D.set_zoom(_active_pcam.zoom)
else:
camera_2D.set_zoom(_active_pcam.Properties.zoom)
else:
camera_3D.set_global_transform(_active_pcam_3D_glob_transform)
func _refresh_transform() -> void:
if _is_2D:
_active_pcam_2D_glob_transform = _active_pcam.get_global_transform()
else:
_active_pcam_3D_glob_transform = _active_pcam.get_global_transform()
func _process_pcam(delta: float) -> void:
if _active_pcam_missing or not is_child_of_camera: return
if not trigger_pcam_tween:
_pcam_follow(delta)
if viewfinder_needed_check:
show_viewfinder_in_play()
viewfinder_needed_check = false
if Engine.is_editor_hint():
if not _is_2D:
if _active_pcam.get_camera_3D_resource():
camera_3D.set_fov(_active_pcam.get_camera_fov())
camera_3D.set_h_offset(_active_pcam.get_camera_h_offset())
camera_3D.set_v_offset(_active_pcam.get_camera_v_offset())
else:
if tween_duration < _active_pcam.get_tween_duration():
_tween_pcam(delta)
else:
tween_duration = 0
trigger_pcam_tween = false
show_viewfinder_in_play()
_pcam_follow(delta)
func show_viewfinder_in_play() -> void:
if _active_pcam.Properties.show_viewfinder_in_play:
if not Engine.is_editor_hint() && OS.has_feature("editor"): # Only appears when running in the editor
var canvas_layer: CanvasLayer = CanvasLayer.new()
get_tree().get_root().get_child(0).add_child(canvas_layer)
framed_viewfinder_node = framed_viewfinder_scene.instantiate()
canvas_layer.add_child(framed_viewfinder_node)
else:
if framed_viewfinder_node:
framed_viewfinder_node.queue_free()
func _get_pcam_node_group() -> Array[Node]:
return get_tree().get_nodes_in_group(PcamGroupNames.PCAM_GROUP_NAME)
func _get_pcam_host_group() -> Array[Node]:
return get_tree().get_nodes_in_group(PcamGroupNames.PCAM_HOST_GROUP_NAME)
func _process(delta):
if not is_instance_valid(_active_pcam): return
if _should_refresh_transform:
# _refresh_transform()
if _is_2D:
_active_pcam_2D_glob_transform = _active_pcam.get_global_transform()
else:
_active_pcam_3D_glob_transform = _active_pcam.get_global_transform()
_should_refresh_transform = false
_process_pcam(delta)
func _physics_process(delta: float) -> void:
_should_refresh_transform = true
##################
# Public Functions
##################
func pcam_added_to_scene(pcam: Node) -> void:
_pcam_list.append(pcam)
_find_pcam_with_highest_priority()
func pcam_removed_from_scene(pcam) -> void:
_pcam_list.erase(pcam)
if pcam == _active_pcam:
_active_pcam_missing = true
_active_pcam_priority = -1
_find_pcam_with_highest_priority()
func pcam_priority_updated(pcam: Node) -> void:
if Engine.is_editor_hint() and _active_pcam.Properties.priority_override: return
if not is_instance_valid(pcam): return
var current_pcam_priority: int = pcam.get_priority()
if current_pcam_priority >= _active_pcam_priority and pcam != _active_pcam:
_assign_new_active_pcam(pcam)
elif pcam == _active_pcam:
if current_pcam_priority <= _active_pcam_priority:
_active_pcam_priority = current_pcam_priority
_find_pcam_with_highest_priority()
else:
_active_pcam_priority = current_pcam_priority
func pcam_priority_override(pcam: Node) -> void:
if Engine.is_editor_hint() and _active_pcam.Properties.priority_override:
_active_pcam.Properties.priority_override = false
_assign_new_active_pcam(pcam)
update_editor_viewfinder.emit()
func pcam_priority_override_disabled() -> void:
update_editor_viewfinder.emit()
func get_active_pcam() -> Node:
return _active_pcam

View file

@ -0,0 +1,14 @@
class_name Camera3DResource
extends Resource
## The time it takes to tween to this property
@export_flags_3d_physics var cull_mask: int = 1048575
## Horizontally offsets the Camera3D
@export var h_offset: float = 0
## Vertically offsets the Camera3D
@export var v_offset: float = 0
## Adjusts Camera3D FOV
@export var fov: float = 75

View file

@ -0,0 +1,13 @@
class_name PhantomCameraTween
extends Resource
const Constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd")
## The time it takes to tween to this property
@export var duration: float = 1
## The transition bezier type for the tween
@export var transition: Constants.TweenTransitions = Constants.TweenTransitions.LINEAR
## The ease type for the tween
@export var ease: Constants.TweenEases = Constants.TweenEases.EASE_IN_OUT

View file

@ -0,0 +1,439 @@
@tool
extends Control
const PcamGroupNames = preload("res://addons/phantom_camera/scripts/group_names.gd")
const Constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd")
var _selected_camera: Node
var _active_pcam_camera
var pcam_host_group: Array[Node]
var editor_interface: EditorInterface
####################
# Dead Zone Controls
####################
@onready var dead_zone_center_hbox: VBoxContainer = %DeadZoneCenterHBoxContainer
@onready var dead_zone_center_center_panel: Panel = %DeadZoneCenterCenterPanel
@onready var dead_zone_left_center_panel: Panel = %DeadZoneLeftCenterPanel
@onready var dead_zone_right_center_panel: Panel = %DeadZoneRightCenterPanel
@onready var target_point: Panel = %TargetPoint
var aspect_ratio_container: AspectRatioContainer
@onready var aspect_ratio_containers: AspectRatioContainer = %AspectRatioContainer
@onready var camera_viewport_panel: Panel = aspect_ratio_containers.get_child(0)
@onready var _framed_viewfinder: Control = %FramedViewfinder
@onready var _dead_zone_h_box_container: Control = %DeadZoneHBoxContainer
@onready var sub_viewport: SubViewport = %SubViewport
###########################
# Viewfinder Empty Controls
###########################
@onready var _empty_state_control: Control = %EmptyStateControl
@onready var _empty_state_icon: Control = %EmptyStateIcon
@onready var _empty_state_text: RichTextLabel = %EmptyStateText
@onready var _add_node_button: Button = %AddNodeButton
@onready var _add_node_button_text: RichTextLabel = %AddNodeTypeText
###################
# Priority Override
###################
@onready var _priority_override_button: Button = %PriorityOverrideButton
@onready var _priority_override_name_label: Label = %PriorityOverrideNameLabel
# TODO - Should be in a central location
const _camera_2d_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg")
const _camera_3d_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg")
const _pcam_host_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/PhantomCameraHostIcon.svg")
const _pcam_2D_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/PhantomCameraGizmoIcon2D.svg")
const _pcam_3D_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/PhantomCameraGizmoIcon3D.svg")
const _overlay_color_alpha: float = 0.3
var _no_open_scene_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg")
var _no_open_scene_string: String = "[b]2D[/b] or [b]3D[/b] scene open"
var is_2D: bool
var is_scene: bool
var has_camera_viewport_panel_size: bool = true
var min_horizontal: float
var max_horizontal: float
var min_vertical: float
var max_vertical: float
func _ready():
visibility_changed.connect(_visibility_check)
set_process(false)
aspect_ratio_containers.set_ratio(get_viewport_rect().size.x / get_viewport_rect().size.y)
# TODO - Don't think this is needed / does anything?
var root_node = get_tree().get_root().get_child(0)
if root_node is Node3D || root_node is Node2D:
%SubViewportContainer.set_visible(false)
if root_node is Node2D:
is_2D = true
else:
is_2D = false
_set_viewfinder(root_node, false)
if Engine.is_editor_hint():
get_tree().node_added.connect(_node_added)
get_tree().node_removed.connect(_node_added)
else:
_empty_state_control.set_visible(false)
_priority_override_button.set_visible(false)
func _exit_tree() -> void:
if Engine.is_editor_hint():
if get_tree().node_added.is_connected(_node_added):
get_tree().node_added.disconnect(_node_added)
get_tree().node_removed.disconnect(_node_added)
if aspect_ratio_containers.resized.is_connected(_resized):
aspect_ratio_containers.resized.disconnect(_resized)
if _add_node_button.pressed.is_connected(_add_node):
_add_node_button.pressed.disconnect(_add_node)
if is_instance_valid(_active_pcam_camera):
if _active_pcam_camera.Properties.is_connected(Constants.DEAD_ZONE_CHANGED_SIGNAL, _on_dead_zone_changed):
_active_pcam_camera.Properties.disconnect(Constants.DEAD_ZONE_CHANGED_SIGNAL, _on_dead_zone_changed)
if _priority_override_button.pressed.is_connected(_select_override_pcam):
_priority_override_button.pressed.disconnect(_select_override_pcam)
func _process(_delta: float):
if not visible or not is_instance_valid(_active_pcam_camera): return
var unprojected_position_clamped: Vector2 = Vector2(
clamp(_active_pcam_camera.Properties.viewport_position.x, min_horizontal, max_horizontal),
clamp(_active_pcam_camera.Properties.viewport_position.y, min_vertical, max_vertical)
)
target_point.position = camera_viewport_panel.size * unprojected_position_clamped - target_point.size / 2
if not has_camera_viewport_panel_size:
_on_dead_zone_changed()
func _node_added(node: Node) -> void:
if editor_interface == null: return
_visibility_check()
func scene_changed(scene_root: Node) -> void:
if scene_root is Node2D:
# print("Is 2D node")
is_2D = true
is_scene = true
_add_node_button.set_visible(true)
# var camera: Camera2D = scene_root.get_viewport().get_camera_2d()
var camera: Camera2D = _get_camera_2D()
_check_camera(scene_root, camera, true)
elif scene_root is Node3D:
# print("Is 3D node")
# Is 3D scene
is_2D = false
is_scene = true
_add_node_button.set_visible(true)
var camera: Camera3D = scene_root.get_viewport().get_camera_3d()
_check_camera(scene_root, camera, false)
else:
# print("Not a 2D or 3D scene")
is_scene = false
# Is not a 2D or 3D scene
_set_empty_viewfinder_state(_no_open_scene_string, _no_open_scene_icon)
_add_node_button.set_visible(false)
func _visibility_check():
if not editor_interface or not visible: return
if not is_instance_valid(editor_interface):
is_scene = false
# Is not a 2D or 3D scene
_set_empty_viewfinder_state(_no_open_scene_string, _no_open_scene_icon)
_add_node_button.set_visible(false)
return
var root: Node = editor_interface.get_edited_scene_root()
if root is Node2D:
# print("Is a 2D scene")
is_2D = true
is_scene = true
_add_node_button.set_visible(true)
# TODO: Figure out why the line below doesn't work...
# var camera: Camera2D = root.get_viewport().get_camera_2d()
var camera: Camera2D = _get_camera_2D()
_check_camera(root, camera, true)
elif root is Node3D:
# Is 3D scene
is_2D = false
is_scene = true
_add_node_button.set_visible(true)
var camera: Camera3D = root.get_viewport().get_camera_3d()
_check_camera(root, camera, false)
# editor_interface.get_selection().clear()
# editor_interface.get_selection().add_node(pcam_host_group[0].get_active_pcam())
else:
is_scene = false
# Is not a 2D or 3D scene
_set_empty_viewfinder_state(_no_open_scene_string, _no_open_scene_icon)
_add_node_button.set_visible(false)
if not _priority_override_button.pressed.is_connected(_select_override_pcam):
_priority_override_button.pressed.connect(_select_override_pcam)
func _get_camera_2D() -> Camera2D:
var camerasGroupName = "__cameras_%d" % editor_interface.get_edited_scene_root().get_viewport().get_viewport_rid().get_id()
var cameras = get_tree().get_nodes_in_group(camerasGroupName)
for camera in cameras:
if camera is Camera2D and camera.is_current:
return camera
return null
func _check_camera(root: Node, camera: Node, is_2D: bool) -> void:
var camera_string: String
var pcam_string: String
var color: Color
var color_alpha: Color
var camera_icon: CompressedTexture2D
var pcam_icon: CompressedTexture2D
if is_2D:
camera_string = Constants.CAMERA_2D_NODE_NAME
pcam_string = Constants.PCAM_2D_NODE_NAME
color = Constants.COLOR_2D
camera_icon = _camera_2d_icon
pcam_icon = _pcam_2D_icon
else:
camera_string = Constants.CAMERA_3D_NODE_NAME
pcam_string = Constants.PCAM_3D_NODE_NAME
color = Constants.COLOR_3D
camera_icon = _camera_3d_icon
pcam_icon = _pcam_3D_icon
if camera:
# Has Camera
var pcam_host: PhantomCameraHost
if camera.get_children().size() > 0:
for cam_child in camera.get_children():
if cam_child is PhantomCameraHost:
pcam_host = cam_child
if pcam_host:
if get_tree().get_nodes_in_group(PcamGroupNames.PCAM_GROUP_NAME):
# Pcam exists in tree
_set_viewfinder(root, true)
# if pcam_host.get_active_pcam().get_get_follow_mode():
# _on_dead_zone_changed()
_set_viewfinder_state()
# Related to: https://github.com/ramokz/phantom-camera/issues/105
# REMOVE BELOW WHEN 2D VIEWFINDER IS SUPPORTED
if not is_2D:
%NoSupportMsg.set_visible(false)
elif is_2D:
%NoSupportMsg.set_visible(true)
### REMOVAL END
else:
# No PCam in scene
_update_button(pcam_string, pcam_icon, color)
_set_empty_viewfinder_state(pcam_string, pcam_icon)
else:
# No PCamHost in scene
_update_button(Constants.PCAM_HOST_NODE_NAME, _pcam_host_icon, Constants.PCAM_HOST_COLOR)
_set_empty_viewfinder_state(Constants.PCAM_HOST_NODE_NAME, _pcam_host_icon)
else:
# No PCamHost in scene
_update_button(Constants.PCAM_HOST_NODE_NAME, _pcam_host_icon, Constants.PCAM_HOST_COLOR)
_set_empty_viewfinder_state(Constants.PCAM_HOST_NODE_NAME, _pcam_host_icon)
else:
# No Camera
_update_button(camera_string, camera_icon, color)
_set_empty_viewfinder_state(camera_string, camera_icon)
func _update_button(text: String, icon: CompressedTexture2D, color: Color) -> void:
_add_node_button_text.set_text("[center]Add [img=32]" + icon.resource_path + "[/img] [b]"+ text + "[/b][/center]");
var button_theme_hover: StyleBoxFlat = _add_node_button.get_theme_stylebox("hover")
button_theme_hover.border_color = color
_add_node_button.add_theme_stylebox_override("hover", button_theme_hover)
func _set_viewfinder_state() -> void:
_empty_state_control.set_visible(false)
_framed_viewfinder.set_visible(true)
target_point.set_visible(true)
if is_instance_valid(_active_pcam_camera):
if _active_pcam_camera.get_follow_mode() == Constants.FollowMode.FRAMED:
_dead_zone_h_box_container.set_visible(true)
else:
_dead_zone_h_box_container.set_visible(false)
func _set_empty_viewfinder_state(text: String, icon: CompressedTexture2D) -> void:
_framed_viewfinder.set_visible(false)
target_point.set_visible(false)
_empty_state_control.set_visible(true)
_empty_state_icon.set_texture(icon)
if icon == _no_open_scene_icon:
_empty_state_text.set_text("[center]No " + text + "[/center]")
else:
_empty_state_text.set_text("[center]No [b]" + text + "[/b] in scene[/center]")
if _add_node_button.pressed.is_connected(_add_node):
_add_node_button.pressed.disconnect(_add_node)
_add_node_button.pressed.connect(_add_node.bind(text))
func _add_node(node_type: String) -> void:
if not editor_interface: return
var root: Node = editor_interface.get_edited_scene_root()
match node_type:
_no_open_scene_string:
pass
Constants.CAMERA_2D_NODE_NAME:
var camera: Camera2D = Camera2D.new()
_instantiate_node(root, camera, Constants.CAMERA_2D_NODE_NAME)
Constants.CAMERA_3D_NODE_NAME:
var camera: Camera3D = Camera3D.new()
_instantiate_node(root, camera, Constants.CAMERA_3D_NODE_NAME)
Constants.PCAM_HOST_NODE_NAME:
var pcam_host: PhantomCameraHost = PhantomCameraHost.new()
pcam_host.set_name(Constants.PCAM_HOST_NODE_NAME)
if is_2D:
# get_tree().get_edited_scene_root().get_viewport().get_camera_2d().add_child(pcam_host)
_get_camera_2D().add_child(pcam_host)
pcam_host.set_owner(get_tree().get_edited_scene_root())
else:
# var pcam_3D := get_tree().get_edited_scene_root().get_viewport().get_camera_3d()
get_tree().get_edited_scene_root().get_viewport().get_camera_3d().add_child(pcam_host)
pcam_host.set_owner(get_tree().get_edited_scene_root())
Constants.PCAM_2D_NODE_NAME:
var pcam_2D: PhantomCamera2D = PhantomCamera2D.new()
_instantiate_node(root, pcam_2D, Constants.PCAM_2D_NODE_NAME)
Constants.PCAM_3D_NODE_NAME:
var pcam_3D: PhantomCamera3D = PhantomCamera3D.new()
_instantiate_node(root, pcam_3D, Constants.PCAM_3D_NODE_NAME)
func _instantiate_node(root: Node, node: Node, name: String) -> void:
node.set_name(name)
root.add_child(node)
node.set_owner(get_tree().get_edited_scene_root())
func _set_viewfinder(root: Node, editor: bool):
pcam_host_group = root.get_tree().get_nodes_in_group(PcamGroupNames.PCAM_HOST_GROUP_NAME)
if pcam_host_group.size() != 0:
if pcam_host_group.size() == 1:
var pcam_host: PhantomCameraHost = pcam_host_group[0]
if is_2D:
_selected_camera = pcam_host.camera_2D
_active_pcam_camera = _selected_camera.get_child(0).get_active_pcam() as PhantomCamera2D
if editor:
var camera_2D_rid: RID = _selected_camera.get_canvas_item()
# TODO - Missing 2D viewport support - https://github.com/ramokz/phantom-camera/issues/105
RenderingServer.viewport_attach_camera(sub_viewport.get_viewport_rid(), camera_2D_rid)
else:
_selected_camera = pcam_host.camera_3D
_active_pcam_camera = _selected_camera.get_child(0).get_active_pcam() as PhantomCamera3D
if editor:
var camera_3D_rid: RID = _selected_camera.get_camera_rid()
RenderingServer.viewport_attach_camera(sub_viewport.get_viewport_rid(), camera_3D_rid)
if _selected_camera.keep_aspect == Camera3D.KeepAspect.KEEP_HEIGHT:
aspect_ratio_containers.set_stretch_mode(AspectRatioContainer.STRETCH_HEIGHT_CONTROLS_WIDTH)
else:
aspect_ratio_containers.set_stretch_mode(AspectRatioContainer.STRETCH_WIDTH_CONTROLS_HEIGHT)
_on_dead_zone_changed()
set_process(true)
if not pcam_host.update_editor_viewfinder.is_connected(_on_update_editor_viewfinder):
pcam_host.update_editor_viewfinder.connect(_on_update_editor_viewfinder.bind(pcam_host))
if not aspect_ratio_containers.resized.is_connected(_resized):
aspect_ratio_containers.resized.connect(_resized)
if not _active_pcam_camera.Properties.is_connected(_active_pcam_camera.Constants.DEAD_ZONE_CHANGED_SIGNAL, _on_dead_zone_changed):
_active_pcam_camera.Properties.connect(_active_pcam_camera.Constants.DEAD_ZONE_CHANGED_SIGNAL, _on_dead_zone_changed)
# aspect_ratio_container
# TODO - Might not be needed
# _active_pcam_camera.Properties.disconnect(_on_dead_zone_changed)
else:
for pcam_host in pcam_host_group:
print(pcam_host, " is in a scene")
func _resized() -> void:
_on_dead_zone_changed()
func _on_dead_zone_changed() -> void:
if not is_instance_valid(_active_pcam_camera): return
if camera_viewport_panel.size == Vector2.ZERO:
has_camera_viewport_panel_size = false
return
else:
has_camera_viewport_panel_size = true
var dead_zone_width: float = _active_pcam_camera.Properties.follow_framed_dead_zone_width * camera_viewport_panel.size.x
var dead_zone_height: float = _active_pcam_camera.Properties.follow_framed_dead_zone_height * camera_viewport_panel.size.y
dead_zone_center_hbox.set_custom_minimum_size(Vector2(dead_zone_width, 0))
dead_zone_center_center_panel.set_custom_minimum_size(Vector2(0, dead_zone_height))
dead_zone_left_center_panel.set_custom_minimum_size(Vector2(0, dead_zone_height))
dead_zone_right_center_panel.set_custom_minimum_size(Vector2(0, dead_zone_height))
min_horizontal = 0.5 - _active_pcam_camera.Properties.follow_framed_dead_zone_width / 2
max_horizontal = 0.5 + _active_pcam_camera.Properties.follow_framed_dead_zone_width / 2
min_vertical = 0.5 - _active_pcam_camera.Properties.follow_framed_dead_zone_height / 2
max_vertical = 0.5 + _active_pcam_camera.Properties.follow_framed_dead_zone_height / 2
# target_point.position = Vector2(viewport_width / 2, viewport_height / 2)
####################
## Priority Override
####################
func _on_update_editor_viewfinder(pcam_host: PhantomCameraHost) -> void:
if pcam_host.get_active_pcam().Properties.priority_override:
_active_pcam_camera = pcam_host.get_active_pcam()
_priority_override_button.set_visible(true)
_priority_override_name_label.set_text(_active_pcam_camera.name)
_priority_override_button.set_tooltip_text(_active_pcam_camera.name)
else:
_priority_override_button.set_visible(false)
func _select_override_pcam() -> void:
editor_interface.get_selection().clear()
editor_interface.get_selection().add_node(_active_pcam_camera)