# \[AGMaker Dive In\] プラグインアクション：特定の Node2D を移動方向に向けて回転させる

**URL:** <https://guild.rpgmakerofficial.com/t/topic/1783>\
**Category:** モジュール/プラグイン共有\
**Tags:** gdscript, agmplugin\
**Created:** [2026 年 6 月 17 日午前 5:11 UTC](https://guild.rpgmakerofficial.com/t/topic/1783 "2026-06-17T05:11:02Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Justus](https://sea2.discourse-cdn.com/flex002/user_avatar/guild.rpgmakerofficial.com/justus/32/55_2.png) [@Justus](https://guild.rpgmakerofficial.com/u/Justus)\
**Post date:** [2026 年 6 月 17 日午前 5:11 UTC](https://guild.rpgmakerofficial.com/t/topic/1783/1 "2026-06-17T05:11:02Z")

</div>

## ライセンス契約

このプラグインは、Action Game Maker 内で無料プロジェクトまたは商用プロジェクトを問わず、あらゆる目的で自由に使用または改変することができます。ただし、再配布は行わないでください。  
クレジットの表記は必須ではありませんが、あれば幸いです。

## プラグインの効果

このプラグインアクションの名前は `JatkRotateToMovementDemo` です。  
その機能は、VS エディタ内でカスタムアクションとして動作し、特定の `Node2D` を毎フレーム回転させて、実際の移動変位の方向を向くようにすることです。

以前の手法は `GameObject` のみに適応し `velocity` に依存していましたが、このバージョンはフレーム間の位置差から方向を計算するため、`GameObject` と `Area2DGameObject` の両方で同じロジックを使用できます。

## 重要なポイント

- `JatkRotateToMovementDemo.gd` をプロジェクト内の任意の場所に配置するだけで十分です。
- 任意の VS エディタで「Execute Action」を追加すると、新しい `JusAgmToolkit` タブの下にこの新しいアクションが表示されます。
- このアクションは、オーナー自体を回転させるか、オーナー配下の特定の `Node2D` を回転させることができます。
- `rotate_target` を空のままにすると、デフォルトでオーナー自体が回転します。
- 2 つの回転モード（`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`  
最小の変位二乗しきい値です。フレーム間の変位二乗がこの値より大きい場合のみ回転が発生します。二乗値を使用することで、不要な平方根計算を回避できます。

### スクリプト

```python
@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

	# 日本語: 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 で同じロジックを共有できます。
	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 の角度補間を流用しつつ、入力だけを移動量の向きへ置き換えています。
			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](https://guild.rpgmakerofficial.com/uploads/short-url/o7TcUDx56mpDPXbjhcRac2AusU.gd) (5.5 KB)
