[Dive Into AGMaker] Plugin Node - Rotate Specific Node2D to Point in the Direction of Movement

License Agreement

You may freely use or modify this plugin in Action Game Maker for any purpose, whether for creating free or commercial projects. However, please do not redistribute it.
Credit is not necessary, but will be appreciated.

Plugin Effects

The name of this plugin node is JatkRotateToMovement.
Its function is to rotate a Node2D every physics frame so that it points in the direction of the actual movement displacement of a selected Node2D source.

This is the node version of the earlier JatkRotateToMovementDemo action idea. The action version is convenient when you want the behavior to live inside a VS state. This node version is convenient when you want the rotation behavior to stay in the scene tree as a reusable component, just like a normal Godot node.

Because it calculates direction from the position difference between frames, it does not depend on velocity. It can work with GameObject, Area2DGameObject, or any ordinary Node2D whose global_position changes over time.

Key Points

  • You only need to place JatkRotateToMovement.gd anywhere you like inside your project.
  • Add a normal Node in your scene, attach this script to it, and configure it in the Inspector.
  • movement_source is the Node2D whose movement should be observed.
  • node_to_rotate is the Node2D that should be rotated.
  • If movement_source is left empty, the node uses its parent Node2D as the movement source.
  • If node_to_rotate is left empty, the node first tries to rotate its parent Node2D.
  • This means you can put the component under a visual model, set movement_source to the moving character, and leave node_to_rotate empty to rotate the model itself.
  • It supports two rotation modes: Instant and Smooth.
  • rotation_speed is only used in Smooth mode.
  • min_displacement_squared can be used to ignore very tiny displacement jitter.

Core

Operation Steps

  1. Copy the JatkRotateToMovement.gd script below and place it anywhere you like inside your project, then save it.
  2. Wait for AGMaker/Godot to refresh the script class.
  3. Select the moving object or the visual Node2D you want to rotate.
  4. Press Ctrl+A to add a normal Node, then attach JatkRotateToMovement.gd to it.
  5. Configure movement_source, node_to_rotate, rotation_mode, rotation_speed, and min_displacement_squared as needed.
  6. If the component is a child of the same Node2D that should provide movement and be rotated, you can leave both movement_source and node_to_rotate empty.
  7. If the component is under a visual model but should follow the movement of a character object, set movement_source to the character and leave node_to_rotate empty.

Parameter Explanation

  • movement_source
    The Node2D whose frame-to-frame global position difference is used as the movement direction. If left empty, the parent Node2D is used.
  • node_to_rotate
    The Node2D that should be rotated. If left empty, the parent Node2D is used first.
  • rotation_mode
    Instant aligns immediately to the current displacement direction. Smooth rotates toward it gradually.
  • rotation_speed
    Only used in Smooth mode. Larger values make the rotation catch up faster.
  • min_displacement_squared
    The minimum squared displacement threshold. Rotation only happens when the frame-to-frame displacement squared is larger than this value. Using a squared value also avoids unnecessary square root work.

When to Use the Node Version

Use JatkRotateToMovement when the rotation behavior belongs to the scene object itself and should keep running regardless of which VS state is active.

Use JatkRotateToMovementDemo when the rotation behavior should be started and stopped by a specific VS state as a custom action.

Both versions are based on the same core idea: derive facing direction from actual displacement, not from velocity.

Script

class_name JatkRotateToMovement extends Node

enum RotationMode {
	INSTANT,
	SMOOTH,
}

@export var node_to_rotate: Node2D
@export var movement_source: Node2D
@export var rotation_mode: RotationMode = RotationMode.SMOOTH
@export var rotation_speed: float = 5.0
@export var min_displacement_squared: float = 0.0001

var _resolved_node_to_rotate: Node2D
var _resolved_movement_source: Node2D
var _previous_global_position: Vector2 = Vector2.ZERO
var _has_previous_global_position: bool = false


func _ready() -> void:
	process_physics_priority = 100
	_resolve_runtime_nodes()


func _physics_process(delta: float) -> void:
	if _resolved_movement_source == null or not is_instance_valid(_resolved_movement_source):
		_resolve_runtime_nodes()
		return

	if _resolved_node_to_rotate == null or not is_instance_valid(_resolved_node_to_rotate):
		_resolved_node_to_rotate = _resolve_node_to_rotate()
		if _resolved_node_to_rotate == null:
			return

	var current_global_position: Vector2 = _resolved_movement_source.global_position
	if not _has_previous_global_position:
		_previous_global_position = current_global_position
		_has_previous_global_position = true
		return

	var displacement: Vector2 = current_global_position - _previous_global_position
	_previous_global_position = current_global_position

	if displacement.length_squared() < min_displacement_squared:
		return

	_apply_rotation_from_displacement(displacement, delta)


func _resolve_runtime_nodes() -> void:
	_resolved_movement_source = _resolve_movement_source()
	_resolved_node_to_rotate = _resolve_node_to_rotate()
	_has_previous_global_position = false

	if _resolved_movement_source == null:
		printerr("[JatkRotateToMovement] movement_source is not set and parent is not a Node2D")
		return

	if _resolved_node_to_rotate == null:
		printerr("[JatkRotateToMovement] node_to_rotate is not set and no fallback Node2D is available")
		return

	_previous_global_position = _resolved_movement_source.global_position
	_has_previous_global_position = true


func _resolve_movement_source() -> Node2D:
	if movement_source != null:
		return movement_source

	return get_parent() as Node2D


func _resolve_node_to_rotate() -> Node2D:
	if node_to_rotate != null:
		return node_to_rotate

	var parent_node_2d: Node2D = get_parent() as Node2D
	if parent_node_2d != null:
		return parent_node_2d

	if _resolved_movement_source != null:
		return _resolved_movement_source

	return _resolve_movement_source()


func _apply_rotation_from_displacement(displacement: Vector2, delta: float) -> void:
	var target_angle: float = displacement.angle()

	match rotation_mode:
		RotationMode.INSTANT:
			_resolved_node_to_rotate.global_rotation = target_angle
		RotationMode.SMOOTH:
			var current_angle: float = _resolved_node_to_rotate.global_rotation
			var angle_diff: float = wrapf(target_angle - current_angle, -PI, PI)
			var rotation_weight: float = minf(1.0, maxf(rotation_speed, 0.0) * maxf(delta, 0.0))
			_resolved_node_to_rotate.global_rotation = current_angle + angle_diff * rotation_weight

JatkRotateToMovement.gd (3.0 KB)

1 Like