【深入AGMaker】插件节点-旋转特定Node2D使其指向运动方向

版权协议

你可以免费地在Action Game Maker中以任何目的使用或修改该插件,用于制作任何免费或是商用的项目。
但是请不要再分发它。署名不是必须的,但会受到感谢。

插件效果

这个插件节点的名字为JatkRotateToMovement。
它的作用是每个物理帧旋转一个Node2D,使其指向某个Node2D来源对象真实发生位移的方向。

这是之前JatkRotateToMovementDemo动作思路的节点版本。动作版本适合把行为放进某个VS状态里,由状态控制开始和结束。节点版本则适合把旋转行为作为场景树里的常驻组件使用,就像普通Godot节点一样。

因为它根据前后帧的位置差计算方向,所以不依赖velocity。只要global_position会随时间变化,它就可以用于GameObject、Area2DGameObject,也可以用于普通Node2D。

要点

  • 只需要把JatkRotateToMovement.gd放在项目中的任何位置
  • 在场景中添加一个普通Node,把这个脚本挂上去,然后在检查器里配置即可
  • movement_source是被观察运动方向的Node2D
  • node_to_rotate是真正需要被旋转的Node2D
  • 如果不填写movement_source,默认使用父级Node2D作为运动来源
  • 如果不填写node_to_rotate,会优先旋转父级Node2D
  • 因此你可以把组件放在视觉模型节点下面,把movement_source指向正在移动的角色,然后让node_to_rotate留空,用它来旋转模型本身
  • 可以使用Instant和Smooth两种旋转模式
  • rotation_speed只在Smooth模式下生效
  • 可以使用min_displacement_squared来过滤掉非常小的抖动位移

核心

操作步骤

  1. 把下方的JatkRotateToMovement.gd脚本整个复制出来,放到你项目中的任何位置,保存。
  2. 等待AGMaker/Godot刷新脚本类。
  3. 选中正在移动的对象,或者选中你想旋转的视觉Node2D。
  4. 按Ctrl+A添加一个普通Node,然后把JatkRotateToMovement.gd挂到这个节点上。
  5. 按需配置movement_source、node_to_rotate、rotation_mode、rotation_speed和min_displacement_squared。
  6. 如果这个组件就是挂在同一个负责移动且需要旋转的Node2D下面,可以让movement_source和node_to_rotate都留空。
  7. 如果这个组件挂在视觉模型下面,但要跟随角色对象的移动方向,就把movement_source指向角色,并让node_to_rotate留空。

参数说明

  • movement_source
    用来计算两帧之间全局位置差的Node2D。留空时默认使用父级Node2D。
  • node_to_rotate
    需要旋转的Node2D。留空时优先使用父级Node2D。
  • rotation_mode
    Instant会立即对齐到当前位移方向。Smooth会平滑地旋转过去。
  • rotation_speed
    只在Smooth模式下使用。数值越大,追随位移方向的速度越快。
  • min_displacement_squared
    最小位移平方阈值。只有当两帧之间的位移平方大于这个值时才会发生旋转。用平方值的方式也可以避免多余的开方计算。

什么时候使用节点版本

如果这个旋转行为属于场景对象自身,并且应该不管当前VS状态是什么都持续运行,就使用JatkRotateToMovement。

如果这个旋转行为应该由某个具体VS状态开始和结束,就使用自定义动作JatkRotateToMovementDemo。

两个版本的核心思路是一样的:根据真实位移推导朝向,而不是依赖velocity。

脚本

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)