Skip to content

Instantly share code, notes, and snippets.

@viniciusemferreira
Created March 25, 2025 21:43
Show Gist options
  • Save viniciusemferreira/808d5703eb7b253e6ddba47b91b01a36 to your computer and use it in GitHub Desktop.
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.
@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