WIP: refactor camera anchor system

This commit is contained in:
Reed 2026-01-12 17:51:15 +08:00
parent 97ea4ae12e
commit 98178a2bd2
26 changed files with 560 additions and 23 deletions

View File

@ -4,7 +4,7 @@
#
#'''
#@tool
#extends Node
extends Node
#
#@onready var camera_2d: Camera2D = %Camera2D
#@onready var camera_shake_player: CameraShakePlayer = %CameraShakePlayer

View File

@ -0,0 +1,21 @@
[gd_scene load_steps=5 format=3 uid="uid://cw6buluknvjj"]
[ext_resource type="Script" uid="uid://djk7tg2puphgv" path="res://_camera/camera_test.gd" id="1_05blt"]
[ext_resource type="Script" uid="uid://py4h5jxlncro" path="res://addons/reedcamera/scripts/CameraPointer.gd" id="1_e7rkk"]
[ext_resource type="Script" uid="uid://dwr1s51svvank" path="res://addons/reedcamera/scripts/camera_tools/CameraShakeController.gd" id="2_rbequ"]
[ext_resource type="Script" uid="uid://bhl5it46hv4n2" path="res://addons/reedcamera/scripts/camera_tools/CameraAnchorController.gd" id="4_877nu"]
[node name="PlateformerCamera" type="Camera2D"]
script = ExtResource("1_05blt")
[node name="CameraPointer" type="Node" parent="."]
script = ExtResource("1_e7rkk")
metadata/_custom_type_script = "uid://py4h5jxlncro"
[node name="ReedCameraShakeController" type="Node" parent="CameraPointer"]
script = ExtResource("2_rbequ")
metadata/_custom_type_script = "uid://dwr1s51svvank"
[node name="ReedCameraAnchorController" type="Node" parent="CameraPointer"]
script = ExtResource("4_877nu")
metadata/_custom_type_script = "uid://bhl5it46hv4n2"

View File

@ -1,8 +1,9 @@
extends RemoteTransform2D
func _ready() -> void:
var global_camera: Camera2D = CameraSystem.get_cached_camera()
if not global_camera:
push_error("[CameraFollower]:No Global Camera Founded")
return
remote_path = global_camera.get_path()
pass
#var global_camera: Camera2D = CameraSystem.get_cached_camera()
#if not global_camera:
#push_error("[CameraFollower]:No Global Camera Founded")
#return
#remote_path = global_camera.get_path()

8
_camera/camera_test.gd Normal file
View File

@ -0,0 +1,8 @@
extends Camera2D
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept"):
$CameraPointer/ReedCameraShakeController.play_shake(preload("res://addons/reedcamera/_example/test_shake.tres"))
#elif event.is_action_pressed("ui_right"):
#self.global_position += Vector2(100,0)

View File

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

View File

@ -78,6 +78,7 @@ unique_name_in_owner = true
shape = SubResource("RectangleShape2D_qnulu")
[node name="Sprite2D" type="Sprite2D" parent="."]
visible = false
texture_filter = 1
position = Vector2(0, -2)
texture = SubResource("AtlasTexture_basl5")

View File

@ -0,0 +1,33 @@
[gd_scene load_steps=5 format=3 uid="uid://ck8m7revy150r"]
[ext_resource type="PackedScene" uid="uid://gwhff4qaouxy" path="res://_player/Avatar.tscn" id="1_irquc"]
[ext_resource type="Texture2D" uid="uid://dted7geb331y2" path="res://_asset/ksw/character.png" id="2_350jv"]
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_350jv"]
radius = 41.0
height = 134.0
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_e1o4i"]
radius = 45.0
height = 145.0
[node name="Avatar" instance=ExtResource("1_irquc")]
[node name="CollisionShape2D" parent="." index="1"]
visible = false
shape = SubResource("CapsuleShape2D_350jv")
[node name="Sprite2D" parent="." index="2"]
visible = true
texture_filter = 0
texture = ExtResource("2_350jv")
[node name="LocomotionComponent" parent="." index="5"]
_can_move = false
[node name="CollisionShape2D" parent="HitBox" index="0"]
visible = false
position = Vector2(0, 0)
shape = SubResource("CapsuleShape2D_e1o4i")
[editable path="LocomotionComponent/WallDetector"]

View File

@ -0,0 +1,30 @@
[gd_scene load_steps=5 format=3 uid="uid://ck8m7revy150r"]
[ext_resource type="PackedScene" uid="uid://gwhff4qaouxy" path="res://_player/Avatar.tscn" id="1_irquc"]
[ext_resource type="Texture2D" uid="uid://dted7geb331y2" path="res://_asset/ksw/character.png" id="2_350jv"]
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_350jv"]
radius = 41.0
height = 134.0
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_e1o4i"]
radius = 45.0
height = 145.0
[node name="Avatar" instance=ExtResource("1_irquc")]
[node name="CollisionShape2D" parent="." index="1"]
visible = false
shape = SubResource("CapsuleShape2D_350jv")
[node name="Sprite2D" parent="." index="2"]
visible = true
texture_filter = 0
texture = ExtResource("2_350jv")
[node name="CollisionShape2D" parent="HitBox" index="0"]
visible = false
position = Vector2(0, 0)
shape = SubResource("CapsuleShape2D_e1o4i")
[editable path="LocomotionComponent/WallDetector"]

View File

@ -44,8 +44,8 @@ func _enter() -> void:
elif i == 3 or i == 7:
csp = agent.camera_shake_preset.get("xy_light")
if csp:
CameraSystem.camera_shake_player.play(csp)
#if csp:
#CameraSystem.camera_shake_player.play(csp)
if root.grap_hook_state._jump_grace_timer > 0:
_hook_to_jump()

View File

@ -0,0 +1,14 @@
[gd_scene load_steps=3 format=3 uid="uid://b2rtcqvak066v"]
[ext_resource type="Texture2D" uid="uid://c673bap4b12fx" path="res://icon.svg" id="1_6ducv"]
[ext_resource type="PackedScene" uid="uid://cw6buluknvjj" path="res://_camera/PlateformerCamera.tscn" id="2_owtx0"]
[node name="Test" type="Node2D"]
[node name="Icon" type="Sprite2D" parent="."]
position = Vector2(-1, -2)
texture = ExtResource("1_6ducv")
[node name="PlateformerCamera" parent="." instance=ExtResource("2_owtx0")]
ignore_rotation = false
position_smoothing_enabled = true

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ReedCameraShakePreset" load_steps=2 format=3 uid="uid://wm3cmccp1ydl"]
[ext_resource type="Script" uid="uid://wlqopoksgvjc" path="res://addons/reedcamera/resource/ReedCameraShakePreset.gd" id="1_8o8fw"]
[resource]
script = ExtResource("1_8o8fw")
amplitude = Vector2(10, 10)
frequency = 100.0
metadata/_custom_type_script = "uid://wlqopoksgvjc"

View File

Before

Width:  |  Height:  |  Size: 869 B

After

Width:  |  Height:  |  Size: 869 B

View File

@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bsdmq0essfmpk"
path="res://.godot/imported/camera_anchor_icon.svg-bc0c9f7b183031f0db701d2e858a9063.ctex"
path="res://.godot/imported/camera_anchor_icon.svg-d54e4ec18108e371c1abc23152af31de.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://_asset/icon/camera_anchor_icon.svg"
dest_files=["res://.godot/imported/camera_anchor_icon.svg-bc0c9f7b183031f0db701d2e858a9063.ctex"]
source_file="res://addons/reedcamera/icon/camera_anchor_icon.svg"
dest_files=["res://.godot/imported/camera_anchor_icon.svg-d54e4ec18108e371c1abc23152af31de.ctex"]
[params]

View File

@ -0,0 +1,55 @@
extends RefCounted
class_name ReedCameraShakePlayer
var _preset: ReedCameraShakePreset
var _time := 0.0
var _strength := 0.0
var _active := false
var _noise := FastNoiseLite.new()
var _noise_seed := randi()
func play(preset: ReedCameraShakePreset) -> void:
_preset = preset
_time = 0.0
_strength = 0.0
_active = true
_noise.seed = _noise_seed
func stop() -> void:
_active = false
func is_active() -> bool:
return _active
func evaluate(delta: float) -> Vector2:
if not _active or not _preset:
return Vector2.ZERO
_time += delta
var total := _preset.fade_in + _preset.hold + _preset.fade_out
if _time >= total:
_active = false
return Vector2.ZERO
# ===== 强度曲线 =====
if _time < _preset.fade_in:
_strength = _time / _preset.fade_in
elif _time < _preset.fade_in + _preset.hold:
_strength = 1.0
else:
var t := (_time - _preset.fade_in - _preset.hold) / _preset.fade_out
_strength = 1.0 - t
# ===== Noise 偏移 =====
var shake_t := _time * _preset.frequency
var offset := Vector2(
_noise.get_noise_1d(shake_t),
_noise.get_noise_1d(shake_t + 1000)
)
return Vector2(
offset.x * _preset.amplitude.x,
offset.y * _preset.amplitude.y
) * _strength

View File

@ -0,0 +1 @@
uid://5xuwd0cde3y8

View File

@ -0,0 +1,9 @@
# CameraShakePreset.gd
extends Resource
class_name ReedCameraShakePreset
@export var amplitude := Vector2(6, 6) # 最大位移
@export var frequency := 25.0 # 抖动频率
@export var fade_in := 0.05
@export var hold := 0.1
@export var fade_out := 0.15

View File

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

View File

@ -1,6 +1,6 @@
##TODO:清楚掉這裏和PhantomCamera相關的部分
@tool
@icon("uid://bsdmq0essfmpk")
@icon("res://addons/reedcamera/icon/camera_anchor_icon.svg")
class_name CameraAnchor extends Node2D
const _CONSTANTS = preload("res://addons/reedcamera/_data/CameraSystemConst.gd")
@ -10,6 +10,10 @@ const _CONSTANTS = preload("res://addons/reedcamera/_data/CameraSystemConst.gd")
@export var priority: int = 0
##此Anchor是否有效
@export var enabled: bool = true
## =========================
## Blend Config
## =========================
@export_subgroup("Blending")
##是否要存在相機過渡時間
@export var use_blend: bool = true
##過度時間
@ -49,12 +53,6 @@ const _CONSTANTS = preload("res://addons/reedcamera/_data/CameraSystemConst.gd")
limit_right = value
if Engine.is_editor_hint():
queue_redraw()
@export_subgroup("Follow")
@export var follow_player: bool = false
var _camera_global : Node = null
## 编辑器预览面板设置
@export_group("Editor Preview")
@ -66,6 +64,15 @@ var _camera_global : Node = null
@export var limit_preview_line_width: float = 2.0
#@export_subgroup("Follow")
#@export var follow_player: bool = false
var _camera_global : Node = null
var _priority: int :
set(value):
if _priority != value:
@ -147,7 +154,6 @@ func _get_camera_global() -> Object:
if Engine.has_singleton(_CONSTANTS.CAMERA_SYSTEM_NAME):
_camera_global = Engine.get_singleton(_CONSTANTS.CAMERA_SYSTEM_NAME)
print("")
return _camera_global

View File

@ -0,0 +1,125 @@
@tool
extends Node
class_name CameraPointer
const _CONSTANTS := preload("res://addons/reedcamera/_data/CameraSystemConst.gd")
var _camera: Camera2D
var _editor_valid := false
var _runtime_registered := false
var _warned := false
var _final_offset := Vector2.ZERO
var _base_position: Vector2
func _enter_tree() -> void:
if Engine.is_editor_hint():
call_deferred("_editor_verify")
func _exit_tree() -> void:
if Engine.is_editor_hint():
return
_request_unregister()
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return
if not _camera:
return
_update_base_position()
_update_camera_offset()
_apply_camera_transform()
func _update_base_position():
for child in get_children():
if child.has_method("get_base_position"):
_base_position = child.get_base_position()
return
# fallback
_base_position = _camera.global_position
func _update_camera_offset():
var offset := Vector2.ZERO
for child in get_children():
if child.has_method("get_camera_offset"):
offset += child.get_camera_offset()
_final_offset = offset
func _apply_camera_transform():
_camera.global_position = _base_position
_camera.offset = _final_offset
func _notification(what: int) -> void:
if not Engine.is_editor_hint():
return
match what:
NOTIFICATION_PARENTED, NOTIFICATION_UNPARENTED:
#print("重設parent")
call_deferred("_editor_verify")
## 編輯器層面的通過性驗證
func _editor_verify() -> void:
_camera = _find_camera()
_editor_valid = _camera != null
#print(_editor_valid)
if not _editor_valid:
_emit_config_warning()
else:
_warned = false
func _ready() -> void:
if Engine.is_editor_hint():
return
_camera = _find_camera()
if not _camera:
return
_request_register()
func _find_camera() -> Camera2D:
var p := get_parent()
return p as Camera2D if p is Camera2D else null
func _request_register() -> void:
var sys := Engine.get_singleton(_CONSTANTS.CAMERA_SYSTEM_NAME)
if not sys:
return
_runtime_registered = sys.request_register_pointer(self)
func _request_unregister() -> void:
if not _runtime_registered:
return
var sys := Engine.get_singleton(_CONSTANTS.CAMERA_SYSTEM_NAME)
if sys:
sys.request_unregister_pointer(self)
_runtime_registered = false
func _emit_config_warning() -> void:
if not Engine.is_editor_hint():
return
if _warned:
return
push_warning(
"[CameraPointer] Invalid configuration: parent node must be Camera2D. "
+ "Current parent: %s" % (get_parent() if get_parent() else "null")
)
_warned = true
func get_camera() -> Camera2D:
return _camera

View File

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

View File

@ -5,6 +5,63 @@ const _CONSTANTS = preload("res://addons/reedcamera/_data/CameraSystemConst.gd")
var _screen_size : Vector2i
var _camera_points : Array[CameraPointer]
var _current_camera_point : CameraPointer
signal anchor_registered(anchor: CameraAnchor)
signal anchor_unregistered(anchor: CameraAnchor)
var _anchors : Array[CameraAnchor]
#region 相機指針
func request_register_pointer(ptr: CameraPointer) -> bool:
if not is_instance_valid(ptr):
return false
var cam := ptr.get_camera()
if not is_instance_valid(cam):
return false # System 决定:无效 camera 不接受注册
# 去重
if ptr in _camera_points:
_current_camera_point = ptr
return true
_camera_points.append(ptr)
# 你可以在这里做选择策略:比如最新优先/priority
_current_camera_point = ptr
return true
func request_unregister_pointer(ptr: CameraPointer) -> void:
_camera_points.erase(ptr)
if _current_camera_point == ptr:
_current_camera_point = _camera_points.back() if _camera_points.size() > 0 else null
func get_current_camera_pointer() -> CameraPointer:
return _current_camera_point
func get_camera() -> Camera2D:
return _current_camera_point.get_camera() if _current_camera_point else null
#endregion
#region 相機錨點
func register_anchor(anchor: CameraAnchor) -> void:
if anchor in _anchors:
return
_anchors.append(anchor)
anchor_registered.emit(anchor)
func unregister_anchor(anchor: CameraAnchor) -> void:
if _anchors.has(anchor):
_anchors.erase(anchor)
anchor_unregistered.emit(anchor)
func get_all_anchors() -> Array[CameraAnchor]:
return _anchors.duplicate()
#endregion
func _enter_tree() -> void:
if not Engine.has_singleton(_CONSTANTS.CAMERA_SYSTEM_NAME):
Engine.register_singleton(_CONSTANTS.CAMERA_SYSTEM_NAME, self)

View File

@ -0,0 +1,99 @@
extends Node
class_name ReedCameraAnchorController
const _CONSTANTS = preload("res://addons/reedcamera/_data/CameraSystemConst.gd")
## =========================
## Private
## =========================
var _anchors: Array[CameraAnchor] = []
var _current_anchor: CameraAnchor
var _current_position := Vector2.ZERO
var _tween: Tween
## =========================
## Config
## =========================
@export var enabled := true
@export var move_duration := 0.4
@export var ease_type := Tween.EASE_OUT
@export var trans_type := Tween.TRANS_CUBIC
## =========================
## Runtime State
## =========================
#var _current_position: Vector2
var _target_position: Vector2
#var _tween: Tween
var _has_target := false
## =========================
## Lifecycle
## =========================
func _ready() -> void:
var sys := Engine.get_singleton(_CONSTANTS.CAMERA_SYSTEM_NAME)
if not sys:
return
_anchors = sys.get_all_anchors()
sys.anchor_registered.connect(_on_anchor_added)
sys.anchor_unregistered.connect(_on_anchor_removed)
_evaluate()
func _on_anchor_added(anchor: CameraAnchor) -> void:
_anchors.append(anchor)
_evaluate()
func _on_anchor_removed(anchor: CameraAnchor) -> void:
_anchors.erase(anchor)
if _current_anchor == anchor:
_current_anchor = null
_evaluate()
func _evaluate() -> void:
var winner := _pick_best_anchor()
if winner == _current_anchor:
return
_switch_to(winner)
func _pick_best_anchor() -> CameraAnchor:
var best: CameraAnchor
var best_p := -INF
for a in _anchors:
if not a.enabled:
continue
if a.priority > best_p:
best = a
best_p = a.priority
return best
func _switch_to(anchor: CameraAnchor) -> void:
if not anchor:
return
_current_anchor = anchor
var target := anchor.global_position
if _tween and _tween.is_running():
_tween.kill()
if not anchor.use_blend or anchor.blend_time <= 0.0:
_current_position = target
return
_tween = get_tree().create_tween()
_tween.set_trans(Tween.TRANS_CUBIC)
_tween.set_ease(Tween.EASE_OUT)
_tween.tween_property(self, "_current_position", target, anchor.blend_time)
## =========================
## CameraPointer Interface
## =========================
func get_base_position() -> Vector2:
return _current_position

View File

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

View File

@ -0,0 +1,66 @@
@tool
extends Node
class_name ReedCameraShakeController
## =========================
## Config
## =========================
@export var enabled := true
## =========================
## Runtime
## =========================
var _players: Array[ReedCameraShakePlayer] = []
var _current_offset := Vector2.ZERO
func _ready() -> void:
if Engine.is_editor_hint():
return
set_process(true)
func _exit_tree() -> void:
_players.clear()
_current_offset = Vector2.ZERO
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return
if not enabled:
_current_offset = Vector2.ZERO
return
var offset := Vector2.ZERO
for p in _players:
offset += p.evaluate(delta)
# 移除已经结束的 player
_players = _players.filter(func(p): return p.is_active())
_current_offset = offset
## =========================
## Public API
## =========================
func play_shake(preset: ReedCameraShakePreset) -> void:
if not preset:
return
var player := ReedCameraShakePlayer.new()
player.play(preset)
_players.append(player)
func stop_all() -> void:
for p in _players:
p.stop()
_players.clear()
_current_offset = Vector2.ZERO
## =========================
## Pipeline Output
## =========================
func get_camera_offset() -> Vector2:
return _current_offset

View File

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

View File

@ -17,16 +17,13 @@ config/icon="res://icon.svg"
[autoload]
ReedCameraSystem="*res://addons/reedcamera/scripts/ReedCameraGlobal.gd"
CameraSystem="*res://_camera/CameraSystem.tscn"
GlobalEvent="*res://_shared/GlobalEvent.gd"
ReedVFX="*res://addons/reedfx/vfx/ReedVFXSystem.tscn"
ReedSceneRegistry="*res://addons/reedscene/scene/SceneRegistry.gd"
ReedCameraSystem="*res://addons/reedcamera/scripts/ReedCameraGlobal.gd"
[display]
window/size/viewport_width=640
window/size/viewport_height=360
window/size/window_width_override=1920
window/size/window_height_override=1080
window/stretch/mode="canvas_items"