[Deep Dive into AGMaker] Plugin Action - Rotate a Specific Node2D to Face the Direction of Movement
License Agreement
You are free to use or modify this plugin for any purpose within Action Game Maker, for creating either free or commercial projects.
However, please do not redistribute it. Attribution is not required but is appreciated.
Plugin Effect
The name of this plugin action is JatkRotateToMovementDemo.
Its function is to serve as a custom action in the VS editor, causing a specified Node2D to rotate every frame so that it points in the direction of the object’s actual displacement.
Unlike previous versions that only worked with GameObject and relied on velocity, this version calculates direction based on the position difference between consecutive frames. Therefore, both GameObject and Area2DGameObject can use the same logic.
Key Points
- Simply place
JatkRotateToMovementDemo.gdanywhere in your project. - In any VS editor, try adding an “Execute Action”; you should see this new action under the new
JusAgmToolkittab. - This action can rotate the owner itself or a specific
Node2Dunder the owner. - If
rotate_targetis left blank, it defaults to rotating the owner itself. - Two rotation modes are available:
InstantandSmooth. rotation_speedonly takes effect inSmoothmode.min_displacement_squaredcan be used to filter out very small jitter movements.- This action is suitable for use in states where the owner is a
GameObjectorArea2DGameObject.
Core
Steps
- Copy the entire
JatkRotateToMovementDemo.gdscript below and place it anywhere in your project, then save. - Wait for AGMaker/Godot to refresh the script classes.
- Open any VS editor.
- Try adding an “Execute Action”.
- Under the newly appeared
JusAgmToolkittab, find the action namedJatkRotateToMovementDemo. - Add it to the appropriate state, then configure
rotate_target,rotation_mode,rotation_speed, andmin_displacement_squaredas needed. - If you want to rotate the owner itself, leave
rotate_targetblank. If you want to rotate a child node, setrotate_targetto point to thatNode2D.
Parameter Description
rotate_target
The path to theNode2Dthat needs to be rotated. If left blank, the owner itself is rotated by default.rotation_mode
Instantimmediately aligns to the current displacement direction.Smoothrotates smoothly towards it.rotation_speed
Used only inSmoothmode. The higher the value, the faster the rotation follows 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 also avoids unnecessary square root calculations.
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("en"):
return "Teaching demo: rotates a node from the displacement direction of a GameObject or Area2DGameObject without relying on velocity."
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
# English: Snapshot the starting position so later updates can derive facing from displacement instead of 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
# English: We only care about the actual frame-to-frame displacement, so GameObject and Area2DGameObject share the same logic.
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:
# English: Smooth mode keeps the old RotateToVelocity angular interpolation, but now feeds it with displacement direction.
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)