Installed SmartCamera2D plugin.

This commit is contained in:
gdz
2025-12-07 21:32:24 +01:00
parent 269f2daf6c
commit 9429d48e32
13 changed files with 321 additions and 0 deletions

1
README.md Normal file
View File

@@ -0,0 +1 @@
# SmartCamera2D for Godot 4.x

View 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

View File

@@ -0,0 +1 @@
uid://brublmhrsdc7l

View 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

View 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

View 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)

View File

@@ -0,0 +1 @@
uid://cl0y12rxbo7ub

View 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.

View 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

View File

@@ -0,0 +1 @@
uid://xrddv2epi3ty

View 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"

View 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")

View File

@@ -0,0 +1 @@
uid://b6pcau1vjhp20