입력에 따른 애니메이션
[Unit] Player character and movement > [Tutorial] Create Input Actions for player character movement
🔎 적 애니메이션 설정
-
적(Enemy) 애니메이터 설정
- 계층 구조(Hierarchy)에서 ‘Enemy’ 선택.
- 인스펙터(Inspector) 맨 아래 [Add Component] 클릭 → Animator 검색해서 추가.
- 프로젝트(Project) 창 빈 곳에 마우스 우클릭 → Create > Animator Controller 클릭 (이름: Enemy).
- 만든 Enemy 파일을 아까 만든 Animator 컴포넌트의 Controller 칸으로 드래그해서 넣기.
-
블렌드 트리(Blend Tree) 만들기
- 상단 메뉴 Window > Animation > Animator 클릭 (창 열기).
- 애니메이터 창 빈 공간에 마우스 우클릭 > Create State > From New Blend Tree 클릭.
- 생성된 주황색 ‘Blend Tree’ 노드를 더블 클릭해서 안으로 입장.
- 오른쪽 인스펙터에서 Blend Type을 2D Simple Directional로 변경.
- Parameters 탭(왼쪽 상단)에서 [+] 버튼 눌러서 Move X, Move Y (Float) 두 개 만들기.
- 인스펙터 중간의 Parameters 칸에서 첫 번째는 Move X, 두 번째는 Move Y 선택.
## ❓블렌드 트리가 무엇인가요?
-
핵심 작동 원리
블렌드 트리는 우리가 설정한 좌표(Pos X, Pos Y)와 실제 게임의 입력값을 비교합니다.
- 좌표 설정: EnemyLeft는 (-1, 0), EnemyUp은 (0, 1) 식으로 위치를 지정합니다.
- 혼합(Blending): 입력값이 (-0.7, 0.7)처럼 대각선 방향이라면, 유니티는 왼쪽 걷기와 위쪽 걷기 애니메이션을 적절한 비율로 섞어서 보여줍니다.
-
블렌드 트리의 종류
가장 자주 쓰이는 것은 다음 두 가지입니다.
- 1D Blend: 변수 하나만 사용 (예: 이동 속도에 따라 대기 → 걷기 → 달리기)
- 2D Simple Directional: 변수 두 개 사용 (예: 상하좌우 이동 방향에 따른 걷기)
-
애니메이션 클립 넣기 (Motion)
- 인스펙터의 Motion 리스트 아래에 있는 [+] 버튼을 4번 눌러서 칸 만들기.
- 각 칸의 ⊙(동그라미 버튼)을 눌러서 EnemyLeft, EnemyRight, EnemyUp, EnemyDown을 하나씩 선택.
- 그 옆의 Pos X, Pos Y 숫자를 아래처럼 직접 타이핑해서 입력:
Left: -0.5, 0 / Right: 0.5, 0 / Down: 0, -0.5 / Up: 0, 0.5
- 적 스크립트
using UnityEngine;
public class EnemyController : MonoBehaviour
{
// Public variables
public float speed;
public bool vertical;
public float changeTime = 3.0f;
// Private variables
Rigidbody2D rigidbody2d;
Animator animator;
float timer;
int direction = 1;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
timer = changeTime;
}
// Update is called every frame
void Update()
{
timer -= Time.deltaTime;
if (timer < 0)
{
direction = -direction;
timer = changeTime;
}
}
// FixedUpdate has the same call rate as the physics system
void FixedUpdate()
{
Vector2 position = rigidbody2d.position;
if (vertical)
{
position.y = position.y + speed * direction * Time.deltaTime;
animator.SetFloat("Move X", 0); // "Move X"라는 변수 값을 0으로 만듦
animator.SetFloat("Move Y", direction);
}
else
{
position.x = position.x + speed * direction * Time.deltaTime;
animator.SetFloat("Move X", direction);
animator.SetFloat("Move Y", 0);
}
rigidbody2d.MovePosition(position);
}
void OnTriggerEnter2D(Collider2D other)
{
PlayerController player = other.gameObject.GetComponent<PlayerController>();
if (player != null)
{
player.ChangeHealth(-1);
}
}
}
🔎 플레이어(Player) 설정 (State Machine)
- Player 프리팹 열기.
- 인스펙터에서 Animator 추가하고, 제공된 Player 컨트롤러 연결.
- 애니메이터 창에서 ‘Moving’에서 ‘Idle’로 가는 화살표 클릭.
- 인스펙터에서 Has Exit Time 체크 해제 (즉시 멈추게 하기).
-
그 아래 Conditions의 [+] 버튼 클릭 → Speed / Less / 0.1로 설정.
Has Exit Time (비활성화): “움직임이 멈추면(조건 충족) 현재 걷기 애니메이션이 끝날 때까지 기다리지 말고 즉시 대기(Idle) 상태로 넘어가라”는 뜻입니다. 조작감을 빠릿하게 만듭니다.
Trigger (Hit, Launch): Float처럼 숫자가 아니라, “방금 맞았어!” 하고 스위치를 한 번 딸깍 누르는 것과 같습니다. 일회성 동작에 사용합니다.
플레이어 스크립트
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
// Variables related to player character movement
public InputAction MoveAction;
Rigidbody2D rigidbody2d;
Vector2 move;
public float speed = 3.0f;
// Variables related to the health system
public int maxHealth = 5;
int currentHealth;
// get: 데이터를 요청받았을 때 실행되는 '입구(함수)'
// return: 그 입구로 들어온 사람에게 들려보낼 '결과물'
public int health { get { return currentHealth; } }
// Variables related to temporary invincibility 무적
public float timeInvincible = 2.0f;
bool isInvincible;
float damageCooldown; // 무적 쿨타임
// Variables related to animation
Animator animator;
Vector2 moveDirection = new Vector2(1, 0); // (X, Y)
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
MoveAction.Enable();
rigidbody2d = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
currentHealth = maxHealth;
}
// Update is called once per frame
void Update()
{
move = MoveAction.ReadValue<Vector2>();
// 플레이어가 움직이고 있다면 (0이 아니라면), 부동소수점문제 해결 위해 approximately 사용
if (!Mathf.Approximately(move.x, 0.0f) || !Mathf.Approximately(move.y, 0.0f))
{
moveDirection.Set(move.x, move.y); // 현재 방향을 기억
moveDirection.Normalize(); // 길이 1로 정규화
}
animator.SetFloat("Look X", moveDirection.x);
animator.SetFloat("Look Y", moveDirection.y);
animator.SetFloat("Speed", move.magnitude);
if (isInvincible)
{
damageCooldown -= Time.deltaTime;
if (damageCooldown < 0)
{
isInvincible = false;
}
}
}
// FixedUpdate has the same call rate as the physics system
void FixedUpdate()
{
Vector2 position = (Vector2)rigidbody2d.position + move * speed * Time.deltaTime;
rigidbody2d.MovePosition(position);
}
// 외부에서 데미지를 주거나(amount가 음수), 힐을 줄 때(amount가 양수) 호출하는 함수
public void ChangeHealth(int amount)
{
if (amount < 0) // 데미지 줄 때
{
if (isInvincible)
{
return;
}
isInvincible = true;
damageCooldown = timeInvincible;
animator.SetTrigger("Hit"); // Hit(피격) 애니메이션을 딱 한 번만 실행
}
/* Mathf.Clamp 설명
현재 체력에 받은 양을 더하되, 그 결과가 0보다 작아지거나 maxHealth보다 커지지 않게 '고정'합니다.
예: 체력이 5인데 힐을 100 받아도 최대치인 5로 유지됨!
*/
currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
Debug.Log(currentHealth + "/" + maxHealth);
}
}