[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.gdanywhere 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 newJusAgmToolkittab. - This action can rotate either the owner itself or a specific
Node2Dunder the owner. - If
rotate_targetis left empty, it rotates the owner itself by default. - It supports two rotation modes:
InstantandSmooth. rotation_speedis only used inSmoothmode.min_displacement_squaredcan be used to ignore very tiny displacement jitter.- This action is intended for states whose owner is a
GameObjectorArea2DGameObject.
Core
Operation Steps
- Copy the
JatkRotateToMovementDemo.gdscript below and place it anywhere you like inside your project, then save it. - Wait for AGMaker/Godot to refresh the script classes.
- Open any VS editor.
- Try adding an
Execute Action. - Under the new
JusAgmToolkittab, find the action namedJatkRotateToMovementDemo. - Add it to a suitable state, then configure
rotate_target,rotation_mode,rotation_speed, andmin_displacement_squaredas needed. - If you want to rotate the owner itself, leave
rotate_targetempty. If you want to rotate a child node, pointrotate_targetto thatNode2D.
Parameter Explanation
rotate_target
TheNode2Dpath that should be rotated. If left empty, the owner itself is rotated.rotation_mode
Instantaligns immediately to the current displacement direction.Smoothrotates toward it gradually.rotation_speed
Only used inSmoothmode. 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)