diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac12729 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# SmartCamera2D for Godot 4.x \ No newline at end of file diff --git a/Scenes/Main/camera_controller.gd b/Scenes/Main/camera_controller.gd new file mode 100644 index 0000000..2679f89 --- /dev/null +++ b/Scenes/Main/camera_controller.gd @@ -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 \ No newline at end of file diff --git a/Scenes/Main/camera_controller.gd.uid b/Scenes/Main/camera_controller.gd.uid new file mode 100644 index 0000000..e0e1377 --- /dev/null +++ b/Scenes/Main/camera_controller.gd.uid @@ -0,0 +1 @@ +uid://brublmhrsdc7l diff --git a/addons/smartcamera2D/Camera2D.svg b/addons/smartcamera2D/Camera2D.svg new file mode 100644 index 0000000..e51e3c6 --- /dev/null +++ b/addons/smartcamera2D/Camera2D.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/smartcamera2D/Camera2D.svg.import b/addons/smartcamera2D/Camera2D.svg.import new file mode 100644 index 0000000..ce48fae --- /dev/null +++ b/addons/smartcamera2D/Camera2D.svg.import @@ -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 diff --git a/addons/smartcamera2D/CameraControl.gd b/addons/smartcamera2D/CameraControl.gd new file mode 100644 index 0000000..39b42a3 --- /dev/null +++ b/addons/smartcamera2D/CameraControl.gd @@ -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) diff --git a/addons/smartcamera2D/CameraControl.gd.uid b/addons/smartcamera2D/CameraControl.gd.uid new file mode 100644 index 0000000..9060d0f --- /dev/null +++ b/addons/smartcamera2D/CameraControl.gd.uid @@ -0,0 +1 @@ +uid://cl0y12rxbo7ub diff --git a/addons/smartcamera2D/LICENSE b/addons/smartcamera2D/LICENSE new file mode 100644 index 0000000..5b3dd09 --- /dev/null +++ b/addons/smartcamera2D/LICENSE @@ -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. diff --git a/addons/smartcamera2D/SmartCamera2D.gd b/addons/smartcamera2D/SmartCamera2D.gd new file mode 100644 index 0000000..26b047c --- /dev/null +++ b/addons/smartcamera2D/SmartCamera2D.gd @@ -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 diff --git a/addons/smartcamera2D/SmartCamera2D.gd.uid b/addons/smartcamera2D/SmartCamera2D.gd.uid new file mode 100644 index 0000000..77c6fda --- /dev/null +++ b/addons/smartcamera2D/SmartCamera2D.gd.uid @@ -0,0 +1 @@ +uid://xrddv2epi3ty diff --git a/addons/smartcamera2D/plugin.cfg b/addons/smartcamera2D/plugin.cfg new file mode 100644 index 0000000..3563927 --- /dev/null +++ b/addons/smartcamera2D/plugin.cfg @@ -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" diff --git a/addons/smartcamera2D/plugin.gd b/addons/smartcamera2D/plugin.gd new file mode 100644 index 0000000..042b7b6 --- /dev/null +++ b/addons/smartcamera2D/plugin.gd @@ -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") diff --git a/addons/smartcamera2D/plugin.gd.uid b/addons/smartcamera2D/plugin.gd.uid new file mode 100644 index 0000000..bcb38e1 --- /dev/null +++ b/addons/smartcamera2D/plugin.gd.uid @@ -0,0 +1 @@ +uid://b6pcau1vjhp20