Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(14일차) - 애니메이션 활용 및 다중 적용

by 독기품은토끼 2025. 5. 30.
✅ 오늘의 수업 목표
1. Side door 애니메이션 구현
2. Sprite 애니메이션 활용

1. DoorAnimation 실습

🥕 예행 작업
1. Script 생성 (Door Event 2)

 

어제는 미닫이 문에 애니메이션을 적용했다면, 오늘은 자동문에 애니메이션을 적용해 보겠다.

씬은 그대로 DoorAnimation씬을 사용한다.

 

1. Side door

우선 Side door 오브젝트를 배치해 주고, 콜라이더를 적용해 준다.

플레이어 오브젝트에 리지드바디가 적용되어 있으니 문에는 콜라이더만 있어도 된다.

 

사이드 도어 오브젝트의 피봇은 중심에 있다.

우리는 문을 옆으로 여는 애니메이션을 구현할 것이기 때문에 문제가 없으니 프리팹을 해제하지 않고 그대로 진행한다.

 

1.1. 애니메이션 적용

 

1.2. 열림/닫힘 구현

 

어제 실습이랑 크게 다른 게 없어서 상세 설명은 생략하겠다.

 

1.3. Tag 설정

만약 우리가 구현한 문이 세이브 포인트의 문이라면 몬스터가 같이 따라오는 상황이 없어야 한다.

그래서 Player 태그를 갖고 있는 오브젝트만 문 안으로 들어갈 수 있도록 Tag를 구현해 준다.

if (other.gameObject.CompareTag("Player")) {
    animator.SetTrigger(openKey); }

 

1.4. 재사용성을 높인 코드

어제 구현한 코드는 트리거 호출을 스크립트 파일해서 했다.

오늘은 트리거 호출을 유니티 에디터에서 하도록 조금 수정해 주었다.

using UnityEngine;

public class DoorEvent2 : MonoBehaviour
{
    private Animator animator;

    // 이렇게 구현하면 트리거 수정을 위해 스크립트를 다시 열 필요가 없음
    public string openKey;
    public string closeKey;

    void Start()
    {
        animator = GetComponent<Animator>();
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.CompareTag("Player"))
        {
            animator.SetTrigger(openKey);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.gameObject.CompareTag("Player"))
        {
            animator.SetTrigger(closeKey);
        }
    }
}

 

애니메이터에 여러 가지 문열기(미닫이 열기, 자동문 열기, 회전문 등)를 구현해 놓았다면

이렇게 키 값을 가져와서 사용하는 것이 좋을 것 같다.. (추측임 잘 몰라욤 ^-^)

 

2. 코인 애니메이션

 

2.1. Easing(이지잉) 효과

유니티 애니메이션 기본 설정은 느리게 시작 → 빠르게 → 다시 느리게 → 끝 처럼 처리된다.

그래서 Easing(이지잉) 효과가 나타나게 되는데

360도 회전처럼 일정한 속도로 계속 돌아야 하는 애니메이션에서는 이런 이지잉 효과는 오히려 방해가 된다.

마지막에 느려지는 코인

 

그래서 애니메이션 창에서 Curves 탭에 들어가 우리는 이 곡선을 Linear(직선)으로 변경해 주었다.

 

 

2.2. 코인 획득

 

public int coinCount = 0;

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            coinCount++;
            Debug.Log(coinCount);
        }
    }

 

위 코드의 문제점을 확인해 보자.

코인 획득을 각 코인 오브젝트 내부에서 직접 count 값을 관리하는 방식으로 구현하면,

코인이 파괴되면서 해당 값도 함께 사라지기 때문에 전체 코인 개수를 셀 수 없다.

따라서 공통으로 접근 가능한 static 정적 변수를 활용하는 것이 좋다.

using UnityEngine;

public class Coin : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            CoinMovement.coinCount++;
            Debug.Log($"현재까지 {CoinMovement.coinCount} 코인 획득");
            Destroy(this.gameObject);
        }
    }
}

 

코인 스크립트는 그냥 코인 먹는 것(+먹으면 파괴)만 구현하고,

using UnityEngine;

public class CoinMovement : MonoBehaviour
{
    public float moveSpeed = 5f;

    public static int coinCount = 0;

    void Update()
    {
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");

        Vector3 dir = new Vector3(h, 0, v).normalized;

        transform.position += dir * moveSpeed * Time.deltaTime;

        transform.LookAt(transform.position + dir);
    }
}

 

 

플레이어 스크립트에서 코인 개수를 누적하는 정적 변수를 선언해 주면 된다.

 

2. Sprite

🥕 예행 작업
1. Scene 생성 (Sprite Animation)
2. Script 생성 (Character Movement)
3. Assets 다운로드

 

1. 오브젝트

1.1. 에셋 다운로드

 

Jungle Asset Pack by Jesse Munguia

Awesome pixel-art Jungle Asset Pack

jesse-m.itch.io

 

1.2. 동작에 따른 다른 애니메이션

Idle
Run

 

저번 포스팅에서는 캐릭터의 움직임을 한 가지의 애니메이션으로만 적용했다면,

이번에는 캐릭터의 동작에 따라 다른 애니메이션을 적용한다.

 

가만히 있을 때에는 Idle 애니메이션을, 움직일 때에는 Run 애니메이션을 적용해 줄 것이다.

 

1.3. Sprite 설정

 

  • Sprite (2D and UI)
    → 게임 오브젝트로 사용할 것이기 때문에 일반적인 이미지 타입이 아닌 Sprite 타입으로 설정
  • Single
    → 이미지가 여러 개가 아닌 단일 스프라이트이기 때문에 Single로 설정
  • Pixels Per Unit
    → 게임 내에서 적절한 크기로 보이도록 픽셀 밀도 조정
  • Pivot
    → 스프라이트의 기준점을 하단에 맞춰 오브젝트 위치 설정
  • Filter Mode: Point (no filter)
    → 이미지에 블러 없이 픽셀을 선명하게 표시

 

이미지를 모두 선택하고 씬뷰에 삽입하면 알아서 애니메이션이 적용되므로 애니메이션 적용 정리는 생략

 

1.4. 캐릭터 움직임 구현

using UnityEngine;

public class CharacterMovement : MonoBehaviour
{
    private Rigidbody2D characterRb;

    public float moveSpeed;
    private float h;

    void Start()
    {
        characterRb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        h = Input.GetAxis("Horizontal");
    }

    private void FixedUpdate()
    {
        Move();
    }

    private void Move()
    {
        // 리지드바디는 물리적 이동이라 Fixed에서 구현해주어야 함
        characterRb.linearVelocityX = h * moveSpeed;
    }
}

 

움직임은 제대로 동작하니, 이제 동작에 따라 다른 애니메이션이 적용되도록 해보자

(Fixed Update를 사용하는 이유는 12일차 포스팅에 정리되어 있다.)

 

2. Sprite Renderer

애니메이션을 구현하기 위해 Sprite Renderer 컴포넌트를 활용한다.

GetComponentsInChildren<SpriteRenderer>()

 

 

위 코드는 오브젝트의 스프라이트를 배열로 가져오고, 이 배열의 요소들은 각각 Idle 애니메이션, Run 애니메이션이 적용된 스프라이트 오브젝트를 참조한다.

 

2.1. 동작에 따른 애니메이션 적용

using UnityEngine;

public class CharacterMovement : MonoBehaviour
{
    private Rigidbody2D characterRb;
    public SpriteRenderer[] renderes;

    public float moveSpeed;
    private float h;

    void Start()
    {
        characterRb = GetComponent<Rigidbody2D>();

        // 꺼져있는 오브젝트까지 찾기 위하여 true 넣어주기
        renderes = GetComponentsInChildren<SpriteRenderer>(true);
    }

    void Update()
    {
        h = Input.GetAxis("Horizontal");
    }

    private void FixedUpdate()
    {
        Move();
    }

    /// <summary>
    /// 캐릭터 움직임에 따라 이미지의 Flip 상태가 변하는 코드
    /// </summary>
    private void Move()
    {
        if (h != 0) // 움직일 때
        {
            renderes[0].gameObject.SetActive(false); // Idel
            renderes[1].gameObject.SetActive(true); // Run

            // 리지드바디는 물리적 이동이라 Fixed에서 구현해주어야 함
            characterRb.linearVelocityX = h * moveSpeed;
        }
        else // 안 움직일 때
        {
            renderes[0].gameObject.SetActive(true); // Idel
            renderes[1].gameObject.SetActive(false); // Run
        }
    }
}

 

가만히 있을 때 Idle 애니메이션, 움직일 때 Run 애니메이션이 적용됨을 확인할 수 있다.

하지만 오른쪽으로 이동했을 때 캐릭터의 시야 방향도 오른쪽으로 옮겨져야 하는데 후진을 해버린다.

이 부분을 좀 수정해 주자

 

2.2. 캐릭터 시야 방향 (플립)

우리가 캐릭터 움직임을 구현할 때

Sprite Renderer을 사용한 이유는 바로 스프라이트 렌더러의 옵션인 Flip을 사용하기 위해서다.

 

Flip은 오브젝트를 뒤집는 기능으로 코드로 잘만 구현해 주면 우리가 원하는 대로 움직임을 구현할 수 있다.

 

 private void Move()
 {
     if (h != 0) // 움직일 때
     {
         renderes[0].gameObject.SetActive(false); // Idel
         renderes[1].gameObject.SetActive(true); // Run

         // 리지드바디는 물리적 이동이라 Fixed에서 구현해주어야 함
         characterRb.linearVelocityX = h * moveSpeed;
     }
     else // 안 움직일 때
     {
         renderes[0].gameObject.SetActive(true); // Idel
         renderes[1].gameObject.SetActive(false); // Run
     }

     // 시야 방향 (플립)
     if (h > 0)
     {
         renderes[0].flipX = false;
         renderes[1].flipX = false;
     }
     else if (h < 0)
     {
         renderes[0].flipX = true;
         renderes[1].flipX = true;
     }
 }

 

3. OnCollision

이번에는 캐릭터의 점프 애니메이션(라기보단 이미지...)을 구현해 줄 것이다.

 

캐릭터의 점프 상태를 구현하기 위해 OnCollision 메소드를 활용해 줄 것이다.

 

 

  • 바닥에 닿았을 때(OnCollisionEnter2D) → 점프 이미지를 비활성화하고 Idle/Run 상태로 전환
  • 바닥에서 떨어졌을 때(OnCollisionExit2D) → 캐릭터가 공중에 있다고 판단하여 Idle/Run을 비활성화하고 Jump 상태로 전환

using UnityEngine;

public class CharacterMovement : MonoBehaviour
{
    private Rigidbody2D characterRb;
    public SpriteRenderer[] renderes; // Flip 기능이 렌더러에 있어서

    public float moveSpeed;
    public float jumpPower = 10f;
    private float h;

    private bool isGround;

    void Start()
    {
        characterRb = GetComponent<Rigidbody2D>();

        // 꺼져있는 오브젝트까지 찾기 위하여 true 넣어주기
        renderes = GetComponentsInChildren<SpriteRenderer>(true);
    }

    void Update()
    {
        h = Input.GetAxis("Horizontal");

        // 순간적인 물리 움직임은 Fixed에 구현하면 즉시 반응이 아니여서 Update에 구현
        Jump();
    }

    private void FixedUpdate()
    { 
        Move();
    }

    private void OnCollisionEnter2D(Collision2D other)
    {
        isGround = true;

        renderes[2].gameObject.SetActive(false); // Jump
    }

    private void OnCollisionExit2D(Collision2D other)
    {
        isGround = false;

        renderes[0].gameObject.SetActive(false); // Idle
        renderes[1].gameObject.SetActive(false); // Run
        renderes[2].gameObject.SetActive(true); // Jump
    }

    /// <summary>
    /// 캐릭터 움직임에 따라 이미지의 Flip 상태가 변하는 코드
    /// </summary>
    private void Move()
    {
        if (!isGround) return;

        if (h != 0) // 움직일 때
        {
            renderes[0].gameObject.SetActive(false); // Idel
            renderes[1].gameObject.SetActive(true); // Run

            // 리지드바디는 물리적 이동이라 Fixed에서 구현해주어야 함
            characterRb.linearVelocityX = h * moveSpeed;

            if (h > 0) // 오른쪽볼 때
            {
                renderes[0].flipX = false;
                renderes[1].flipX = false;
                renderes[2].flipX = false;
            }
            else if (h < 0) // 왼쪽볼 때
            {
                renderes[0].flipX = true;
                renderes[1].flipX = true;
                renderes[2].flipX = true;
            }
        }
        else // 안 움직일 때
        {
            renderes[0].gameObject.SetActive(true); // Idel
            renderes[1].gameObject.SetActive(false); // Run
        }
    }

    private void Jump()
    {
        if (Input.GetButtonDown("Jump")) // = Input.GetKeyDown(KeyCode.Space)
        {
            characterRb.AddForceY(jumpPower, ForceMode2D.Impulse);
        }
    }
}

 

 

 

 


 

 

 

이번 수업은 애니메이션 복습이 거의 주였고, 코드 구현이 다라 블로그에 코드만 띡 올릴까 123만번 고민했음..

하지만 아니야.. 아니야.. 나중에 난 분명 까먹을지도 몰라.. 이러면서 또 꾸역꾸역 정리..

원래는 수업 중간중간 캡쳐를 뜨는데 오늘은 대부분 했던 내용이라 캡쳐를 생략한 부분이 굉장히 많아서 이전 포스팅 왔다 갔다 하면서 복습해야 할 것 같움

 

그리고 친구들이 내 블로그 보더니 왜 워터마크를 안 하냐 대거 속출해서 워터마크를 한번 적용해 보았어요....

지지리 궁상 아냐? 했는데 아니라고.. 이걸 왜 안하냐고.. 당연히 해야하는 거 아니냐는 말에 팔랑귀인 나는 홀린 듯이 쏘옥😶

암튼.. 앞으로도 잘 부탁드립니다..👀