AnimationTree 스테이트 머신
Godot 4
스테이트 머신, 트랜지션, 블렌드 트리, OneShot, travel() 대 조건 비교의 완벽 가이드 — 실제 GDScript 코드 예제와 함께.
목차
1. 소개
AnimationTree는 복잡한 애니메이션 블렌딩과 상태 전환을 위한 Godot 4의 시스템입니다. 코드 곳곳에 흩어진 AnimationPlayer.play() 호출로 여러 애니메이션을 관리하려고 시도해 본 적이 있다면, 그것이 얼마나 빠르게 감당할 수 없게 되는지 잘 아실 겁니다. AnimationTree는 상태와 트랜지션으로 이루어진 시각적 그래프로 이 문제를 해결합니다.
AnimationTree는 놀랍도록 강력하지만, 공식 문서는 빈약해서 개발자들이 감으로 헤매게 되는 경우가 많습니다. 이 가이드는 기본 설정부터 고급 블렌드 트리까지, 프로젝트에 그대로 복사해 넣을 수 있는 실제 GDScript 코드와 함께 모든 것을 차근차근 설명합니다.
스테이트 머신, 트랜지션, 블렌드 스페이스(1D & 2D), 공격용 OneShot, travel() 대 조건 비교, 그리고 완전한 캐릭터 컨트롤러 패턴.
2. 사전 준비
AnimationTree를 설정하기 전에 다음이 필요합니다:
- Idle, Walk, Run, Jump 애니메이션이 최소한 이미 만들어져 있는
AnimationPlayer노드 - AnimationPlayer는 AnimationTree를 추가하는 노드의 형제 또는 자식이어야 합니다(보통 둘 다 캐릭터 루트 노드의 자식입니다)
- Godot 4.x (이 가이드는 Godot 4 API를 사용합니다 — AnimationTree API는 Godot 3에서 크게 바뀌었습니다)
Godot 4에서는 AnimationTree.animation_player가 AnimationTree.anim_player로 대체되었습니다. 재생 파라미터 경로도 바뀌었습니다. Godot 3에서 마이그레이션하는 경우 공식 마이그레이션 가이드를 확인하세요.
3. 기본 설정
AnimationTree 설정은 네 단계로 이루어집니다:
-
AnimationTree 노드 추가
— AnimationPlayer 옆에, 캐릭터(예:
CharacterBody2D또는CharacterBody3D)의 자식으로 추가합니다. -
anim_player설정 — 인스펙터에서 "Anim Player" 속성이 AnimationPlayer 노드를 가리키도록 지정합니다. -
tree_root설정 — 인스펙터에서 "Tree Root" 속성을 클릭하고 새AnimationNodeStateMachine을 생성합니다. -
active = true설정 — 인스펙터에서 "Active" 체크박스를 켜거나 코드에서 설정합니다.
씬 트리는 다음과 같은 모습이어야 합니다:
CharacterBody2D (or CharacterBody3D)
+-- Sprite2D (or Sprite3D)
+-- CollisionShape2D
+-- AnimationPlayer <-- has Idle, Walk, Run, Jump animations
+-- AnimationTree <-- points to AnimationPlayer above
GDScript
# Minimal code setup (usually done via the editor instead):
@onready var anim_tree: AnimationTree = $AnimationTree
func _ready() -> void:
# If you set these in the Inspector, you don't need this code
anim_tree.anim_player = ^"../AnimationPlayer"
anim_tree.tree_root = AnimationNodeStateMachine.new()
anim_tree.active = true
실제로는 거의 항상 에디터에서 AnimationTree를 구성합니다. 위 코드는 완전성을 위해 보여드리는 것이며, 보통 스크립트에서는 anim_tree.active = true만 있으면 충분합니다(이마저도 에디터에서 설정할 수 있습니다).
4. 스테이트 머신 기초
AnimationTree의 스테이트 머신은 단순한 개념으로 동작합니다: 상태(state)는 애니메이션을 나타내고, 트랜지션은 상태 사이를 전환하는 조건을 정의합니다.
상태 추가하기
AnimationTree의 tree_root가 AnimationNodeStateMachine으로 설정되면, 인스펙터에서 더블 클릭해 스테이트 머신 에디터를 엽니다:
- 그래프 영역에서 마우스 오른쪽 버튼을 클릭하고 Add Animation을 선택합니다
- AnimationPlayer에서 애니메이션을 선택합니다(Idle, Walk, Run, Jump 등)
- 필요한 각 애니메이션 상태에 대해 반복합니다
Start는 진입점입니다 — 첫 번째 트랜지션은 항상 여기에서 시작합니다. End는 선택 사항이며 스테이트 머신이 완료되었음을 알립니다(중첩 스테이트 머신에서 유용합니다).
트랜지션 추가하기
두 상태 사이에 트랜지션을 만들려면:
- 출발 상태 노드를 클릭합니다
- 목적지 상태로 드래그하여 트랜지션 화살표를 만듭니다
- 트랜지션 화살표를 클릭해 인스펙터에서 구성합니다
트랜지션 속성
| 속성 | 설명 |
|---|---|
advance_mode |
Auto — 조건이 참일 때 발동합니다. Enabled — travel()에 항상 사용 가능합니다. Disabled — 차단됩니다. |
advance_condition |
불리언 파라미터의 이름(예: is_moving). 참일 때 트랜지션이 자동으로 발동합니다. |
xfade_time |
크로스페이드 지속 시간(초). 애니메이션 사이를 부드럽게 블렌딩합니다. 일반적으로 0.1 – 0.3초. |
switch_mode |
Immediate — 즉시 전환합니다. Sync — 재생 위치를 맞춥니다. AtEnd — 현재 애니메이션이 끝날 때까지 기다립니다. |
플랫포머 캐릭터를 위한 전형적인 설정:
Start --> Idle
Idle --> Walk (condition: is_moving)
Walk --> Idle (condition: is_idle)
Idle --> Jump (condition: is_jumping)
Walk --> Jump (condition: is_jumping)
Jump --> Fall (condition: is_falling)
Fall --> Idle (condition: is_on_floor, switch_mode: Immediate)
5. 코드에서 스테이트 머신 제어하기
스테이트 머신을 구동하는 주요 방법은 두 가지입니다: 조건 파라미터 설정(자동 트랜지션)과 travel() 호출(수동 트랜지션). 두 방식을 함께 섞어 사용할 수도 있습니다.
extends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback = anim_tree.get("parameters/playback")
func _physics_process(delta: float) -> void:
# ... movement logic here ...
move_and_slide()
_update_animation_parameters()
func _update_animation_parameters() -> void:
# Approach 1: Set condition parameters — transitions fire automatically
anim_tree.set("parameters/conditions/is_moving", velocity.length() > 10.0)
anim_tree.set("parameters/conditions/is_idle", velocity.length() <= 10.0)
anim_tree.set("parameters/conditions/is_on_floor", is_on_floor())
anim_tree.set("parameters/conditions/is_jumping", velocity.y < 0 and not is_on_floor())
anim_tree.set("parameters/conditions/is_falling", velocity.y > 0 and not is_on_floor())
# Approach 2: Use travel() for direct control
# if velocity.length() > 10.0:
# state_machine.travel("Walk")
# else:
# state_machine.travel("Idle")
트랜지션에 설정된 조건의 경우 파라미터는 parameters/conditions/<condition_name> 패턴을 따릅니다. 조건 이름은 에디터에서 트랜지션에 설정한 advance_condition과 정확히 일치해야 합니다.
6. travel() 대 조건
조건 기반 (이동에 권장)
매 프레임 불리언 파라미터를 설정하고 트랜지션이 자동으로 발동하도록 두면 됩니다. 이 방식은 더 선언적이며 코드를 깔끔하게 유지합니다. 스테이트 머신이 트랜지션 로직, 크로스페이드, 예외 상황을 대신 처리해 줍니다.
# Declarative: just describe the current state of the world
anim_tree.set("parameters/conditions/is_moving", velocity.length() > 10.0)
anim_tree.set("parameters/conditions/is_on_floor", is_on_floor())
travel() (일회성 동작에 권장)
travel()은 상태 전환을 요청합니다. 트랜지션 규칙을 준수하므로 — 현재 상태에서 목표까지 유효한 경로가 없으면 호출은 무시됩니다. 덕분에 반복적으로 호출해도 안전합니다. 공격, 이모트, 컷신 애니메이션 같은 일회성 트리거에 사용하세요.
# travel() — requests a transition (respects transition rules)
state_machine.travel("Jump")
# Get current state name
var current: StringName = state_machine.get_current_node()
print(current) # "Idle", "Walk", etc.
# Check if travel is possible
var is_playing: bool = state_machine.is_playing()
print(is_playing)
조건은 연속적인 상태(대기, 걷기, 달리기, 낙하)에. travel()은 이벤트로 유발되는 상태(공격, 회피, 상호작용)에. 많은 프로젝트가 둘 다 사용합니다: 이동에는 조건, 전투 동작에는 travel().
7. 블렌드 트리
블렌드 트리를 사용하면 이산적인 상태 사이를 딱딱하게 전환하는 대신, 연속적인 값을 기준으로 여러 애니메이션 사이를 부드럽게 보간할 수 있습니다. 걷기/달리기 속도 블렌딩과 방향성 이동에 안성맞춤입니다.
BlendSpace1D
단일 축을 따라 두 개 이상의 애니메이션 사이를 1D로 블렌딩합니다. 흔한 사용 예: 이동 속도를 기준으로 Walk와 Run을 블렌딩.
에디터에서 스테이트 머신 안에(또는 독립적인 트리 루트로) BlendSpace1D 노드를 만듭니다. 애니메이션 포인트를 추가합니다:
# BlendSpace1D setup (in editor):
# Point 0.0 = Walk animation
# Point 1.0 = Run animation
# Control from code:
var speed_factor: float = clamp(velocity.length() / max_speed, 0.0, 1.0)
anim_tree.set("parameters/WalkRun/blend_position", speed_factor)
BlendSpace2D
두 축을 사용하는 2D 블렌드. 8방향 이동이나 캐릭터가 어느 방향으로든 움직일 수 있는 탑다운 게임에 완벽합니다.
# BlendSpace2D setup (in editor):
# Place animations at positions:
# Idle at (0, 0)
# WalkRight at (1, 0), WalkLeft at (-1, 0)
# WalkUp at (0, -1), WalkDown at (0, 1)
# Diagonals at corners
# Control from code:
var input_dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
anim_tree.set("parameters/Movement/blend_position", input_dir)
BlendSpace2D는 여러 블렌드 모드를 지원합니다: 기본 삼각 분할(triangulation)은 대부분의 경우 잘 동작합니다. 보간 없는 픽셀 아트 스타일 애니메이션을 원한다면 이산 모드(가장 가까운 포인트로 스냅)를 선택할 수도 있습니다.
8. 자주 쓰는 노드 타입
AnimationTree는 여러 노드 타입을 지원하며, 이들을 조합해 복잡한 애니메이션 동작을 만들 수 있습니다:
| 노드 타입 | 사용 사례 |
|---|---|
AnimationNodeStateMachine |
트랜지션이 있는 스테이트 머신. 가장 흔한 루트 노드. 상태는 애니메이션이거나 중첩된 스테이트 머신일 수 있습니다. |
AnimationNodeBlendSpace1D |
한 축을 따르는 1D 블렌드. 걷기/달리기 속도, 조준 각도 등. |
AnimationNodeBlendSpace2D |
두 축을 사용하는 2D 블렌드. 방향성 이동, 스트레이프 블렌딩. |
AnimationNodeBlendTree |
블렌드 연산의 그래프. 여러 블렌드 노드를 사용자 정의 로직으로 조합합니다. |
AnimationNodeAdd2 |
가산 블렌딩. 한 애니메이션 위에 다른 애니메이션을 겹칩니다(예: 걷기 위에 조준 오프셋). |
AnimationNodeTimeScale |
속도 제어. 런타임에 애니메이션을 더 빠르거나 느리게 재생합니다. |
AnimationNodeOneShot |
일회성 애니메이션 오버레이. 공격, 이모트, 피격 반응에 완벽합니다. |
AnimationNodeTransition |
크로스페이드로 여러 입력 사이를 전환합니다. 더 단순한 설정에서 스테이트 머신의 대안입니다. |
9. OneShot 패턴 (공격, 이모트)
OneShot 노드는 AnimationTree에서 가장 유용한 패턴 중 하나입니다. 기본 애니메이션 위에 일회성 애니메이션을 재생하고(예: 걷는 도중 공격 스윙 재생), 이후 자동으로 기본 애니메이션으로 되돌아갑니다.
BlendTree에서 설정
OneShot을 사용하려면 AnimationTree의 루트(또는 그 안의 상태)가 BlendTree여야 합니다:
# BlendTree graph setup:
#
# [StateMachine] ---> [OneShot "AttackOneShot"] ---> [Output]
# (base locomotion) ^
# |
# [Animation "Attack"]
# (shot input)
#
# The StateMachine provides the base (idle/walk/run).
# The Attack animation is connected to the OneShot's "shot" input.
코드에서 트리거하기
GDScriptextends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
func _input(event: InputEvent) -> void:
if event.is_action_pressed("attack"):
_play_attack()
func _play_attack() -> void:
# Fire the one-shot animation
anim_tree.set(
"parameters/AttackOneShot/request",
AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
)
func _process(delta: float) -> void:
# Check if the one-shot is currently active
var is_attacking: bool = anim_tree.get("parameters/AttackOneShot/active")
if is_attacking:
# Optionally disable movement during attack
pass
OneShot 리퀘스트 상수
| 상수 | 효과 |
|---|---|
ONE_SHOT_REQUEST_FIRE |
OneShot 애니메이션 재생 시작 |
ONE_SHOT_REQUEST_ABORT |
OneShot을 취소하고 즉시 기본으로 되돌아감 |
ONE_SHOT_REQUEST_FADE_OUT |
OneShot을 페이드 아웃(fadeout_time 속성 사용) |
콤보 시스템의 경우, 여러 OneShot 노드를 순차적으로 사용하거나 OneShot의 shot 입력 안에 Attack1 → Attack2 → Attack3 상태를 가진 중첩 스테이트 머신을 사용하세요.
10. 실전 예제: 완전한 캐릭터 컨트롤러
다음은 스테이트 머신 이동과 OneShot 공격을 결합한 완전한 2D 플랫포머 캐릭터 스크립트입니다. 여러분의 프로젝트에 맞게 응용할 수 있는 프로덕션 수준의 패턴입니다.
GDScript — player.gdextends CharacterBody2D
const SPEED := 200.0
const JUMP_VELOCITY := -350.0
const SPRINT_MULTIPLIER := 1.6
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var playback: AnimationNodeStateMachinePlayback = anim_tree.get("parameters/playback")
@onready var sprite: Sprite2D = $Sprite2D
var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")
func _ready() -> void:
anim_tree.active = true
func _physics_process(delta: float) -> void:
_apply_gravity(delta)
_handle_jump()
_handle_movement()
move_and_slide()
_update_animation()
func _apply_gravity(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
func _handle_jump() -> void:
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
func _handle_movement() -> void:
var direction := Input.get_axis("move_left", "move_right")
var is_sprinting := Input.is_action_pressed("sprint")
var current_speed := SPEED * (SPRINT_MULTIPLIER if is_sprinting else 1.0)
if direction != 0.0:
velocity.x = direction * current_speed
sprite.flip_h = direction < 0.0
else:
velocity.x = move_toward(velocity.x, 0.0, SPEED)
func _update_animation() -> void:
# Skip animation updates during attack
var is_attacking: bool = anim_tree.get("parameters/AttackOneShot/active")
if is_attacking:
return
if not is_on_floor():
if velocity.y < 0:
playback.travel("Jump")
else:
playback.travel("Fall")
elif abs(velocity.x) > 10.0:
if Input.is_action_pressed("sprint"):
playback.travel("Run")
else:
playback.travel("Walk")
else:
playback.travel("Idle")
func _input(event: InputEvent) -> void:
if event.is_action_pressed("attack") and is_on_floor():
anim_tree.set(
"parameters/AttackOneShot/request",
AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
)
Root = BlendTree. 내부: StateMachine 노드(Idle/Walk/Run/Jump/Fall 상태)가 OneShot 노드("AttackOneShot")에 연결되고, 이것이 다시 Output에 연결됩니다. 공격 애니메이션은 OneShot의 "shot" 입력에 연결됩니다.
11. 문제 해결
AnimationTree에 active = true가 설정되어 있고 anim_player 속성이 유효한 AnimationPlayer를 가리키는지 확인하세요. 또한 참조하는 이름의 애니메이션이 AnimationPlayer에 실제로 존재하는지 확인하세요.
현재 상태와 목표 상태 사이에 유효한 트랜지션 경로가 존재하는지 확인하세요. 경로가 없으면 travel()은 조용히 실패합니다. state_machine.get_current_node()로 실제로 어떤 상태에 있는지 디버깅하세요.
blend_position 값이 블렌드 스페이스 포인트 범위 안에 들어가는지 확인하세요. 포인트가 0.0과 1.0에 있다면 5.0 값은 기대대로 동작하지 않습니다. clamp()를 사용하세요.
에디터(인스펙터 체크박스)나 _ready() 함수에서 active = true를 설정하세요. AnimationTree는 활성화되기 전까지 아무것도 하지 않습니다.
다음을 다시 한 번 확인하세요: (1) 트랜지션의 advance_mode가 Auto로 설정되어 있는지, (2) advance_condition 이름이 코드에서 설정한 것과 정확히 일치하는지(대소문자 구분), (3) _physics_process()에서 매 프레임 파라미터를 설정하고 있는지.
AnimationTree는 애니메이션 재생만 처리합니다. 이동 로직(velocity, move_and_slide())은 별개이며 스크립트의 _physics_process()에서 구현해야 합니다.
AI가 AnimationTree를 만들게 하고 싶으신가요?
Godot MCP Pro는 스테이트 머신 생성, 상태와 트랜지션 추가, 블렌드 트리 구성, 파라미터 설정을 — 모두 단 하나의 프롬프트로 처리할 수 있습니다. 원하는 애니메이션 동작을 AI 어시스턴트에게 말하기만 하면, 전체 AnimationTree를 대신 구축해 줍니다.
- create_animation_tree
- add_state_machine_state
- add_state_machine_transition
- set_blend_tree_node
- set_tree_parameter
- get_animation_tree_structure