[深入 AGMaker] 插件动作 - 旋转特定 Node2D 以指向移动方向

许可协议

您可以自由地在 Action Game Maker 中出于任何目的使用或修改此插件,无论是创建免费项目还是商业项目。但是,请勿重新分发它。
署名并非必需,但我们会非常感激。

插件效果

此插件动作的名称为 JatkRotateToMovementDemo。
其功能是在 VS 编辑器中作为自定义动作运行,每一帧旋转特定的 Node2D,使其指向实际移动位移的方向。

与之前仅适配 GameObject 且依赖 velocity 的方法不同,此版本通过计算帧与帧之间的位置差来确定方向,因此 GameObject 和 Area2DGameObject 都可以使用相同的逻辑。

关键点

  • 您只需将 JatkRotateToMovementDemo.gd 放置在项目中的任何位置即可。
  • 在任何 VS 编辑器中,尝试添加一个 Execute Action,您应该能在新的 JusAgmToolkit 选项卡下找到此新动作。
  • 此动作可以旋转所有者本身,也可以旋转所有者下的特定 Node2D。
  • 如果 rotate_target 留空,则默认旋转所有者本身。
  • 它支持两种旋转模式:Instant(即时)和 Smooth(平滑)。
  • rotation_speed 仅在 Smooth 模式下使用。
  • min_displacement_squared 可用于忽略极微小的位移抖动。
  • 此动作旨在用于所有者为 GameObject 或 Area2DGameObject 的状态。

核心

操作步骤

  1. 复制下方的 JatkRotateToMovementDemo.gd 脚本,并将其放置在项目中的任何位置,然后保存。
  2. 等待 AGMaker/Godot 刷新脚本类。
  3. 打开任意 VS 编辑器。
  4. 尝试添加一个 Execute Action。
  5. 在新的 JusAgmToolkit 选项卡下,找到名为 JatkRotateToMovementDemo 的动作。
  6. 将其添加到合适的状态,然后根据需要配置 rotate_target、rotation_mode、rotation_speed 和 min_displacement_squared。
  7. 如果您想旋转所有者本身,请留空 rotate_target。如果您想旋转子节点,请将 rotate_target 指向该 Node2D。

参数说明

  • rotate_target
    应被旋转的 Node2D 路径。如果留空,则旋转所有者本身。
  • rotation_mode
    Instant 会立即对齐到当前的位移方向。Smooth 会逐渐旋转以对齐该方向。
  • rotation_speed
    仅在 Smooth 模式下使用。数值越大,旋转追赶速度越快。
  • min_displacement_squared
    最小平方位移阈值。仅当帧与帧之间的位移平方大于此值时才会发生旋转。使用平方值还可以避免不必要的平方根计算。

脚本

@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] 所有者必须是 Node2D、GameObject 或 Area2DGameObject。")
		return

	_target_node_2d = _resolve_rotate_target(_owner_node_2d)
	if _target_node_2d == null:
		push_error("[JatkRotateToMovementDemo] 未找到有效的 Node2D 旋转目标。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)