Created
March 25, 2025 21:43
-
-
Save viniciusemferreira/808d5703eb7b253e6ddba47b91b01a36 to your computer and use it in GitHub Desktop.
This was supposed to be an aim mechanic, but it ended up looking like the mechanic seen in Dome Keeper, where you drag the boxes, so we should find a better name for it later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@tool | |
class_name AimComponent | |
extends Node2D | |
## Component that handles aim mechanics with physics-based mouse following | |
## | |
## This component creates a physical cursor that follows the mouse position with | |
## configurable acceleration, drag, and mass properties to simulate different | |
## weapon handling characteristics. | |
# Signals | |
signal aim_position_changed(position: Vector2) | |
signal aim_direction_changed(direction: Vector2) | |
signal aim_speed_changed(speed: float) | |
# Exported Configuration | |
@export_group("Physics Properties") | |
@export var max_speed: float = 1000.0 | |
@export var acceleration: float = 3000.0 | |
@export var drag: float = 10.0 | |
@export var mass: float = 1.0 | |
@export var distance_threshold: float = 5.0 # Distance at which aim is considered "arrived" | |
@export_group("Visual Settings") | |
@export var cursor_size: float = 8.0 | |
@export var cursor_color: Color = Color.WHITE | |
@export var show_connection_line: bool = true | |
@export var connection_line_color: Color = Color(1, 1, 1, 0.5) | |
@export_group("Gameplay Modifiers") | |
@export var max_distance_from_parent: float = 200.0 # Maximum aim distance | |
@export var start_offset: Vector2 = Vector2.ZERO # Offset from parent center | |
# Private Variables | |
var _velocity: Vector2 = Vector2.ZERO | |
var _target_position: Vector2 = Vector2.ZERO | |
var _last_aim_direction: Vector2 = Vector2.RIGHT | |
var _mouse_position: Vector2 = Vector2.ZERO | |
var _previous_mouse_position: Vector2 = Vector2.ZERO | |
var _mouse_movement: Vector2 = Vector2.ZERO | |
# Lifecycle Methods | |
func _ready() -> void: | |
# Initialize position | |
if not Engine.is_editor_hint(): | |
position = start_offset | |
_target_position = position | |
_previous_mouse_position = get_global_mouse_position() | |
func _process(delta: float) -> void: | |
# Update visual in editor | |
if Engine.is_editor_hint(): | |
queue_redraw() | |
return | |
# Update mouse position and calculate movement | |
_previous_mouse_position = _mouse_position | |
_mouse_position = get_global_mouse_position() | |
_mouse_movement = _mouse_position - _previous_mouse_position | |
# Get parent position | |
var parent_center = get_parent_global_position() | |
# Calculate direction to mouse regardless of distance | |
var direction_to_mouse = (_mouse_position - parent_center).normalized() | |
# Calculate the target position - always respond to mouse movement direction | |
# but clamp the actual position to max distance | |
if _mouse_movement.length_squared() > 0: | |
# Calculate what percentage of the way to the mouse we should move | |
var distance_to_mouse = (_mouse_position - parent_center).length() | |
# If mouse is beyond max distance, point the target in that direction | |
# but at the clamped max distance | |
var clamped_distance = min(distance_to_mouse, max_distance_from_parent) | |
_target_position = parent_center + direction_to_mouse * clamped_distance | |
# Physics-based aim movement | |
_update_aim_physics(delta) | |
# Emit signals for aim changes | |
_emit_aim_signals() | |
# Update visuals | |
queue_redraw() | |
func _draw() -> void: | |
# Draw aim cursor | |
draw_circle(Vector2.ZERO, cursor_size, cursor_color) | |
# Draw line to mouse if enabled | |
if show_connection_line and not Engine.is_editor_hint(): | |
var line_end = to_local(_mouse_position) | |
draw_line(Vector2.ZERO, line_end, connection_line_color, 0.2, true) | |
# Public API | |
## Force aim to a specific position | |
func set_aim_position(new_position: Vector2) -> void: | |
global_position = new_position | |
_velocity = Vector2.ZERO | |
emit_signals() | |
## Get the current aim direction (normalized) | |
func get_aim_direction() -> Vector2: | |
return _last_aim_direction | |
## Reset aim to parent position with no velocity | |
func reset_aim() -> void: | |
global_position = get_parent_global_position() + start_offset | |
_velocity = Vector2.ZERO | |
emit_signals() | |
## Apply an impulse to the aim (like recoil) | |
func apply_impulse(impulse: Vector2) -> void: | |
_velocity += impulse / mass | |
## Set the physics properties at runtime | |
func set_physics_properties(new_acceleration: float, new_drag: float, new_mass: float) -> void: | |
acceleration = new_acceleration | |
drag = new_drag | |
mass = new_mass | |
## Manually emit all signals | |
func emit_signals() -> void: | |
aim_position_changed.emit(global_position) | |
aim_direction_changed.emit(_last_aim_direction) | |
aim_speed_changed.emit(_velocity.length()) | |
# Private Methods | |
func _update_aim_physics(delta: float) -> void: | |
# Calculate direction and distance to target | |
var direction_to_target = _target_position - global_position | |
var distance_to_target = direction_to_target.length() | |
if distance_to_target > distance_threshold: | |
# Apply acceleration toward target | |
var acceleration_force = direction_to_target.normalized() * acceleration * delta | |
_velocity += acceleration_force | |
# Apply drag (higher drag = slower aim) | |
_velocity = _velocity.lerp(Vector2.ZERO, drag * delta) | |
# Clamp to max speed | |
if _velocity.length() > max_speed: | |
_velocity = _velocity.normalized() * max_speed | |
# Update position | |
global_position += _velocity * delta | |
# Ensure position stays within max distance from parent | |
var parent_pos = get_parent_global_position() | |
var current_distance = global_position.distance_to(parent_pos) | |
if current_distance > max_distance_from_parent: | |
var direction_to_parent = (parent_pos - global_position).normalized() | |
global_position = parent_pos - direction_to_parent * max_distance_from_parent | |
# Update aim direction (normalized vector from parent to aim) | |
_last_aim_direction = (global_position - parent_pos).normalized() | |
else: | |
# Apply gentle deceleration if close to target | |
_velocity = _velocity.lerp(Vector2.ZERO, drag * 2 * delta) | |
global_position += _velocity * delta | |
func _emit_aim_signals() -> void: | |
# Only emit signals when aim actually changed | |
if _velocity.length_squared() > 0.1: | |
aim_position_changed.emit(global_position) | |
aim_direction_changed.emit(_last_aim_direction) | |
aim_speed_changed.emit(_velocity.length()) | |
func get_parent_global_position() -> Vector2: | |
# Safely get parent position | |
var parent = get_parent() | |
if parent is Node2D: | |
return parent.global_position | |
return Vector2.ZERO |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment