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

[Dive Into AGMaker] Plugin Release - 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 action is JatkRotateToMovementDemo.
Its function is to work as a custom action inside the VS editor, rotating a specific Node2D every frame so that it points in the direction of actual movement displacement.

Unlike the earlier approach that only fit GameObject and relied on velocity, this version calculates direction from the position difference between frames, so both GameObject and Area2DGameObject can use the same logic.

Key Points

  • You only need to place JatkRotateToMovementDemo.gd anywhere you like inside your project.
  • In any VS editor, try adding an Execute Action, and you should be able to find this new action under the new JusAgmToolkit tab.
  • This action can rotate either the owner itself or a specific Node2D under the owner.
  • If rotate_target is left empty, it rotates the owner itself by default.
  • 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.
  • This action is intended for states whose owner is a GameObject or Area2DGameObject.

Core

Operation Steps

  1. Copy the JatkRotateToMovementDemo.gd script below and place it anywhere you like inside your project, then save it.
  2. Wait for AGMaker/Godot to refresh the script classes.
  3. Open any VS editor.
  4. Try adding an Execute Action.
  5. Under the new JusAgmToolkit tab, find the action named JatkRotateToMovementDemo.
  6. Add it to a suitable state, then configure rotate_target, rotation_mode, rotation_speed, and min_displacement_squared as needed.
  7. If you want to rotate the owner itself, leave rotate_target empty. If you want to rotate a child node, point rotate_target to that Node2D.

Parameter Explanation

  • rotate_target
    The Node2D path that should be rotated. If left empty, the owner itself is rotated.
  • 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.

Script

@tool
extends AGMPluginAction
class_name JatkRotateToMovementDemo

enum RotationMode {
	INSTANT,
	SMOOTH,
}

@export_group("Rotation Target")
@export_node_path("Node2D") var rotate_target: NodePath

@export_group("Rotation Settings")
@export_enum("Instant", "Smooth") var rotation_mode: int = RotationMode.SMOOTH:
	set(value):
		rotation_mode = value
		notify_property_list_changed()
@export_range(0.0, 100000.0, 0.1, "or_greater") var rotation_speed: float = 5.0
@export_range(0.0, 100000.0, 0.0001, "or_greater") var min_displacement_squared: float = 0.0001

var _owner_node_2d: Node2D
var _target_node_2d: Node2D
var _previous_global_position: Vector2 = Vector2.ZERO
var _is_active: bool = false


func get_plugin_tab_name() -> String:
	return "JusAgmToolkit"


func get_group_name() -> String:
	return "Demo"


func get_description() -> String:
	var locale: String = _get_editor_locale()
	if locale.begins_with("zh"):
		return "教学示例:根据 GameObject 或 Area2DGameObject 的位移方向旋转节点,不依赖 velocity。"
	if locale.begins_with("ja"):
		return "教材用サンプル: GameObject または Area2DGameObject の移動量の向きからノードを回転し、velocity には依存しません。"
	return "Teaching demo: rotates a node from the displacement direction of a GameObject or Area2DGameObject without relying on velocity."


func on_state_enter(p_owner: Object) -> void:
	if Engine.is_editor_hint():
		return

	_reset_runtime_state()
	_owner_node_2d = p_owner as Node2D
	if _owner_node_2d == null:
		push_error("[JatkRotateToMovementDemo] Owner must be a Node2D, GameObject, or Area2DGameObject.")
		return

	_target_node_2d = _resolve_rotate_target(_owner_node_2d)
	if _target_node_2d == null:
		push_error("[JatkRotateToMovementDemo] No valid Node2D rotate target was found. owner=%s configured_path=%s" % [
			_owner_node_2d.name,
			str(rotate_target),
		])
		_reset_runtime_state()
		return

	# 中文:进入状态时先记录当前位置,后续用“位置差”而不是 velocity 来推导朝向。
	# English: Snapshot the starting position so later updates can derive facing from displacement instead of velocity.
	# 日本語: state 開始時に現在位置を記録し、以後は velocity ではなく位置差から向きを求めます。
	_previous_global_position = _owner_node_2d.global_position
	_is_active = true


func on_state_update(_p_owner: Object, p_delta: float) -> void:
	if Engine.is_editor_hint() or not _is_active:
		return

	if _owner_node_2d == null or not is_instance_valid(_owner_node_2d):
		_reset_runtime_state()
		return

	if _target_node_2d == null or not is_instance_valid(_target_node_2d):
		_reset_runtime_state()
		return

	var current_global_position: Vector2 = _owner_node_2d.global_position
	var displacement: Vector2 = current_global_position - _previous_global_position
	_previous_global_position = current_global_position

	# 中文:这里只关心两帧之间真实发生了多少位移,因此 GameObject 和 Area2DGameObject 都能共用同一套逻辑。
	# English: We only care about the actual frame-to-frame displacement, so GameObject and Area2DGameObject share the same logic.
	# 日本語: ここではフレーム間の実際の移動量だけを見るため、GameObject と Area2DGameObject で同じロジックを共有できます。
	if displacement.length_squared() < min_displacement_squared:
		return

	_apply_rotation_from_displacement(displacement, p_delta)


func on_state_exit(_p_owner: Object) -> void:
	if Engine.is_editor_hint():
		return

	_reset_runtime_state()


func _validate_property(property: Dictionary) -> void:
	if property.name == "rotation_speed" and rotation_mode != RotationMode.SMOOTH:
		property.usage = PROPERTY_USAGE_NO_EDITOR


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

	match rotation_mode:
		RotationMode.INSTANT:
			_target_node_2d.global_rotation = target_angle
		RotationMode.SMOOTH:
			# 中文:平滑模式沿用旧 RotateToVelocity 的角度插值思路,只是输入方向改成位移方向。
			# English: Smooth mode keeps the old RotateToVelocity angular interpolation, but now feeds it with displacement direction.
			# 日本語: スムーズ回転は旧 RotateToVelocity の角度補間を流用しつつ、入力だけを移動量の向きへ置き換えています。
			var current_angle: float = _target_node_2d.global_rotation
			var angle_diff: float = wrapf(target_angle - current_angle, -PI, PI)
			_target_node_2d.global_rotation = current_angle + angle_diff * minf(1.0, maxf(rotation_speed, 0.0) * maxf(p_delta, 0.0))


func _resolve_rotate_target(owner_node_2d: Node2D) -> Node2D:
	if owner_node_2d == null:
		return null

	if rotate_target == NodePath():
		return owner_node_2d

	var target_node: Node = owner_node_2d.get_node_or_null(rotate_target)
	if target_node == null and owner_node_2d.owner != null:
		target_node = owner_node_2d.owner.get_node_or_null(rotate_target)

	return target_node as Node2D


func _reset_runtime_state() -> void:
	_owner_node_2d = null
	_target_node_2d = null
	_previous_global_position = Vector2.ZERO
	_is_active = false


func _get_editor_locale() -> String:
	if Engine.is_editor_hint() and ClassDB.class_exists("EditorInterface"):
		var editor_settings: EditorSettings = EditorInterface.get_editor_settings()
		if editor_settings:
			var editor_language: Variant = editor_settings.get_setting("interface/editor/editor_language")
			if editor_language is String and not editor_language.is_empty():
				return editor_language

	return TranslationServer.get_locale()

JatkRotateToMovementDemo.gd (5.5 KB)