전체 페이지뷰

2017년 7월 19일 수요일

Unity Tutorial: Survival Shooter, part 1


Environment setup



이번에 따라해볼 게임은 주어진 공간 내에서 다가오는 적들을 자유로이 총으로 맞추는 슈팅 게임입니다.

새 프로젝트를 Survival Shooter라는 이름으로 생성해 주었습니다.


먼저 레이아웃 환경 설정부터 설명하고 있습니다. 유니티에 익숙하고 자신만의 스타일이 있다면 그대로 사용해도 좋지만 처음이라면 2 by 3로 하고 프로젝트창은 옮겨 Hierarchy 아래로 오게 하는 세팅을 권하고 있습니다.


저도 유사하게 바꾸고 2 by 3 modified라고 이름지어 저장했습니다.

강사가 튜토리얼마다 다른지라 폴더 지정이라던지 세세한 것들이 조금씩 다릅니다. 그런 것들은 제가 마음에 드는 것을 따르기로 하겠습니다. 따라서 영상과 조금 다른 부분이 있을지도 모릅니다.

영상에는 생략되어 있는데 에셋을 다운받아야 하겠습니다. 링크의 에셋을 Add to download하고 Open in Unity하면 유니티에서 다운받을 수 있게 창이 열립니다. 유니티에서 다운로드하고 임포트합니다.

전처럼 Create>Folder하고 _Scenes라는 폴더를 만들어 거기에 아직 아무것도 만들지 않은 이 씬을 Level 01이라는 이름으로 저장하도록 하겠습니다.

자 이제 이 아무 것도 없는 공간에 배경을 넣겠습니다. 배경은 Prefabs 폴더에 Environment라는 이름으로 준비되어 있는데 이것을 드래그하여 Hierarchy창으로 가져다 놓습니다.


영상과는 다르게 제가 생성한 씬에는 자동으로 Directional Light가 설정되어 있는데 그것을 지우고 Prefabs 폴더의 Lights를 Hierarchy창으로 끌어다 놓습니다.

이 배경에서 플레이어가 돌아다니는 것을 쫓고 총을 발사하기 위해서 카메라로부터 바닥으로 보이지 않는 선을 만드는 ray cast라는 기법을 사용하게 됩니다. 그러나 우리의 바닥은 레고라던지 부서진 기차 장난감들이 늘어놓아져 있어 높이가 일정치 않습니다. 따라서 이것을 단순화 시키기 위해 Quad 오브젝트를 추가해 보겠습니다.

Hierarchy창에서 Create>3D Object>Quad 하여 오브젝트를 추가하고 필요하다면 Reset하여 제 위치에 오게 합니다. 그리고 바닥에 깔리도록 X Rotation을 90으로 설정해줍니다. 그리고 전체 게임 영역을 커버하기 위해 Scale을 100, 100, 0으로 설정하고 이름을 Floor로 바꿉니다.


이 Floor는 실제 바닥이 아니기 때문에 인스펙터 창에서 톱니 바퀴를 눌러 Remove ComponentMesh Renderer를 제거하여 보이지 않게 해 줍니다.

이제 Floor 오브젝트를 기타 다른 오브젝트들과 독립시키기 위해 레이어를 Floor로 따로 지정합니다.

이제 배경음악을 먼저 설정하려고 합니다. Hierarchy창에서 Create>Create Empty하여 빈 오브젝트를 생성하고 이름을 BackgroundMusic으로 정합니다. 그리고 그 오브젝트에 Add Component>Audio>Audio Source하여 컴포넌트를 추가하고 Audio Clip란 옆의 작은 동그라미를 누르면 팝업창이 떠서 음악을 선택할수 있게 됩니다. Background Music 클립을 선택합니다.


그리고 프로퍼티 중 Play On Awake는 체크 해제, Loop는 체크하고, 볼륨을 0.1 정도로 맞춰주면 세팅이 끝납니다.





Player character



플레이어 캐릭터 모델은 Assets>Models>Characters폴더에 Player라는 이름으로 들어 있습니다. 드래그하여 Hierarchy에 놓습니다. 역시 origin위치에 있는지 확인하고 아니라면 Reset하여 제자리에 가게 합니다.

이 플레이어가 다른 것과 상호작용할 때 구분할 수 있도록 태그를 붙입니다. 인스펙터창의 Tag 목록을 드랍다운하면 Player가 나오는데 그것으로 정합니다.


다음은 플레이어의 animation controller를 붙일 차례입니다. 이 컨트롤러가 플레이어의 모든 움직임을 가능케 할 것입니다. 그에 앞서 먼저 이 플레이어가 어떠한 동작을 가지고 있는지 봅시다. Player 프리팹을 선택하고 인스펙터 창을 보면 상단에 Model/Rig/Animations 탭이 있습니다. 그 중 Animations를 선택해 봅니다.  아래 그림과 같이 Move, Idle, Death의 세 상태가 있고 아래에 미리보기가 있습니다.


Maya와 같은 3D 툴로 만들어진 이 모델에는 저러한 애니메이션 상태가 들어 있고, animation controller로 언제 어떤 에니메이션이 보여질지를 선택하게 됩니다. 이 콘트롤러를 저장할 폴더를 먼저 만듭시다. 루트 폴더에 Animation이라는 폴더를 생성합니다. 그리고 그 폴더 안에 Create>Animator Controller를 선택하고 이름은 PlayerAC라고 정해줍니다.

그리고 생성된 PlayerAC을 드래그하여 Hierarchy의 Player에 갖다 넣습니다. 그렇게 하면 자동으로 컨트롤러가 연결된 것을 인스펙터 창에서 확인 가능합니다.


영상과는 다르게 Apply Root Motion 프로퍼티가 체크해제 되어 있는데 이 튜토리얼에서 아무 차이 없다고 하니 그냥 두겠습니다. PlayerAC를 더블클릭하면 Animator창이 하나 새로 나타납니다. 여기서 앞으로 에니메이션을 직접 다루게 되는 모양입니다.


프로젝트 창의 Models>Characters폴더에 Player.fpx파일이 있습니다. 화살표를 펼쳐서 하위항목을 보면 앞서 언급한 세개의 상태가 있습니다.


하나씩 드래그 하여 Animator창으로 가져다 놓습니다.


처음 끌어다 놓은 Death가 오렌지색입니다. 오렌지색은 디폴트 상태인데 이 게임이 죽은 것을 디폴트로 둘 정도로 슬픈 게임은 아니므로 변경하도록 하겠습니다. Idle을 우클릭하고 Set as Layer Default State를 선택하면 이제 Idle이 오렌지색으로 바뀝니다.


디폴트 상태는 이제 어떤 때에 다른 상태의 에니메이션을 선택하는지를 유니티에게 알려줘야 합니다. 그 때 필요한 것이 Parameter입니다(에니메이터 창 좌상단에 탭으로 존재합니다). 여기에서 + 버튼을 누르면 Float, int, bool, trigger 타입 중 선택하여 새로운 parameter를 만들 수 있습니다.



Bool 형의 IsWalking, Trigger 형의 Die라는 파라미터를 만듭니다.


여기서 끝나는 것이 아닙니다. 이제 실제 어떤 동작이 어떻게 이행(transition)되는 것인지를 에니메이터에게 알려줘야 합니다. transition을 이어줬다는 것은 '이 상태에서 이 상태로 이동 가능하다'는 뜻입니다. Idle에 우클릭하여 Make Transition이라는 것을 선택하면 하얀 화살선이 나타납니다. 그것을 Move로 가져다두고 클릭하면 두 개가 이어집니다.



영상과는 달리 Entry라는 것과 오렌지색 transition이 있는데 그것은 신경쓰지 않으셔도 됩니다. 이제 정확히 어떤한 때에 이 transition이 일어나는가를 서술하기 위해 하얀 화살선을 클릭하면 선이 파란색으로 바뀝니다. 그리고 인스펙터 창을 보면 transition의 프로퍼티들이 있습니다. 그 중 Condition이라는 것이 있는데 현재는 빈 칸으로 되어 있습니다. 여기서 + 버튼을 누르고 IsWalking, true로 맞춰줍니다. 말 그대로 IsWalking이 true일 때 이 transition이 일어난다는 것입니다.


덧붙여서 Has Exit Time도 체크 해제합니다(해제 하지 않으면 에러가 납니다). 끝났으면 Move에 우클릭하여 Make Transition을 선택하고 Idle로 이어서 상호간에 transition 가능하도록 해줍니다. 그리고 이번엔 Condition을 IsWalking, false로 합니다.

이번엔 Die의 차례입니다. Die는 어떤 상태에서라도 일어날 수 있는 일입니다. 따라서 Any State에 우클릭 Make Transition을 선택하고 Die와 연결해 줍니다.


이어진 하얀 화살선을 선택하고 Condition을 Die로 해줍니다.

이제 우리의 캐릭터에 물리적 실체를 부여해줄 차례입니다. Hierarchy창에서 Player를 선택하고 Add Component>Physics>Rigidbody를 선택합니다. DragAngular Drag 항목을 Infinity로 타이핑해줍니다(Inf까지만 치고 엔터 눌러도 됩니다). Drag가 뭔지 몰라 찾아보니 항력이라고 합니다. 움직이는 반대 방향으로 받는 저항력을 말하는 거죠. 생각해 보건데 우리 캐릭터에 움직이라고 방향키를 한번 눌러줬을 때 저항력이 무한대가 아니라면 눌렀던 손을 떼도 조금 더 힘을 가해줬던 방향으로 더 움직일 겁니다. 0이라면 그 방향으로 계속 가겠죠. 이 게임에서 그런 컨트롤은 어울리지 않습니다. 누르면 가고 손 떼면 멈춰야죠.

그리고 아래의 Constraints를 펼쳐보면 Freeze Position, Freeze Rotation이 있습니다. 운동의 제한방향을 뜻하는 것입니다.

바닥 아래로 떨어지면 안 되니까 Freeze Position은 Y를 체크하고, Freeze Rotation은 X, Z에 체크합니다.


이제 실제적 충돌 처리를 위해 콜라이더를 더합니다. Add Component>Physics>Capsule Collider를 추가하고, Center를 0.2, 0.6, 0으로, Height는 1.2로 바꿔줍니다.


그리고 Add Component>Audio>Audio Source도 추가합니다. Audio Clip 프로퍼티 옆 동그라미를 눌러 Player Hurt를 선택하고, Play On Awake는 체크 해제합니다.

다음으로 Player의 움직임을 컨트롤할 스크립트를 연결할 겁니다. Assets>Scripts>Player 폴더 내에 몇개의 스크립트가 있습니다. 그 중 PlayerMovement라는 이름으로 빈 클래스가 있는데, 이 스크립트를 드래그하여 Hierarchy 창의 Player에 가져다 놓으면 스크립트가 Player 오브젝트에 추가됩니다.


그리고 에디터로 스크립트를 엽니다.

게임 전체에 사용될 변수 몇 개를 선언하는 것으로 시작합니다.

public float speed = 6f; //플레이어 속도
Vector3 movement;  //플레이어 움직임을 저장할 벡터
Animator anim;     
Rigidbody playerRigidbody;
int floorMask;    // ray cast를 적용할 layer mask
float camRayLength = 100f; // 카메라에서 씬까지의 ray 길이
cs

public 한 개와 private 몇개입니다. 그리고 Awake() 메소드 내에서 레퍼런스들을 연결합니다.

private void Awake()
{
    floorMask = LayerMask.GetMask("Floor");
    anim = GetComponent<Animator>();
    playerRigidbody = GetComponent<Rigidbody>();
}
cs

LayerMask.GetMask()를 사용하여 Floor 레이어에 대한 mask를 설정하고, 레퍼런스도 연결합니다.

그리고, 물리적 변화가 있을 때 호출되는 FixedUpdate()에서 Input을 받아옵니다.

float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
cs

여기서 잠깐 GetAxisRaw의 인자가 되는 axisName 문자열에는 어떤 것이 있나 살펴봅시다.

유니티 화면 상단에서 Edit>Project Settings>Input을 선택하면 인스펙터창에 Input의 종류가 나타납니다.

총 18개의 축들이 있고, 각각을 펼쳐보면 어떤 버튼에 해당하는지 등에 대한 자세한 정보가 있습니다.  그 중 우리는 Horizontal, Vertical을 사용하였습니다.

이제 FixedUpdate에서는 입력된 값에 따라 플레이어를 움직이고, 회전하고, 그에 따라 적절한 에니메이션을 보여줘야 합니다. 그것에 쓰일 메소드를 FixedUpdate 밖에 따로 작성합니다.


첫 번째는 Move()이며, 말 그대로 위치를 변화 시켜주는 메소드입니다.

private void Move(float h, float v)
{
    movement.Set(h, 0f, v);
    movement = movement.normalized * speed * Time.deltaTime;
    playerRigidbody.MovePosition(transform.position + movement);
}
cs

입력된 값만큼의 벡터를 받아와 표준화하고, 이동합니다.

두 번째는 Turning()입니다.
여기서 ray casting이 필요합니다. 제가 또 이 분야에 문외한이라 레이 캐스팅을 한번 알아보고 가야할 듯 합니다.


레이 캐스트란 3차원의 어느 지점으로부터 정해진 방향으로 가상의 빛을 쏴서 이 빛과 충돌되는 객체를 감지하는 기법입니다. 우리의 플레이어가 서 있는데 게임상의 어떤 지점을 클릭하면, 카메라로부터 마우스가 클릭된 쪽으로 ray를 생성합니다. 이 레이가 우리의 바닥 기준점인 Floor Quad와 충돌되는 것을 감지하면, 구체적인 Hit point를 얻어낼 수 있게 되고, 플레이어가 그 쪽을 바라보게 한다는 것 같습니다.

따라서 가장 먼저 해야할 것은 Ray를 생성하는 것입니다.

// 화면의 마우스 커서에서 카메라 방향으로 광선 생성
Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
cs

다음으로 광선이 바닥과 부딪힌 곳에 대한 정보를 저장할 변수를 만듭니다.

그리고, 생성된 Ray가 바닥과 부딪힌다면 플레이어가 그곳을 바라보게 하는 코드를 작성합니다.

private void Turning()
{
    // 화면의 마우스 커서에서 카메라 방향으로 광선 생성
    Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
 
    // 광선이 부딪힌 곳에 관한 정보를 저장하는 RaycastHit 변수 생성
    RaycastHit floorHit;
 
    if (Physics.Raycast(camRay,out floorHit, camRayLength, floorMask))
    {
        // 플레이어로부터 floorHit까지의 벡터 생성
        Vector3 playerToMouse = floorHit.point - transform.position;
 
        // 벡터가 확실히 바닥면 상에 존재하도록 만들어줌
        playerToMouse.y = 0f;
 
        // 플레이어로부터 벡터를 바라보는 회전을 생성
        Quaternion newRotation = Quaternion.LookRotation(playerToMouse);
 
        // 플레이어가 이 회전방향으로 회전하게 함
        playerRigidbody.MoveRotation(newRotation);
    }
}
cs


마지막은 Animating()입니다. Input값이 둘 다 0이 아니면 걷도록 만들어주면 됩니다.
private void Animating(float h, float v)
{
    bool walking = h != 0f || v != 0f;
    anim.SetBool("IsWalking", walking);
}
cs


이 세 개의 메소드를 FixedUpdate()내에서 사용합니다.
 private void FixedUpdate()
{
    ...
 
    Move(h, v);
    Turning();
    Animating(h, v);
}
cs


PlayerMovement.cs의 현재까지 작성한 전체 코드는 아래와 같습니다.

using UnityEngine;
 
public class PlayerMovement : MonoBehaviour
{
    public float speed = 6f; //플레이어 속도
 
    Vector3 movement;  //플레이어 움직임을 저장할 벡터
    Animator anim;     
    Rigidbody playerRigidbody;
    int floorMask;    // ray cast를 적용할 layer mask
    float camRayLength = 100f; // 카메라에서 씬까지의 ray 길이
 
    private void Awake()
    {
        // layer mask 생성
        floorMask = LayerMask.GetMask("Floor");
 
        // 레퍼런스 설정
        anim = GetComponent<Animator>();
        playerRigidbody = GetComponent<Rigidbody>();
    }
 
    private void FixedUpdate()
    {
        // 입력된 축 저장
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
 
        Move(h, v);
        Turning();
        Animating(h, v);
    }
 
    private void Move(float h, float v)
    {
        // movement 벡터 설정
        movement.Set(h, 0f, v);
 
        // 벡터를 표준화하고, 초당 속도에 비례하게 만듬
        movement = movement.normalized * speed * Time.deltaTime;
 
        // 표준화된 벡터만큼 플레이어 위치 이동
        playerRigidbody.MovePosition(transform.position + movement);
    }
 
    private void Turning()
    {
        // 화면의 마우스 커서에서 카메라 방향으로 광선 생성
        Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
 
        // 광선이 부딪힌 곳에 관한 정보를 저장하는 RaycastHit 변수 생성
        RaycastHit floorHit;
 
        if (Physics.Raycast(camRay,out floorHit, camRayLength, floorMask))
        {
            // 플레이어로부터 floorHit까지의 벡터 생성
            Vector3 playerToMouse = floorHit.point - transform.position;
 
            // 벡터가 확실히 바닥면 상에 존재하도록 만들어줌
            playerToMouse.y = 0f;
 
            // 플레이어로부터 벡터를 바라보는 회전을 생성
            Quaternion newRotation = Quaternion.LookRotation(playerToMouse);
 
            // 플레이어가 이 회전방향으로 회전하게 함
            playerRigidbody.MoveRotation(newRotation);
        }   
    }
 
    private void Animating(float h, float v)
    {
        bool walking = h != 0f || v != 0f;
        anim.SetBool("IsWalking", walking);
    }
}
cs

저장 후 유니티로 돌아갑니다.

실행시켜 보면 플레이어가 방향키로 움직이고 마우스가 향하는 쪽을 바라보는 것을 알 수 있습니다.

댓글 1개:

  1. 정말정말 감사합니다! 이제 유니티 입문자인데 캐릭터 안움직여서 정말.. 힘들었거든요ㅠㅠ 덕분에 잘되고 있습니다!! 감사합니다^_^

    답글삭제