【Deep Dive into AGMaker】Plugin Node - Rotating a specific Node2D to face the direction of motion

Copyright Agreement

You are free to use or modify this plugin in Action Game Maker for any purpose, including creating both free and commercial projects.
However, please do not redistribute it. Attribution is not required, but appreciated.

Plugin Functionality

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

This is the node-based version of the previous JatkRotateToMovementDemo action concept. The action version is suitable for embedding behavior within a VS state, controlled by the state to start and stop. The node version is suitable for using the rotation behavior as a persistent component in the scene tree, just like a standard Godot node.

Since it calculates direction based on the difference in position between frames, it does not rely on velocity. As long as global_position changes over time, it can be used with GameObject, Area2DGameObject, or even standard Node2D.

Key Points

  • Simply place JatkRotateToMovement.gd anywhere in your project.
  • Add a standard Node to the scene, attach this script, and configure it in the inspector.
  • movement_source is the Node2D whose movement direction is being observed.
  • node_to_rotate is the Node2D that actually needs to be rotated.
  • If movement_source is left empty, the parent Node2D is used by default as the movement source.
  • If node_to_rotate is left empty, the parent Node2D is prioritized for rotation.
  • Therefore, you can place the component under a visual model node, point movement_source at the moving character, and leave node_to_rotate empty to rotate the model itself.
  • Two rotation modes are available: Instant and Smooth.
  • rotation_speed only takes effect in Smooth mode.
  • You can use min_displacement_squared to filter out very small jittery displacements.

Core

Steps

  1. Copy the entire JatkRotateToMovement.gd script below and save it anywhere in your project.
  2. Wait for AGMaker/Godot to refresh the script class.
  3. Select the object that is moving, or select the visual Node2D you want to rotate.
  4. Press Ctrl+A to add a standard Node, and attach JatkRotateToMovement.gd to this node.
  5. Configure movement_source, node_to_rotate, rotation_mode, rotation_speed, and min_displacement_squared as needed.
  6. If this component is attached directly under the same Node2D responsible for movement and rotation, you can leave both movement_source and node_to_rotate empty.
  7. If this component is attached under a visual model but needs to follow the movement direction of the character object, point movement_source at the character and leave node_to_rotate empty.

Parameter Description

  • movement_source
    The Node2D used to calculate the global position difference between two frames. Defaults to the parent Node2D if left empty.
  • node_to_rotate
    The Node2D that needs to be rotated. Prioritizes the parent Node2D if left empty.
  • rotation_mode
    Instant aligns immediately to the current displacement direction. Smooth rotates smoothly towards it.
  • rotation_speed
    Used only in Smooth mode. Higher values result in faster tracking of the displacement direction.
  • min_displacement_squared
    Minimum displacement squared threshold. Rotation only occurs if the squared displacement between two frames exceeds this value. Using the squared value avoids unnecessary square root calculations.

When to Use the Node Version

Use JatkRotateToMovement if this rotation behavior belongs to the scene object itself and should run continuously regardless of the current VS state.

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

The core concept of both versions is the same: derive orientation from actual displacement rather than relying on 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)