게임 엔진/Unity

Unity 8일차 - 점프점프 게임-1(물리엔진, Unity 제공 컴포넌트, 애니메이션)

FakeZero 2022. 2. 3. 01:20

  이걸 더 이상 일차라고 불러야 할지 모르겠지만, 쨌든 8일차입니다. 남은 것들 빠르게 처리하도록 하겠습니다.


1. 계획하기

  이번에 만들 게임은 점프점프 게임입니다. 어떻게 보면 플랫폼 게임이라고도 볼 수 있습니다.

 

1.만들 게임: 점프점프 게임

2. 필요한 리소스: 플레이어, 발판, 배경화면, 도착지점

3. 게임이 구동될 플렛폼: PC

4. 필요한 기능: 플레이어를 움직이는 기능, 오브젝트가 생성되는 기능, 게임 전반을 감독하는 기능

 

  필요한 스크립트는 이제 언급하지 않겠습니다. 필요한 기능을 알았다면 기능에 맞춰 스크립트를 생성하면 되기 때문입니다


2. 리소스 준비하기

chapter6.zip
0.22MB

(본 파일은 <유니티 교과서>의 출판사, [길벗]에서 제공한 것임을 밝히며 2차 수정 및 배포를 금지합니다.)


3. 프로젝트 생성 및 사전 설정

2D 템플릿으로 프로젝트를 만들도록 합시다. 저는 프로젝트 이름을 ClimbCloud로 해두었습니다. 프로젝트 생성이 끝난 뒤에는 리소스를 에셋 창에 불러오도록 합시다.

  이제 사전 설정에 들어가야 합니다. 이번 게임은 새로로 긴 화면을 이용할 것이기 때문에 해상도 설정이 필요합니다.

 

  잠시 Scene 탭에서 Game 탭으로 넘어가 봅시다.

  위 과정을 따라하면 새로운 해상도를 만들 수 있습니다. 다만 만들고나서 적용했을 때 세로로 해상도가 너무 긴 탓에 화면이 전부 보이지 않을 수 있습니다.

  그럴땐 Scale을 줄여주면 모든 화면을 볼 수 있게 됩니다. 자 이제 Scene 화면으로 돌아갑시다.

  Scene 탭으로 돌아왔다면 도구바에서 File을 클릭하고 Save As를 눌러서 에셋 폴더에 이름을 GameScene이라고 해서 을 저장하도록 합시다. 이 씬이라는 것이 무엇인지에 대해서는 추후에 다시 말하겠습니다.

  저렇게 씬 파일이 만들어졌다면 성공입니다.


4. 물리 엔진에 대해서

  자 드디어 이것을 쓸 때가 왔습니다. 바로 물리 엔진 입니다.

 

  게임은 체험입니다. 플레이어들에게 여러가지 체험을 즐기게 해주는 문화죠. 그렇기에 게임의 세상에서 물리법칙은 대부분 이루어지는 것이 보통입니다. 그러나 모든 게임 개발자가 물리학에 능통한 것이 아닙니다. 그렇기에 개발된 것이 바로 물리 엔진입니다.

 

  물리 엔진은 우리가 기본적인 몇가지만 알고 있다면 게임의 세상에 손쉽게 물리 법칙을 구현할 수 있도록 도와줍니다. 현실에 기반한 물리 법칙부터 게임에 필요한 새로운 법칙들을 구현하는데 까지도 도움을 줍니다.

 

  다만, 물리엔진을 잘못 만들거나 잘못 쓰게 되면 게임이 버그 투성이가 되는 것을 여러분들은 이미 익히 알고 계실 겁니다. 물리 엔진 버그로 유명한 [스케이트 3]나 작년(2021년)에 발매된 문제의 게임, [사이버펑크 2077]도 물리 엔진과 관련된 버그가 매우 많았습니다.

 

  이처럼 물리 엔진은 잘 사용하면 극강의 체험을 플레이어에게 제공하지만 제대로 다루지 못하면 게임 진행에 조차 문제를 일으키는 양날의 검 같은 존재입니다. 게임 개발 과정중 물리 엔진과 관련된 부분을 맡은 사람들은 이 물리 엔진을 이해하고 플레이어들이 게임을 플레이하면서 위화감이 없도록 디자인하는 것이 중요한 역량이 될 것입니다.

 

  자, 서론이 길었습니다. 방금의 얘기를 들으면 물리 엔진이 매우 다루기 어려워 보이지만 그것은 게임의 규모가 커졌을 경우입니다. 즉, 오브젝트간의 상호작용이 많으면 많을수록 물리엔진을 다루기가 어려워진다는 것이죠. 이번 강의에선 간단한 작업을 통해 물리 엔진을 찍어먹어 보도록 합시다.

 

  유니티의 물리 엔진은 Physics라는 이름을 가지고 있습니다. 앞으로 Physics라는 이름이 보이면 특별한 말이 없을 경우 유니티의 물리 엔진이라고 생각하셔도 좋습니다. Physics는 유니티에 기본적으로 탑재되어 있는 표준 물리 엔진으로 게임 개발자가 필요할 경우 새로운 물리 엔진을 추가하거나 편집하는 경우도 있습니다. 하지만 오늘 강의해선 표준 물리 엔진만 다뤄 보도록 합시다.


5. 유니티가 제공하는 컴포넌트

  유니티에서 물리엔진을 사용하려면 유니티에서 기본적으로 제공하는 컴포넌트를 이용해야합니다. 대부분의 것들을 이 컴포넌트를 통해 재현할 수 있으며 다양한 요소를 컨트롤 하는 것이 가능합니다.

 

  컴포넌트를 이용하기 위해서는 컴포넌트를 추가할 대상이 있어야, 즉, 물리 엔진의 영향을 받을 오브젝트가 있어야 하므로 오브젝트 배치를 시작해봅시다.

  가장 먼저 플레이어를 먼저 배치해봅시다. 에셋 창에서 cat을 Scene 창에 배치하고 우측의 Inspector 창에서 Transform 항목의 Position을 0, 0, 0으로 해줍시다. 다음은 플레이어 캐릭터에 물리 엔진을 적용하기 위해 컴포넌트를 추가해 봅시다.

  위 과정을 거치면 우리가 사용할 컴포넌트 중 하나인 Rigidbody 2D 컴포넌트를 오브젝트에 추가해 줄 수 있습니다.(그냥 Rigidbody는 3D 게임에서 사용합니다.)

 

  자 이제 한번 게임을 실행시켜 봅시다.

 

  워낙에 짧아서 제대로 못 봤을 수도 있겠지만 영상에는 분명히 스크립트를 단 한 줄도 짜지 않았는데도 불구, 화면 아래로 떨어지는 오브젝트의 모습이 찍혔습니다. 이것이 바로 물리 엔진의 힘입니다. 게임 개발 작업을 더욱 쉽고 간편하면서도 고급스럽게 만들 수 있게됩니다.

 

  이제 두 번째 컴포넌트를 적용해봅시다. 이번에 필요한 것은 Circle Collider 2D라는 컴포넌트입니다.

  제대로 추가 되었다면 이렇게 추가된 것을 확인할 수 있을 것입니다.

 

  아마 이 컴포넌트를 추가할 때에 Circle, 즉, 원형 이외에도 다양한 모양의 Collider를 보았을 것입니다. 그리고 눈치 채셨습니까? 컴포넌트의 추가와 함께 플레이어 캐릭터에게 녹색 원이 생겼습니다.

 

  이 녹색 원은 콜라이더(Collider)라고 부릅니다. 콜라이더는 콜라이더가 지정한 경계에 다른 오브젝트가 충돌하였는지를 인식하는 역할을 합니다. 즉, 충돌 판정을 자동으로 해주는 것입니다.

 

  여기서 우리는 이전 강좌를 떠올릴 수 있습니다. 바로 7일차에서 했던 충돌판정 스크립트 입니다. 그때도 원을 이용했었습니다만 이렇게까지 눈에 한번에 보이는 방법이 아니었기에 머릿속에서 가상의 원을 그리고 스크립트를 작성해야했습니다. 그러나 이제 그럴 필요가 없습니다. 왜냐하면 그 역할을 모두 콜라이더가 해주기 떄문입니다. 프로그래머가 해야하는 것은 콜라이더가 보내오는 충돌 판정 정보를 이용해 '오브젝트 간의 상호작용'(앞으로 인터랙션(Interaction)이라 하겠습니다.)을 프로그래밍 하는 것 뿐입니다.

 

  이처럼 컴포넌트는 게임 개발 과정에 엄청난 편리함을 제공합니다. 이러한 컴포넌트 및 기능을 얼마나 마련해둘 것인가는 게임 엔진 개발자의 역량이며 이에 따라서 게임 엔진의 좋고 나쁨이 갈리기도 합니다. 이전에도 말한 적이 있듯, 게임 엔진은 게임 개발을 쉽게 할 수 있도록 만들어진 도구입니다. 이 게임 엔진 안에는 아직도 우리가 모르는 수많은 편리한 기능들이 잠들어 있습니다.

 

  본론으로 돌아와서 콜라이더는 오브젝트의 충돌 판정을 맡는 만큼 모양도 다양해야 합니다. 바로 그걸 위한 다양한 모양의 콜라이더들인 것입니다. 여러분들이 게임을 개발할 때에 오브젝트의 모양에 따라 적절한 콜라이더를 고르면 이것은 게임의 질과 게임의 최적화와도 직결됩니다.

 

  자, 이제 충돌 판정을 할 수 있게 되었으니 충돌 할 오브젝트를 배치합시다.

  cloud 에셋을 Scene화면에 배치하고 0, 2, 0의 위치로 옮겨 줍시다. 그 후, 물리 엔진 적용을 위해 이번엔 Box Collider 2D와 Rigidbody 2D 컴포넌트도 추가해줍시다.

 

  컴포넌트를 적용하긴 했지만 이대로 가면 구름은 그저 떨어지기만 할 것입니다. 구름은 중력의 영향을 받으면 안되는 것입니다.

  위와 같이 cloud의 Rigidbody 2D 컴포넌트에서 Body Type을 Kinematic으로 설정해 줍시다. Kinematic이 적용된 오브젝트는 중력과 외부의 힘에 영향을 받지 않습니다.

 

  영상에서 보는 것처럼 더 이상 플레이어 캐릭터가 화면 밖으로 나가지 않는 것을 확인 할 수 있습니다. 그러나 자세히 보면 플레이어 캐릭터가 구름 위에 약간 떠있는 것을 볼 수 있습니다. 이것은 구름과 플레이어 캐릭터의 콜라이더가 크게 설정되어 있기 떄문입니다.

 

  사소한 것이라고 생각할지도 모르겠지만 이런 디테일들은 게임의 질과도 연결됩니다. 디테일을 살리면 살릴수록 게임의 평가는 높아지는 것이 당연합니다.

 

  일단 플레이어의 콜라이더 부터 조정하도록 하겠습니다. 이번 플레이어 캐릭터의 경우 사실 원형 콜라이더보다도 캡슐형 콜라이더가 더 유용하게 쓸 수 있습니다. 이미 유니티 에서도 캡슐형 콜라이더는 제공하고 있지만 이번에는 원형 콜라이더와 사각형 콜라이더를 조합해서 반 캡슐형 콜라이더를 만들도록 하겠습니다.

  사실 크기 정도는 Edit Collider 항목의 오른쪽에 있는 버튼을 누르면 쉽게 조정할 수 있습니다. 하지만 이번엔 수치를 직접 적어서 콜라이더를 조정해줍시다. Offset은 0, -0.3로 Radius는 0.15로 설정해줍시다.

  이번에는 Box Collider 2D 컴포넌트를 만들고 Size를 0.3, 0.6으로 만들어 줍시다. 이렇게 하면 반 캡슐형의 콜라이더가 완성됩니다.

 

  어라? 콜라이더가 플레이어 캐릭터보다 작은 것 같죠? 다시 조정해서 딱 맞게 해야할 것 같지만 막상 게임을 실행해보면 일부러 조금 작게 만든 이유를 알 수 있을 것입니다.

 

  이대로 콜라이더만 조정해선 안됩니다. 왜냐하면 플레이어 캐릭터 발 밑의 콜라이더가 원형인 만큼 발판이나 플레이어 캐릭터가 조금만 움직여도 중력의 영향으로 플레이어 캐릭터가 쓰러질 것이기 때문입니다. 하지만 걱정하지 마십시오. 이미 유니티에선 대응책이 마련되어 있습니다. 이번 문제는 물리 엔진과 관련 되어 있으므로 Rigidbody 2D 컴포넌트로 돌아가겠습니다.

  중력에 의해서 넘어진다는 것은 2D 좌표계 기준 Z축의 회전과 관련이 있습니다. 그렇기 때문에 이번 문제를 해결하는 방법은 간단합니다. 돌아가지 않게 만들면 되죠.

 

  Rigidbody 2D의 항목중 Constraints를 펼치면 Freeze Rotation 항목이 보일 것입니다. Z가 있는 체크 박스에 체크를 하면 이제 Z축을 기준으로 한 중력에 의한 회전은 완전히 막혔습니다. 물론 중력 이외의 외부 힘도 마찬가지입니다.

 

  플레이어 캐릭터의 콜라이더 및 물리 엔진 조정은 여기까지 입니다. 이번엔 구름에 손을 대도록 합시다.

  구름은 별거 없습니다. Box Collider 2D 컴포넌트의 Size를 1.4, 0.5로 바꿔주도록 합시다.

 

  이번에도 역시 콜라이더가 오브젝트보다 조금 작습니다. 자, 그럼 과연 이렇게 한 결과가 어떻게 되었는지 확인해봅시다.

  보셨습니까? 아까보다 훨씬 자연스러운 모습으로 플레이어 캐릭터가 서 있는 것을 볼 수 있습니다. 이것이 디테일입니다. 사소한 것 하나하나가 게임의 품질을 좌우합니다. 이렇게 자연스럽게 서 있는 것을 보는 것 만으로도 '성의 없이 만들었다'라는 생각은 들지 않게 만듭니다.

 

  게이머의 평가가 무엇보다 중요한 게임시장에서 디테일이란 게임 시장에서 게임의 명운을 좌지우지하기도 합니다.


6. 플레이어 조작 스크립트 작성

  이번엔 플랫폼 게임에서 절대로 빠져서는 안되는 게임의 핵심, 플레이어 조작 스크립트를 만들어보도록 합시다. 다만 방식은 이전에 화살표 피하기 게임을 만들었을때와는 다릅니다. 왜냐하면 이제 우리에겐 물리 엔진이 있기 때문이죠. 오브젝트의 움직임을 결정하는 요소가 늘어난 이상 이전의 방법을 사용하기엔 퀄리티가 떨어집니다. 물리엔진도 이용하며 좀 더 자연스러운 움직임을 추구하는 스크립트를 작성해야합니다.

 

  우리가 땅바닥에서 점프했을 때를 기억해봅시다. 처음에는 몸의 근육의 힘으로 중력을 이기고 하늘 위로 점프합니다. 그리고 최고점에 도달해서 가속도가 0이 되었을 때, 중력에 의해서 아래 방향으로 다시 가속도가 생기고 땅으로 떨어지기 시작합니다.

 

  그럼 이 상황을 게임에 대입해봅시다. 최고점에서 땅에 떨어지기 까지는 이미 물리 엔진으로 구현되어 있음을 알 수 있습니다. 그렇다면 우리가 해야하는 것은 플레이어 캐릭터를 위로 밀어주어 하늘 위로 올려주는 일 뿐입니다. 그러니 우리가 스크립트로 만들어야 하는 것은 바로 이 부분인 것입니다.

 

  새로운 C# 스크립트를 작성합시다. 전 이름을 PlayerController이라 지었습니다. 코드는 다음과 같습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    float jumpForce = 680.0f;

    void Start()
    {
        this.rigid2D = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        //점프한다.
        if(Input.GetKeyDown(KeyCode.Space))
        {
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }
    }
}

  오늘도 어김없이 이때까지 등장하지 않았던 코드만 보도록 합시다.

Rigidbody2D rigid2D;

  Rigidbody 2D 컴포넌트에서 메서드를 꺼내오기 위해 rigid2D라는 이름의 변수를 만들었습니다. 이제 Rigidbody2D라고 쓰지 않고 rigid2D라고만 써도 같은 의미를 가지게 됩니다.

this.rigid2D.AddForce(transform.up * this.jumpForce);

  Rigidbody2D 컴포넌트에 있는 AddForce라는 메서드를 사용할 껍니다. 이 메서드는 말 그대로 대상에게 힘을 가하는 역할을 합니다.

 

  transform.up은 단위 벡터 값입니다. 단위 벡터란 길이가 1인 벡터값을 의미합니다. 즉, transform.up은 백터 값으로 (x, y, z) = (0, 1, 0) 의 값을 가지는 겁니다.(up이 위 방향이니 1의 방향도 당연히 y축 위쪽 방향입니다)

  참고로 단위 벡터는 다방향이 아닌 한 방향으로만 값을 가집니다.

 

  아무튼 이번 게임에서 점프하는데 이용할 힘은 위쪽 방향으로 1 * jumpForce(680) 만큼입니다.

 

  점프하는 스크립트가 작성이 되었으니 이제 확인해봅시다. cat 오브젝트에 PlayerController 스크립트를 적용하고 제대로 작동하는지 확인해봅시다.

 

  문제점을 발견하셨습니까? 맞습니다. 너무 가볍습니다. 점프력이 너무 대단해서 하늘을 뚫어서 화면 밖으로 나갔다가 돌아옵니다. 이런 경우 스크립트를 조정해서 힘을 줄일 수도 있겠지만 이번 경우에는 더 간단한 방법이 있습니다. 바로 중력을 늘리는 것입니다.

    cat 오브젝트의 Rigidbody 2D 컴포넌트에서 Gravity Scale을 3으로 바꿔줍시다. 이것으로 중력을 세배로 만들 수 있습니다. 이제 다시 게임을 실행시켜보죠.

 

  아까 전보다 확실히 자연스러워진 모습이 보이죠? 같은 방법으로 좌우 이동을 만들도록 합시다. 다시 PlayerController 스크립트를 키고 다음과 같이 수정하도록 합시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    float jumpForce = 680.0f;
    float walkForce = 30.0f;
    float maxWalkSpeed = 2.0f;

    void Start()
    {
        this.rigid2D = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        //점프한다.
        if(Input.GetKeyDown(KeyCode.Space))
        {
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }

        //좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

        //플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

        //스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }
    }

  처음나온 부분과 설명이 필요한 부분 설명 들어갑니다.

	//좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

  왼쪽 화살표를 눌렀느냐 오른쪽 화살표를 눌렀느냐에 따라서 방향이 결정되야 하므로 위와 같은 식이 되었습니다.

	//플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

  Mathf는 계산을 위한 클래스입니다. 정확히는 계산을 편리하게 할 수 있도록 도와주는 클래스라고 할 수 있죠. 절댓값, 최대/최솟값, 올림, 버림, 반올림, 근삿값 등등을 이 Mathf 클래스를 이용해 구할 수 있습니다.

 

  오늘 사용한 Abs는 바로 절댓값이며 this.rigid2D.velocity.x는 이 스크립트가 적용된 오브젝트의 x축 방향 속도를 구합니다. 다만 이전에도 말했듯 속도는 벡터값으로 방향이 존재합니다. 따라서 절댓값으로 변환해주어야 계산할 때 문제가 생기지 않습니다.

	//스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }

  실질적으로 오브젝트에 속도를 부여하는 코드는 이 코드들입니다.

 

  조건문 speedx < this.maxWalkSpeed를 통해 최대 속력을 제한합니다.

  transform.right는 오른쪽 방향(x축 오른쪽)의 단위 벡터, key가 방향을 결정하고, this.walkforce가 실질적으로 주는 힘이 됩니다.

 

  이제 한번 움직여 보도록 합시다.

 

  제대로 움직이는군요. 하지만 명확히 부자연스러운 부분이 보입니다. 바로 머리의 방향이 바뀌지 않는 것입니다. 이걸 바꾸기 위해선 누르는 키에 따라 플레이어 오브젝트의 이미지를 반전시켜야할 필요가 있을 것 같습니다. 코드를 다음과 바꿔주도록 하죠.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    float jumpForce = 680.0f;
    float walkForce = 30.0f;
    float maxWalkSpeed = 2.0f;

    void Start()
    {
        this.rigid2D = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        //점프한다.
        if(Input.GetKeyDown(KeyCode.Space))
        {
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }

        //좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

        //플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

        //스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }

        //움직이는 방향에 따라 플레이어 이미지 반전
        if(key != 0)
        {
            transform.localScale = new Vector3(key, 1, 1);
        }
    }
}

  추가된 부분을 보도록 합시다.

        //움직이는 방향에 따라 플레이어 이미지 반전
        if(key != 0)
        {
            transform.localScale = new Vector3(key, 1, 1);
        }

  key 값이 0이 아닐 때에 스크립트가 적용된 오브젝트의 스케일, 즉, 배율을 변화시키는데요. 배율 또한 벡터값을 가지고 있기 때문에 Vector3 클래스를 이용해 x축 방향의 스케일에 변화를 주는것 입니다.

 

  이제 한 번 다시 실행해봅시다.

 

  이제 조금은 자연스러워 보이는군요. 게다가 사선으로도 움직일 수 있게되었습니다.


7. 애니메이션

  이제 드디어 이번 회차의 마지막 단계입니다.

 

  바로 게임 캐릭터들의 움직임을 만드는 것! 즉, 애니메이션입니다.

 

  캐릭터들의 애니메이션을 만드는 방법은 여러가지가 있겠지만 가장 대표적인 방법과 널리 쓰여지고 있는 방법은 아래와 같습니다.

 

  2D:

  다들 어렸을 적, 교과서 모서리에 플립북을 만들어 보신 적이 있으십니까? 2D 캐릭터의 애니메이션을 만드는 방법은 바로 이것과 같습니다. 각각의 모션을 그려서 빠르게 재생하는 것이죠. 게임뿐 아니라 2D 애니메이션을 만드는 가장 기본적인 방법이죠.

 

  3D:

  3D도 사실 2D와 근본적인 차이는 없습니다. 다만 차원이 추가되었기 때문에 고려해야할 것이 늘었습니다. 때문에 2D보다 훨씬 방법이 복잡하죠. 물론 현재는 꽤나 만들기 쉽도록 장치가 마련되어있으니 걱정하지 않아도 됩니다. 다만, 게임엔진을 사용한 3D 애니메이션의 경우 컴퓨터의 높은 사양을 요구로 합니다. 컴퓨터의 사양이 받침되지 않을 경우 애니메이션을 만들며 점점 컴퓨터에 과부하가 걸려 느려질 것입니다.

 

  저희가 지금 사용할 것은 2D 애니메이션입니다. 리소스를 받았을 때, 플레이어 이미지가 움직이는 그림이 여러장 있었죠? 이 그림 한 장을 우리는 스프라이트라고 부릅니다. 그리고 스프라이트를 활용해 만드는 플립북 애니메이션 같은 애니메이션을 스프라이트 애니메이션이라 부르죠.

 

  애니메이션을 만들기 위해선 애니메이션을 만들기 위한 애니메이터가 필요하겠죠. 게임 엔진에서 쓰는 애니메이터는 보통 메카님 구조를 이용합니다. 어떤 구조로 되어있는지 보기 위해 일단 애니메이터를 추가해보도록 합시다.

  이렇게 해서 애니메이터를 불러올 수 있게 됩니다. 계속 새 창으로 두면 불편하니 콘솔 탭 옆에 두도록 합시다

  이런 식으로 탭을 드래그 하면 순서를 바꾸거나 다른 장소로 옮기거나 할 수 있습니다. 유용하게 사용하시길 바랍니다.

 

  애니메이션은 총 3가지를 통해 제어하게 됩니다.

  1. 애니메이터로 만들어진 애니메이션 파일

  2. 애니메이터로 만든 애니메이션 파일을 제어할 애니메이터 컨트롤러 파일

  3. 애니메이션을 실행할 Animator 컴포넌트

 

  애니메이션 파일은 말 그대로 실행할 애니메이션의 정보를 가지고 있는 파일입니다.

 

  애니메이터 컨트롤러는 이 파일을 할당한 오브젝트가 어떤 때에 애니메이션 파일을 실행할지 제어합니다.

 

  그리고 Animator 컴포넌트에 애니메이터 컨트롤러 파일을 할당 함으로써 오브젝트가 애니메이션을 실행하기 시작합니다.

 

  그럼 먼저 애니메이션 파일을 만들어 봅시다.

애니메이션 파일의 이름은 Walk로 해 두었습니다. 처음 만든 애니메이션 파일에는 Loop Time이 꺼져 있을 껍니다. 걷기 애니메이션은 끝나고 나서 다시 반복되어야 하므로 Loop Time을 켜주도록 하겠습니다.

 

  애니메이션을 만들기 전에 이 애니메이션을 누구에게 적용할지 부터 정하도록 하겠습니다. 그러기 위해선 애니메이터 컨트롤러 파일과 Animator 컴포넌트가 필요합니다.

  이렇게 Animator 컴포넌트가 적용이 되었습니다. Animator 컴포넌트를 보면 애니메이터 컨트롤러 파일을 할당하는 곳이 있습니다. 따라서 할당할 애니메이터 컨트롤러 파일도 만들어야합니다.

  cat이라는 이름으로 애니메이터 컨트롤러 파일을 만들어 주었습니다. 이제 이 파일을 cat 오브젝트의 Animator 컴포넌트에 할당해주도록 합시다.

  이렇게 하면 할당이 끝납니다. 이제 언제 애니메이션이 실행이 될지 설정해주도록 합시다. 그러기 위해선 새로운 탭이 필요합니다. Animator 탭 입니다.

  보통 Game탭 옆에 생깁니다. 그렇지 않다면 드래그해서 옮겨 주도록 합시다. 이 창에선 애니메이션 컨트롤러 파일을 수정할 수 있습니다. 기본적으로는 선택되어 있는 오브젝트에 할당 된 애니메이터 컨트롤러 파일을 수정하고 에셋 창에 있는 애니메이터 컨트롤러 파일을 더블 클릭해도 해당 파일을 수정할 수 있습니다. 현재는 cat 오브젝트가 선택되어 있기 때문에 cat 애니메이션 컨트롤러 파일을 수정하게 됩니다. 여러가지 새로 보이는 것들이 있으니 어느정도 설명을 드리도록 하겠습니다.

  ①레이어(Layer) 탭: 애니메이션 파일이 어떻게 움직일지 그룹지어 놓는 곳입니다. 한 애니메이터 컨트롤러 안에 여러가지 레이어를 만들어서 다양한 애니메이션을 실행할 수 있도록 도와줍니다.

 

  ②레이어 수정탭(임시): 정확히 수정탭이라고 불리지는 않겠지만 임시로 이렇게 부르도록 하겠습니다. 이곳에서 애니메이션이 어떻게 실행될지 제어합니다.

 

  ③파라미터(Parameters) 탭: 애니메이션을 제어할 변수나 트리거를 만들거나 수정합니다. 정수형이나 실수형 등등 말 그대로 변수들을 만들어서 애니메이션 제어에 사용합니다.

 

  현재 레이어 수정 탭에 보면 하늘색과 녹색, 붉은색의 무언가가 보입니다. 이것을 노드라고 합니다. 이것들을 서로 연결하여 애니메이션을 제어합니다. 애니메이션 파일도 이 안에 노드로서 들어가지만 이 하늘색, 녹색, 붉은색의 노드들은 특별한 역할을 맡고 있습니다.(처음에는 Exit 노드는 안보일텐데 마우스 휠을 조작하면 좀 멀리 떨어져 있는 것을 알 수 있습니다.)

노드명 역할
Entry 애니메이션을 시작할 때는 Entry 노드로 전환한다.
Any State 현재 상태에 관계없이 특정 애니메이션으로 전환하고자 할 때 사용한다.
Exit 애니메이션을 종료하고자 할 때는 Exit 노드로 전환한다.

  설명은 여기까지 하고 바로 애니메이션을 집어넣어 봅시다.

  레이어에 아무것도 없을 때에 들어간 애니메이션 노드는 색깔이 주황색이 되며 바로 Entry 노드에 연결됩니다. 따라서 주황색 노드는 레이어에서 가장 처음 실행되는 애니메이션이란 뜻입니다.

 

  Walk 애니메이션은 Parameters의 변수가 아니라 스크립트를 통해 어떻게 실행될 것인지 제어할 것입니다. 하지만 그 전에 현재 Walk 애니메이션은 비어있는 상태입니다. 따라서 애니메이터에서 Walk 애니메이션을 만들어보도록 합시다. Scene 탭으로 돌아가 cat 오브젝트를 선택한 다음 Animator 탭을 열어보도록 합시다.

  cat 오브젝트를 선택으므로 애니메이터에도 cat 오브젝트에 할당된 cat 애니메이터 컨트롤러 안에 있는 애니메이션을 수정하게 됩니다. 이번에도 탭에 대한 설명을 짧게 하겠습니다.

  ①Preview 버튼: 말 그대로 만들어진 애니메이션을 미리보기 하는 기능입니다. 토글 버튼으로 켰다, 껏다 할 수 있으며 켜져 있는 동안은 Scene 탭에서 애니메이션이 실행되게 됩니다.

 

  ②녹화 버튼: 붉은 색 원이 있는 버튼입니다. 이 버튼을 누르고 Scene창의 오브젝트에 변화를 줄 경우 애니메이터에 변화를 준 해당 요소들이 추가되며 변화가 기록됩니다. 이 기능은 나중에 3D 애니메이션을 만들 때에 더욱 유용하게 사용될 것입니다. 물론 2D 애니메이션을 만들때도 유용하게 사용됩니다.

 

  ③재생 버튼들: 미디어 플레이어에서 많이 본 버튼들 일껍니다. 재생 버튼이나 넘기기 버튼등이죠. 직접 사용해보면 바로 어떤 기능인지 알 수 있습니다.

 

  ④애니메이션 선택: 위 사진에서 Walk라고 써져 있는 부분입니다. 현재는 Walk 애니메이션이 선택되어 있습니다. 이 버튼을 누르면 선택한 애니메이터 컨트롤러 파일에 들어가있는 애니메이션의 목록이 나타나며 원하는 애니메이션을 선택해 수정할 수 있습니다.

 

  ⑤타임 라인: 시간이 적혀있는 부분입니다. 저 부분을 클릭해 어떤 시점에 애니메이션을 보거나 만들거나 수정할지 정할 수 있습니다.

 

  아직 기능은 많지만 나중에 차차 설명하거나 하도록 하겠습니다. 지금은 대략적으로 이런 기능들이 있다는 것을 알아두면 됩니다.

https://docs.unity3d.com/kr/2018.4/Manual/animeditor-AnimatingAGameObject.html

 

게임 오브젝트 애니메이션화 - Unity 매뉴얼

새 애니메이션 클립 에셋을 저장한 이후에 클립에 키프레임을 추가할 수 있습니다.

docs.unity3d.com

  더 자세한 설명이 알고 싶다면 위 링크를 들어가면 알 수 있습니다.

 

  이제 본격적으로 애니메이션을 만들어 보겠습니다.

  Add Property를 눌러서 Sprite Renderer 탭의 Sprite의 오른쪽에 있는 +를 눌러서 스프라이트 프로퍼티를 추가하도록 합시다. 그러면 타임 라인 밑에 다이아몬드 모양이 생깁니다. 저것은 키프레임이라고 합니다. 저 키프레임들을 기준으로 애니메이션이 흘러갑니다.

 

  현재 생성된 키프레임에는 현재 선택된 cat 오브젝트가 가진 스프라이트 에셋이 들어가 있습니다. 즉, 기본적으로 서 있는 그림입니다.

 

  이제 움직이는 모션을 만들어봅시다. 다만 스프라이트를 이용한 애니메이션의 경우 여러장의 그림 에셋이 필요하므로 에셋 창도 같이 보기 위해 애니메이션 탭을 잠시 분리하겠습니다.

  1초 지점(00:01이 아니라 1:00이 1초 입니다.)에 생성되어 있는 두 키프레임 중 더 짙은 줄에 있는 키프레임(위에 있는 키프레임)을 선택해서 지우도록 합시다. 이 짙은 줄에 있는 키 프레임은 해당 시간에서 실행되는 모든 키프레임을 선택합니다.

  키프레임이 있는 공간에서 마우스 휠을 조작하면 보이는 범위를 조절할 수 있습니다. 이걸 이용해서 에셋 창의 cat_walk1 이미지를 0.07초 부분에 배치하도록 합시다.

 

  이 방법으로 cat_walk2는 0.14, cat_walk3는 0.21에 배치하고 0.21초의 모든 키프레임을 복사해서 0.28에 붙여넣기 하도록 합시다.

  전부 끝내면 이런 상태가 됩니다. 이제 잠시 Animation 탭을 Console 탭 옆에 두도록 합시다.

 

  애니메이션이 하나 만들어졌으니 한번 확인해보도록 하겠습니다. 단, 게임을 플레이하는 것이 아니라 미리보기 기능을 이용할 껍니다. Animation 탭에 있는 재생버튼을 이용하도록 합시다.

 

  제대로 움직이는 것을 확인할 수 있었습니다. 하지만 이대로라면 플레이어가 조작하지 않아도 계속해서 애니메이션이 재생될 것입니다. 이것은 스크립트에서 조절하도록 합시다. PlayerController 스크립트를 열고 다음과 같이 수정하도록 합시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    Animator animator;
    float jumpForce = 680.0f;
    float walkForce = 30.0f;
    float maxWalkSpeed = 2.0f;

    void Start()
    {
        this.rigid2D = GetComponent<Rigidbody2D>();
        this.animator = GetComponent<Animator>();
    }

    void Update()
    {
        //점프한다.
        if(Input.GetKeyDown(KeyCode.Space))
        {
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }

        //좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

        //플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

        //스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }

        //움직이는 방향에 따라 플레이어 이미지 반전
        if(key != 0)
        {
            transform.localScale = new Vector3(key, 1, 1);
        }

        //플레이어 속도에 맞춰 애니메이션 속도를 바꾼다.
        this.animator.speed = speedx / 2.0f;
    }
}

  아마 설명할 것이 더이상 없다고 생각합니다. 마지막에 추가된 코드만 설명하자면 Animator 컴포넌트의 speed 변수를 speedx 변수에 맞춰 변화시켜주는 것입니다. 즉, 플레이어 캐릭터의 속도에 따라 애니메이션 재생 속도가 바뀝니다.

 

  이제 한 번 게임을 실행시켜보도록 하겠습니다.

 

이 영상은 애니메이션을 제대로 확인하기 위해 임시로 PlayerController 스크립트에 60 프레임 고정 코드를 추가해두었습니다.

  잘 되는군요. 하지만 아직 남은 애니메이션이 있습니다. 바로 점프 애니메이션 입니다.

 

  Jump 라는 애니메이션 파일을 만듭시다. Jump는 점프키를 눌렀을 때 한 번만 재생되면 되므로 Loop Time에 체크를 하지 않아도 됩니다.

  만든 Jump 애니메이션을 레이어에 끌어와서 노드로 만듭시다. 아직은 아무것도 연결된 선이 없습니다. 이 선을 우리는 트랜지션(Transition)이라고 합니다.

 

  Walk 애니메이션이 실행되다가 점프키가 눌리면 Jump 애니메이션이 재생되야 하므로 Walk 애니메이션에서 Jump 애니메이션으로 트랜지션을 만들어 줍시다.

  Walk 노드에 우클릭하고 Make Trasition을 눌러서 Jump 노드에 이어줍니다. Jump 애니메이션은 점프키가 눌린 시점에서 재생 되어야 합니다. 따라서 이번에 사용할 파라미터는 트리거 파라미터 입니다.

  JumpTrigger이라는 이름으로 새로운 파라미터를 만들어 주었습니다.

 

  파라미터의 오른쪽에 있는 원은 초기값입니다. 원이 비어있으면(비활성화) 트리거가 안눌린 것이고 흰 색 원으로 채워져 있다면(활성화) 눌려있다고 하고 게임이 시작되게 됩니다. 이번 경우 안눌려 있는 상태에서 시작해야하므로 비활성화인 상태로 둡니다. 이 외에도 Int 형, Float 형, Bool 형 모두 초기값을 설정할 수 있습니다.

 

  이제 만들어진 트랜지션을 눌러 Walk에서 Jump로 전환되는 조건을 설정합시다.

  만든 파라미터를 이용하기에 앞서 기본적인 설정을 하겠습니다. 위의 사진과 같이 설정해주시면 됩니다. 저 항목들의 의미는 다음과 같습니다.

항목명 설명
Has Exit Time 애니메이션 재생이 종료되면 자동으로 다른 애니메이션으로 전환할지 여부를 선택한다.
Exit Time 애니메이션의 종료시간을 정규화 시간(0.0~1.0)으로 설정한다.
Transition Duration 다음 애니메이션으로 전환하는 시간을 정규화 시간(0.0~1.0)으로 설정한다.
Transition Offset 다음 애니메이션을 재생하는 시간을 정규화 시간(0.0~1.0)으로 설정한다.

  Transition Duration과 Transition Offset의 경우 밑의 그래프로도 조작할 수 있으니 참고하시길 바랍니다.

 

  이제 만든 파라미터를 이용해 봅시다.

  위와 같이 JumpTrigger이라는 파라미터를 통해 트랜지션을 애니메이션을 제어할 수 있도록 설정이 되었습니다. 하지만 아직 파라미터는 무엇에 의해 변화하는지 모르는 상태입니다. 따라서 스크립트를 통해 JumpTrigger 파라미터를 제어하겠습니다. PlayerController 스크립트를 열어서 다음과 같이 수정해줍시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    Animator animator;
    float jumpForce = 680.0f;
    float walkForce = 30.0f;
    float maxWalkSpeed = 2.0f;

    void Start()
    {
        Application.targetFrameRate = 60;
        this.rigid2D = GetComponent<Rigidbody2D>();
        this.animator = GetComponent<Animator>();
    }

    void Update()
    {
        //점프한다.
        if(Input.GetKeyDown(KeyCode.Space))
        {
            this.animator.SetTrigger("JumpTrigger");
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }

        //좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

        //플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

        //스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }

        //움직이는 방향에 따라 플레이어 이미지 반전
        if(key != 0)
        {
            transform.localScale = new Vector3(key, 1, 1);
        }

        //플레이어 속도에 맞춰 애니메이션 속도를 바꾼다.
        if(this.rigid2D.velocity.y == 0)
        {
            this.animator.speed = speedx / 2.0f;
        }
        else
        {
            this.animator.speed = 1.0f;
        }
    }
}

( 25, 50~57번 줄이 추가 되었습니다.)

(앞으로 이후 강좌에서도 기존의 스크립트에 코드가 추가 될 경우 몇 번 줄이 추가 되었는지 적어두도록 하겠습니다.)

 

  점프 버튼을 누르면 JumpTrigger 파라미터가 활성화 되도록 프로그램 해두고 점프 했을 때만 애니메이션 스피드가 1이 되도록 해두었습니다. 아직 Jump 애니메이션이 완성되지 않았으니 모두 해결한 뒤에 시험해보도록 합시다.

 

  아직 Animator탭에서 해야하는 일은 남았습니다. Jump에서 Walk로 가는 트랜지션을 만들지 않았기 때문입니다. Jump 애니메이션이 끝나기만 하면 Walk로 돌아가면 되므로 다음과 같이 설정해주기만 하면 됩니다.

  Walk->Jump 트랜지션은 Walk가 아직 종료되지 않아도 Jump로 바꾸기 위해서 멈춰야 하기 때문에 Has Exit Time에 체크를 해제 했지만 Jump->Walk는 끝나면 자동으로 Walk로 바뀌어도 되기 때문에 체크를 해제하지 않았습니다.

 

  이제 Scene탭으로 돌아가 cat 오브젝트를 클릭한 후 Animation 탭으로 돌아가서 Jump 애니메이션을 택하고 애니메이션을 다음과 같이 만들어 줍시다.

  스프라이트 프로퍼티를 추가할때 1:00에 만들어진 키프레임을 삭제하고

  0:05에 cat_jump1

  0:10에 cat_jump2

  0:20에 cat_jump3

  0:30에 0:20의 키프레임을 복사해서 붙여넣읍시다.

 

  이제 점프 모션이 모두 만들어졌습니다. 한번 미리보기로 확인해봅시다.

 

  제대로 만들어졌습니다. 이제 게임을 실행해봐서 제대로 작동하는지 확인하도록 합시다.

이 영상은 애니메이션을 제대로 확인하기 위해 임시로 PlayerController 스크립트에 60 프레임 고정 코드를 추가해두었습니다.

  제대로 작동하는군요. 이렇게 해서 애니메이션은 전부 완성이 되었습니다.


  오늘은 새로운 요소들이 많아서 강좌가 매우 길어졌습니다. 하지만 그만큼 새로운 기능과 요소들을 많이 다루었습니다. 아주 편리한 기능들이며 Unity를 사용한 게임개발에 반드시 필요한 기능들이므로 잘 숙지하셨으면 좋겠습니다.

 

  다음 시간엔 배경을 만들고 프리팹을 사용해 발판을 더 만들고 목표 지점과 클리어 장면까지 만들어서 완성시키도록 하겠습니다.

 

  긴 강좌 읽고 따라하시느라 정말 수고 많으셨습니다!