본문 바로가기
메타버스기반게임콘텐츠기획/그날의 강의

(9-3) 유니티unity : 다운 받은 에셋으로 캐릭터, 움직임, 애니메이션 만들기

by Queenut 2021. 12. 22.

캐릭터에게 움직임을 붙이는 코딩을 한다.

 

 

 

우선, 해당 작업을 위한 리소스 다운.

Window → Assets Store.

https://assetstore.unity.com/packages/templates/tutorials/3d-beginner-tutorial-resources-143848

 

3D Beginner: Tutorial Resources | 자습서 | Unity Asset Store

Use 3D Beginner: Tutorial Resources from Unity Technologies to elevate your next project. Find this & more 자습서 and templates on the Unity Asset Store.

assetstore.unity.com

 

 

 

 * '캐릭터'의 Inspector

Assets → Models → John Lemon.

 

파일을 클릭한 채 Inspector 창을 보면 모델이 가진 여러 속성이 보이는데,

전부 설명하긴 어렵고, 게임 기획에 관련된 몇 개만 찝어보면.

 

- Model → Scale Factor : 보통 기본값 1(만들어진 처음 상태). 숫자가 1보다 커지면 크기가 커지고 1보다 작아지면 작아진다. 레벨의 크기 같은 이유로 수정해야 할 때 이걸 만지면 된다.

- Rig : 캐릭터의 리깅 정보(뼈대, 관절 따위를 설정한 정보). 캐릭터가 녹아내린 연체동물이 아닌 이상 뼈대와 관절이 있어야 하는데 그 정보를 담고 있다.

- Animation : 캐릭터의 애니메이션. 현재 아무것도 없는데 오늘 해야 할 일이 여기이다.

- Materials : 대개 커스터마이징 하지 않는 이상 그대로 둔다.

 

 

 

이 캐릭터를 Scene으로 드래그해 놓으면 프리팹파일이 Hierarchy에 만들어지는데 아이콘 모양이 조금 다르다.

파란 글씨의 Prefab 표시. 근데 아이콘 모양이 다르다.

이건 이 파일이 읽기 전용이라는 뜻인데, 읽기 전용 상태에서는 수정이 어려우니 새로운 프리팹으로 만들어줘야 한다.

 

이미 존재하는, Assets → Prefabs 폴더에 Hierarchy 상의 John Lemon 모델 프리팹을 드래그.

이거 읽기 전용인데 새로운 프리팹 만들거야?

응, 그대로 가져와서 프리팹 만들거야. Original Prefab 클릭.

그럼 맨날 보던 프리팹 아이콘으로 만들어진다. 이름은 일단 간편하게 Player로 바꾼다(프리팹 파일의 이름도 바꿔줘야 한다).

 

 

 

 

잘 보면 Player 안에는 John Lemon(이하 레몬)과 Root가 있다.

레몬은 자식이 없지만 Root는 뭔가 많이 갖고 있어서 확인해보니 엉덩이, 왼쪽 다리, 왼쪽 발, 오른쪽 다리... 이런 신체부위이다. 게다가 각 박스 안에는 Transform을 제외하고 아무 속성이 없다.

 

레몬은 외형적인 속성을 갖고 있다. 기본 아바타라고 할 수도 있는데, 재질, 색, 피부의 질감 등 어떻게 보여야 할 지의 정보를 담고 있다. 그리고 캐릭터는 어쨌든 인간형태이므로 Skinned Mesh Renderer를 사용한다.

잡다한 외형 속성. 디자이너가 만들 때 지정된 것.

 

Root는 리깅 한 뼈대 정보들이다. 캐릭터가 애니메이션/모션을 할 때, 자연스럽게 보이도록 만들어 주는데 대개 좌표 정도만 지정한다. 미친 퀄리티의 게임은 어쩌면 팔다리 관절 다 따로 작업할지도....

Root → Hips → Spine1 → LeftShoulder → LeftArm → LeftForeArm → Rotation의 Y축 수치를 90으로 바꾸자 팔이 꺾인다.

 

다시 원래대로 돌려주고.

현재 상태에선 플레이 모드를 해도 아무 반응이 없다.

애니메이션을 지정하지 않았기 때문.

Component → Animator → Controller 가 None이다.

 


 

 * 캐릭터에게 애니메이션 넣기

애니메이션은 하나만 재생되지 않는다(걷고 뛰고 점프하고 등등).

캐릭터의 움직임(달리다가 점프하고, 멈추고, 다시 뛰고 등)에 따라 다른 애니메이션이 자연스럽게 연결되도록 보여주려고 한다.

 

 

 - 애니메이터 만들기

마우스 우클릭→ create→ Animation Controller

 

만든 애니메이션 컨트롤러를 Player에게 연결시켜준다.

노란 칸에 만든 컨트롤러가 들어가도록 만들자.

 

 

 

이제 애니메이션을 조정하자.

Window - Animation - Animator.

Scene 옆에 탭으로 생긴다.

- Layers : 애니메이션들이 보이는 보드. 여러 계층 단위로 설정할 수 있지만 극초보인 우리는! 기본 레이어만 갖고 한다.

- Parameters : 쉽게 매개변수라고 생각하면 된다. 변수를 만들 수 있는 공간. 자세한 건 조금 후에 나온다.

 

오른쪽의 까만 격자 보드에서

- 왼쪽 버튼: 클릭 혹은 드래그해서 선택.

- 오른쪽 버튼: 클릭으로 기능들 불러오기.

- 휠: 클릭 시 보드 움직임 가능, 확대/축소 가능.

 


 - 애니메이션 넣어주기

이 애니메이터에 애니메이션 파일을 넣어주자. 클릭 & 드래그.

일단 John의 파일만 건드려보자.

- Idle : 평상시.

- Walk : 걸을 때.

 

처음 넣는 파일은 자동으로 Entry와 연결된다.

이것은 게임이 처음 시작됐을 때 혹은 캐릭터가 처음 생성됐을 때, 가장 먼저 기본으로 보여줄 애니메이션을 결정한다.

 

당연히 처음 시작되는 캐릭터는 Idle의 모습을 보여야 한다.

만약 잘못 연결되었다면 Entry 우클릭 → Set StateMachine Default State. 새롭게 연결.

 

 

 

 

시작 애니메이션(가만히 있을 때)을 넣었으니 이젠 움직일 때의 애니메이션을 넣어야 한다.

정지했다가, 걸었다가, 다시 정지하고. 이것이 자연스럽게 연결되어야 한다.

 

John_Idle 우클릭 → Make Transition → 화살표를 John_Walk에 연결.

 

 

화살표 클릭 후 Inspector창.

어디에서 어디로 연결되었는지와 기타 세팅을 볼 수 있다.

- Has Exit Time : 애니메이션이 Idle에서 끝나면 Walk로 변할 때 걸리는 시간을 설정할 수 있다.

 

BUT, 우리는 플레이어가 키를 누를 때마다 애니메이션을 변하게 만들고 싶다!

때문에 해제한다.

 

 

그리고 조건Conditions을 넣어줘야 하는데, 조건을 넣을 변수는 Animator → Parameters에서 만들 수 있다.

 

 

추가 버튼+을 누르면 4가지 선택을 할 수 있다.

- Float : 실수형(1, 1.56, 5933.94839...) 변수

- Int : 정수형(1, 2, 56, 17894...) 변수

- Bool : True/False로만 판단하는 변수

- Trigger : 애니메이션에 보통 있는데, 어떤 상태가 되면 바로 작동하는 변수.

 

 

트리거 같지만, 우린 걷고 있는지(참, 1), 멈췄는지(거짓, 0)만 판단할 것이라 Bool로 만든다.

체크되어 있다. = true

체크가 풀려 있다. = false ← 이렇게 두면 '가만히 있을 때가 0'이라고 입력되는 것.

 

이 변수를 해당 화살표의 Conditions에 넣어주면 즉,

'John_Idle에서 IsWalk 이 true면 John_Walk으로 옮겨가겠다.'는 의미가 된다.

 

 

이젠 걷다가 멈춰야 하니까

John_Walk 에서 John_Idle 로 향하는 화살표를 만들어주고, 반대로 해보자.

Has Exit Time 은 똑같이 해제해주고, (변수는 이미 만들었으니 생략하고)

Conditions → IsWalk → false.

 

 

아, 혹시 진행중에 콘솔창에 노란 경고가 뜬다면,

너 아직 뭐 놓친 거 아니지..? 나 연결 안 된 거 같은데...

수정 중이라 뜨는 경고일 뿐이다. 작업을 전부 수행하고 나면 사라지니까 괜찮다.

 

 

작업 완료 후 Player →  Overrides → Apply All 로 수정을 적용시키자.

 

 

 


 

 

 * 캐릭터에 물리력을 더하다.

물리적인 연산: 벽에 부딪히면 넘어갈 수 없고, 물건들에 부딪힐 수 있게 만드는 것.

Mesh Renderer는 겉모양일 뿐, 물리적인 실체는 없기에 만들어줘야 한다.

 

Player → Add Component → Physics.

이 안에 든 Collider들은 물리력을 위한 충돌체라고 생각하면 된다. 콜라이더의 형태에 따라 여러가지로 나뉠 뿐, 하는 기능은 비슷하다(=슈팅게임의 히트박스).

 

대체로 단순한 형태의 도형을 사용한다. 여기엔 Capsule Collider를 사용해본다.

 

 

기본 형태는 구형의 형태. 보통 캡슐모양처럼 길게 늘여서 사용한다. 캐릭터의 발 끝이 0, 0, 0 의 위치이다.

- Edit Collider : 마우스로 Scene에서 직접 크기를 늘였다, 줄였다 할 수 있다.

- Is Trigger : 트리거 변수로 켜졌다 꺼졌다 할 건지.

- Material : None으로 하면 보이지 않지만 충돌하는, 딱 우리가 원하는 형태로 만드는 것이다.

- Center : 콜라이더의 위치.

- Radius : (구형이니까) 반지름.

- Height : 높이

- Direction : (캡슐모양으로)늘어나는 방향.

 

하고 싶은 대로 조정하면 된다.

 

 

(번외)

리본메뉴 밑에 있는 Pivot/Center - Global/Local 버튼에 대해서,

- Pivot/Center: 조정할 수 있는 사각형이 생기는 위치. 피봇은 내가 선택한 오브젝트로 생기고, 센터는 가운데로 생긴다.

- Global/Local : Transform의 좌표는 벡터에 있는 수치 값이 임의의 가상공간 안에서 임의의 한 점을 우리가 0, 0, 0이라고 정한 것일 뿐, 정말 그곳이 정 중앙인 것은 아니다. 글로벌은 바로 그 중앙을 통해 좌표를 정한다.

그러나 로컬은, 예를 들어 Hips이란 부모가 있고, 안에 LeftLeg, RightLeg, Spine과 같은 자식들이 있는데 이 자식들은 0, 0, 0의 위치를 Hips로 둔다. 즉, 안에 있는 자식들은 상위에 있는 부모의 원점을 원점이라고 생각하는데 이것을 로컬 좌표라고 한다.

 

때문에 되도록 버튼은 PivotGlobal 로 활성화하는 것이 좋다.

 

 


 * 물리법칙을 더하다.

앞은 충돌체라면 이것은 물리법칙이다.

 

Player → Add Component → Physics → Rigidbody.

- Mass : 질량. 무거운 물체, 가벼운 물체 등. 무게에 따라 받는 물리법칙이 다르니까(속도 등) 그런거 표현하려고. 무거우면 숫자가 커지고 가벼우면 작아진다.

- Drag : 마찰. 클수록 마찰이 세짐. 밀었을 때, 마찰 숫자가 적은 물체는 잘 밀린다.

- Angular Drag : 회전이 되는 물건이라면 회전에도 마찰 계수 입력이 가능하다.

- Use Gravity : 중력. 중력의 영향을 받는 물체는 체크하는데, 게임에 만드는 거의 대부분의 물체에 체크된다.

     ((만약 평지에 경사도 없고 계단 올라가는 것도 없다면 Constraints 포시젼 Y를 고정시켜도 된다.))

     (물론 아주 간단한 게임에서만 이렇게 하는거!)(실습하는거니까 그렇게 하는거!!)(아직 레벨에 바닥이 없어서 그러는거!!!)

- Constraints : Transform의 포지션, 로테인을 고정하는 기능. 캐릭터가 X축이나 Z축 회전(Rotation)은 하면 안되니까, 변경되지 않도록 설정할 수 있음.

 

 

 

 * 캐릭터를 플레이하게 만들자.

 

일단, 캐릭터가 디디고 이동할 수 있도록 맵이 필요하다.

프리팹에 리소스에서 제공한 Level을 끌어온다.

 

레벨의 Transform을 reset시키고, 캐릭터의 위치도 스타트 위치에 두자.

해당 좌표가 리소스에서 제공하는 스타트 위치.

다만 Y축은 혹시 캐릭터가 레벨에 낄 수 있으니까 조금 띄워준다.

 

 

완료하면 이 위치에 있다.

 

 

움직임 구현하기 위해 스크립트를 작성한다.

Script 폴더에 Create → C# Script → 파일 이름은 대충 PlayerMovement.

 

만들고자 하는 움직임

 : Transform에 Position값을 바꾸는 것. + WASD키 입력을 받았을 때 구현되도록.

 

 (여기서부턴 진도를 따라가느라 작성을 많이 못 했다.)

 

 

 

1. 버튼이 눌렸는지 검출하는 변수.

Horizontal : 왼쪽과 오른쪽. a키와 d키.

Input.GetAxis() : 유니티 자체에 있는, 어떤 인풋값이 들어왔는지를 알려주는 메서드.

 : 축의 오른쪽 값이 눌렸다면 1 호출, 안 눌리면 0 호출. 왼쪽 키가 눌리면 -1 호출, 아니면 0 호출.

왼쪽 안 눌림 오른 쪽
-1 0 1

그러나 이렇게 단정지어 설명할 수 없다.

1과 0과 -1 사이엔 무수히 많은 값(0.2, 0.5, 0.6... -0.2, -0.6...)이 있기 때문이다.

보통의 PC게임은 -1, 0, 1 만을 구현하지만 조이패드로 움직이는 콘솔게임은 값마다 차이를 두곤 한다.

 

Vertical : 위와 아래. w키와 s키. 다른 것은 Horizontal과 마찬가지이다.

 

작성 후, Debug.Log로 잘 작동되나 확인해보자.

대소문자 구별 확실히 해라!
가만히 있을 때(수평, 수직 값)는 무수히 늘어난다.

 

 

유니티는 많은 변수, 메서드 등을 담고 있는데 혹시 확인하고 싶다면

Edit - Project Settings 를 확인하면 된다.

위에 쓴 Horizontal 과 Vertical 은 Input Manager - Axes 에 있다.

 

 

 

2. 버튼(wasd)에 대해 이동하는 변수.

가고 싶은 방향(벡터3의 값))을 위에서 정의한 키와 맞게 설정한다.

Vector3 movement = new Vector3 (horizontal, 0f, vertical);
movement.Normalize();

여기서 호라이즌탈과 버티칼이 소문자인 이유는 유니티의 변수를 가져온 것이 아니라 방금 정의한 float 변수를 가져왔기 때문이다.

Vector3을 가진 movement 변수를 각각 horizontal과 0(Y축은 움직일 필요가 없으니까)과 vertical이라는 인수를 가졌다고 정의하고,

이 변수에 벡터의 길이를 1로 만들기 위해 .Normalize(); 를 사용한다.

Normalize : 정규화. 벡터의 길이를 1로 만드는데, 1인 경우 연산속도도 빨라지고, 특정 방향으로의 이동을 표현할 때 그냥 속도만 곱해주면 시간 당 이동한 위치가 나오게 된다.

 

 

 

3. 움직임을 나타내는 변수

public class PlyerMovement : MonoBehaviour
{
       public float speed = 1f;
       ...
       
       void FixedUpdate()
       {
             transform.position = transform.position + movement * Time.deltaTime * speed;
             ...

굵은 글씨를 한글로 풀어서 쓴다면,
움직임 = [현재 위치] + [움직이고 싶은 위치] * [균일한 속도가 나오도록 만드는 변수] * [스피드]

스피드speed는 에디터에서 조정이 가능하도록 업데이트 메소드 외부로 뺐다.

 

Time.deltaTime : 컴퓨터의 성능에 따라 프레임 차이가 나지 않도록 이전 프레임과 현 프레임의 시간차를 동일하게 만드는 변수.

 

Update() : 매 프레임마다 실행된다 = 영화도 원래 아주 짧은 순간의 사진들이 모여 움직이는 것처럼 보이는 것.

즉, 매 프레임(렌더링)할 때마다 수행.

FixedUpdate() : 매 물리연산 할 때마다 실행.

 

 

Q. 애가... 애가 벽에 부딪히면 트리플악셀을 합니다!!

A. 전에 붙인 Rigidbody를 통해서 물리계산이 되기 때문에 도는 것. 방향 고정을 하지 않았기 때문에 코드를 완성하고 나면 괜찮아 질 것이다(=괜찮다).

 

 

 

4. 움직일 때 걷는 애니메이션이, 멈출 때 멈추는 애니메이션이 뜨도록 만들기

4-1. Animator에 접근할 수 있는 변수를 만들기.

이동과 정지에 따라 다른 애니메이션이 보이도록 만들기 위해, 만들어둔 Animator에 접근할 수 있어야 한다. 

Unity Editor 내에 있는 컴포넌트를 가져오려면 가져오려는 각 컴포넌트마다 변수 선언을 해줘야 한다.

클래스가 자료형이 되기도 한다.

public class PlayerMovement : MonoBehaviour
{
      Animator animator;
      Rigidbody rigidbody;

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

Unity Editor 내에 있는 컴포넌트를 가져오려면 가져오려는 각 컴포넌트마다 변수 선언을 해줘야 한다.

일단 유니티 내에 있는 Animatoranimator로 가져오고,

이 스크립트가 처음 실행될 때(Start()), animator를 컴포넌트에 있는 그 Animator로 연결 한다고 알려주는 것(GetComponent<>).

GetComponent<> : 게임 오브젝트에 붙어있는 다른 컴포넌트를 쭉 살펴보고, 입력된 것과 똑같은 자료형이 있으면 반환해준다.

rigidbody도 동일하다.

 

이게 상속받는다는 그건가?

지금 내가 이해한게 맞긴 한건가.....

 

 

4-2. 지금 캐릭터가 움직이고 있는지 아닌지(IsWalk) 확인.

지금 움직이고 있는지 아닌지에 대한 정보를 알기 위해서 horizontal과 vertical이 1에 가까우면 true, 0에 가까우면 false를 출력하도록 만든다. 그리고 움직이고 있다면 애니메이션을 변경하도록 만든다.

public class PlyerMovement : MonoBehaviour
{
       Animator animator;
       ...

       void FixedUpdate()
       {
             bool hasHorizontalInput = Mathf.Approximately(horizontal, 0f) == false;

             bool hasVerticalInput = !Mathf.Approximately(vertical, 0f);

             bool isWalking = hasHorizontalInput || hasVerticalInput;

             animator.SetBool("IsWalk", isWalking);
             ...

걷는지(true) 아닌지(false)의 정보니까 bool.

Mathf.Approximately() : 첫 번째 인수와 두 번째 인수를 비교해서 그 둘이 가까워질수록 true를 출력하는 메소드.

그럼 0f 말고 1f 해도 되지 않나? 왜 0으로 해서 굳이 ==false 나 ! 를 붙였을까?

 

= Mathf.Approximately(horizontal, 0f) == false;

= Mathf.Approximately(horizontal, 0f) != true;

= !Mathf.Approximately(horizontal, 0f);

   셋 전부 같다.

 

|| : or 연산을 하는 논리 연산자. 둘 중 하나만 참이어도 참이다.

(( &&: And 연산을 하는 논리 연산자. 둘 모두 참이어야 참이다.))

 

논리 연산자(C# 스터디 9-3장 참고) : 참 거짓을 여러가지 합쳐서 도출하고 싶을때.

조건을 어떤 식으로 합칠지 생각해봐라~

 

animator.SetBool("IsWalk"isWalking); : 위에서 진행한, 애니메이터의 파라미터를 불러온 것. 큰따옴표로 묶어야 한다.

 

 

Q. 왜 무슨 키를 누르든 앞으로만 가지??

A. 스피드를 3 쯤 올려라

 

 

 

5. 그럼 해당 방향으로 얼마나 움직일지.

private void OnAnimatorMove()
    {
        rigidbody.MovePosition(rigidbody.position + movement * animator.deltaPosition.magnitude);
        rigidbody.MoveRotation(rotation);
    }

MovePosition: 얼마나 움직여줄지의 값. ([물리적으로 현재위치] + [어디로 움직일지] * [애니메이션 자체의 움직임(John_Walk 애니메이션 실행시 캐릭터는 자체적으로 앞으로 이동한다.)을 보완하기 위한 값])

animator에서 가져온 delta위치의 크기?라고 알면 되려나?

MoveRotation : 어떤 방향일지의 값↓.

 

 

6. 그럼 해당 방향은 어떻게 알지?

public class PlayerMovement : MonoBehaviour
{
       Quaternion rotation;
       ...

       void FixedUpdate()
       {
              Vector3 desiredForward;
              desiredForward = Vector3.RotateTowards(transform.forward, movement, turnSpeed * Time.deltaTime, 0f);

              rotation = Quaternion.LookRotation(desiredForward);
              ...

일단 변수 rotation은 유니티 자체적으로 만들어둔 Quaternion에서 가져왔다고 하고.

 

Vector3에서 가져온, 원하는 방향에 대한 변수(desiredForward)를 만든다.

이 변수는 Vector3.RotateTowards로 선언하는데

각 매개변수로

[지금 보는 방향 or 현재 위치], [타겟의 위치], [어떤 빠르기로 턴 하는지] * [균일한 프레임 시간차로], [크기] 를 가진다.

 

rotation = 회전 움직임.

 

 

 

완성본.

PlayerMovement.cs
0.00MB