Installed SmartCamera2D plugin.
This commit is contained in:
17
Scenes/Main/camera_controller.gd
Normal file
17
Scenes/Main/camera_controller.gd
Normal file
@@ -0,0 +1,17 @@
|
||||
extends Node2D
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
pass # Replace with function body.
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
if Input.is_action_pressed("MoveUp"):
|
||||
global_position += Vector2.UP * 2
|
||||
if Input.is_action_pressed("MoveDown"):
|
||||
global_position += Vector2.DOWN * 2
|
||||
if Input.is_action_pressed("MoveLeft"):
|
||||
global_position += Vector2.LEFT * 2
|
||||
if Input.is_action_pressed("MoveRight"):
|
||||
global_position += Vector2.RIGHT * 2
|
||||
1
Scenes/Main/camera_controller.gd.uid
Normal file
1
Scenes/Main/camera_controller.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://brublmhrsdc7l
|
||||
1
addons/smartcamera2D/Camera2D.svg
Normal file
1
addons/smartcamera2D/Camera2D.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#8da5f3" d="M9 2a3 3 0 0 0-3 2.777 3 3 0 1 0-3 5.047V12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-1l3 2V7l-3 2V7.23A3 3 0 0 0 9 2z"/></svg>
|
||||
|
After Width: | Height: | Size: 203 B |
37
addons/smartcamera2D/Camera2D.svg.import
Normal file
37
addons/smartcamera2D/Camera2D.svg.import
Normal file
@@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bq6ud1dmn8fds"
|
||||
path="res://.godot/imported/Camera2D.svg-e2316bbab95f65a3786cbb6cb8741380.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/smartcamera2D/Camera2D.svg"
|
||||
dest_files=["res://.godot/imported/Camera2D.svg-e2316bbab95f65a3786cbb6cb8741380.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
14
addons/smartcamera2D/CameraControl.gd
Normal file
14
addons/smartcamera2D/CameraControl.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
extends Node
|
||||
|
||||
signal apply_flash
|
||||
signal apply_shake
|
||||
|
||||
var active = true
|
||||
|
||||
func apply_camera_flash(color: Color, duration = 0.3):
|
||||
if not active: return
|
||||
apply_flash.emit(color, duration)
|
||||
|
||||
func apply_camera_shake(force: float = 2.0, duration = 0.4):
|
||||
if not active: return
|
||||
apply_shake.emit(force, duration)
|
||||
1
addons/smartcamera2D/CameraControl.gd.uid
Normal file
1
addons/smartcamera2D/CameraControl.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cl0y12rxbo7ub
|
||||
21
addons/smartcamera2D/LICENSE
Normal file
21
addons/smartcamera2D/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 AndreMicheletti
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
205
addons/smartcamera2D/SmartCamera2D.gd
Normal file
205
addons/smartcamera2D/SmartCamera2D.gd
Normal file
@@ -0,0 +1,205 @@
|
||||
@tool
|
||||
extends Camera2D
|
||||
|
||||
class_name SmartCamera2D
|
||||
|
||||
enum TARGET_MODE {
|
||||
PARENT,
|
||||
SINGLE,
|
||||
MULTIPLE,
|
||||
GROUP
|
||||
}
|
||||
|
||||
@export_category("GENERAL CONFIG")
|
||||
@export var default_zoom: Vector2 = Vector2.ONE
|
||||
|
||||
@export_category("TARGET CONFIG")
|
||||
@export var target_mode: TARGET_MODE:
|
||||
set(value):
|
||||
target_mode = value
|
||||
notify_property_list_changed()
|
||||
|
||||
@export var target: NodePath
|
||||
@export var targets: Array[NodePath]
|
||||
@export var group_name: String
|
||||
@export var adjust_zoom: bool = true
|
||||
@export_range(1.0, 999.9) var adjust_zoom_margin: float
|
||||
@export_range(0.001, 999.9) var adjust_zoom_min: float = 0.1
|
||||
@export_range(0.001, 999.9) var adjust_zoom_max: float = 1.0
|
||||
@export_range(-1, 999, 1) var adjust_zoom_priority_node_index: int = -1
|
||||
|
||||
@export_category("EFFECTS CONFIG")
|
||||
@export var effects_layer: int = 2
|
||||
|
||||
var target_node: Node2D
|
||||
var target_nodes: Array[Node]
|
||||
var extra_position = Vector2.ZERO
|
||||
|
||||
@onready var refresh_target_timer = Timer.new()
|
||||
@onready var canvas_layer = CanvasLayer.new()
|
||||
@onready var camera_flash: ColorRect
|
||||
|
||||
func _ready():
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
CameraControl.apply_flash.connect(apply_camera_flash)
|
||||
CameraControl.apply_shake.connect(apply_camera_shake)
|
||||
zoom = default_zoom
|
||||
_make_refresh_timer()
|
||||
_make_effects_layers()
|
||||
if target_mode == TARGET_MODE.SINGLE:
|
||||
ensure_target_node()
|
||||
elif target_mode == TARGET_MODE.MULTIPLE:
|
||||
ensure_target_nodes()
|
||||
call_deferred("_refresh_targets")
|
||||
|
||||
func _make_refresh_timer():
|
||||
refresh_target_timer.connect("timeout", _refresh_targets)
|
||||
refresh_target_timer.wait_time = 0.3
|
||||
add_child(refresh_target_timer)
|
||||
refresh_target_timer.start()
|
||||
|
||||
func _make_effects_layers():
|
||||
canvas_layer.layer = effects_layer
|
||||
add_child(canvas_layer)
|
||||
camera_flash = create_color_rect()
|
||||
|
||||
func apply_camera_shake(force: float = 2.0, duration = 0.8):
|
||||
var tween = create_tween()
|
||||
var step_dur = duration / 5.0
|
||||
tween.tween_property(self, "extra_position", Vector2(-force, force), step_dur)
|
||||
tween.tween_property(self, "extra_position", Vector2(force, force), step_dur)
|
||||
tween.tween_property(self, "extra_position", Vector2(force, -force), step_dur)
|
||||
tween.tween_property(self, "extra_position", Vector2(-force, -force), step_dur)
|
||||
tween.tween_property(self, "extra_position", Vector2.ZERO, step_dur)
|
||||
|
||||
func apply_camera_flash(color: Color, duration = 0.3):
|
||||
var screen_size = get_viewport_rect().size
|
||||
camera_flash.size = screen_size
|
||||
var tween = create_tween()
|
||||
var final_color = Color(color, 0.0)
|
||||
var initial_color = Color(color, 0.2)
|
||||
camera_flash.visible = true
|
||||
tween.tween_property(camera_flash, "color", final_color, duration) \
|
||||
.from(initial_color)
|
||||
await tween.finished
|
||||
camera_flash.visible = false
|
||||
|
||||
func _process(delta):
|
||||
if not Engine.is_editor_hint():
|
||||
_sanitize_targets()
|
||||
if target_mode == TARGET_MODE.PARENT:
|
||||
position = Vector2.ZERO
|
||||
elif target_mode == TARGET_MODE.SINGLE:
|
||||
_process_target_single(delta)
|
||||
elif target_mode == TARGET_MODE.MULTIPLE:
|
||||
_process_target_multiple(delta)
|
||||
elif target_mode == TARGET_MODE.GROUP:
|
||||
_process_target_group(delta)
|
||||
position = position + extra_position
|
||||
|
||||
func _process_target_single(delta):
|
||||
if not target_node:
|
||||
printerr("[SmartCamera2D] TARGET NODE IS NULL")
|
||||
return
|
||||
global_position = target_node.global_position
|
||||
|
||||
func _process_target_multiple(delta):
|
||||
if target_nodes.size() < 0:
|
||||
printerr("[SmartCamera2D] NOT ENOUGH TARGET NODES")
|
||||
return
|
||||
adjust_camera_zoom()
|
||||
|
||||
func _process_target_group(delta):
|
||||
if target_nodes.size() < 0:
|
||||
printerr("[SmartCamera2D] NOT ENOUGH TARGET NODES")
|
||||
return
|
||||
adjust_camera_zoom()
|
||||
|
||||
func ensure_target_node():
|
||||
if target_node: return
|
||||
target_node = get_node(target)
|
||||
|
||||
func ensure_target_nodes():
|
||||
target_nodes.clear()
|
||||
for path in targets:
|
||||
target_nodes.append(get_node(path))
|
||||
|
||||
func adjust_camera_zoom():
|
||||
if target_nodes.is_empty():
|
||||
return
|
||||
if target_nodes.size() == 1:
|
||||
zoom = default_zoom
|
||||
global_position = target_nodes[0].global_position
|
||||
return
|
||||
var screen_size = get_viewport_rect().size
|
||||
var min_pos = target_nodes[0].global_position
|
||||
var max_pos = target_nodes[0].global_position
|
||||
for node in target_nodes:
|
||||
min_pos = min_pos.min(node.global_position)
|
||||
max_pos = max_pos.max(node.global_position)
|
||||
var size = max_pos - min_pos
|
||||
var zoom_x = screen_size.x / size.x if size.x != 0 else 1.0
|
||||
var zoom_y = screen_size.y / size.y if size.y != 0 else 1.0
|
||||
var zoom_safe = clamp(min(zoom_x, zoom_y) / adjust_zoom_margin, adjust_zoom_min, adjust_zoom_max)
|
||||
zoom = Vector2(zoom_safe, zoom_safe)
|
||||
var center_position = (min_pos + max_pos) / 2.0
|
||||
global_position = center_position
|
||||
|
||||
var priority_i = adjust_zoom_priority_node_index
|
||||
if priority_i > -1:
|
||||
if not is_point_visible(target_nodes[priority_i].global_position, screen_size):
|
||||
center_position = target_nodes[priority_i].global_position
|
||||
|
||||
func is_point_visible(point: Vector2, screen_size: Vector2) -> bool:
|
||||
var half_screen = (screen_size / 2.0) / zoom
|
||||
var camera_min = global_position - half_screen
|
||||
var camera_max = global_position + half_screen
|
||||
return point.x >= camera_min.x and point.x <= camera_max.x and point.y >= camera_min.y and point.y <= camera_max.y
|
||||
|
||||
func create_color_rect():
|
||||
var color_rect = ColorRect.new()
|
||||
color_rect.visible = false
|
||||
color_rect.color = Color.TRANSPARENT
|
||||
color_rect.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
color_rect.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
color_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
canvas_layer.add_child(color_rect)
|
||||
return color_rect
|
||||
|
||||
func _refresh_targets():
|
||||
if target_mode == TARGET_MODE.SINGLE:
|
||||
if not target_node:
|
||||
target_node = get_node(target)
|
||||
elif target_mode == TARGET_MODE.GROUP:
|
||||
target_nodes = get_tree().get_nodes_in_group(group_name)
|
||||
|
||||
func _sanitize_targets():
|
||||
if target_node and target_node.is_queued_for_deletion():
|
||||
target_node = null
|
||||
if not target_nodes.is_empty():
|
||||
target_nodes = target_nodes.filter(_filter_existing_node)
|
||||
|
||||
func _filter_existing_node(variant) -> bool:
|
||||
if variant == null:
|
||||
return false
|
||||
if not variant is Node2D:
|
||||
return false
|
||||
if variant is Node2D and variant.is_queued_for_deletion():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _validate_property(property: Dictionary):
|
||||
var multiple_properties = [
|
||||
"adjust_zoom", "zoom_margin", "adjust_zoom_margin",
|
||||
"adjust_zoom_min", "adjust_zoom_max", "adjust_zoom_priority_node_index"
|
||||
]
|
||||
if property.name == "target" and target_mode != TARGET_MODE.SINGLE:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == "group_name" and target_mode != TARGET_MODE.GROUP:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == "targets" and target_mode != TARGET_MODE.MULTIPLE:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name in multiple_properties and target_mode not in [TARGET_MODE.MULTIPLE, TARGET_MODE.GROUP]:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
1
addons/smartcamera2D/SmartCamera2D.gd.uid
Normal file
1
addons/smartcamera2D/SmartCamera2D.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://xrddv2epi3ty
|
||||
7
addons/smartcamera2D/plugin.cfg
Normal file
7
addons/smartcamera2D/plugin.cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="smartcamera2D"
|
||||
description="A plug-and-play camera controller with smooth follow, screen shake, zoom"
|
||||
author="Async Studio"
|
||||
version="1.0"
|
||||
script="plugin.gd"
|
||||
14
addons/smartcamera2D/plugin.gd
Normal file
14
addons/smartcamera2D/plugin.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
func _enter_tree():
|
||||
# Initialization of the plugin goes here.
|
||||
# Add the new type with a name, a parent type, a script and an icon.
|
||||
add_custom_type("SmartCamera2D", "Camera2D", preload("SmartCamera2D.gd"), preload("Camera2D.svg"))
|
||||
add_autoload_singleton("CameraControl", "CameraControl.gd")
|
||||
|
||||
func _exit_tree():
|
||||
# Clean-up of the plugin goes here.
|
||||
# Always remember to remove it from the engine when deactivated.
|
||||
remove_custom_type("SmartCamera2D")
|
||||
remove_autoload_singleton("CameraControl")
|
||||
1
addons/smartcamera2D/plugin.gd.uid
Normal file
1
addons/smartcamera2D/plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b6pcau1vjhp20
|
||||
Reference in New Issue
Block a user