mirror of
https://gitlab.com/MaddoScientisto/cirnogodot.git
synced 2026-06-13 17:15:53 +00:00
Camera 2D
This commit is contained in:
parent
bee29ba9ea
commit
4cb902053d
18 changed files with 791 additions and 24 deletions
104
Scenes/CameraController.gd
Normal file
104
Scenes/CameraController.gd
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
class_name CameraController
|
||||
extends Camera2D
|
||||
|
||||
## Whether to use pixel snap.
|
||||
@export var pixel_snap: bool = true
|
||||
## Whether camera movement should be smooth.
|
||||
@export var enable_smoothing: bool = true
|
||||
|
||||
## The current target being followed.
|
||||
var _active_target: CameraTarget
|
||||
|
||||
var _previous_pixel_snap_delta := Vector2.ZERO
|
||||
## The current camera velocity, for smooth damping.
|
||||
var _current_velocity := Vector2.ZERO
|
||||
## The current exact position of the camera.
|
||||
var _current_position: Vector2
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Add to group so we can look it up later.
|
||||
add_to_group("camera_controllers")
|
||||
|
||||
## Critically damped spring, based on Game Programming Gems 4 Chapter 1.10. https://archive.org/details/game-programming-gems-4/page/95/mode/2up
|
||||
## Returns a 2-tuple of [next_position, next_velocity].
|
||||
func smooth_damp(current: float, target: float, current_velocity: float, smooth_time: float, max_speed: float, delta: float) -> Array[float]:
|
||||
smooth_time = max(smooth_time, 0.0001)
|
||||
var omega := 2.0 / smooth_time
|
||||
|
||||
var x := omega * delta
|
||||
var x_exp := 1.0 / (1.0 + x + 0.48 * x * x + 0.235 * x * x * x)
|
||||
var change := current - target
|
||||
var original_target := target
|
||||
|
||||
# Clamp max speed.
|
||||
var max_change := max_speed * smooth_time
|
||||
change = clamp(change, -max_change, max_change)
|
||||
target = current - change
|
||||
|
||||
var temp := (current_velocity + omega * change) * delta
|
||||
current_velocity = (current_velocity - omega * temp) * x_exp
|
||||
var output := target + (change + temp) * x_exp
|
||||
|
||||
# Prevent overshooting.
|
||||
if (original_target - current > 0.0) == (output > original_target):
|
||||
output = original_target
|
||||
current_velocity = (output - original_target) / delta
|
||||
|
||||
return [output, current_velocity]
|
||||
|
||||
func _unhandled_input(_event: InputEvent) -> void:
|
||||
if Input.is_key_pressed(KEY_1):
|
||||
pixel_snap = not pixel_snap
|
||||
print("Camera pixel snap: ", pixel_snap)
|
||||
if Input.is_key_pressed(KEY_2):
|
||||
enable_smoothing = not enable_smoothing
|
||||
print("Camera smoothing: ", enable_smoothing)
|
||||
|
||||
# It's important that the camera position gets updated in _process instead of _physics_process,
|
||||
# since it needs to be dependent on frame rate.
|
||||
func _process(delta: float) -> void:
|
||||
# Update position.
|
||||
var next_position: Vector2
|
||||
var target: Vector2 = _active_target.global_position
|
||||
|
||||
if enable_smoothing:
|
||||
# Handle target movement with smooth_damp.
|
||||
# Replace this with any smooth follow / lerp of your choosing, but be careful to use `delta`
|
||||
# properly -- improper use can result in jitter. Don't do lerp(current, target, delta).
|
||||
var res_x := smooth_damp(_current_position.x, target.x, _current_velocity.x, 0.2, INF, delta)
|
||||
var res_y := smooth_damp(_current_position.y, target.y, _current_velocity.y, 0.2, INF, delta)
|
||||
next_position.x = res_x[0]
|
||||
next_position.y = res_y[0]
|
||||
_current_velocity.x = res_x[1]
|
||||
_current_velocity.y = res_y[1]
|
||||
else:
|
||||
# Set next camera position to the exact target.
|
||||
next_position = target
|
||||
|
||||
_current_position = next_position
|
||||
|
||||
if pixel_snap:
|
||||
# IMPORTANT: perform pixel snap so that the camera movement doesn't interfere with the
|
||||
# sub-pixel "smooth movement" we do in the shader.
|
||||
var snapped_position = (next_position + Vector2(0.5, 0.5)).floor()
|
||||
_previous_pixel_snap_delta = snapped_position - next_position
|
||||
next_position = snapped_position
|
||||
else:
|
||||
_previous_pixel_snap_delta = Vector2.ZERO
|
||||
|
||||
# Set the camera position.
|
||||
global_position = next_position
|
||||
# IMPORTANT: Work around godot bug where camera doesn't update immediately: https://github.com/godotengine/godot/issues/74203
|
||||
force_update_scroll()
|
||||
|
||||
## Returns the position delta between the pixel-snapped position and the actual controlled camera position.
|
||||
## If pixel snap is off, returns Zero.
|
||||
func get_pixel_snap_delta() -> Vector2:
|
||||
return _previous_pixel_snap_delta
|
||||
|
||||
## Registers a camera target to follow.
|
||||
func register_target(target: CameraTarget) -> void:
|
||||
assert(not _active_target)
|
||||
_active_target = target
|
||||
_current_position = _active_target.global_position
|
||||
8
Scenes/CameraTarget.gd
Normal file
8
Scenes/CameraTarget.gd
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
class_name CameraTarget
|
||||
extends Node2D
|
||||
|
||||
func _ready() -> void:
|
||||
# Attempt to register with controller.
|
||||
var result: Node = get_tree().get_first_node_in_group("camera_controllers")
|
||||
if result and result is CameraController:
|
||||
result.register_target(self)
|
||||
7
Scenes/PixelPerfectRendering.gd
Normal file
7
Scenes/PixelPerfectRendering.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extends Node2D
|
||||
|
||||
@onready var sub_viewport: SubViewport = $SubViewport
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
# SubViewports don't receive input by default, so we have to propagate it.
|
||||
sub_viewport.push_input(event)
|
||||
18
Scenes/SubViewportSprite.gd
Normal file
18
Scenes/SubViewportSprite.gd
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
extends Sprite2D
|
||||
|
||||
@onready var orig_pos := position
|
||||
|
||||
func _ready() -> void:
|
||||
# It's important that this script gets processed _after_ the "game scene", which includes the camera controller.
|
||||
# That is why we have placed it after the SubViewport. While we're at it, we can
|
||||
# use a larger priority number so that this node gets processed later in the process graph.
|
||||
process_priority = 1000
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
# Set the pixel snap delta from the camera so that we can have smooth camera movement.
|
||||
var pixel_snap_delta := Vector2.ZERO
|
||||
var result: Node = get_tree().get_first_node_in_group("camera_controllers")
|
||||
if result and result is CameraController:
|
||||
pixel_snap_delta = result.get_pixel_snap_delta()
|
||||
# Hard-code scaling ratio of 6 (you'd want to calculate this in a real game).
|
||||
position = orig_pos + pixel_snap_delta * 6
|
||||
12
Scenes/game.gd
Normal file
12
Scenes/game.gd
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
extends Node2D
|
||||
|
||||
## Whether to use the debug player camera for testing. Otherwise, use the CameraController with non-pixel-perfect settings (hard-coded zoom).
|
||||
@export var use_debug_player_camera: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
var camera_controller: CameraController = get_tree().get_first_node_in_group("camera_controllers")
|
||||
if use_debug_player_camera:
|
||||
var debug_player_camera: Camera2D = get_tree().get_first_node_in_group("debug_player_camera")
|
||||
camera_controller.enabled = false
|
||||
debug_player_camera.zoom = Vector2(1, 1)
|
||||
debug_player_camera.enabled = true
|
||||
32
Scenes/game.tscn
Normal file
32
Scenes/game.tscn
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://cwfaxgr8pgfga"]
|
||||
|
||||
[ext_resource type="Script" path="res://Scenes/game.gd" id="1_s0gpj"]
|
||||
[ext_resource type="Script" path="res://Scenes/PixelPerfectRendering.gd" id="2_gdejn"]
|
||||
[ext_resource type="PackedScene" uid="uid://bv451a8wgty4u" path="res://Scenes/test.tscn" id="3_l2ygy"]
|
||||
[ext_resource type="Script" path="res://Scenes/SubViewportSprite.gd" id="4_adb7a"]
|
||||
|
||||
[sub_resource type="ViewportTexture" id="ViewportTexture_m5h5j"]
|
||||
viewport_path = NodePath("PixelPerfectRendering/SubViewport")
|
||||
|
||||
[node name="Game" type="Node2D"]
|
||||
script = ExtResource("1_s0gpj")
|
||||
|
||||
[node name="PixelPerfectRendering" type="Node2D" parent="."]
|
||||
editor_description = "The children of this node are responsible for handling pixel-perfect rendering in the game world."
|
||||
script = ExtResource("2_gdejn")
|
||||
|
||||
[node name="SubViewport" type="SubViewport" parent="PixelPerfectRendering"]
|
||||
handle_input_locally = false
|
||||
snap_2d_vertices_to_pixel = true
|
||||
canvas_item_default_texture_filter = 0
|
||||
size = Vector2i(322, 182)
|
||||
render_target_update_mode = 4
|
||||
|
||||
[node name="GameScene" parent="PixelPerfectRendering/SubViewport" instance=ExtResource("3_l2ygy")]
|
||||
|
||||
[node name="SubViewportSprite" type="Sprite2D" parent="."]
|
||||
texture_filter = 1
|
||||
position = Vector2(960, 540)
|
||||
scale = Vector2(6, 6)
|
||||
texture = SubResource("ViewportTexture_m5h5j")
|
||||
script = ExtResource("4_adb7a")
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
[gd_scene load_steps=20 format=3 uid="uid://bghghp5ep4w2j"]
|
||||
[gd_scene load_steps=22 format=3 uid="uid://bghghp5ep4w2j"]
|
||||
|
||||
[ext_resource type="Script" path="res://Scripts/PlayerMovement.cs" id="1_m27vu"]
|
||||
[ext_resource type="Texture2D" uid="uid://la06powu57hu" path="res://Sprites/Cirno_Big.png" id="2_bwf6x"]
|
||||
[ext_resource type="PackedScene" uid="uid://b1qnfiuokpvsr" path="res://Scenes/bullet.tscn" id="2_ov36d"]
|
||||
[ext_resource type="Script" path="res://addons/smoothing/smoothing_2d.gd" id="4_j4xhu"]
|
||||
[ext_resource type="Script" path="res://Scenes/CameraTarget.gd" id="5_cxvyt"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ai4rh"]
|
||||
size = Vector2(6, 8)
|
||||
|
||||
[sub_resource type="AtlasTexture" id="AtlasTexture_omx2u"]
|
||||
atlas = ExtResource("2_bwf6x")
|
||||
region = Rect2(0, 0, 8, 16)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ai4rh"]
|
||||
size = Vector2(6, 8)
|
||||
|
||||
[sub_resource type="AtlasTexture" id="AtlasTexture_pdst4"]
|
||||
atlas = ExtResource("2_bwf6x")
|
||||
region = Rect2(0, 0, 8, 16)
|
||||
|
|
@ -139,7 +141,19 @@ BulletScene = ExtResource("2_ov36d")
|
|||
Muzzle = NodePath("Muzzle")
|
||||
metadata/_edit_group_ = true
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("RectangleShape2D_ai4rh")
|
||||
|
||||
[node name="Muzzle" type="Marker2D" parent="."]
|
||||
position = Vector2(5, 0)
|
||||
|
||||
[node name="Node2D" type="Node2D" parent="."]
|
||||
|
||||
[node name="Smoothing2D" type="Node2D" parent="."]
|
||||
script = ExtResource("4_j4xhu")
|
||||
flags = 55
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="Smoothing2D"]
|
||||
visible = false
|
||||
scale = Vector2(2.5, 1.75)
|
||||
texture = SubResource("AtlasTexture_omx2u")
|
||||
|
|
@ -147,15 +161,10 @@ hframes = 3
|
|||
vframes = 4
|
||||
frame = 1
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("RectangleShape2D_ai4rh")
|
||||
[node name="CameraTarget" type="Node2D" parent="Smoothing2D"]
|
||||
script = ExtResource("5_cxvyt")
|
||||
|
||||
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."]
|
||||
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="Smoothing2D"]
|
||||
y_sort_enabled = true
|
||||
sprite_frames = SubResource("SpriteFrames_q0rt3")
|
||||
animation = &"walk_left"
|
||||
|
||||
[node name="Muzzle" type="Marker2D" parent="."]
|
||||
position = Vector2(5, 0)
|
||||
|
||||
[node name="Node2D" type="Node2D" parent="."]
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue